Вы находитесь на странице: 1из 994

ASP.

NET CORE MVC


с примерами на С#
ДЛЯ ПРОФЕССИОНАЛОВ

6-е издание
Pro
ASP.NET Core MVC
Sixth Edition

Adam Freeman
ASP.NET Core MVC
с примерами на С#
ДЛ Я П Р О Ф Е С С И о· Н А Л О В

6-е издание

Адам Фримен

Москва· Санкт-Петербург· Киев


2017
ББК 32.973.26-018.2.75
Ф88
УДК 681.3.07
Компьютерное издательство "Диалектика"
Зав. редакцией С.Н. Тригуб
Перевод с английского Ю.Н. Артеменко
Под редакцией Ю.Н. Артеменко

По общим вопросам обращайтесь в издательство "Диалектика" по адресу:


info@dialektika.com,http://www.dialektika.com

Фримен, Адам.

Ф88 ASP.NET Core MVC с примерами на С# для профессионалов , 6-е изд. : Пер.
с англ. - СпБ. : ООО "Альфа-книга", 2017. - 992 с. : ил . - Парал. тит. англ .
ISBN 978-5-9908910-4-3 (рус .)
ББК 32.973.26-018.2.75

Все названия программных продуктов являются зарегистрированными торговыми мар­


ками соответствующих фирм .
Никакая часть настоящего издания ни в каких целях не может быть воспроизведена в
какой бы то ни было форме и какими бы то ни было средствами, будь то электронные или
механические, включая фотокопирование и запись на магнитный носитель, если на это н ет
письменного разрешения издательст в а APгess, Berkeley, СА.
Authorized translatioп from the Eпglish laпguage editioп puЬlished Ьу APress, !пс " Copyright
© 2016 Ьу Adam Free maп.
All rights reserved. No part of this work тау Ье reproduced or traпsmitted lп апу form or Ьу апу
meaпs, electroпic or mechaпical , lпcludiпg photocopylпg, recordlng, or Ьу any lnformatioп storage
or retrieval system, without the prior wrltteп permissioп of the copyright owner апd the puЬlish e r.
1Тademarked names may appear in this book. Rather th aп use а trademark symbol with every
оссurгепсе of а trademarked пате , we use the п ames опlу iп ап editorial fashioп апd to tl1e beп ­
efit of the trademark owпer. with по iпteпtloп of iпfringemeпt of the trademark.
Russiaп laпguage editioп is puЬlished Ьу Dialektika Computer Books PuЬlishiпg according to
the Agreemeпt with R&I Eпterprises Iпterпatioпal , Copyright © 2017.

Научно-популярное издание
Адам Фримен

ASP.NET Core МVС с примерами на С#


для профессионалов
6-е издание
Верстка Т.Н. Артеменко
Художественный редактор В.Г. Павлютин

Подписано в печать 23.01.2017. Формат 70Х100/ 16.


Гарнитура Тimes. Печать офсетная.
Усл. печ . л .79,98. Уч . -изд. л . 58,4.
ТИраж 500 энз. Заназ No 648 .
Отпечатано способом ролевой струйной печати
в АО • Первая Образцовая типография •
Филиал • Чеховский Печатный Двор »
142300, Московская область, г. Чехов, ул. Полиграфистов, д. l
ООО "Альфа-ннига", 195027, Саннт-Петербург, Магнитогорская ул " д. 30
ISBN 978-5-9908910-4-3 (рус.) © Компьютерное издательство "Диалентина", 2017
ISBN 978-1-484-20398-9 (англ.) © Ьу Adam Freeman, 2017
Оглавление

Часть 1. Введение в инфраструктуру ASP.NET Core MVC 19


Глава 1. Основы ASP.NET Core MVC 20
Глава 2. Ваше первое приложение MVC 29
Глава 3. Паттерн MVC, проекты и соглашения 68
Глава 4. Важные функциональные возможности языка С# 82
Глава 5. Работа с Razor 115
Глава 6. Работа с Visual Studio 136
Глава 7. Модульное тестирование приложений MVC 170
Глава 8. SportsStore: реальное приложение 201
Глава 9. SportsStore: навигация 244
Глава 10. SportsStore: завершение построения корзины для покупок 276
Глава 11. SportsStore: администрирование 298
Глава 12. SportsStore: защита и развертывание 325
Глава 13. Работа с Visual Studio Code 348

Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC 373


Глава 14. Конфигурирование приложений 374
Глава 15. Маршрутизация URL 426
Глава 16. Дополнительные возможности маршрутизации 465
Глава 17. Контроллеры и действия 500
Глава 18. Внедрение зависимостей 543
Глава 19. Фильтры 576
Глава 20. Контроллеры API 614
Глава 21. Представления 644
Глава 22. Компоненты представлений 678
Глава 23. Дескрипторные вспомогательные классы 708
Глава 24. Использование дескрипторных вспомогательных классов для форм 741
Глава 25. Использование других встроенных дескрипторных
вспомогательных классов 766
Глава 26. Привязка моделей 793
Глава 27. Проверка достоверности моделей 828
Глава 28. Введение в ASP.NET Core ldeпtity 861
Глава 29. Применение ASP.NET Core ldentity 898
Глава 30. Расширенные средства ASP.NET Core ldentity 926
Глава 31. Соглашения по модели и ограничения действий 958

Предметный указатель 987


Содержание

Об авторе 18

Часть 1. Введение в инфраструктуру ASP.NET Core MVC 19

Глава 1. Основы ASP.NET Core MVC 20


История развития ASP. NЕТ Core МVС 20
ASP.NEТWeb Foгms 21
Первоначальная инфраструктура МVС Fгamewoгk 22
Обзор ASP.NET Core 23
Основные преимущества ASP.NET Core МVС 24
Архитектура МVС 24
Расширяемость 24
Жесткий контроль над HTML и НТТР 25
Тестируемость 25
Мощная система маршрутизации 25
Современный АРI-интерфейс 26
Межплатформенная природа 26
Инфраструктура ASP. NEТ Core МVС имеет открытый код 27
Что необходимо знать? 27
Канава структура книги? 27
Часть I. Введение в инфраструктуру ASP.NEТ Соге МVС 27
Часть П . Подробные сведения об инфраструктуре ASP.NEТ Core МVС 28
Что нового в этом издании? 28
Iде можно получить код примеров? 28
Резюме 28

Глава 2. Ваше первое приложение MVC 29


Установка Visual Studio 29
Создание нового проекта ASP. NЕТ Саге МVС 31
Добавление первого контроллера 33
Понятие маршрутов 36
Визуализация неб - страниц 37
Создание и визуализация представления 37
Добавле ние динамического вывода 40
Создание простого приложения для ввода данных 42
Предварительная настройка 42
Проектирование модели данных 43
Создание второго действия и строго типизированного представле ния 44
Ссылка на методы действий 46
Построение формы 47
Получение данных формы 49
Добавление проверки достоверности 56
Стилизация содержимого 61
Резюме 67
Содержание 7

Глава З. Паттерн MVC, проекты и соглашения 68


История создания МVС 68
Особенности паттерна МVС 68
Понятие моделей 69
Понятие контроллеров 70
Понятие представлений 70
Реализация MVC в ASP.NEТ Core 70
Сравнение MVC с другими паттернами 71
Паттерн интеллектуального пользовательского интерфейса 71
Проекты ASP. NEТ Core МVС 75
Создание проекта 76
Соглашения в проекте МVС 79
Резюме 81

Глава 4. Важные функциональные возможности языка С# 82


Подготовка проекта для примера 83
Включение ASP. NЕТ Core МVС 84
Создание компонентов приложения МVС 85
Использование nul l-условной операции 87
Связывание в цепочки nu ll-условных операций 88
Комбинирование nul l -условной операции и операции объединения с nu ll 89
Использование автоматически реализуемых свойств 90
Использование инициализаторов автоматически реализуемых свойств 91
Со здание автоматически реализуемых свойств только для чтения 92
Использование интерполяции строк 93
Использование инициализаторов объектов и коллекций 94
Использование инициализатора индексированной коллекции 96
Использование расширяющих методов 97
Применение расширяющих методов к интерфейсу 98
Создание фильтрующих расширяющих методов l 00
Использование лямбда- выражений 101
Определени е функций l 03
Использование методов и свойств в форме лямбда-выражений 105
Использование автоматического выведения типа и анонимных типов l 07
Использование анонимных типов l 08
Использование асинхронных методов l 09
Работа с задачами напрямую 110
Применение ключевых слов a sync и awa i t 111
Получение имен 113
Резюме 114

Глава 5. Работа с Razor 115


Подготовка проекта для примера 116
Определение модели 11 7
Создание контроллера 11 7
Создание представления 118
Работа с объектом модели 119
Использование файла импортирования представлений 121
Работа с компоновками 123
Создание компоновки 123
8 Содержание

Применение компоновки 125


Использование файла запуска представления 126
Использование выражений Razor 128
Вставка значений данных 129
Установка значений атрибутов 130
Использование условных операторов 131
Проход по содержимому массивов и коллекций 133
Резюме 135

Глава 6. Работа с Visual Studio 136


Подготовка проекта для примера 137
Создание модели 137
Создание контроллера и представления 139
Управление программными пакетами 141
Инструмент NuGet 141
Инструмент Bower 143
Итеративная разработка 147
Внесение изменений в представления Razoг 147
Внесение изменений в классы С# 148
Использование средства Вгоwsег Link 156
Подготовка файлов JavaScript и CSS для развертывания 161
Включение доставки статического содержимого 161
Добавление в прое1tт статического содержимого 162
Обновление представления 164
Пакетирование и минификация в приложениях МVС 166
Резюме 169

Глава 7. Модульное тестирование приложений MVC 170


Подготовка проекта для примера 171
Включение встроенных дескрипторных вспомогательных классов 171
Добавление действий к контроллеру 172
Создание формы для ввода данных 172
Обновление представления Index 173
Модульное тестирование приложений МVС 174
Создание проекта модульного тестирования 175
Изолирование компонентов для модульного тестирования 182
Улучшение модульных тестов 190
Параметризация модульного теста 190
Улучшение фиктивных реализаций 194
Резюме 200

Глава 8. SportsStore: реальное приложение 201


Начало работы 202
Создание проекта МVС 202
Создание проекга модульного тестирования 207
Проверка и запуск приложения 209
Начало работы с моделью предметной области 209
Создание хранилища 210
Создание фиктивного хранилища 210
Регистрация службы хранилища 211
Содер жа ние 9
Отображение списка товаров 212
Добавление контроллера 213
Добавление и конфигурирование представления 215
Установка стандартного маршрута 216
Запуск приложения 217
Подготовка базы данных 218
Установка Entity Fгarnework Core 219
Создание классов базы данных 220
Создание юшсса хранилища 221
Определение строки подключения 222
Конфигурирование приложения 223
Создание и применение миграции базы данных 225
Добавление поддержки разбиения на страницы 226
Отображение ссьmок на страницы 228
Улучшение URL 236
Стилизация содержимого 237
Установка пакета Bootstrap 238
Применение стилей Bootstrap к компоновке 238
Создание частичного представления 241
Резюме 242

Глава 9. SportsStore: навигация 244


Добавление навигационных элементов управления 244
Фильтрация списка товаров 244
Улучшение схемы URL 248
Построение меню навигации по категориям 252
Корректировка счетчика страниц 259
Построение корзины для покупок 262
Определение модели корзины 262
Создание кнопок добавления в корзину 266
Включение поддержки сеансов 267
Реализация контроллера для корзины 269
Отображение содержимого корзины 272
Резюме 275

Глава 1О. SportsStore: завершение построения корзины для покупок 276


Усовершенствование модели корзины с помощью службы 276
Создание класса корзины, осведомленного о хранилище 276
Регистрация службы 277
Упрощение контроллера Cart 278
Завершение функциональности корзины 279
Удаление элементов из корзины 279
Добавление виджета с итоговой информацией по корзине 281
Отправка заказов 284
Создание класса модели 284
Добавление реализации процесса оплаты 285
Реализация обработки заказов 287
Завершение построения контроллера Order 291
Отображение сообщений об ошибках проверки достоверности 295
Отображение итоговой страницы 296
Резюме 297
1О Содержание

Глава 11. SportsStore: администрирование 298


Управление заказами 298
Расширение модели 298
Добавление действий и представления 299
Добавление средств управления каталогом 302
Создание контроллера CRUD 303
Реализация представления списка 304
Редактирование сведений о товарах 306
Создание новых товаров 320
Удаление товаров 321
Резюме 324
Глава 12. SportsStore: защита и развертывание 325
Защита средств администрирования 325
Добавление в проект пакета Jdentity 325
Создание базы данных Identity 326
Применение базовой политики авторизации 330
Создание контроллера Accoun t и представлений 332
Тестирование политики безопасности 336
Развертывание приложения 336
Создание баз данных 337
Подготовка приложения 338
Применение миграций баз данных 343
Процесс развертывания приложения 343
Резюме 346
Глава 13. Работа с Visual Studio Code 348
Настройка среды разработки 348
Установка Node.js 349
Проверка установки Node 350
Установка Git 350
Проверка установки Git 351
УстановкаYeoman, Bower и Gulp 351
Установка .NETCore 351
Проверка установки .NЕТ Core 352
Установr\а Visual Studio Code 353
Проверка установки Visual Studio Code 353
Установка расширения С# для Visual Studio Code 353
Создание проекта ASP. NET Core 355
Подготовка проекта с помощью Visual Studio Code 356
Добавление в проект пакетов NuGet 357
Добавление в проект пакетов клиентской стороны 358
Конфигурирование приложения 359
Построение и запуск проекта 359
Воссоздание приложения Partylnvites 360
Создание модели и хранилища 360
Создание базы данных 363
Создание контроллеров и представлений 365
Модульное тестирование в Visual Studio Code 369
Конфигурирование приложения 369
Создание модульного теста 371
Прогон тестов 371
Резюме 372
Содержание 11

Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC 373

Глава 14. Конфигурирование приложений 374


Подготовка проекта для примера 376
Конфигурационные файлы JSON 377
Конфигурирование решения 378
Конфигурирование проекта 380
Класс Prograrn 383
Класс Startup 385
Особенности использования класса Star tup 386
Службы ASP.NEТ 387
Промежуточное программное обеспечение ASP.NEТ 390
Особенности вызова метода Configu re () 400
Добавление оставшихся компонентов промежуточного программного обеспечения 408
Использование данных конфигурации 413
Конфигурирование служб МVС 419
Работа со сложными конфигурациями 421
Создание разных внешних конфигурационных файлов 421
Создание разных методов конфигурирования 422
Создание разных классов конфигурирования 424
Резюме 425

Глава 15. Маршрутизация URL 426


Подготовка проекта для примера 428
Создание класса модели 429
Создание контроллеров 430
Создание представления 431
Введение в шаблоны URL 432
Создание и регистрация простого маршрута 434
Определение стандартных значений 435
Определение встроенных стандартных значений 436
Использование статических сегментов URL 438
Определение специальных переменных сегментов 442
Использование специальных переменных в качестве параметров метода действия 445
Определение необязательных сегментов URL 446
Определение маршрутов переменной длины 448
Ограничение маршрутов 451
Ограничение маршрута с использованием регулярного выражения 454
Использование ограничений на основе типов и значений 455
Объединение ограничений 456
Определение специального ограничения 458
Использование маршрутизации с помощью атрибутов 460
Подготовка для маршрутизации с помощью атрибутов 460
Применение маршрутизации с помощью атрибутов 461
Применение ограничений к маршрутам 463
Резюме 464
12 Содержание

Глава 16. Дополнительные возможности маршрутизации 465


Подготовка проекта для примера 467
Генерация исходящих URL в представлениях 468
генерирование исходящих ссылок 468
ГенерацияURL (без ссылок) 478
генерирование URL в методах действий 478
Настройка системы маршрутизации 479
Изменение конфигурации системы маршрутизации 479
Создание специального класса маршрута 481
Применение специального класса маршрута 484
Маршрутизация на контроллеры МVС 485
Работа с областями 491
Создание области 492
Создание маршрута для области 492
Заполнение области 493
Генерирование ссылок на действия в областях 496
Полезные советы относительно схемы URL 497
Делайте URL чистыми и понятными человеку 497
GET и POST: выбор правильного запроса 499
Резюме 499
Глава 17. Контроллеры и действия 500
Подготовка проекта для примера 501
Подготовка представлений 503
Поняти е контроллеров 505
Создание контроллеров 506
Создание контроллеров РОСО 506
Использование базового класса Controller 508
Получение данных контекста 509
Получение данных из объектов контекста 510
Использование параметров метода действия 514
генерирование ответа 516
Генерирование ответа с использованием объекта контекста 516
Понятие результатов действий 517
Генерирование НТМL-ответа 520
Передач а данных из метода действия в представление 523
Выполнение перенаправления 528
Возвращение разных типов содержимого 535
Реагирование с помощью содержимого файлов 538
Возвращение ошибок и кодов Н1ТР 540
Другие классы результатов действий 542
Резюме 542
Глава 18. Внедрение зависимостей 543
Подготовка проекта для примера 544
Создание модели и хранилища 545
Создание контроллера и представления 547
Создание проекта модульного тестирования 548
Создание слабо связанных компонентов 549
Исследование сильно связанных компонентов 550
Содержание 13
Введение в средство внедрения зависимостей ASP. NET 556
Подготовка к внедрению зависимостей 556
I\онфигурирование поставщика служб 558
Модульное тестирование контроллера с зависимостью 560
Использование цепочек зависимостей 560
Использование внедрения зависимостей для конкретных типов 563
Жизненные ЦИIUIЫ служб 565
Использование переходного жизненного цикла 566
Использование жизненного цикла, ограниченного областью действия 570
Использование жизненного цикла одиночки 571
Использование внедрения в действия 572
Использование атрибутов внедрения в свойства 573
Запрашивание объекта реализации вручную 57 4
Резюме 575

Глава 19. Фильтры 576


Подготовка проекта для примера 577
Включение SSL 579
Создание контроллера и представления 579
Использование фильтров 581
Понятие фильтров 584
Получение данных контекста 585
Использование фильтров авторизации 585
Создание фильтра авторизации 586
Использование фильтров действий 588
Создание фильтра действий 590
Создание асинхронного фильтра действий 591
Использование фильтров результатов 592
Создание фильтра результатов 593
Создание асинхронного фильтра результатов 595
Создание гибридного фильтра действий/результатов 596
Использование фильтров исIUiючений 598
Создание фильтра исключений 599
Использование внедрения зависимостей для фильтров 600
Распознавание зависимостей в фильтрах 601
Управление жизненными циклами фильтров 605
Создание глобальных фильтров 608
Порядок применения фильтров и его изменение 61 О
Изменения порядка применения фильтров 612
Резюме 613

Глава 20. Контроллеры API 614


Подготовка проекта для примера 615
Создание модели и хранилища 615
Создание контролле ра и представлений 61 7
Конфигурирование приложения 619
Роль контроллеров REST 621
Проблема скорости 621
Проблема эффективности 622
Проблема открытости 622
14 Содержание

Введение в RESТ и контроллеры АР! 623


Создание контроллера АР! 623
Тестирование контроллера АР! 628
Использование контроллера АР! в браузере 631
Форматирование содержимого 634
Стандартная политика содержимого 635
Согласование содержимого 636
Указание формата данных для действия 639
Получение формата данных из маршрута или строки запроса 639
Включение полного согласования содержимого 641
Получение разных форматов данных 643
Резюме 643

Глава 21. Представления 644


Подготовка проекта для примера 645
Создание специального механизма визуализации 647
Создание специальной реализации интерфейса I View 649
Создание реализации интерфейса IViewEngine 650
Регистрация специального механизма визуализации 651
Тестирование механизма визуализации 652
Работа с механизмом Razor 654
Подготовка проекта для примера 654
Прояснение представлений Razor 656
Добавление динамического содержимого к представлению Razor 660
Использование разделов компоновки 661
Использование частичных представлений 666
Добавление содержимого JSON в представления 669
Конфигурирование механизма Razor 671
Расширители местоположений представлений 672
Резюме 677

Глава 22. Компоненты представлений 678


Подготовка проекта для примера 679
Создание моделей и хранилиш 680
Создание контроллера и представлений 682
Конфигурирование приложения 685
Понятие компонентов представлений 686
Создание компонента представления 686
Создание компонентов представлений РОСО 686
Наследование от базового класса ViewComponent 688
Понятие результатов компонентов представлений 690
Получение данных контекста 695
Создание асинхронных компонентов представлений 701
Создание гибридных компонентов контроллеров/представлений 703
Создание гибридных представлений 704
Прим енение гибридного класса 706
Резюме 707
Глава 23. Дескрипторные вспомогательные классы 708
Подготовка проекта для примера 709
Создание модели и хранилища 710
Содержание 15
Создание контроллера, компоновки и представлений 711
Конфигурирование приложения 713
Создание дескрипторного вспомогательного класса 715
Определение дескрипторного вспомогательного класса 715
Регистрация дескрипторных вспомогательных классов 719
Использование дескрипторного вспомогательного класса 719
Управление областью действия дескрипторного вспомогательного класса 721
Усовершенствованные возможности дескрипторных вспомогательных классов 726
Создание сокращающих элементов 726
Вставка содержимого перед и после элементов 728
Получение данных контекста представления и использование внедрения
зависимостей 732
Работа с моделью представления 734
Согласование дескрипторных вспомогательных классов 736
Подавление выходного элемента 738
Резюме 740

Глава 24. Использование дескрипторных вспомогательных классов для форм 7 41


Подготовка проекта для примера 7 43
Изменение регистрации дескрипторных вспомогательных классов 7 43
Переустановка представлений и компоновки 7 43
Работа с элементами f о rrn 7 45
Установка цели формы 7 46
Использование средства противодействия подделке 7 46
Работа с элементами inpu t 7 49
Конфигурирование элементов inpu t 7 49
Форматирование значений данных 751
Работа с элементами label 754
Работа с элементами select и option 756
Использование источника данных для заполнения элемента se lect 758
генерирование элементов option из перечисления 758
Работа с элементами textarea 763
Дескрипторные вспомогательные классы для проверки достоверности форм 764
Резюме 765

Глава 25. Использование других встроенных дескрипторных


вспомогательных классов 766
Подготовка проекта для примера 767
Использование вспомогательного класса для среды размещения 768
Использование вспомогательных классов для JavaScript и CSS 769
Управление файлами JavaScript 769
Управление таблицами стилей CSS 778
Работа с якорными элементами 781
Работа с элементами i rng 782
Использование кеша данных 783
Установка времени истечения для кеша 786
Использование вариаций кеша 788
Использование URL, относительных к приложению 789
Резюме 792
16 Содержание

Глава 26. Привязка моделей 793


Подготов:ка прое:кта для примера 794
Создание модели и хранилища 795
Создание контроллера и представления 796
Конфигурирование приложения 798
Понятие привязки моделей 799
Стандартные значения привязки 80 l
Привязка простых типов 802
Привязка сложных типов 803
Привязка массивов и коллекций 813
Привязка коллекций сложных типов 817
Указание источника данных привяз:ки моделей 820
Выбор стандартного источника данных привяз:ки 820
Использование заголовков в качестве источников данных привязки 82 1
Использование тел запросов в качестве источни:ков данных привязки 824
Резюме 827
Глава 27. Проверка достоверности моделей 828
Подготовка проекта для примера 830
Создание модели 831
Создание контроллера 831
Создание компоновки и представлений 832
Необходимость в проверке достоверности модели 834
Явная проверка достоверности модели 835
Отображение пользователю ошибок проверки достоверности 838
Отображение сообщений об ошибках проверки достоверности 839
Отображение сообщений об ошибках проверки достоверности на уровне свойств 844
Отображение сообщений об ошибках проверки достоверности на уровне модели 845
Указание правил проверки достоверности с помощью метаданных 849
Создание специального атрибута проверки достоверности для свойства 852
Выполнение проверки достоверности на стороне :клиента 854
Выполнение удаленной проверки достоверности 857
Резюме 860

Глава 28. Введение в ASP.NET Core ldentity 861


Подготовка проекта для примера 863
Создание контроллера и представления 864
Настрой:ка ASP. NЕТ Core Identity 866
Добавление пакета Identity в приложение 866
Создание :класса пользователя 867
Создание класса контекста базы данных 869
Конфигурирование настройки строки под:ключения к базе данных 869
Конфигурирование служб и промежуточного программного обеспечения Identity 870
Identity
Создание базы данных 872
Использование ASP.NEТ Core Identity 872
Перечисление пользовательских учетных записей 873
Создание пользователей 875
Проверка паролей 879
Проверка деталей, связанных с пользователем 886
Содержание 17
Завершение средств администрирования 890
Реализация средства удаления 891
Реализация возможности редантирования 892
Резюме 897

Глава 29. Применение ASP.NET Core ldentity 898


Подготовка проекта для примера 898
Аутентификация пользователей 899
Подготовка к реализации аутентификации 901
Добавление аутентификации пользователей 904
Тестирование аутентификации 907
Авторизация пользователей с помощью ролей 908
Создание и удаление ролей 909
Управление членством в ролях 914
Использование ролей для авторизации 919
Помещение в базу данных начальных данных 923
Резюме 925

Глава 30. Расширенные средства ASP.NET Core ldentity 926


Подготовка проекта для примера 927
Добавление специальных свойств в класс пользователя 927
Подготовка миграции базы данных 931
Тестирование специальных свойств 931
Работа с заявками и политиками 932
Понятие заявок 933
Создание заявок 937
Использование политю< 940
Использование политик для авторизации доступа к ресурсам 946
Использование сторонней аутентификации 951
Регистрация приложения в Google 951
Включение аутентификации Google 952
Резюме 957

Глава 31. Соглашения по модели и ограничения действий 958


Подготовка проекта для примера 959
Создание модели представления, контроллера и представления 960
Использование модели приложения и соглашений по модели 962
Модель приложения 963
Роль соглашений по модели 967
Создание соглашения по модели 968
Порядок вьmолнения соглашений по модели 973
Создание глобальных соглашений по модели 974
Использование ограничений действий 976
Подготовка проекта для примера 977
Ограничения действий 978
Создание ограничения действия 979
Распознавание зависимостей в ограничениях действий 984
Резюме 986
Предметный указатель 987
Посвящается моей прекрасной жене Джеки Гриффиmс.

Об авторе
Адам Фримев - опытный специалист в области информационных технологий,
занимавший в едущие позиции во многих компаниях, последней из которых был гло­
бальный банк, где он работал на должностях директора по внедрению технологий и
руководителя административной службы. После ухода из банка Адам уделяет все свое
время писательской деятельности и бегу на длинные дистанции .

О техническом рецензенте
Фабио Клау.цио Ферраччати - ведущий консультант и главный аналитик/раз­
работчик, исполь зующий технологии Microsoft. Он работает в компании Brain Force
(www . Ыua r anc i o . com). Фабио является сертифицированным Microsoft разработчи­
ком реш е ний для .NET (Microsoft Certified Solution Developer for .NET), сертифициро­
ванным Microsoft разработчиком приложений для .NET (Microsoft Certified Application
Developer for .NET), сертифицированным Microsoft профессионалом (Microsoft Certifled
Professional), а также плодовитым автором и техническим рецензентом. За посл едние
десять лет он написал множество статей для итальянских и м еждународных журна­
лов и выступал в качестве соавтора в более чем 10 книгах по разнообразным темам ,
связанным с 1щмпьютерами.

Ждем ваших отзы вов!


Вы, читатель этой книги , и есть главный ее критик. Мы ценим ваше мнение и хо­
тим знать, что было сдел ано нами правильно, что можно было сделать лучше и что
еще вы хотели бы увидеть изданным нами. Нам интересны любые ваши замечания в
наш адрес.

Мы ждем ваших комментариев и надеемся на них . Вы можете прислать нам бу­


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

книги более интересными для вас.


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

E-mail: info@dialektika.com
WWW: http : //www . dialektika . com

Наши почтовые адр е са:

в России: 195027, Санкт-Петербург, Магнитогорская ул., д. 30 , ящик 116


в Украине: 03150, Киев, а/я 152
ЧАСТЬ 1

Введение
в инфраструктуру
ASP.NET Core МVС

д ля разработчиковASP.NET
веб-приложений , использующих платформу Microsoft,
инфраструктура Core MVC представляется как радикальное изме­
нен и е . Особое значение придается чистой архитектуре, паттернам проектирова­
ния и удобству тестирования , к тому же не предпринимается попытка скрывать,
каким образом работает веб-среда.

Первая часть этой книги поможет понять в общих чертах основополагающие


идеи разработки приложений MVC, включая новые возможности ASP. NЕТ Core
MVC, и увидеть на деле, как применяется инфраструктура .
ГЛАВА 1
Основы
ASP.NET Core МVС

A SP.NEТ Core MVC - это инфраструктура для разработки веб-приложений про­


изводства Micгosoft , которая сочетает в себе эффективность и аккуратность ар­
хитектуры "модель-представление-контроллер" (model-view-controller - MVC), идеи
и приемы гибкой разработки. а также лучшие части платформы .NET. В настоящей
главе вы узнаете, почему компания Micгosoft создала инфраструктуру ASP.NET Соге
МVС, увидите, как она соотносится со своими предшественниками и альтернативами,
а также ознакомитесь с обзором нововведений ASP.NET Core MVC и с тем, что будет
рассматриваться в книге.

История развития ASP.NET Core MVC


Первая версия платформы ASP.NET появилась в 2002 году, в то время, когда компа­
ния Micгosoft стремилась сохранить господствующее положение в области разработки
традиционных настольных приложений и видела в Интернете угрозу. На рис. 1.1 по ­
казано , как выглядел на то время стек технологий Micгosoft.

ASP.NETWeb Forms
Набор компонентов пользовательского интерфейса (страниц, кнопок и т.д.)
плюс объектно-ориентированная программная модель
графического пользовательского
интерфейса с сохранением состояния

ASP.NET
Способ размещения приложений .NET в llS (веб-сервер Microsoft),
позволяющий взаимодействовать с запросами и ответами НТТР

.NET
Мно гоязычная платформа управляемого кода
(совершенно новая на то время и по праву ставшая поворотным пунктом в развитии)

Рис . 1. 1. Стек тех нолог ий ASP.NET Web Forms


Глава 1. Основы ASP.NET Core MVC 21

ASP.NET Web Forms


В Web Forms разработчики из Microsoft пытались скрыть как протокол НТГР (с при­
сущим ему отсутствием состояния), так и язык HTML (которым на тот момент не вла­
дели многие разработчики) за счет моделирования поль зовательского интерфейса
как иерархии объектов , представляющих серверные элементы управления. Каждый
элемент управления отслеживал собственное состояние между запросами, по мере
необходимости визуализируя себя в виде НТМL-разметки и автоматически соединяя
события клиентской стороны (например, щелчки на кнопках) с соответствующим ко­
дом их обработки на стороне сервера . Фактически Web Forms - это гигантский уровень
абстракции, предн азначенный для доставки классического управляемого событиями
графического пользовательского интерфейса через веб-среду.
Идея заключалась в том, чтобы разработка веб-приложений выглядела почти так
же. как разработка настольных приложений. Разработчики могли оперировать по­
нятиями пользовательского интерфейса, запоминающего состояние, и не иметь дело с
последовательностями независимых запросов и ответов НТГР. У компании Microsoft
появилась возможность переместить армию разработчиков настольных Windоws­
приложений в новый мир веб-приложений.

Что было не так с ASP.NET Web Forms?


Разработка с использ ованием традиционной технологии ASP.NET Web Forms в при­
нципе была хорошей, но реальность оказалась более сложной.

• Тяжеловесност ь состояния представления (View State). Действительный меха­


низм поддержки состояния между запросами, известный как View State, вызвал
необходимость передачи крупных блоков данных между клиентом и сервером.
Объем таких данных мог достигать сотен килобайт даже в скромных веб-прило­
жениях. Эти данные путешествуют туда и обратно с каждым запросом, приводя
к замедлению реакции и увеличивая требование к ширине полосы пропускания
сервера.

• ){{изненный 4икл страницы . Механизм соединения событий клиентской сто­


роны с ~юдом обработчиков событий на стороне сервера, являющийся частью
жизненного цикла страницы, мог быть сложным и хрупким . Лишь немногим
разработчикам удавалось успешно манипулировать иерархией элементов уп­
равления во время выполнения, не получая ошибки состояния представления
или не сталкиваясь с ситуацией, когда некоторые обработчики событий зага­
дочным образом отказывались запускаться.

• Ложное ощущение разделения обязанностей. Модель отделенного кода ASP.NET


Web Forms предоставляла способ вынесения кода приложения из НТМL-разметки
в специальный файл отделенного кода. Это было сделано для разделения логи­
ки и представления, но в действительности попустительствовало смешиванию
кода представления (например, манипуляций деревом элементов управления
серверной стороны) с прикладной логикой (скажем , обработкой информации из
базы данных) в гигантских классах отделенного кода. Конечный результат мог
быть хрупким и непонятным.

• Ограниченный контроль над НТМL-разметкой. Серверные элементы управле­


ния визуализировали себя в виде НТМL-разметки, но она не обязательно была
такой. как хотелось. В ранних версиях Web Forms выходная НТМL-разметка не
22 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

соответствовала веб-стандартам или неэффективно применяла каскадные таб­


лицы стилей (Cascading Style Sheets - CSS), а серверные элементы управления
генерировали непредсказуемые и сложные атрибуты идентификаторов, к кото ­
рым было трудно получать доступ с помощью JavaScript. Эти проблемы были
значительно смягчены в последних выпусках Web Forms, но получение ожидае­
мой НТМL- разметки по-прежнему могло быть затруднительным .

• Негерметич.ная абстракция. Инфраструктура Web Forms пыталась скрывать де ­


тали, связанные с HTML и НТТР, где только возможно . При попытке реализовать
специальное поведение абстракция часто отбрасывалась, поэтому для генериро­
вания желаемой НТМL- разметки приходилось воссоздавать механизм обратной
отправки событий или предпринимать другие сложные действия.

• Низкое удобство тестирования. Проектировщики Web Forms даже не пред ­


полагали, что автоматизированное тестирование станет неотъемлемой час­
тью процесса разработки программного обеспечения . Построенная ими тесно
связанная архитектура не была приспособлена для модульного тестирования.
Интеграционное тестирование также могло превратиться в сложную задачу.

С технологией Web Forms не все было плохо, и в Microsoft приложили немало уси­
лий по улучшению степени соответствия стандартам, упрощению процесса разра­
ботки и даже заимствованию ряда возможностей из первоначальной инфраструкту­
ры ASP.NET MVC Framework для их применения к Web Forms. Технология Web Forms
превосходна, когда необходимы быстрые результаты, и с ее помощью вполне реально
построить и запустить веб-приложение умеренной сложности всего за один день. Но
если не проявлять осторожность во время разработки , то обнаружится, что созданное
приложение трудно тестировать и сопровождать.

Первоначальная инфраструктура MVC Framework


В октябре 2007 года компания Microsoft объявила о выходе новой платформы раз­
работки, построенной на основе существующей платформы ASP.NET, которая была
задумана как прямая реакция на критику Web Forms и популярность конкурирующих
платформ наподобие Ruby on Rails. Новая платформа получила название ASP.NET
МVС Framework и отражала формирующиеся тенденции в разработке веб - приложе­
ний, тшше как стандартизация HTML и CSS, веб - службы REST, эффективное модуль­
ное тестирование, а также идея о том, что разработчики должны принять факт от ­
сутствия состояния у НТТР.
Концепции, которые подкрепляли первоначальную инфраструктуру МVС Framework,
теперь кажутся естественными и очевидными, но в 2007 году они отсутствовали в
мире разработки веб-приложений .NET. Появление ASP.NET МVС Framework возврати­
ло платформу разрабопш веб-приложений Microsoft в современную эпоху.
Инфраструктура МVС Framework также сигнализировала о важном изменении в
отношении компании Microsoft, которая ранее пыталась контролировать каждый ком­
понент в инструментальных средствах, необходимых для веб-приложений . Компания
Microsoft встроила в инфраструктуру MVC Framework инструменты с открытым ко­
дом, такие кaкjQuery, учла проектные соглашения и передовой опыт конкурирующих
(и более успешных) платформ, а также выпустила в свет исходный код MVC Framework
для изучения разработчиками.
Глава 1. Основы ASP.NET Core MVC 23
Что было не так с первоначальной инфраструктурой MVC Framework?
На то время для Microsoft имело смысл создавать инфраструктуру МVС Framework
повер х существующей платформы, содержащей большой объем монолитной низ­
коуровневой функциональности, которая обеспечила преимущество в начале про­
цесса разработки и которую хорошо знали и понимали разработчики приложений
ASP.NET.
Компромиссы потребовали основать MVC Framework на платформе, которая из­
начально предназначалась для Web Forms. Разработчики MVC Framework привыкли
использовать конфигурационные параметры и кодовые настройки, отключающие или
реконфигурирующие средства, которые не имели ни малейшего отношения к их веб­
приложениям , но требовались для того, чтобы все заработало.
С ростом популярности MVC Framework компания Microsoft приступила к добав ­
лению ряда ее основных возможностей к Web Forms. Результат был все более и более
странным, когда индивидуальные особенности проекта, требуемые для поддержки
MVC Framework, расширялись с целью поддержки Web Forms, и предпринимались до­
полнительные проектные ухищрения, чтобы подогнать все друг к другу. Одновременно
в Microsoft начали расширять ASP.NET новыми инфраструктурами для создания веб­
служб (Web АР!) и организации коммуникаций в реальном времени (SignalR) . Новые
инфраструктуры добавили собственные соглашения по конфигурированию и разра­
ботке, каждое из которых обладало своими преимуществами и причудами, породив в
результате фрагментированную смесь.

Обзор ASP.NET Core


В 20 15 году Microsoft заявила о новом направлении для ASP.NEТ и MVC Framework,
которое в итоге привело к появлению инфраструктуры ASP.NET Core МVС, рассматри­
ваемой в настоящей книге.
Платформа ASP.NET Core построена на основе
.NET Core, которая представляет со ­
бой м ежплатформенную версию .NET Framework без интерфейсов программирования
приложений (АР!), специфичных для Windows. Господствующей операционной сис­
темой по-прежнему является Windows, но веб - приложения все чаще размещаются в
небольших и простых контейнерах на облачных платформах . За счет принятия меж­
платформенного подхода компания Microsoft расширила область охвата .NЕТ, сделав
возможным развертывание приложений ASP.NET Core на более широком наборе сред
размещения, а в качестве бонуса предоставила разработчикам возможность созда­
вать веб-приложения ASP.NET Core на машинах Linux и OS X/ macOS .
ASP.NET Core - это совер шенно новая инфраструктура. Она проще, с нею легче
р а ботать, и она свободна от наследия, сопровождающего Web Forms. Будучи основан­
ной на .NET Core, она поддерживает разработку веб-приложений для ряда платформ
и контейнеров.
Инфраструктура ASP.NET Core МVС предоставляет функциональность первона­
чальной инфраструктуры ASP.NET MVC Framework, построенной поверх новой плат ­
формы ASP.NET Core. Она включает функциональность, которая ранее предлагалась
Web АР!, поддерживает более естественный способ генерирования сложного содержи­
мого и делает основные задачи разработки, такие как модульное тестирование, более
простыми и предсказуемыми .
24 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

Основные преимущества ASP.NET Core MVC


В последующих разделах кратко описано, чем эта новая платформа MVC превос­
ходит унаследованную инфраструктуру Web Forms и первоначальную инфраструктуру
MVC Framework, и что именно позволит вновь вывести ASP.NET на передний край.

Архитектура MVC
Инфраструктура ASP.NET Core MVC следует паттерну под названием "модель­
предсmавление-конmроллер" (model-view-controller - MVC), который управляет фор­
мой веб-приложения ASP.NET и взаимодействиями между содержащимися в нем
компонентами.

Важно различать архитектурный паттерн МVС и реализацию ASP.NEТ Core МVС.


Паттерн МVС далеко не нов (его появление датируется 1978 годом и связано с про­
ектом Smalltalk в Xerox PARC), но в наши дни он завоевал популярность в качестве
паттерна для веб-приложений по перечисленным ниже причинам.

• Взаимодействие пользователя с приложением, которое придерживается паттер­


ном MVC, следует естественному циклу: пользователь предпринимает действие,
а в ответ приложение изменяет свою модель данных и доставляет обновленное
представление пользователю. Затем цикл повторяется. Это удобно укладывает­
ся в схему веб-приложений, предоставляемых в виде последовательностей за­
просов и ответов НТТР.

• Веб-приложения, нуждающиеся в сочетании нескольких технологий (например,


баз данных, НТМL-разметки и исполняемого кода), обычно разделяются на на­
бор слоев или уровней. Получ енные в результате таких сочетаний комбинации
естественным образом отображаются на концепции в паттерне MVC.
Инфраструктура ASP.NET Core МVС реализует паттерн МVС и при этом обеспечи­
вает гораздо лучшее разделение обязанностей по сравнению с Web Forms. На самом
деле в ASP.NET Core MVC внедрена разновидность паттерна MVC, которая особенно
хорошо подходит для веб-приложений. Дополнительные сведения по теории и прак­
тике применения этой архитектуры приведены в главе 3.

Расширяемость
Инфр аструктуры ASP.NET Core и ASP.NET Core МVС построены в виде последова­
тельности независимых компонентов, которые имеют четко определенные характе­

ристики, удовлетворяют интерфейсу .NET или соэданы на основе абстрактного базо­


вого класса. Основные компоненты можно легко заменять другими компонентами с
собственной реализацией. В общем случае для каждого компонента инфраструктура
ASP.NET Core MVC предлагает три воэможности.

• Использование стандартной реализации компонента в том виде, как есть (чего


должно быть достаточно для большинства приложений).

• Создание подкласса стандартной реализации с целью корре1пировки существу­


ющего поведения.

• Полная замена компонента новой реализацией интерфейса или абстрактного


базового класса.

Разнообразные компоненты, а также способы и причины их возможной настройки


или замены будут рассматриваться, начиная с главы 14.
Глава 1. Основы ASP.NET Саге MVC 25

Жесткий контроль над HTML и НТТР


Инфраструктура ASP.NET Core МVС генерирует ясную и соответствующую стан­
дартам разметку. Ее встроенные дескрипторные вспомогательные классы (tag helper)
производят соответствующий стандартам вывод, но существует также гораздо более
значимое философское изменение по сравнению с Web Forms. Вместо генерации гро­
мадного объема НТМL-разметки, над которой вы имеете очень небольшой контроль,
инфраструктура ASP.NET Core MVC поощряет создание простой и элегантной размет­
ки. стилизованной с помощью CSS.
Конечно, если вы действительно хотите добавить готовые виджеты для таких
сложных элементов пользовательского интерфейса, как окна выбора даты или кас­
кадные меню , то подход "никаких специальных требований", принятый в ASP.NET
Core МVС, позволяет легко использовать наилучшие клиентские библиотеки. подоб­
ные jQuery, Aпgular или Bootstrap CSS. Инфраструктура ASP.NET Core MVC настолько
тесно сплетена с этими библиотеками, что компания Microsoft включила их подде­
ржку как встроенных частей стандартного шаблона проектов для веб-приложений.
Инфраструктура ASP.NET Core МVС работает в гармонии с НТТР. Вы имеете кон­
троль над запросами, передаваемыми между браузером и сервером, так что можете
точно настраивать пользовательский интерфейс по своему усмотрению. Технология
Ajax является легкой в применении, и создание веб-служб для получения браузерных
НТТР-запросов представляют собой простой процесс, описанный в главе 20.

Тестируемость
Естественное разнесение различных обязанностей приложения по независимым
друг от друга частям, которое поддерживается архитектурой ASP.NET Core MVC, поз­
воляет с самого начала делать приложение легко сопровождаемым и удобным для
тестирования. Вдобавок каждый фрагмент платформы ASP.NET Core и инфраструк­
туры ASP.NET Core МVС может быть изолирован и заменен в целях модульного тес­
тирования , которое допускается выполнять с использованием любой популярной ин­
фраструктуры тестирования с оп,рытым кодом, такой кан xUnit, рассматриваемой в
главе 7.
В этой книге вы увидите примеры написания ясных и простых модульных тестов
для контроллеров и действий MVC, которые предоставляют фиктивные либо имити­
рованные реализации компонентов инфраструктуры для эмуляции любого сценария
с применением разнообразных стратегий тестирования и имитации. Даже если вам
никогда ранее не приходилось создавать модульные тесты, у вас будет все необходи­
мое для успешного старта.

Тестируемость касается не только модульного тестирования . Приложения ASP.NET


Core МVС успешно работают также с инструментами тестирования, встроенными в
средства автоматизации пользовательского интерфейса. Можно создавать тестовые
сценарии, которые имитируют взаимодействие с пользователем, не выдвигая догадки
о том , какие структуры НТМL-элементов, классы CSS или идентификаторы сгенери­
рует инфраструктура, и не беспокоясь о неожиданных изменениях структуры.

Мощная система маршрутизации


По мере совершенствования технологии построения веб-приложений эволюциони­
ровал стиль унифицированных указателей ресурсов (uniform resource locator - URL).
26 Часть 1. Введение в инфраструктуру AS P.NET Core MVC

Адреса URL, подобные приведенному ниже:

/App_v2/U ser/Page . as p x?ac t io n=show%2 0p rop&prop id= 8 2742


встречаются все реже, а на смену им приходит более простой и понятный формат
следующего вида:

/ t o -rent/chica g o/2303 - sil v er -street


Существует ряд веских причин для того, чтобы заботиться о структуре URL-
aдpecoв . Во-первых, поисковые механизмы придают вес ключевым словам, содержа­
щимся в URL. Поиск по словосочетанию "rent in Chicago" (аренда в Чикаго) с большей
вероятностью обнаружит более простой URL. Во-вторых, многие веб-пользователи
достаточно сообразительны, чтобы понять URL, и ценят возможность навигации
путем ввода запроса в адресной строке своего браузера. В-третьих, когда структура
URL-aдpeca понятна. люди с большей вероятностью пройдут по нему, поделятся им
с другими или даже продиктуют его по телефону . В-четвертых, при таком подходе в
Интернете не раскрываются технические детали, структура каталогов и имен файлов
приложения; следовательно, вы вольны изменять лежащую в основе сайта реализа­
цию, не нарушая работоспособности всех входящих ссьшок .
В более ранних инфраструктурах понятные URL- aдpeca реализовать было трудно,
но в ASP.NET Core MVC используется средство, известное как маршрутизация URL,
которое обеспечивает предоставление понятных URL- aдpecoв по умолчанию. Это дает
контроль над схемой URL и ее взаимосвязью с приложением, обеспечивая свободу
создания понятного и удобного для пользователей шаблона URL без необходимости
следования какому-то заранее определенному шаблону. И, разумеется. это означает
тан:же простоту определения современной схемы URL в стиле REST, если она нужна.
Подробное описание маршрутизации URL приведено в главах 15 и 16.

Современный АРl-интерфейс
Платформа Microsoft .NET развивалась с каждым крупным выпуском, поддержи­
вая - и даже определяя - многие передовые аспекты современного программирова­
ния . Инфраструктура ASP.NEТ Core MVC построена для платформы .NET Core, поэто­
му ее АРI-интерфейс может в полной мере задействовать последние новшества языка
и исполняющей среды. знакомые программистам на С# , в том числе ключевое слово
awa i t, расширяющие методы, лямбда-выражения, анонимные и динамические типы,
а также язык интегрированных запросов (Language Integrated Query - LINQ) .
Многие методы и паттерны кодирования АРI-интерфейса ASP.NET Core MVC сле­
дуют более четкой и выразительной композиции, чем это было возможно в ранних
версиях инфраструктуры . Не переживайте, если вы пока не в курсе последних функ­
циональных возможностей языка С#: в главе 4 предоставлена сводка по самым важ­
ным средствам С# для разработки приложений МVС .

Межплатформенная природа
Предшествующие версии ASP.NET были специфичными для Windows, требуя на­
стольный компьютер с Windows для написания веб-приложений и сервер Windows
для их развертывания и выполнения . Компания Microsoft сделала инфраструктуру
ASP.NEТ Core межплатформенной, как в отношении разработки, так и в плане раз­
вертывания . Продукт .NET Core доступен для различных платформ , включая Linux и
OS X/ macOS. и вероятно будет переноситься на другие платформы.
Глава 1. Основы ASP.NET Саге MVC 27
Большая часть разработки приложений ASP.NET Core MVC в ближайшем будущем,
скорее всего, будет выполняться с применением Visual Studio, но компания Microsoft
также создала межплатформенный инструмент разработки под названием Visual
Studio Code, появление которого означает, что разработка ASP.NEТ Core MVC больше
не ограничивается Windows.

Инфраструктура ASP.NET Core MVC имеет открытый код


В отличие от предшествующих платформ для разработки веб-приложений от
Microsoft вы можете загрузить исходный код ASP.NEТ Core и ASP.NET Core МVС и даже
модифицировать и компилировать его с целью получения собственных версий инфра­
структур. Это бесценно при отладке кода, обращающегося 1< системному компоненту,
когда требуется пошагово вьmолнить его код (и даже почитать исходные комментарии
программистов). Это также полезно, если вы создаете усовершенствованный компо­
нент и хотите посмотреть, какие существуют возможности разработки, или узнать,
как действительно работают встроенные компоненты.
Исходный код ASP.NET Core и ASP.NET Core МVС доступен для загрузки по адресу:

https://github .com/aspnet

Что необходимо знать?


Чтобы извлечь максимум из этой книги, вы должны быть знакомы с основами раз­
работки веб-приложений, понимать HTML и CSS, а также иметь практический опыт
работы с языком С#. Не беспокойтесь, если детали разработки клиентской стороны,
такие как JavaScript. для вас несколько туманны. Основной акцент в книге делается
на разработке серверной стороны. и благодаря примерам вы сможете подобрать то,
что вам нужно. В главе 4 приводится сводка по наиболее полезным средствам языка
С# для разработки MVC, которую вы сочтете удобной, если переходите на последние
версии .NET с более раннего выпуска.

Какова структура книги?


Эта 1\нига разделена на две части, в каждой из которых раскрывается набор свя­
занных тем.

Часть 1. Введение в инфраструктуру ASP.NET Core MVC


Книга начинается с помещения ASP.NET Core MVC в контекст разработки. Здесь
объяснены преимущества и практическое влияние паттерна МVС, рассмотрен способ,
которым инфраструктура ASP.NET Core MVC вписывается в современную разработку
веб-приложений, а также описаны инструменты и средства языка С#, необходимые
каждому программисту ASP.NEТ Core МVС.
В главе 2 мы углубимся в детали и создадим простое веб-приложение, чтобы по­
лучить представление о том, каковы основные компоненты и строительные блоки, и
каким образом они сочетаются друг с другом. Однако большинство материала этой
части посвящено разработке проекта под названием SportsStore, посредством которо­
го демонстрируется реалистичный процесс разработки, начиная с постановки задачи
и заканчивая развертыванием, с привлечением основных функциональных возмож­
ностей ASP. NEТ Core MVC.
28 Часть/ . Введение в инфраструктуру ASP.NET Саге MVC

Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC


Во второй части объясняется внутренняя работа средств ASP.NET Core MVC, ко­
торые использовались при построении приложения SportsStore. Будет показано, как
работает каждое средство, объяснена его роль и описаны доступные варианты конфи­
гурирования и настройки. Широкий контекст, представленный в первой части, под­
робно раскрывается во второй части.

Что нового в этом издании?


Настоящее издание пересмотрено и расширено с целью описания инфраструкту­
ры ASP.NET Core МVС, которая отражает полную смену способа поддержки разработ­
ки веб-приложений компанией Microsoft. Ранние версии МVС Framework были пост­
роены на основе платформы ASP.NET, которая первоначально создавалась для Web
Forms. Это обеспечило преимушество предоставления зрелого фундамента для раз­
работки MVC, но такими путями, которые допустили просачивание деталей работы
Web Forms. Одни средства открывали доступ к внутренним особенностям Web Forms.
не имеющим отношения к приложениям MVC, а другие могли порождать непредска­
зуемые результаты.

Кроме того, фундамент ASP.NET предоставлялся с применением сборок, которые


были включены в .NET Framework, что означало возможность внесения крупных из­
менений только при выпуске новой версии .NЕТ. Это стало проблемой, поскольку темп
изменения разработки веб-приложений превосходил частоту изменения .NET.
Инфраструктура ASP.NET Core MVC полностью переписана с сохранением филосо­
фии и общего проектного решения, заложенного в ранних версиях, но с обновлени­
ем АРI-интерфейса для улучшения дизайна и производительности веб-приложений.
Инфраструктура ASP.NET Core МVС зависит от платформы ASP.NET Core, которая сама
является полной переделкой базового стека веб-технологий: главенствующая роль Web
Forms исчезла, а сильная связь с выпусками .NET Framework была разорвана.
Вы можете счесть степень изменений тревожной, если имеете опыт работы с МVС 5,
но не паникуйте . Лежащие в основе 1юнцепции остались теми же самыми, а многие
изменения только выглядят более значительными и сложными, чем есть на самом
деле. Во второй части книги приведена сводка по изменениям для каждого крупного
средства, которая облегчит переход с МVС 5 на ASP.NET Core MVC.

Где можно получить код примеров?


Все примеры, рассмотренные в книге, доступны для загрузки на веб-сайте из­
дательства. Загружаемый файл включает все проекты вместе с их содержимым .
Загружать код необязательно, но это самый простой путь для экспериментирования
с примерами, а также использования фрагментов кода в собственных проектах.

Резюме
В этой главе был объяснен контекст, в котором существует инфраструктура ASP.NET
Core MVC, и описано ее развитие от Web Forms и первоначальной инфраструктуры
ASP.NET MVC Framework. Кроме того, рассматривались преимущества применения
ASP.NEТ Core МVС и структура этой книги. В следующей главе вы увидите ASP.NET
Core MVC в действии, благодаря простой демонстрации средств, которые проявляют
все ее пр еимущества.
ГЛАВА 2
Ваше первое
приложение МVС

л учший способ оценки инфрастру1<туры, предназначенной для разработки про­


граммного обеспечения, заключается в том, чтобы приступить непосредственно
к ее использованию. В этой главе мы создадим простое приложение ввода данных с
применением ASP.NET Core MVC. Мы будем решать эту задачу пошагово, чтобы вы
поняли, каким образом строится приложение MVC. Для простоты мы пока опустим
некоторые технические подробности . Но не беспокойтесь - если вы только начина­
ете знакомство с MVC, то узнаете много интересного. Когда что-либо используется
без пояснения, то приводится ссылка на главу, в которой находятся все необходимые
детали .

Установка Visual Studio


Настоящая книга опирается на среду Visual Studio 2015, ноторая предлагает все,
что понадобится для разработки ASP.NET Core МVС . Мы будем применять бесплат­
ную редакцию Visual Studio 2015 Community, доступную для загрузки на веб-сайте
www . v is ua l st u dio . сот . При установке Visual Studio вы должны удостовериться в
том , что отмечен флажок Microsoft Web Developer Tools (Инструменты разработчика
веб-приложений от Microsoft).

Совет. Среда Visual Studio поддерживает только Windows . Создавать приложения ASP.NET
Core MVC можн о и на других платформах, используя продукт Visual Studio Code, но он
не предоставляет все инструменты, требуемые для выполнения примеров в этой книге.
За подробностями обращайтесь в главу 13.

При наличии существующей установленной копии Visual Studio вы должны при­


менить обновл ение Visual Studio Update 3, которое предоставляет поддержку для ра­
боты с приложениями ASP.NET Core. В новых установках Visual Studio это обновление
применяется автоматически. Оно доступно для загрузки по адресу;

h t t p: //go . mic r os o ft . com/fw l ink/? Lin ki d= 69 1 129


Далее потребуется загрузить и установить платформу .NET Core, устано­
вочный файл которой находится по адресу h ttps : //go . mic r osoft .c o m/
fwli n k /? Link i d = Bl 72 45 . Загружаемый установочный файл .NET Core обязателен
даже для новых установок Visual Studio.
30 Часть 1. Введение в инфраструктуру ASP.NET Соге MVC

Финальный шаг предусматривает установку инструмента под названием gi t , до­


ступного для загрузки по адресу h ttp s : / / gi t -scm. c om/ d own l o a d . Продукт Visual
Studio включает собственную версию g i t , но она не работает должным образом и
приводит к получению непредсказуемых результатов, когда используется другими

инструментами. в том числе Bower, который будет описан в главе 6. Во время уста­
новки gi t удостоверьтесь, что указываете программе установки на необходимость до­
бавления пути к инструменту в перем енную среды РА ТН (рис. 2. 1). Это позволит ср еде
Visual Studio найти новую версию gi t .

~~~-------------~-----------~
...., Git 2.9.0 Setup

Adjustin9 your РАТН t!nvironment


Но•и \\'OIJd you likr to ~ Glt IТom the cOl'f'VМnd line?

------- -------------------
О UR Glt from Git Sash onty
1hlsls lhe sofost choke о. VO<X РАПi ~il notЬe moditied ot <>I. You"111 onlyЬe
.ы. tD.,.. lhe Git СО111М11d line tools rтom Git В."1.

@ Use Git from the Windows Con1m• nd Prompt


Т№ optfon is ~s.sfe asltonlyadds some minirмl Gitwr~ to YQU"
РАТН to avold ciJttering YotX ~t ·дi th optiornlf t.ni.X tools. You wil Ье
.Ы. tD use Git fтom Ьoth Git в..t1 ш! lhe 1'Г111dом Coomand Prompt.

О Use Git ;)hd optional UnЬc tools from the Windows Command Pron1pt
Вoth Git .00 lhe optlonal Ur<x tools ~il Ье odded to VO<X РАПi .
\Yaming: Th1s w\11 oVl!rrlde Wim!ows tooJs /Jke "fmd" and "юrt•. Only
use tl>ls optIOD if VoU Wld<!t'Stand the impical!on5.

C.::_~_JI Next > 1r~~-::J

Рис. 2.1. Добавление пути к g i t в переменную среды РАТН

Запустите Visual Studio, выберите в меню Tools (Сервис) пункт Options (Параметры)
и перейдите в раздел Projects and SolutionsqExternal Web Tools (Проекты и решения<=>
Внешние веб-инструменты), как показано на рис. 2.2. Снимите отм етку с флажка
$ (VS I NSTALLD I R) \We b \External \ gi t , чтобы блокировать з апуск встроенной в
Visual Studio версии g it, и проверьте , отмечен ли флажок$ (РА Т Н ), чтобы мог при­
м еняться только что установленный инструмент gi t .

!
1
SeatckOpьc.-is(Ctrf•E)

~?~:~c·:~ons
.Р lootions of cttrn<tl toot~:
-------,
~@;JwG
1

:B~u.":~l.d;•:in~dR:~un:; ng'i
1
1 &') S(РАТН]
1 ! 0 S(VSJNSTдt lDIR)\Wt b\Exte 1мf\gil
i
1
1

:, 1!
А Prci)tcts • nd Solutions
G1:nu1I
.NЕТ Соц~ P1oje<ts
i
1 '·------------------------........)1
1 !>tt<Nf\'/ф 1ook'
VSOtf;щ lts
! Re5tttoDl!f.щtts j

VC •• Oi rcc l o rit.~ The p•th~ listed obtwewШ Ье s.t11rched when V'кu.гl Studio U1U 3rd·(nrtytcols such IJ
1 VC• ~ P1oj«t Sdtings G1un~ Gulp, 8owtr, cr npm.

•...::.~ P<Oj«~
, _
-···- -~· -·-~· ---·- ... ------ -· · - · - ------- .. -~· ··-··-

L _________ - - - ~-о_к_~I Г~~~ _J ;

Рис. 2.2. Конфигурирование запуска gi t в Visual Studio


Глава 2. Ваше первое приложение MVC 31

Будущее ASP.NET Core MVC и Visual Studio


В Microsoft недооценили, сколько времени потребовалось для создания ASP.NET Core и ASP.NET
Core MVC. Первоначально запланированные даты выпусков совпали с выходом Visual Studio 2015,
но задержки со стороны ASP.NET означают, что когда писались эти строки, разработка следую­
щей версии Visual Studio уже началась. Инструментальная поддержка для создания приложений
ASP.NET Core MVC может измениться , когда будет выпущена следующая версия Visual Studio. После
того как инструменты стабилизируются, я предоставлю обновление с инструкциями, требующими­
ся для создания примеров приложений. Новости будут доступны на веб-сайте издательства.

Создание нового проекта ASP.NET Core MVC


Мы собираемся начать с создания нового проекта ASP.NET Core МVС в среде Visual
Studio. Выберите в меню File (Файл) пункт NewqProject (СоздатьQ Проект), чтобы открыть
диалоговое окно New Project (Новый проект). Перейдя в раздел Templateso::>Visual С#<=:>
Web (ШaблoныQVisual С#QВеб) в панели слева, вы увидите шаблон проекта ASP.NET
Core Web Applicatioп (.NET Core) (Веб-приложение ASP.NET Core (.NET Core)). Выберите
этот тип проекта (рис. 2.3).

1 t> Rtcent ~"!_ЕТ Fr"m~;~ :б~1 - _: . Sort Ьу: \ Dtf-;~ij_--_-- -·-~. ") i~~ :::: ~мchi~7~tit!d "Тtmplal F:j_Ctrl ... E) ~~-!: .-;
i• lnstall<d
@j A5P.NПWe.bApp1icгtion{.NE1 fr~me.work) VisualC• Туре: Visual (#
! • Template~ Project ttmplat6 for crtating ASP.N П
Core "pplic<1tions for Windows, l inux and
... Visual(#
OS Х щing .NE1 Core.

w.ь @ A.SP.NП Core Wtb Applit•tion (.NП fr"mework) Vт~ua/ с~ ' AppfКAtion lnslghts
.NПCore О Add Appf1c<1t1on lnsights to projкt
Android Optimi:tt ptrformt!11Ct 4nd monitor
Cloud us<1Qt in vour li<.•e "PPlit"tion.
bll!ns!Ьil1ty
.os
Reporting
S1fvttlight
Help me unde.r~t""d Applkation lns19hts
Click h(r~ 10 по onl•"( ond f1t'!d 1wRIJ.tg. P11\'iicySt<11ll!.merit
f> Online

Name: P artylnvitм
1
location: ВloWft". 1

l Solutiori ntn,,e:

l-------~~,~=~~
P.-trty!nvite.~ 0 Cre.111e. dlltctory for.solutiol'\
О Add to Source Contro1

,~~~~~Od-L Сап~
Рис. 2.3. Шаблон проекта ASP.NET Core Web Applicatioп (.NET Core) в Visual Studio

Совет. При выборе шаблона проекта может возникать путаница из-за сильно похожих на­
званий шаблонов. Шаблон ASP.NET Web Applicatioп (.NET Framework) (Веб-приложение
ASP.NET (.NET Framework)) предназначен для создания проектов с использованием унас­
ледованных версий ASP.NET и MVC Framework, которые предшествовали ASP.NET Core .
Остальные два шаблона позволяют создавать приложения ASP.NET Core и отличаются
применяемой исполняющей средой, предлагая на выбор .NЕТ Framework или .NET Core.
Разница между ними объясняется в главе 6, но повсеместно в книге используется вари­
ант .NET Core, поэтому для получения идентичных результатов при работе с примерами
приложений вы должны выбирать именно его.
32 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

В пол е Name (Имя) для нового проекта введите Pa r ty i nv i tes и удостоверь­


тесь в том , что флажок Add Application lnsights to Project (Добавить в проект службу
Application Insights) не отмечен (см . рис. 2.3). Для продолжения щелкните на кнопке
ОК. Откроется еще одно диалоговое окно (рис. 2.4), предлагающее установить началь­
ное содержимое проекта .

г-·-·--·------·-----·------·· ·-1 А project template for crtoting an ASP.NEТ Соге


i MP; CoreTcrn;
application with exll mpfe ASP.NEТ MVC Vi~s and
Contfo!lers. This template сап ~lso Ье used for RESТfuf
НТТР service:.
1
1. En1pty ltleb API 1

·1· 11 1
@haпgt'AUt~
1{ _ J Authentication: No Authentlc~tion - - - - -1
------· - ·- - · - 1<;;, MicrcкoftAzu re 1

\G О Host iп thecloud j

~1~.,.-г:з uo ~ I
1

L ~

Рис. 2.4. Выбор начальной конфигурации проекта

Для шаблона ASP.NET Core Web Application (.NET Core) доступны три вариант а ,
каждый из которых приводит к созданию проекта с отличающимся начальным содер­
жимым. Для целей данной главы выберите вариант Web Application (Веб-приложение),
который настроит приложение MVC с заранее определенным содержимым , чтобы не­
медленно приступить к разработке.

На заметку! Это единственная глава, в которой применяется шаблон проекта Web Application.
Мне не нравится пользоваться заранее определенными шаблонами , поскольку они пот­
ворствуют интерпретации ряда важных средств наподобие аутентификации как черны х
ящиков . Моя цель в настоящей книге - предоставить вам достаточный объем знаний для
понимания и управления каждым аспектом приложения MVC, так что в оставшихся мате­
риалах книги применяется шаблон Empty (Пустой). Текущая глава посвящена быстрому
началу процесса разработки , для чего хорошо подходит шаблон Web Application.

Щелкните на кнопке Change Authentication (Изменить аутентификацию) и проверь­


те, что выбран переключатель No Authentication (Аутентификация отсутствует), 1<ак
показано на рис. 2.5. Данный проект не требует какой-либо аутентификации, а в гла­
вах 28-30 объясняется , как защитить приложения ASP. NEТ.
Щелкните на кнопке ОК, чтобы закрыть диалоговое окно Change Authentication
(Изменение аутентификации). Удостоверьтесь в том , что флажок Host in the Cloud
(Разместить в облаке) не отмечен и затем щелкните на кнопке ОК для создания про­
екта Par t yi n v i t e s. После того как среда Visual Studio создала проект, вы увидите в
окне Solution Explorer (Проводник решения) множество файлов и папок (рис . 2.6). Это
стандартная структура для проекта МVС, созданного с использованием шаблона Web
Application, и вскоре вы узнаете на з начение каждого файла и папки , которые были
созданы Visual Studio.
Глава 2. Ваше первое приложение MVC 33

For applic•tions that don't require any user • uthentic•tio".

@ No Auth•ntication

О lndividual User Accounts

О Work And Scl>ool Accounts

О Windows Authentication

г-о~_:] \ Cьncel
Рис. 2.5. Выбор настроек аутентификации

Теперь можете запустить приложение,


Solution Explorer • 1:1 Х
выбрав в меню Debug (Отладка) пункт Start
Debugging (Запустить отладку); если появит- .~ с' ~ f''"Ф •·4'~· @ Wi 1,,, -- .
..:.,;;,,,·"- ··'·' .,;:;;;i~.-:. .~: \о...., •·.1, - ._,

ся запрос на включение отладки, тогда просто .se~5~~l u~on ~plorer (Ctrt,:;~ Р·

щелкните на кнопке ОК. Среда Visual Studio l4J Solution 'Partylnvites' (1 project}
" ;,,.,. Solution Jtems
скомпилирует приложение, с помощью сервера
/;J global.json
приложений IIS Express запустит его и откро­ ..; ·~1 src
ет окно веб-браузера для запроса содержимого
приложения. Результат показан на рис. 2.7. ~ ,1- Properties
~ •·• Reference.s
Когда среда Visual Studio создает проект с при­
менением шаблона Web Application, она добавля­ •·• Depe.ndencies
ет базовый код и содержимое, которое вы видите ~ ;.t; Controllers
после запуска приложения. В оставшейся части ~ ~ Vieo.vs

этой главы мы будем заменять такое содержимое,


lJ appsettings.json
lJ bundleconfigjson
чтобы создать простое приложение МVС.
С" Program.cs
По завершении не забудьте остановить от­ ~ lJ project.json
ладку, закрыв оюю браузера или вернувшись в .D Project_Readme.html
с• Startup.cs
Visual Studio и выбрав в меню Debug пункт Stop
у') web.config
Debugging (Остановить отлад~<у).
Как было только что показано, для отображе­
ния проекта среда Visual Studio открывает окно Рис. 2.6. Начальная структура файлов
браузера. Вы можете выбрать любой установ­ и папок проекта ASP.NET Саге MVC
ленный браузер , щелкнув на кнопке со стрелкой
правее кнопки llS Express в панели инструментов и выбрав нужный вариант из спис­
ка в меню Web Browser (Веб-браузер), как показано на рис. 2.8.
В дальнейшем во всех примерах в книге будет использоваться браузер Google
Chrome или Google Chrome Canary, но вы можете применять любой современный бра­
узер, включая Microsoft Edge и последние версии Internet Explorer.

Добавление первого контроллера


В рамках паттерна MVC входящие запросы обрабатываются контроллерами.
В ASP.NET Core MVC контроллеры - это просто классы С# (обычно унаследованные
от класса Microsoft. AspNetCore . Mvc. Controlle r, являющегося встроенным ба­
зовым классом контроллера МVС).
34 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

Application uses How to Overvi ew Run & Deploy


• ~;а 1>о191-? UWЧ ASl'I Nfl • AddaQ.imrt'Jl!<'or....Ю'/- • Глrщ.Ш:.lt.>."4,.,.......iilwl~•t" · 11-"'У"'" '"""
CCrtMVC ' A'1':-."'lapps.;it!l>l)lflщ\/.glil'l<I ASPtlП ~ • Rllft l(ll'll•&ur.~.\~fТtr~

......
&.-l!l<m.Ul,}f)IП',j(~.$4dil

• Thfm!ng 11~ И«щщр


......,.."lln~
· M~-...Uwl~ll"l'lf.l'f.l>I
!.l1~
• Futnr.n1>00!ЬolNil ' Nf J C:.,."
~,,St.!1rup.,11::!~

· ~-iti~
• .,. •
/INl"ll'.r"
l'ld-Jh •<J !~/: .w.n.ww

· \.lw~'!/.IU~·~
• -'4.Jpatkog'!'>uPlg tll>O~
· ~
0 (.l~t-"'~•wr.t
'""
• A(l('ltl<lrllpN~u""'JI~ • ri.,.y,4<\p(">(j..!l<>:nr~f'l>,1'.IO'.~

o ljjf\l('f~fl''"'l.ЦloJ""JI)( • r~.tdn"3f"\,"!'l'"°(!t,.'Q,l/N>~"'tл1
ptOd!,ГЬIК\Wtlr.'JIИ'I(

'1l0 1t!l · l'iwfytпrrtн

Рис. 2. 7. Выролнение примера проекта

Test. An•lyz• Window H•lp

.-'------->=~. • 1 ~...,L__·_ .........


llSExpress
llS Express
Partyfnvit•s
Web Bro wш (G oogl• Cl11ome)
Google Chrome
Google Chrome C~n~ry ~
lntern et Explorer
Mic rosoft Edge

Рис. 2.8. Выбор браузера

Каждый открытый метод в контроллере называется методом действия, что оз­


начает возможность его вызова из веб-среды через некоторый URL для выполнения
действия. В соответствии с соглашением MVC контроллеры помещаются в папку
Contro lle rs , автоматически создаваемую Visual Studio при настрой1<е проекта.

Совет. Вы вовсе не обязаны соблюдать указанное или большинство других соглашений MVC,
но рекомендуется его придерживаться - и не в последнюю очередь потому, что это по­

может уяснить примеры , приведенные в настоящей книге .

Среда VisuaJ Studio добавляет в проект класс стандартного контроллера, который


вы можете увидеть , раскрыв папку Control le r s в окне Solution Explorer. Файл назы­
вается HomeCont r ol l e r. cs . Файлы классов контроллеров имеют имя, завершающе-
Глава 2. Ваше nервое nриложение MVC 35
еся словом Contro l ler, т. е. в файле Home Controll e r. c s содержится код контролле­
ра по имени Home - стандартного контроллера. используемого в приложениях МVС.
Щелкните на имени файла HomeCont r o ll er . cs в окне Solution Explorer, чтобы среда
Visual Studio открыла е го для редактирования. Вы увидите код С# , приведенный в
ли стинге 2.1.

Листинг 2.1. Первоначальное содержимое файла HomeController. cs


из папки Controllers
using System ;
using System . Co l lectio ns. Ge n e ri c ;
u sing System . Li nq ;
using System . Threa d ing . Tasks ;
using Microsoft . AspNe t Core . Mvc ;
namespace Pa r tyinvites .C ontrol l e r s
puЫic class HomeCon tr olle r : Co ntro ller
puЫic IActionRes ult I ndex() {
return View () ;

puЫic IAct i onResult About() {


ViewData [" Message " ] = " You r app l ication descriptio n pa g e .";
return View() ;

puЫic IActionRes ul t Contac t ( ) {


ViewData [ " Message "] = " You r c ont act page . ";
return View() ;

puЫic IActionResu lt Error() {


return Vi ew() ;

Заменит е код в файле Ho meCon tr o l ler . c s кодом, показанным в листинг е 2.2.


Эде сь были удалены все методы кроме одного , у которого изменен возвращаемый
тип и его р еали з ация, а также удалены операторы u s i ng для неиспользуемых про ­
странств имен.

Листинг 2.2. Изменение файла HomeController. cs


using Microsof t. AspNetCore .Mvc ;
n amespace Par tyi nv i te s.Cont r o ller s
puЫic class HomeCo ntr o l le r : Co n t r o lle r
puЫic str i ng In d ex() {
retu r n " He ll o Wo r l d";
36 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

Изменения не приводят к особо впечатляющим результатам, но их вполне доста­


точно для хорошей демонстрации. Метод по имени I n dex () изменен так. что теперь
он возвращает строку "He ll o Wor l d ". Снова запустите проект, выбрав пункт Start
Debuggiпg из меню Debug в Visual Studio.

Совет. Если вы оставили в функционирующем состоянии приложение из предыдущего разде­


ла , то выберите в меню Debug пункт Restart (Перезапустить) или при желании пункт Stop
Debuggiпg и затем Start Debuggiпg.

Браузер сделает НТТР-запрос серверу. Стандартная конфиrурация MVC предусмат ­


ривает, что данный запрос будет обрабатываться с применением м етода Index () , на­
зываемого методом действия или просто действием, а результат, полученный из этого
метода , будет отправлен обратно браузеру (рис. 2 .9).

_
l0<a!host:S7628 Х

_?_@l~c~~~~~~~-8- ·~-=-=--~=-=---~-=--= ~-j _ _


l: lo World

Рис. 2.9.
-----------·-------
Вывод из метода действия
1
__,

Совет. Обратите внимание, что среда Visual Studio направляет браузер на порт 57628. Внутри
URL, который будет запрашивать ваш браузер, почти наверняка будет присутствовать
другой номер порта, т. к. Visual Studio выделяет произвольный порт при создании проекта .
Если вы заглянете в область уведомлений панели задач Windows, то найдете там значок
для llS Express. Этот значок представляет усеченную версию полного сервера приложе­
ний llS, которая входит в состав Visual Studio и используется для доставки содержимого
и служб ASP.NET во время разработки. Развертывание проекта MVC в производственной
среде будет описано в главе 12 .

Понятие маршруто в
В дополнение к моделям , представлениям и контроллерам в приложениях MVC
применяется система маршрутизации ASP.NEТ, которая определяет, как URL отоб­
ражаются на контроллеры и действия. Маршрут - это правило, которо е использу­
ется для решения о том, как обрабатывать запрос. Когда среда Visual Studio создает
проект MVC, она добавляет ряд стандартных маршрутов , выступающих в качеств е
начальных. Можно запрашивать любой из следующих URL, и они будут направлены

.
на действие


/
/Ноте
Inde x класса H oтeC ont r o lle r :

• / H oтe/Index

Таким образом, когда брауз ер запрашивает http : //ваш- сайт/ или http: //ваш_
сайт/ Н от е , он получает вывод из метода I ndex () класса H oтeController. Можете
Глава 2. Ваше первое приложение MVC 37
опробовать зто самостоятельно, изменив URL в браузере. В настоящий момент он бу­
дет выглядеть как http: //localhost : 57628/, но представляющая порт часть мо­
жет быть другой . Если вы добавите к URL порцию /Home или /Home/Index и нажмете
клавишу <Enter>. то получите от приложения MVC тот же самый результат - строку
" Hello , wo rl d " .
Это хороший пример получения выгоды от соблюдения соглашений, поддержива­
емых ASP.NET Core MVC. В данном случае соглашение заключается в том, что имеет­
ся контроллер по имени HomeController. который будет служить стартовой точкой
приложения MVC. Стандартная конфигурация. 1<0торую V!sual Stud!o создает для но­
вого проекта, предполагает. что мы будем следовать этому соглашению. И поскольку
мы действительно соблюдаем соглашение, мы автоматически получаем поддержку
всех URL из приведенного выше списка. Если не следовать соглашению, то конфигу­
рацию пришлось бы модифицировать для указания на контроллер, созданный вза­
мен стандартного. В рассматриваемом простом примере стандартной конфигурации
вполне достаточно.

Визуализация веб-страниц
Выводом предыдущего примера была не НТМL-разметка. а просто строка "Hel lo
Wor ld ". Чтобы сгенерировать НТМL-ответ на запрос браузера, понадобится создать
представление, которое сообщает MVC. каким образом генерировать ответ для запро­
са, поступившего из браузера.

Создание и визуализация представления


Прежде всего, необходимо модифицировать метод действия Index () , как показа­
но в листинге 2.3 . Для простоты восприятия в этом и во всех будущих листингах из­
менения выделяются полужирным.

Листинг 2.3. Изменение контроллера для визуализации представления


в файле HomeController.cs
using Microsoft . AspNetCore . Mvc;
namespace Partyinvites.Controllers
puЫic class HomeController : Controller
puЬlic ViewResult Index ()
return View("MyView");

Возвращая из метода действия объект ViewResu l t, мы инструктируем МVС о ви­


зуализации представления. Экземпляр ViewResul t создается посредством вызова
метода View () с указанием имени представления. которое должно применяться, т.е.
MyView. Запустив приложение, можно заметить, что инфраструктура MVC пытается
найти представление, кш< отражено в сообщении об ошибке на рис. 2 . 10.
Сообще ние об ошибке исключительно полезно. Оно не только объясняет, что ин­
фраструктура MVC не смогла найти представление, указанное для метода действия,
но также показывает, где производился поиск. Представления хранятся в подпапках
внутри папки Views .
38 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

lrite-rnal Serv~ Error Х

: 1
---- --··· • 1
1 An Linhandled exception оссшгеd while processing the reqL1est.
lnvaliclOpeгatio'lExcep<ion: Т11е viev1 'MyViev/ v1as 110t fou r.d. The foi/01·1ing !ocations
were searcf1ed:
.f /Views/Home/MyView.cshtinl
ii
1

i Niews/Si1ared/MyVie\\1.CSl-1t •nl
f Eпiun:-su,cessful

1 В Q\Jery Cookies Heиders i


1
1. - ···---- - . :i

Рис . 2.1 О. Инфраструктура MVC пытается найти представление

Например, представления, которые связаны с контроллером Home , содержатся в


папке по имени Vi ew s / Home . Представления, не являющиеся специфическими для
отдельного контроллера, хранятся в папке под названием Vie ws / Sha r ed . Среда
Visual Studio создает папки Home и Sh a re d автоматически, когда используется шаб­
лон Web Application, и в рамках начальной подготовки проекта помещает в них не­
сколько представлений-заполнителей.
Чтобы создать представление , щелкните правой кнопкой мыши на папке Home
внутри V i ews в окне Solution Explorer и выберите в контекстном меню пункт Add~New
ltem (Добавить~Новый элемент) . Среда Visual Studio предложит список шаблонов эле­
ментов. Выберите категорию ASP.NET в панели слева и затем укажите элемент MVC
View Page (Страница представления МVС) в центральной панели (рис. 2.11) .

·~ 1nтlle<1 Sort Ь у: ~•foult i SNrcl1 lnst4JJed Tem~l;tts ~Ctr!•E} Р · .j


~J>.NEТ1 MVC (ontroller Clas~ ~ Туре: ASP.N H
ASP.NEТ
Client-side
Code
р MVC View Р•9е
W!:!b АР\ Contioller Class А5Р.NЕТ

f> Online

MVC Viow Start Page ASP.NEТ


~

. MVC View lmport:; Page ASP.NH

~~· R.!zor Tag HeJper д5Р.N ЕТ

№mt: MyVi~.csht1n l

Рис. 2. 11. Создание представления

Совет. В папке Vi e ws уже есть несколько файлов, которые были добавлены Visual Studio с
целью предоставления начальн ого содержим ого , показанного на рис. 2.7. Можете проиг­
норировать эти файлы .
Глава 2. Ваше первое приложение MVC 39
Введите в поле Name (Имя) имя MyVi ew. cshtml и щелкните на кнопке Add
(Доб авить) для создания представления. Среда VisuaJ Studio создаст файл View s/
Home/MyView . c sh tml и откроет его для редактирования. Начальное содержимое
файла представления - это просто ряд комментариев и заполнитель. Замените его
содержимым, приведенным в листинге 2.4.

Совет. Довольно легко создать файл представления не в той папке. Если в итоге вы не по­
лучили файл по имени MyView . cshtml в папке Views/Home , тогда удалите созданный
файл и попробуйте создать заново .

Листинг 2.4. Замена содержимого файла МyView. cshtml из папки Views/Horne

@{
Layout = nu ll;

< ! DOCTYPE html>


<html>
<head>
<meta name= " v iewport " content= " width=device-width" />
<title>Index</title>
</head>
<body>
<div>
Hello World (from the view)
</div>
</body>
</html>

Теперь файл представления содержит главным образо м НТМL-разметку. Исклю­


чением является часть, которая имеет следующий вид:

@{
Layout null;

Такое выражение будет интерпретироваться механизмом визуализации Razor,


который обрабатывает содержимое представлений и генерирует НТМL-р азметку,
отправляемую браузеру. Показанное выше простое выражение Razor сообщает ме­
ханизму Razor о том, что компоновка не применяется; это похоже на шаблон для
НТМL-разметки, который посылается браузеру (и будет описан в главе 5). Мы пока
проигнорируем механизм Razor и возвратимся к нему позже. Чтобы увидеть создан­
ное представление, выберите в меню Debug пункт Start Debugging для запуска прило­
жения. Долж ен получиться результат, приведенный на рис. 2.12.
При первом редактировании метод действия I ndex () возвращал строковое зна­
чение . Это означало, что инфраструктура MVC всего лишь передавала брауз еру стро­
ковое значение в том виде, как есть. Теперь. когда метод Index () возвращает объект
ViewResul t, инф раструктура MVC визуализирует представление и возвращает сгене­
риров анную НТМL-разметку. Мы сообщили инфраструктуре МVС о том, какое пред­
ставление должно использоваться, поэтому с помощью соглашения об именовании
40 Часть 1. Введение в инфраструктуру ASP.NET Соге MVC

она автоматически выполнила его поиск. Соглашение предполагает, что имя файла
представления совпадает с именем метода действия, а файл представления хранится
в папке, названной по имени контроллера: /Views/ Home/MyView . cshtml .
Кроме строк и объектов ViewResul t методы действий могут возвращать другие ре­
зультаты . Например, если мы возвращаем объект RedirectResul t, то браузер будет
перенаправлен на другой URL. Если мы возвращаем объект HttpUnauthorizedResul t ,
то вынуждаем пользователя войти в систему. Все вместе такие объекты называются
результатами действий. Система результатов действий позволяет инкапсулировать
и повторно использовать часто встречающиеся ответы в действиях. В главе 17 мы
рассмотрим их подробнее и продемонстрируем разные способы их применения.

Hello Wo1·id (fl'OШ tl1e vie\v)

Рис. 2.12. Тестирование представления

Добавление динамического вывода


Весь смысл платформы для разработки веб-приложений состоит в конструирова­
нии и отображении динамического вывода. В рамках МVС работа контролле ра заклю­
чается в подготовке данных и передаче их представлению, которое отвечает за их

визуализацию в виде НТМL-разметки.


Один из способов передачи данных из контроллера в представление предусмат­
ривает использование объекта ViewBag , который является членом базового класса
Controller. По существу ViewBag - это динамический объект, в котором можно
устанавливать произвольные свойства, делая их значения доступными в любом визу­
ализируемом далее представлении. В листинге 2.5 демонстрируется передача таким
способом простых динамических данных в файле HomeController. cs.

Листинг 2.5. Установка данных представления в HomeController. cs

using System;
using Mic r osof t. AspNetCore .Mvc ;
namespace Partyinvites . Contro ll ers
puЫic class HomeCon t roller : Con troller
puЫic ViewResu l t Index() {
int hour =
DateTime.Now.Hour;
ViewBag. Greeting =
hour < 12 ? "Good Morning" "Good Afternoon";
return View( "MyView " ) ;
Глава 2. Ваше первое приложение MVC 41
Данные для представления предоставляются во время присваивания значения
свойству ViewBag . Greeting. Свойство Greeting не существует вплоть до момента,
когда ему присваивается значение - это позволяет передавать данные из контроллера

в представление в свободной и гибкой манере, без необходимости в предварительном


определении классов. Чтобы получить значение данных, необходимо еще раз сослаться
на свойство ViewBag. Greeting, но уже в представлении, как показано в листинге 2.6,
содержащем изменение, которое было внесено в файл MyView. cshtml .
Листинг 2.6. Извлечение значения данных ViewBag в файле МyView. cshtml

@{
Layout = null;

<!DOCTYPE html>
<html>
<head>
<meta name =" viewpor t" content="width=device-width" />
<tit l e> I ndex</title>
</head>
<body>
<div>
@ViewBag.Greeting World (from the view)
</div>
</body>
</html>

Добавленный в листинге фрагмент - это выражение Razor, которое оценивается,


когда MVC применяет представление для генерации ответа. Вызов методаView () в
методеI ndex ( ) контроллера приводит к тому, что МVС находит файл представления
MyView . cshtml и запрашивает у механизма визуализации Razor синтаксический
анализ содержимого этого файла. Механизм Razor ищет выражения, подобные до­
бавленному в листинге 2.6, и обрабатывает их. В рассматриваемом примере обработ­
ка выражения означает вставку в представление значения, которое было присвоено
свойству ViewBag . Greeting в методе действия.
Выбор для свойства имени Greeting не диктуется никакими особыми соображе­
ниями. Его можно было бы заменить любым другим именем, и все работало бы точно
так же при условии, что имя, используемое в контроллере, совпадает с именем, ко­

торое применяется в представлении. Присваивая значения более чем одному свойс­


тву, можно пер едавать из контроллера в представление множество значений данных.
После запуска проекта можно увидеть результат внесенных изменений (рис. 2.13).

Рис. 2.13. Динамический ответ, сгенерированный MVC


42 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

Создание простого приложения для ввода данных


В оставшихся разделах этой главы будут исследованы другие базовые функцио­
нальные средства MVC за счет построения простого приложения для ввода данных .
В этом разделе мы собираемся несколько увеличить темп изложения. Целью является
демонстрация инфраструктуры МVС в действии, поэтому некоторые объяснения того .
что происходит "за кулисами", будут пропущены. Однако не беспокойтесь - мы вер­
немся к подробному обсуждению этих тем в последующих главах.

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

• домашняя страница. отображающая информацию о вечеринке;

• форма , которая может использоваться для ответа на приглаш е ние (repoпdez s'il
vous plalt- RSVP);
• проверка достоверности для формы RSVP, которая будет отображать страницу с
выражением благодарности за внимание;

• итоговая страница. которая показывает, кто собирается прийти на вечеринку .

В последующих разделах мы достроим проект МVС, созданный в начале главы , и


добавим в него перечисленные выше средства. Первый пункт можно убрать из спис­
ка. применив то, что было показано ранее - добавить НТМL-разметку с подробной
информацией о вечеринке в существующее представление. В листинге 2. 7 приведено
содержимое файла Views/Home/MyView. cshtml с внесенными дополнениями.

Листинг 2.7. Отображение подробностей о вечеринке в файле МyView. cshtml


@{
Layout = null ;

<!DOCTYPE html>
<htm l >
<head>
<meta name="viewport" content= "w i d th=de v ice-w idth" />
<title>Index</title>
</head>
<body>
<div>
@ViewBag .Gr eeting World (from the view)
<p>We' re going to have an exci ting party . <br />
(То do: sell i t better. Add pictures or something.)
</р>
</div>
</body>
</html >
Глава 2. Ваше первое приложение MVC 43
Мы двигаемся в верном направлении. Если запустить приложение, выбрав в меню
Debug пункт Start Debugging. то отобразятся подробности о вечеринке - точнее, за­
полнитель для подробностей, но идея должна быть понятной (рис. 2.14).

Goocl Moш.i11g \Vorld (fi·oщ tl1e Yie,v)

l
1 \Ve're goiдg to lшУе а11 exc iti.11g party.
(То do' "11 i1 bon« Add piono" 01 •onwd1iug._)______,

Ри с. 2.14. Добавление информации о вечеринке к НТМL-разметке представления

Проектирование модели данных


Буква "М" в аббревиатуре MVC обозначает тodel (модель) , и она является самой
важной частью приложения. Модель - это представление реальных объектов, про­
цессов и правил. которые определяют сферу приложения, известную как предметная
область. Модель , которую часто называют моделью предметной облас ти, содержит
объекты С# (или объекты предметной области), образующие "вселенную" приложе­
ния, и методы, позволяющие манипулировать ими. Представления и контроллеры
открывают доступ клиентам к предметной области в согласованной манере, и любое
корректно разработанное приложение МVС начинается с хорошо спроектированной
модели, которая затем служит центральным узлом при добавлении контроллеров и
представлений .
Для про екта Partylnv i tes сложная модель не требуется, поскольку приложение
совсем простое, и нужно создать только один класс предметной области, который по­
лучит имя GuestRespo nse. Этот объект будет отвечать за хранение, проверку досто­
верности и подтверждение ответа на приглашение (RSVP).
По соглашению MVC классы, которые образуют модель. помещаются в папку по
имени Models . Чтобы создать эту папку. щелкните правой кнопкой мыши на проекте
Partylnv i tes (элемент. содержащий папки Controllers и Views). выберите в кон­
текстном меню пункт Add~New Folder (ДобавитьqНовая папка) и укажите Models в
качестве имени папки.

На заметку! Вы не сможете установить имя новой папки , если приложение все еще фун ­
кционирует. Выберите в меню Debug пункт Stop Debugging, щелкните правой кнопкой
мыши на элементе NewFolder , который добавился в окноSolution Explorer, выберите в
контекстном меню пун кт Rename (Переименовать) и измените имя на Mode ls.

Для создания файла масса щелкните правой кнопкой мыши на папке Models в окне
Solution Explorer и выберите в контекстном меню пункт AddqClass (ДобавитьqКласс) .
Введите GuestResponse . cs для имени нового класса и щелкните на кнопке Add
(Добавить). Приведите содержимое нового файла класса к виду , показанному в лис ­
тинге 2.8.
44 Часть 1. Введение в инфрастру ктуру ASP.NET Core MVC

Листинг 2.8. Класс предметной области GuestResponse, определенный


в файле Gues tResponse . cs внутри папки Model в
namespace Partyinvites . Models {
puЫic class GuestResponse {
puЫic string Name { get ; set;
puЬlic string Email { get; set ; }
puЬlic string Phone { get; set; }
puЫic bool? WillAttend { get; set;

Совет. Вы могли заметить, что свойство WillAttend имеет тип bool, допускающий null,
т.е. оно может принимать значение true, false или null . Обоснование этого будет
приведено в разделе "Добавление проверки достоверности" далее в главе.

Создание второго действия


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

Одной из целей разрабатываемого приложения является включение формы RSVP,


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

Листинг 2.9. Добавление метода действия в файле HomeController. cshtml


using System ;
using Microsoft . AspNetCore . Mvc ;
namespace Partyinvites . Controllers
puЫic class HomeController : Controller
puЬlic ViewResult Index() {
int hour = DateTime . Now.Hour ;
ViewBag. Greeting = hour < 12 ? "Good Morning " "Good Afternoon";
return View( "MyView " );

puЫic ViewResul t RsvpForm ()


return View () ;

Метод действия RsvpForm () вызывает метод View () без аргументов, что сообща­
ет инфраструктуре МVС о необходимости визуализации стандартного пр едставления,
связанного с этим методом действия, которым будет представление с таким же име­
нем, как у метода действия (RsvpForm . cshtml в данном случае).
Глава 2. Ваше первое приложение MVC 45
Щелкните правой кнопкой мыши на папке Views внутри Home и выберите в кон­
текстном меню пункт Add<=:> New ltem (Добавить<=:> Новый элемент). Выберите шаб­
лон MVC View Page (Страница представления MVC) из категории ASP.NET, укажи­
те RsvpForm . cshtml в к ачестве имени нового файла и щелкните на кнопке Add
(Добавить), чтобы создать файл. Приведите содержимое нового файла в соответствие
с листингом 2.10.
Листинг 2.10. Содержимое файла RsvpForm. cshtml из папки Views/Home

@model Partyinvi tes . Mode ls. Gue stRespon se


@{
Layout = null ;

<!DOCTYPE html >


<html>
<head>
<meta name ="viewport " con t e nt =" width=devi ce - wi dth " />
<title>RsvpForm</titl e>
</ head>
<bo dy >
<div>
This is the RsvpForm . c s html View
</d i v>
</body>
</html>

Содержимое состоит в основном из НТМL-разметки , но с добавлением Rаzоr ­


выраж ения @model , которое используется для создания строго mипизирован.н.ого
представле ния. Строго типизированное представление предназначено для визуали ­
зации специ фического типа модели, и если указан жела емый тип (в данном случае
класс GuestResponse из пространства имен Par t yinvite s . Models), то MVC может
создать ряд удобных сокращений, чтобы сделать его проще. Вскоре мы задействуем
преимущество характеристики строгой типизации.
Чтобы протестировать новый метод действия и его представление , запустите при­
ложение, выбрав в м еню Debug пункт Start Debugging, и с помощью брауз ера перей ­
дите на URL вида / Home/RsvpForm.
Инфраструктура МVС применит описанное ранее согл ашение об именовании для
направления запроса методу действия RsvpForm () , определенному в контролле­
ре Home . Этот метод действия указывает MVC о том, что должно визуализировать­
ся стандартное представление, которое посредством еще одного применения того же

соглашения об именовании визуализирует Rs vpFo rm. cs hml из папки Views/ Home .


Результат показан на рис. 2.15.

1· RsvpForm х

г---·-----"·~-·--
~ С Lq:> loca l ho~t 5}628/Home/RsvpFo rm. _ __

[ " ; ;, ;he Rs-;;pi~''" oshtщl Vi_e,_:s.-' - - - - - -·


Рис. 2.15. Визуализация второго представления
46 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

Ссылка на методы действий


Нам необходимо создать в представлении MyView ссылку, чтобы гости могли ви­
деть представление Rsvp Form без обязательного знания URL, который указывает на
специфический метод действия (листинг 2.11).
Листинг 2.11. Добавление ссылки на форму RSVP в файле МyView. cshtml
@{
Layout = null;

<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>Index</title>
</head>
<body>
<div>
@ViewBag.Greeting World (from the view)
<p>We're going to have an exciting party.<br />
(То do: sell it better. Add pictures or something.)
</р>
<аasp-action="RsvpForm">RSVP Now</a>
</div>
</body>
</html>

В листинге 2.11 добавлен элемент а. который имеет атрибут asp - action. Данный
атрибут является примером атрибута дескрuпторного вспомогательного класса, т.е .
инструкцией Razor, которая будет вьшолнена, когда представление визуализируется .
Атрибут asp-action - это инструкция по добавлению к элементу а атрибута href,
содержащего URL для метода действия. Работа дескрипторных вспомогательных клас­
сов объясняется в главах 24, 25 и 26, а пока достаточно знать, что asp-action - про­
стейший вид атрибута дескрипторного вспомогательного класса для элементов а. Он
указывает Razor на необходимость вставки URL для метода действия, определенного в
том же контроллере, для которого визуализируется текушее представление. Запустив
проект, можно увидеть ссылку, которую создал вспомогательный класс (рис. 2.16).

1 lndex
j Rsvpform Х

_____'? i9?~°.~~~~~-;~3__ _
Пu$ is 111~ RsYpF011n.cslitшl \ ' ie\\·

L ~--~- ~----
j'
Рис. 2.16. Ссылка на метод действия
Глава 2. Ваше первое приложение MVC 47
После запуска приложения наведите курсор мыши на ссылку RSVP Now (Ответить
на приглашение) в окне браузера. Вы заметите, что ссылка указывает на следующий
URL (возможно , вашему проекту Visual Studio назначит другой номер порта):

http://localhost : 57628/ Home/R s vpForm


Здесь требуется соблюдать один важный принцип: вы должны использовать средс­
тва , предлагаемые МVС для генерации URL, а не жестко кодировать их в своих пред­
ставлениях. Когда вспомогательный класс создает ат рибут href для элемента а , он
инсп е ктирует конфигурацию приложения, чтобы выяснить , к аким должен быть URL.
В итоге появляется возможность изменять конфигурацию приложения для поддер жки
разных форматов URL без необходимости в обновлении каких-либо пр едставлений .
Особенности работы этого рассматриваются в главе 15.

Построение формы
Теперь, когда строго типизированное представление создано и достижимо из представ­
ления I ndex, займемся подгонкой содержимого файла RsvpForm . c s html, чтобы превра­
тить его в НТМL-форму для редактирования объектов Gue stResponse (листинг 2.12).
Листинг 2.12. Создание представления в виде формы в файле RsvpForm. cshtml
@model Pa r tyinvites . Models . GuestRe sponse
@{
Layout = null ;

< !DOCTYPE htm l >


<html>
<head>
<meta name= "viewpo r t " content= "width=devi ce - wi dth " />
<title>RsvpForm</title>
</head>
<body>
<form asp-action="RsvpForm" method="post">
<р>
<label asp-for="Name">Your name:</label>
<input asp-for="Name" />
</р>
<р>
<laЬel asp-for="Email">Your email:</laЬel>
<input asp-for="Email" />
</р>
<р>
<laЬel asp-for="Phone">Your phone:</label>
<input asp-for="Phone" />
</р>
<р>
<laЬel>Will you attend?</laЬel>
<select asp-for="WillAttend">
<option value=" ">Choose an option</option>
<option value="true">Yes, I '11 Ье there</option>
<option value="false">No, I can•t come</option>
</select>
</р>
<button type="submit">SuЬmit RSVP</button>
</form>
</body>
</html>
48 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

Для каждого свойства IUiacca модели GuestResponse определены элементы label


и input (или элемент select в случае свойства Wi llAttend}. Каждый элемент ассо­
циирован со свойством модели с применением еще одного атрибута дескрипторно­
го вспомогательного IUiacca - asp - for. Атрибуты дескрипторных вспомогательных
IUiaccoв конфигурируют элементы, чтобы привязать их к объекту модели. Вот пример
НТМL-разметки , которую генерируют дескрипторные вспомогательные IUiaccы для от­
правки браузе ру:

<р>

<labe l for="Name">Your name : </label>


<input type="text" id="Name" name="Name" value="">
</р>

Атрибут asp-for в элементе label устанавливает значение атрибута for. Атрибут


asp - for в элементе inp ut устанавливает атрибуты id и name . В данный момент это
не выглядит особенно полезным , но по мере определения прикладной функциональ­
ности вы увидите , что ассоциирование элементов со свойством модели предлагает
дополнительные преимущества.

Более непосредственный результат дает атрибут asp - action в элементе form,


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

<form method= "post " act i on=" /Home /RsvpForm " >
Как и в случае атрибута дескрипторного вспомогательного класса, примененного
к элементу а, преимущество такого подхода заIUiючается в том, что вы можете изме­

нять систему URL, используемую приложением, и содержимое , которое генерируется


дескрипторными вспомогательными классами, автоматически отразит изменения.

Форму можно увидеть, запустив приложение и щел кнув на ссылке RSVP Now
(рис. 2.17).

-------"·--·-·-· ·-----··------------·-"". ~ ~
С @------------------------
localhost:57628/ Home/RsvpForm

Уош 11щnе:
.""."---·-··----·--·;
["."---·--·-·-···"·--·-·--·-_J
Уонr eшail : L ______ ____ _ 1
,_" . __,

Уонr pl1011e: с--------1

\\ ill уон a"end? rch~~e ;~;;~

·---·--------------·
Рис. 2.17. Добавление НТМL-формы к приложени ю
Глава 2. Ваше первое приложение MVC 49

П олучение данных формы


Мы пока еще не указали инфрастру~<туре MVC , что должно быть сделано, когда
форма отправляется серверу. В нынешнем состоянии приложения щелчок на кноп­
ке Submit RSVP (Отправить RSVP) лишь очищает любые значения, введенные в фор­
му . Причина в том , что форма осуществляет обратну~о отправ1<у методу действия
RsvpForm () контроллера Home , который толыю сообщает MVC о необходимости пов­
торной визуализации представления.
Чтобы получить и обработать отправленные данные формы, мы собираемся вос­
пользоваться основной возможностью контроллера. Мы добавим второй метод дейс­
твия RsvpForm () ,чтобы получить в свое распоряжение следующее.

• Метод, который отвечает н.а НТТР-запросы GE T. Каждый раз, 1<огда кто-то щел­
кает на ссьmке, браузер обычно выдает запрос GET. Эта версия действия будет
отвечать за отображение изначально пустой формы , когда ~по-нибудь впервые
посещает / Home/ RsvpFo rm.
• Метод, который отвечает на НТГР-запросы POST. По умолчанию формы, ви­
зуализированные с помощью Ht ml. Be g in Form () ,отправляются браузером как
запросы POST. Эта версия действия будет отвечать за получение отправленных
данных и принятие решения о том, что с ними делать.

Обработка запросов GET и POST в отдельных методах С# способствует обеспечению


аккуратности кода контроллера, т.к. эти два метода имеют разные обязанности. Оба
метода действий вызываются через тот же самый URL, но в зависимости от вида за­
проса - GET или POS T - инфраструктура МVС вызьmает подходящий метод. В листин­
ге 2.13 показаны изменения, которые необходимо вне сти в класс HomeContro ll er.
Листинг 2.13. Добавление метода действия для поддержки запросов POST
в файле HomeController. cs

using Sys tem ;


us i ng Mi c r oso f t .AspNet Cor e .Mvc;
using Partyinvites.Models;
name s pace Part yinvi te s. Con troll e r s
p uЫ ic clas s HomeC ont ro lle r : Cont r oll e r
puЫi c ViewResu l t Index( ) {
int ho ur = Date Time . Now .Hou r ;
Vi e wBag . Greeti ng = hou r < 1 2 ? "Good Morn in g " "Good Af te r noon ";
r e t u rn View ( "MyView" ) ;

[HttpGet]
puЫ i c ViewRe s ul t RsvpFo rm () {
r et ur n Vi ew ( ) ;

[HttpPost]
puЬlic ViewResult RsvpForm(GuestResponse guestResponse)
//Что сделать: сохранить ответ от гостя
return View () ;
50 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

Существующий метод действия RsvpFo r m ( ) был снабжен атрибутом HttpGet,


который указывает MVC на то, что данный метод должен применяться только для
запросов GET . Затем была добавлена перегруженная версия метода Rsvp Form ( ) ,при­
нимающая объект Gu e s tRe s ponse. К ней был применен атрибут Http Post , который
сообщает МVС о том, что этот новый метод будет иметь дело только с запросами POST.
Произведенные добавления объясняются в последующих разделах. Кроме того, было
импортировано пространство имен Pa r t y i n v i t es. Mo del s . Это сделано для то г о,
чтобы на тип модели Gues t Respon se можно было ссылаться без н е обходимости в
указании полностью определ е нного имени класса.

Использование привязки модели


Первая перегруженная версия метода действия Rs vp Form () визуализирует то же
самое представление, что и ранее (файл RsvpF orm . cs h t ml), для генер ации формы,
показанной на рис. 2.17. Вторая перегруженная версия более интересна из -за нали­
чия параметра. Но с учетом того, что этот метод действия будет вызываться в ответ
на НТГР-запрос POST, а тип GuestResponse является классом С# , каким образом они
соединяются между с обой?
Секрет кроется в привязке модели - чрезвычайно полезной функциональной воз­
можности МVС, посредством которой производится разбор входящих данных и при­
менение пар "ключ/значение " в НТГР-запросе для заполнения свойств в типах моде­
лей предметной области.
Привязка модели - мощное и настраиваемое средство, которое избавляет от кро­
потливого и тяжелого труда по взаимодействию с НТГР-запросами напрямую и поз­
воляет работать с объектами С#, а не иметь дело с индивидуальными значениями
данных, отправляемыми браузером. Объект Guest Resp o ns e , который передается
этому м етоду действия в качестве параметра, автоматически заполня ется данными
из полей формы. Привязка модели, включая ее настройку, подробно рассматривается
в главе 26.
Одной из целей приложения является предоставление итоговой страницы с дета­
лями о том, кто придет на вечеринку, что означает необходимость сохранения полу­
чаемых ответов. Мы собираемся делать это с помощью созданной в памяти коллекции
объектов . В реальном приложении такой подход не подойдет, т.к. данные ответов бу­
дут утрачиваться в результате останова или перезапуска приложения , но он позволя­

ет сосредоточить внимание на MVC и создать приложение, которое может быть легко


сброшено в свое начальное состояние.

Совет. В главе 8 будет продемонстрировано использование MVC для постоянного хранения


и доступа к данным как часть более реалистичного примера приложения.

Мы добавили в проект новый файл, щелкнув правой кнопкой мыши на папке


Mod e l s и выбрав в контекстном меню пункт Add9Class (Добавить9Класс). Файл име­
ет имя Repo si tory . cs и содержимое, показанное в листинге 2.14.

Листинг 2.14. Содержимое файла Reposi tory. cs из папки Models

u s in g System . Collec ti o n s .Ge ne ric;


names p ace Par t yi nvit es.Models {
puЫ i c sta ti c cla s s Repo sit ory
p r ivate stat ic List<Gues t Response > responses new Li s t<Gue s tResponse>() ;
Глава 2. Ваше первое прило же ние MVC 51
puЫic static IEnumeraЫe<GuestResponse> Responses {
get {
return responses;

puЫic static void AddResponse(GuestResponse resp onse) {


responses.Add(response);

Класс Repos i tory и его члены объявлены статическими, чтобы облегчить сохра­
нение и извлечение данных из разных мест приложения. Инфраструктура МVС пред­
лагает более сложный подход к определению общей функциональности, называемый
внедрением зависимостей, который будет описан в главе 18, но для простого прило­
ж е ния вроде рассматриваемого вполне достаточно и статического RЛасса.

Сохранение ответов
Теперь , когда есть куда сохранять данные, можно обновить метод действия, кото­
рый получает НТГР-запросы POST (листинг 2.15).

Листинг 2.15. Обновление метода действия в файле HomeController. cs

using System;
using Microsoft . AspNetCore . Mvc ;
using Partyinvites .Mode l s;
namespace Partyinvites . Controlle rs
puЫic class HomeContro l ler : Controller
puЫic ViewResult Index() {
int hour = DateTime .Now. Hour ;
ViewBag . Greeting = h our < 12 ? "Good Morning" " Good Afternoon ";
return View ( " MyView " ) ;

[HttpGet]
puЫic ViewResult RsvpForm() {
return View () ;

[HttpPost]
puЫic ViewResult RsvpForm(GuestResponse g uest Response) {
Repository.AddResponse(guestResponse);
return View ( "Thanks", guestResponse) ;

Все, что необходимо сделать с данными формы , посланными в запросе -


это передать методу Repos i tory. AddResponse () в качестве аргумента объе1п
GuestResponse , который был передан методу действия, чтобы ответ мог быть
сохранен.
52 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

Почему привязка модели не похожа на инфраструктуру Web Forms


В главе 1 было указано , что одним из недостатков традиционной инфраструктуры ASP.NET Web
Forms было сокрытие деталей НТТР и HTML от разработчиков . Вас может интересовать , делает ли
то же самое привязка модели MVC, которая применялась для создания объекта GuestResponse
из НТТР-запроса POST в листинге 2.15.
Нет, не делает. Привязка модели освобождает нас от решения утомительной и подверже нной
ошибкам задачи по инспектированию НТТР-запроса и извлечению всех требующихся значений
данных, но (что самое важное) если мы хотим обрабатывать запрос вручную, то могли бы это
делать, поскольку MVC предоставляет легкий доступ ко всем данных запроса. Ничто не скрыто
от разработчика, но есть несколько удобных средств, которые упрощают работу с НТТР и HTML;
тем не менее , использовать их вовсе не обязательно.

Это может показаться едва заметной разницей, но по мере углубления знаний инфраструктуры
MVC вы увидите, что практика разработки в ней полностью отличается от традиционной инфра­
структуры Web Forms и вы всегда осведомлены о том, как обрабатываются получаемые прило­
жением запросы .

Вызов метода View () внутри метода действия RsvpForm () сообщает МVС о том,
что нужно визуализировать представление по имени Thanks и передать ему объект
GuestResponse. Чтобы создать это представление, щелкните правой кнопкой мыши
на папке Views/Home в окне Solution Explorer и выберите в контекстном меню пункт
Addc:::>New ltem (Добавить<=:> Новый элемент). Укажите шаблон MVC View Page (Страница
представления MVC) из категории ASP.NET, назначьте ему имя Thanks. cshtml и
щелкните на кнопке Add (Добавить) . Среда Visual Studio создаст файл Views/Home/
Thanks . cshtml и от1<роет его для редактирования. Поместите в файл содержимое,
приведенно е в листинге 2.16.
Листинг 2.16. Содержимое файла Thanks. cshtml из папки Views/Home
@model Partyinvites.Models.GuestResponse
@{
Layout = null ;

<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content= "width=device - width " />
<title>Thanks</title>
</head>
<body>
<р>

<hl>Thank you , @Model . Name!</h l >


@if (Model.WillAttend == true) {
@:It' s great that you ' re coming . The drinks are already in the fridge!
else {
@: Sorry to hear that you can ' t make it, but thanks for letting us know.
}
</р>
<p>Click <а asp - action= "ListResponses " >here</a> to see who is coming . </р>
</body>
</html>
Глава 2. Ваше nервое приложение MVC 53
Представление Thanks. csh t ml применяет механизм визуализации Razor для
отображения содержимого на основе значения свойства Gu es t Re s p o ns e , которое пе­
редается методу View () внутри Rsvp Fo r m () . Выражение @mode l синтаксиса Razor
ука зывает тип модели предметной области, с помощью которого представление стро ­
го т ипизировано.

Для доступа к значению свойства в объекте предметной области используется


конструкция Model . ИмяСвойст в а . Например, чтобы получить значение свойства
Name , применяется Mod e l. Name . Не беспокойтесь, если синтаксис Razor пока не по­
нятен - он более подробно объясняется в главе 5.
Теперь. когда создано представление Th a nk s, появился работающий базовый при­
мер обработки формы посредством МVС. Запустите приложение в Visual Studio, вы­
брав в меню Debug пункт Start Debugg ing, щелю-rите на ссылке RSVP Now, введите на
форме какие-нибудь данные и щелкните на кнопке Submit RSVP. Вы увидите резуль­
тат, показанный на рис. 2.18 (он может отличаться, если введено другое имя либо
указано о невозможности посетить вечеринку).

Thank you, Joe!


I Yat1r pl1011e: ~5.~:~234===.:._ -=] It's gi·e~r tlint yat1'r~ co111ing. llie drщks are already iн tl1e fr1dge!
1 \\~ill уа11 ntte11d? г~;~ ii~i~e~~ ~~-- Click~ !О see \ V!IO is сошiнg.
~ uьmit R~~-~ L______Г _____________________ _J

1
[___ ---- ---- -------------- --------------- ------ _,
Рис. 2.18. Представление Tha n ks

Отображение ответов
В конце представления Th a nks. c sht ml мы добавили элемент а для создания ссыл­
ки. которая позволяет отобразить список людей, собирающихся посетить вечеринку .
С применением атрибута дескрипторного вспомогательного класса a s p -acti o n со­
здается URL, который нацелен на метод действия по имени Lis t Re s pon ses ():

<p>C l ick <а asp-action="ListResponses" >h ere</a >


to see who is comin g.< /p >

Наведя курсор мыши на с сылку, rшторую отображает браузер, вы заметите, что


она указывает на URL вида / Home/Lis tRe s pon s e s . Это не соответствует ни одно­
му методу действия в контроллере Н оте , и если вы щелкнете на ссылке, то увиди ­
те пустую страницу. Открыв инструменты разработки браузера и просмотрев от­
в ет, присланный сервером, легко обнаружить , что сервер отправил обратно ошибку
404 - Not Foun d (404 - не найдено) . (Браузер Chrome несколько странен тем, что не
отображает сообщение об ошибке для пользователя, но в главе 14 будет показано, как
генерировать содержательные сообщения об ошибках.}
54 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

Мы устраним проблему путем создания в контроллере Home метода действия, на


который нацелен URL (листинг 2.1 7).

Листинг 2.17. Добавление метода действия в файле HomeController. cs

using System;
using Microsoft.AspNetCore . Mvc ;
using Partyinvites.Models;
using System.Linq;
namespace Partyinvites . Controllers
puЫic class HomeController : Controller
puЫic ViewResult Index() {
int hour = DateTime .Now .H our;
ViewBag. Greeting = hour < 12 ? "Go od Morning " "Good Afternoon ";
return View ( " MyView " ) ;

[HttpGet]
puЫic ViewResult RsvpForm() {
return View();

[HttpPost]
puЫic ViewResult RsvpForm(GuestResponse guestResponse) {
Repository . AddResponse(guestResponse);
return View ( "Thanks", guestResponse) ;

puЫic ViewResult ListResponses() {


return View(Repository.Responses.Where(r => r.WillAttend == true));

Новый метод действия называется ListResponses ();он вызывает метод View (),
используя свойство Reposi tory. Responses в качестве аргумента. Именно так метод
действия предоставляет данные строго типизированному представлению . Коллекция
объектов GuestResponse фильтруется с применением LINQ, так что используются
только ответы с положительным решением об участии в вечеринке.
Метод действия ListResponses не указывает имя представления, которое долж­
но применяться для отображения коллекции объектов GuestResponse, поэтому бу­
дет задействовано соглашение об именовании и MVC инициирует поиск представ­
ления по имени ListResponses. cshtml в папках Views/Home и Views/Shared .
Чтобы создать представление, щелкните правой кнопкой мыши на папке Views/
Home в окне Solution Explorer и выберите в контекстном меню пункт AddqNew ltem
(ДобавитьQ Новый элемент). Выберите шаблон MVC View Page (Страница представле­
ния MVC) из категории ASP.NET, назначьте ему имя ListResponses. cshtml и щелк­
ните на кнопке Add (Добавить). Приведите содержимое нового файла представления
в соответствие с листингом 2.18.
Глава 2. Ваше первое прило жение MVC 55
Листинг 2.18. Отображение принятых приглашений в файле ListResponses. cshtrnl
из папки Views/Home

@model IEnumeraЬle<Partyinvites.Models . GuestResponse>

@{
Layout = null;

< !DOCTYPE html >


<html>
<head>
<meta name="viewport " content= " width=device-width " />
<title>Responses</title>
</head>
<body>
<h2>Here is the list of people attending the party</h2>
<tаЫе>
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th>Phone</ th >
</tr>
</thead>
<tbody>
@foreach (Partyinvites.Models.GuestResponse r in Model) {
<tr>
<td>@r .Name </td>
<td>@r .Email</td>
<td>@r . Phone</td>
</tr>

</tbody>
</tаЫе>
</body>
</html>

Файлы представлений Razor имеют расширение cshtml, потому что содержат


смесь кода С# и элементов HTML. Это можно заметить в листинге 2.18, где исполь­
зуется цикл foreach для обработки всех объектов GuestResponse, которые метод
действия передает представлению с применением метода View () . В отличие от нор­
мального цикла foreach языка С# тело цикла foreach из Razor содержит элементы
HTML, добавляемые к ответу, который будет отправлен обратно браузеру. В данном
представлении для каждого объекта GuestResponse генерируется элемент tr , кото­
рый содержит элементы td, заполненные значениями свойств объекта.
Чтобы увидеть список в работе, запустите приложение, выбрав в меню Debug
пункт Start Debugging, отправьте какие-то данные формы и затем щелкните на ссыл­
ке для просмотра списка ответов. Отобразится сводка по данным, введенным вами с
момента запуска приложения (рис . 2.19). Представление не оформляет данные при­
влекательным образом, но пока этого вполне достаточно, а стилизацией мы займемся
позже в главе.
56 Часть 1. Введение в инфраструктуру ASP.NET Соге MVC

1· ReJ: ponses х

Не1·е is the list of people attending the party

E ш a il Phont>
Joe joe,rg. exiuнp le . co 111 555- 1234
Alice al i ce@e.xaшple . coш 555-5678
1

-----·--------·--------------·---__J
Рис. 2. 19. Отображение списка участников вечеринки

Добавление проверки достоверности


Теперь мы готовы добавить в приложение проверку достоверности вводимых дан­
ных . В отсутствие прове рки достове рности пользователи смогут вводить бессмыслен­
ные данные или даже отправлять пустую форму . В приложении МVС пров е рка до­
стоверности обычно прим еня ется к модели предметной области, а не производится
в пользовательском инт ерфейс е. Это з начит, что проверка достоверности опр еделя­
ется в одном месте , но оказывает воздействие в приложении везде, где испол ьзует­
ся класс модели. Инфраструктура MVC поддерживает де кларативные правила про­
верки дос товерности, опр еделенные с помощь ю атрибутов из пространства им е н
System . Componen t Mode l. DataAnnotations , т.е. ограничения проверки до стовер­
ности выражаются посредством стандартных атрибутов С#. В листинге 2.19 пока з а ­
но, как применить эти атрибут ы к классу модели GuestRespon se.

Листинг 2.19. Применение проверки достоверности в файле GuestResponse. cs

using System.Componentмodel.DataAnnotations;

name s pace Pa rt yi n vi t es . Models


p u Ыi c clas s 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 Пожалуйста , введите свой адрес электрон н ой почты
p u Ыic s t ri n g Email { get ; set ; }
[Required (ErrorMessage = "Please enter your phone numЬer")]
11 П ожа луй с та, вве дите свой н омер т елефо н а
puЫic st ri ng Phone { get ; set ; J
[Required (ErrorMessage = "Please specify whether you' 11 attend")]
11 П ожалуйс т а , укажите , п риме т е л и учас т ие
puЫ i c bool? WillAttend { get ; set ; }
Глава 2. Ваше первое приложение MVC 57
Инфраструктура МVС автоматически обнаруживает атрибуты проверк и достовер­
ности и использует их для проверки данных во время процесса привязки модели. Мы
импортировали пространство имен, которое содержит атрибуты проверки достовер­
ности, так что к ним можно обращаться, не указывая полные имена.

Совет. Как отмечалось ранее, для свойства


WillAttend был выбран булевский тип , допус­
кающий null. Это было сделано для того, чтобы можно было применить атрибут провер­
ки Required. Если бы использовался обычный булевский тип, то значением, получаемым
посредством привязки модели, могло быть только true или false, и отсутствовала бы
возмож ность определить, выбрал ли пользователь значение. Булевский тип, допускающий
null , имеет три разрешенных значения: true, false и null. Браузер отправляет зна­
чение null, если пользователь не выбрал значение, и тогда атрибут Required сообщит
об ошибке проверки достоверности . Это хороший пример того, насколько элегантно ин­
фраструктура MVC сочетает средства С# с HTML и НТТР.

Проверку на наличие проблемы с достоверностью данных можно выполнить с


применением свойства ModelState. IsValid в классе контроллера. В листинге 2.20
показано, как это реализовано в методе действия RsvpForm (), поддерживающем за­
просы POST, внутри класса контроллера Home.
Листинг 2.20. Проверка на наличие ошибок проверки достоверности для данных
формы в файле HorneController. cs

using System;
using Microsoft.AspNetCore.Mvc;
using Partyinvites.Models;
using System.Linq;
namespace Partyinvi tes.Controllers
puЫic class HomeController : Controller
puЫic ViewResult Index() {
int hour = DateTime.Now.Hour;
ViewBag. Greeting = hour < 12 ? " Good Morning" "Good Afternoon";
return View ( "MyView " ) ;

[HttpGet]
puЬlic ViewResul t RsvpForm () {
return View () ;

[HttpPost]
puЫic ViewResult RsvpForm(GuestResponse guestResponse)
if (ModelState.IsValid) {
Repository.AddResponse(guestResponse) ;
return View("Thanks", guestResponse) ;
else {
11 Обнаружена ошибRа проверки достоверности.

return View();

puЬlic ViewResult ListResponses() {


return View(Repository .Response s .Where (r => r.WillAttend true));
58 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

Базовый класс Controller предоставляет свойство по имени ModelState, ко­


торое сообщает информацию о преобразовании данных НТГР-запроса в объекты С#.
Если свойство ModelState. IsValue возвращает true, то известно, что инфраструк­
тура МVС сумела удовлетворить ограничения пров ерки достоверности, указанные че­
рез атрибуты для класса GuestResponse. Когда такое происходит, визуализируется
представление Thanks, как делалось до того.
Если свойство ModelState . IsValue возвращает false, то известно, что есть
ошибки проверки достоверности. Возвращаемый свойством ModelState объект пре­
доставляет подробные сведения о каждой возникшей проблеме, но нам нет нужды
погружаться на такой уровень деталей, поскольку мы можем полагаться на удобное
средство, которое автоматизирует процесс указания пользователю на необходимость
решения любых проблем, вызывая метод View () без параметров.
Когда MVC визуализирует представление, механизм Razor имеет доступ к дета­
лям любых ошибок проверки достоверности, связанных с запросом, и дескриптор­
ные вспомогательные классы могут обращаться к этим деталям, чтобы отображать
сообщения об ошибках пользователю. В листинге 2.21 приведено содержимое файла
представления RsvpForm . cshtml с добавленными атрибутами дескрипторных вспо­
м ог ательных классов для проверки достоверности.

Листинг 2.21. Добавление сводки по проверке достоверности в файл RsvpForm.cshtml

@model Partyinvites . Models . GuestResponse


@{
Layout = null ;
)
< !DOCTYPE html>
<html>
<head>
<meta name= " viewport " content= " width=device - width " />
<title>RsvpForm</ti t le>
</head>
<body>
<form asp-action= " RsvpForm " method="post">
<div asp-validation-summary= 11 All 11 ></div>
<р>

<label asp-for= " Name " >Your name:</label>


<input asp-for="Name " />
</р>
<р>

<label asp-for= " Email " >Your email : </label>


<input asp-for="Email" />
</р>
<р>

<label asp-for="Phone " >Your phone:</label>


<input asp - for= " Phone " />
</р>
<р>

<label>Will you attend?</label>


<select asp-for= " WillAttend " >
<option value= "" >Choose an option</option>
<option value= " true">Yes , I 'll Ье there</option>
<option value= "false">No, I can ' t come</option>
</select>
Глава 2. Ваше первое приложе ние MVC 59
</р>
<b utton type= " submit " >Submit RSVP</butt on>
</form>
</body>
</html>

Атрибут asp - validation - summary применяется к элементу div и отобра­


жает список ошибок проверки достоверности при визуализации представления.
Значение для атрибута asp - va lidat ion-summar y берется из перечисления по име­
ни Val idationSummary, которое указывает типы ошибок проверки достоверности,
помещаемые в сводку . Мы указали All, что является хорошей отправной точкой для
большинства приложений, а в главе 27 будут описаны другие значения.
Чтобы взглянуть, как работает сводка по проверке достоверности, запустите при ­
ложение, заполните поле Name и отправьте форму. не вводя другие данные. Вы уви­
дите сводку с ошибками, показанную на рис. 2.20.

R\Vl)Form х

1L·-·-- · - - · - - - - - - - - -:i
С ф lo.alhost:S7626/Horпe/Rs11pform '(( 1
1· ~ -_.----·-~ __"_ --=~·~-:..=--·---=-·::=--::-:--:---

1 • Рlеме nller yo11.f ешаi\ acklr~s


· • Please enter )'O\lf phone U11J11Ьer
1 • Рlеме specit)· ,,·he1/1o:r yo11'll :11ta1d

j Yot1r шuue; \~~-=--=-


1 . r -
__--· -=--=]
Yo11.f ешмl : l --. . .

1
Уощ pl1011e: с_-=~ ·
1

1 \\'1 11 уон attend" : С~оо~е ал opli~? • J

1(Jiiixn;1-RsvEJ

[_______:·==--·--·-·----------·-·--··- ----·--·-----J
Рис. 2.20. Отображение ошибок проверки достоверности

Метод действия RsvpFo r m () не визуализирует представление Than ks до тех пор ,


пока не будут удовлетворены все ограничения провер1'и достоверности, примененные
к классу GuestResponse. Обратите внимание, что введенные в поле Name данные
были сохранены и отображены снова при визуализации механизмом Razor представ­
ления со сводкой проверки достоверности. Это еще одно преимущество привязки мо­
дели, и оно упрощает работу с данными формы.

На заметку! Если вы работали с ASP.NET Web Forms, то знаете, что в Web Forms имеется
концепция серверных элементов управления, которые сохраняют состояние, сериализи­

руя значения в скрытое поле формы по имени VIEWSTATE. Привязка модели MVC не
имеет никакого отношения к концепциям серверных элементов управления, обратным от­
правкам или состоянию представления, принятым в Web Forms. Инфраструктура MVC не
внедряет скрытое поле VIEWSTATE в визуализированные НТМL-страницы. Взамен она
включает данные, устанавливая атрибуты value элементов управления input.
60 Часть 1. Введение в инфраструк туру ASP.NET Core MVC

подсветка полей с недопустимыми значениями

Атрибуты дескрипторных вспомогательных классов, которые ассоциируют свойс­


тва модели с элементами, обладают удобным средством , которое можно использовать
в сочетании с привязкой модели. Когда свойство класса модели не проходит проверку
достоверности, атрибуты дескрипторных вспомогательных .классов будут генериро ­
вать несколько отличающуюся НТМL-разметку. Вот элемент input , который генери­
руется для поля Phone при отсутствии ошибок проверки достоверности:

<input type= " text " data-val= " true "


data - val-requ ired= "Pl ease enter your phone nurnber "
id= " Phone " narne=" Phone " value= "" >
Для сравнения ниже показан тот же НТМL-элемент после того, .ка.к пользователь
отправил форму, не введя данные в текстовое поле (что является ошибкой проверки
достоверности, поскольку мы применили к свойству Pho ne класса GuestResponse
атрибут проверки Required):
<input type= " text " class="input-validation-error" data-val= " true"
data-val - required= "Pl ease ente r your phone nurnber" id="Ph one "
narne= " Phone " va lue="" >
Отличие выделено полужирным: атрибут дес.крипторного вспомогательного класса
asp-for добавил .к элементу input .класс по имени input-validation-error . Мы
можем воспользоваться этой возможностью, создав таблицу стилей, которая содер­
жит стили CSS для этого класса и другие стили, применяемые различными атрибу­
тами дескрипторных вспомогательных классов.

По принятому в проектах МVС соглашению статическое содержимое, доставляе­


мое клиентам, помещается в папку wwwroot , подпапки которой организованы по типу
содержимого, так что таблицы стилей CSS находятся в папке wwwroot/css , файлы
JavaScrlpt - в папке wwwroot/j s и т.д.
Для создания таблицы стилей щелкните правой .кнопкой мыши на папке wwwroot/
css в окне Solution Explore r и выберите в контекстном меню пункт AddФNew ltem
(ДобавитьФ Новый элемент). В открывшемся диалоговом окне Add New ltem (Добавление
нового элемента) перейдите в раздел Client-side (Клиентская сторона) и выберите шаб­
лон Style Sheet (Таблица стилей) из списка (рис. 2.21).

P· j
r
~ . !:;~: Sort Ьy. Odault

CJ .а. Туре; C!itnHidt

1
.litnt·sidt
Code
1
HTML P•ge C/1cnt ·side

1 (.tS(adir19 ~ty!t
sh<!d. (CS.S)
HT ML\tyledtfinot1oros
u~cd for1i<h
! t> Onli~
1 @j5 TyptS<ript Fi!e. Clttnt·sidt

l§J' T~ScriptJSX F i lc Clicnt·side

rJ Type5(t1pt JSON Conf19ur111on f1le Cl1cnt·~ide

1
rJ Sowc1 Confi9u11tion f 1lc Clltnt-1idc

1
f N.:iu1~ J
Q d"d-) o;,-;;-i j
L --- - __ J

Рис . 2.21 . Создание таблицы стилей CSS


Глава 2. Ваше первое приложение MVC 61

Совет. Среда Visual Studio создает файл style . css в папке wwwroot/css при созда­
нии проекта с использованием шаблона Web Application. В этой главе данный файл не
задействован.

Назначьте файлу имя styl es . css, щелкните на кнопке Add (Добавить), чтобы со­
здать файл таблицы стилей, и приведите его содержимое к виду, показанному в лис­
тинге 2.22.

Листинг 2.22. Содержимое файла styles. css

. field - validation-error {color : #fOO ; }


. field - validation - val id { display : none;
. input - validation- error { border : lpx solid #fOO ; background-color : #fee ; }
. validation - summary - errors { font-weight : bold ; color : #fOO ; }
. validation-summary-valid { display : none; }

Чтобы применить эту таблицу стилей, мы добавили элемент li nk в раздел head


представления RsvpForm (листинг 2.23).

Листинг 2.23. Применение таблицы стилей в файле RsvpForrn.cshtml

<head>
<meta name="viewport " content= "width=device-width" />
<title>RsvpForm</title>
<link rel="stylesheet" href="/css/styles.css" />
</head>

Элемент link использует атрибут href для у1<азания местоположения таблицы


стилей. Обратите внимание, что папка wwwroot в URL опущена. Стандартная кон­
фигурация ASP.NET вrшючает поддержr<у обслуживания статического содержимого,
такого как изображения, таблицы стилей CSS и файлы JavaScrlpt , которая автомати­
чески отображает запросы на папку wwwroot. Процесс конфигурации ASP.NEТ и МVС
рассматривается в главе 14.

Совет. Для работы с таблицами стилей предусмотрен специальный дескрипторный вспомо­


гательный класс, который может оказаться полезным при наличии многочисленных фай­
лов, требующих управления . Подробности ищите в главе 25 .

Благодаря применению таблицы стилей в случае отправ1ш данных, которые вызы­


вают ошибки проверки достоверности, отображается визуально боле е ясные сообще­
ния об ошибках (рис. 2.22).

Стилизация содержимого
Все цели приложения , касающиеся функциональности, достигнуты , но его общий
вид оставляет желать лучшего. Когда вы создаете проект с использованием шаблона
Web Application, как в текущем примере , Visual Studio устанавливает несколько рас­
пространенных пакетов для разработки на стороне клиента. Хотя я не являюсь сто­

ронником применения шаблонов проектов, мне нравятся выбранные Microsoft библи-


62 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

отеки клиентской стороны. Одна из них называется Bootstrap и представляет собой


удобную инфраструктуру CSS, первоначально разработанную в 'I\vitter. которая пос­
тепенно превратилась в крупный проект с открытым кодом и стала главной опорой
разработки веб-приложений.

Rsvpioim Х

С
,-- ---- -·-- - ·------·- -----·- . -
LФ locJ!lюst:S7628/НQ111e/RsvpForm
- -
. ~- ... --~ -. ___ ._.._."-;-::- ~- _.., ·-~~:-·- --···- - -::·-:·;::::-.:-- ... ·::--..::.::
• Plta8• 1.>ntft' ~·out· t>m.;iiJ addi'\'и
• Р1Е-а~.- e11ftr :;1·ощ· phoot' numbtr
• Pl•:a~• \pc.>('if·y " 'htther ~·ou 'U atttod
1
--- -"
!
Yo\U' eшail: 1
....- - - - - =:J 1
Your pl1oue: 1

1 \ViU >"• '"'""*"""' "''"''" 1 1

1 [ suьn~li'RsiiP]
1

L____,___________.__________ J
Рис. 2.22. Автоматическая подсветка полей с ошибками проверки достоверности

На заметку! На момент написания этой книги текущей версией была Bootstrap 3, но версия 4
находилась на стадии разработки. В Microsoft могут принять решение обновить версию
Bootstrap, используемую шаблоном Web Application, в последующих выпусках Visual
Studio, что может привести к отображению содержимого по-другому. В других главах кни­
ги это не должно стать проблемой, т.к. там будет показано, каким образом явно указать
версию пакета , чтобы получить ожидаемые результаты .

Стилизация начального представления


Базовые средства Bootstrap работают за счет применения классов к элементам,
которые соответствуют селекторам CSS, определенным внутри добавленных в папку
wwwroot/liЬ/bootstrap файлов. Подробную информацию о классах, определенных
в библиотеке Bootstrap, можно получить на веб-сайте http: / / getbo ots tr ap . с от , а
в листинге 2.24 демонстрируется использование нескольких базовых стилей в пред­
ставлении MyView. cshtml.

Листинг 2.24. Добавление классов Bootstrap в файл МyView. cshtml

@{
Layout = null;

< ! DOCTYPE html>


Глава 2. Ваше первое прило жение MVC 63
<h t ml>
<head>
<meta name= "vi e wpo r t " content=" widt h=dev i ce - width " />
<title>Index</ ti tle>
<link rel="stylesheet" href="/lib/Ьootstrap/dist/css/Ьootstrap.css" />
</head>
<body>
<div class="text-center">
<hЗ>We're going to have an exciting party!</hЗ>
<h4>And you are invited</h4>
<а class="Ьtn Ьtn-primary" asp-action="RsvpForm">RSVP Now</a>
</d i v>
</body>
</html>

В разметку был добавлен элемент link, атрибут hr ef которого загружает файл


bootstrap . css и з папки wwwroot/liЬ/boot s trap/dist/css . По соглашению пак е ­
ты CSS и JavaScript от независимых поставщиков устанавливаются в папку www roo t /
lib , и в главе 6 будет описан инструмент, предназначенный для управления такими
пакетами.

После импортирования таблиц стилей Bootstrap осталось стилизовать элементы.


Рассматрива емый пример прост, поэтому необходимо использовать совсем немного
классов CSS из Bootstrap: text - cen ter, Ьtn и Ьtn -p rima r y .
Кл асс text - ce n ter центрирует содержимое элемента и его дочерних элементов.
Класс Ьtn стилизует эл емент but t on , inpu t или а в виде симпатичной кнопки. а
класс Ьtn - primary указывает диапазон цветов для этой кнопки . Запустив прил оже­
ние, можно увидеть результат, показанный на рис. 2.23 .
Вам должно быть вполне очевидно , что я - не веб-диз айнер . На самом деле, буду­
чи еще ребенком , я был освобожден от уроков рисования по при чине пол ного отсутс­
твия таланта. Это произвело благоприятный эффект в виде того, что я стал уделять
больш е времени урокам математики, но вместе с тем мои художественные навыки не
развивались примерно с десятилетнего во зраста. Для р е альных проектов я обратился
бы к помощи профессионального дизайнера содержимого, но в настоящем примере я
собираюсь делать все самостоятельно и применять Bootstrap с максимально возмож­
ной сде р ж анностью и с огласованностью, на какую толь ко способен.

lndex х

С !. ф localhost:57628

We're going to have an exciting party!


And you are invited
11
И1Р!~'
_______ ________J
Рис. 2.23. Стилизация представления
64 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

Стилизация представления RsvpForm


В Bootstra p определ е ны классы , которые могут использоваться для стилиз ации
форм . Я н е планирую вдаваться в особые детали, но в листинге 2.25 показано, как
были прим енены эти классы.

Листинг 2.25. Добавление классов Bootstrap в файл RsvpForm. cshtml

@mode l Partyinvite s. Mode l s . Gu e st Res ponse


@{
Layout = n ull ;

< ! DOCTYPE htm l >


<html>
<h ead>
<meta name= " viewport " content= " width =devic e- width " />
<t i tle>Rsvp Form</t i t l e>
<l i n k re l = " s tyleshee t" href =" /cs s /sty l es . css " />
<link rel="stylesheet" href="/lib/Ьootstrap/dist/css/Ьootstrap.css" />
</ h ead>
<bod y>
<div class="panel panel-success">
<div class="panel-heading text-center"><h4>RSVP</h4></div>
<div class="pane l-Ьody">
<form class="p-a-1" asp-action="RsvpForm" method="post">
<div asp-va lidation-swnmary="All"></div>
<div class="form-group">
<laЬe l asp-for="Name">Your name:</laЬe l>
<input class= "form-control" asp-for= "Name" />
</div>
<div cla ss= "form-group">
<laЬel asp-for=" Email">Your email:</laЬel>
<input class="form-control" asp-for="Email" />
</div>
<div class="form-group">
<laЬel asp-for="Phone">Your phone:</laЬel>
<input class="form-control" asp-for="Phone" />
</div>
<div class="form-group">
<laЬel>Will you attend?</laЬel>
<select class="form-control" asp-for="WillAttend">
<option value="">Choose an option</option>
<option value="true">Yes, I'll Ье there</option>
<option value="false">No, I can ' t come</option>
</select>
</div>
<div class="text-center">
<Ьutton class="Ьtn Ьtn-primary" type="suЬmit">
SuЬmit RSVP
</Ьutton>
</div>
</f orm>
</div>
</div>
</body>
</html>
Глава 2. Ваше первое приложе ние MVC 65
Классы Bootstrap в этом примере создают заголовок, просто чтобы придать компо­
новке структурированность. Для стилизации формы используется класс form-group,
J{Оторый стилизует элемент, содержащий label и связанный элемент input или
select. Результаты стилизации можно видеть на рис. 2.24.

1' .~
.

YourpJюne:
!
1
j

1
W1ll you attend? j

·-
Choose an opuon
1
1 1
!
1
1
1
___ j
L . -----~-·-~------·-----·-----··-·-- · - - - - -

Рис. 2.24. Стилизация представления RsvpForm

Стилизация представления Тhanks


Следующим стилизуемым представлением является Thanks. cshtml; в листин­
ге 2.26 показано, как это делается с применением классов CSS, подобных тем, которые
использовались для других представлений. Чтобы облегчить управление приложени­
ем, имеет смысл избегать дублирования кода и разметки везде, где только возможно .
Инфраструктура MVC предлагает несколько средств, помогающих сократить дубли­
рование, которые рассматриваются в последующих главах. К таким средствам отно­
сятся компоновки Razor (глава 5), частичные пр едставления (глава 21) и компоненты
представлений (глава 22).
Листинг 2.26. Добавление классов Bootstrap в файл Thanks. cshtml

@model Partyinvites . Models . GuestResponse


@{
Layout = null;

<!DOCTYPE html>
<h tml>
<hea d >
<meta n ame= "viewp ort " content= " width=device - width " />
<title>Thanks</title>
<link rel="stylesheet" href="/lib/bootstrap/dist/css/bootstrap.css" />
</head>
66 Часть 1. Введение в и нфрас тр уктуру ASP.NET Core MVC

<body class="text-center">
<р>

<hl>Thank you , @Model . Name!</hl>


@if (Mode l. WillAttend == true) {
@: It ' s great that you ' re coming. The drinks are already in the fridge!
else {
@: Sorry to hear that you can ' t make it, but thanks for letting us know.
}
</р>
Click <а class="nav-link" asp-action="ListResponses">here</a>
to see who is coming .
</body>
</html>

На рис. 2.25 показан результат стилизации.

Thank you, Joe!


ll's great that you're coming. The drinks are already in tl1e fridge'
Click t1ere to see who is coming.

- - - - - - - - - - - - - - - - - - - - - - - - - - - --J

Рис. 2.25. Стилизация представления Thanks

Стилизация представления Lis tResponses


Последним мы стилизуем представление ListResponses, которое отображает
список участников вечеринки . Стилизация содержимого следует тому же ба зо вому
подходу, который применялся в отношении всех стилей Bootstrap (листинг 2.27).

Листинг 2.27. Добавление классов Bootstrap в файл ListResponses. cshtml


@model IEnumeraЬle<Partyinvites.Models . Gue s tResponse>

@{
Layout = null ;

<!DOCTYPE html>
<html>
<head>
<meta name= "viewport " content= "widt h=device- widt h" />
<link rel="stylesheet" href="/lib/bootstrap/dist/css/bootstrap.css" />
<tit l e>Responses</t itl e>
</head>
<body>
<div class="panel-body">
<h2>Here is the l ist of people attending the party</h2>
Глава 2. Ваше nервое nриложение MVC 67
<tаЫе class="taЫe taЫe-sm taЫe-striped taЫe-Ьordered">
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th>Phone</th>
</tr>
</ the ad>
<tbody>
@foreach (Partyinvites .Models . GuestResponse r in Mode l) {
<tr>
<td>@r.Name</td>
<td>@r . Email</td>
<td>@r . Phone</td>
</tr>

</tbody>
</tаЫе>
</div>
</body>
</html>

На рис . 2.26 показано, как теперь выглядит таблица участников . Добавление сти­
л ей к данному представлению завершает пример приложения , которое теперь достиг­
ло всех целей разработки и имеет намного более совершенный внешний вид.

i f- i
1[~ Here is the list of people attending the party
1

1
!
1 Name Email Phone

1 Joe joe@example.com 555-1 234

1 Alico a1ice ~ example . com


1
i ВоЬ bob@example.com 255-2345
i
!
______ - ---·-- ---
[ --·- ___ J
Рис. 2.26. Стилизация представления ListResponses

Резюме
В этой главе был создан новый проект МVС , который использовался для построе ­
ния простого приложения ввода данных МVС, что позволило получить первое пред­
ставление об архитектуре ASP.NET Core MVC и применяемом подходе. Некоторые
о сн о вные ср едства (включая синтаксис Razor, маршрутизацию и тестирование) не
рассматривались, но мы вернемся к этим темам в последующих главах. В следующей
глав е будет описаны паттерны проектирования MVC, которые формируют основу эф­
фективной разр аботки с помощью ASP.NET Core МVС .
ГЛАВА 3
Паттерн МVС,
проекты и соглашения

п режде чемCoreприступить
ASP.NEТ MVC,
к углубленному изучению деталей инфраструктуры
необходимо освоить паттерн проектирования МVС, лежащие
в его основе концепции и способ, которым они транслируются в проекты ASP.NET Core
MVC. Возможно, вы уже знакомы с некоторыми идеями и соглашениями, обсуждае ­
мыми в этой главе, особенно если вам приходилось заниматься разработкой сложных
приложений ASP.NET или С#. Если это не так, тогда внимательно читайте настоящую
главу. Хорошее понимание того, что положено в основу MVC, может помочь увязать
функциональные возможности инфраструктуры с контекстом материала, излагаемо­
го в оставшихся главах книги.

История создания MVC


Термин модель-представление-контроллер (model-view-controller) был в употреб­
лении с конца 1970-х годов и происходит из проекта Smalltalk в Xerox PARC, где он
был задуман как способ организации ряда ранних приложений с графическим поль­
зовательским интерфейсом. Некоторые нюансы первоначального паттерна MVC были
связаны с концепциями, специфичными для Smalltalk, такими как экраны и инстру­
менты, но более широкие понятия по-прежнему применимы к приложениям - и осо­
бенно хорошо они подходят для веб-приложений.

Особенности паттерна MVC


Если оперировать высокоуровневыми понятиями, то паттерн MVC означает, что
приложение MVC будет разделено, по крайней мере, на три указанные далее части.

• Модели, содержащие или представляющие данные, с которыми работают


пользователи.

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


пользовательского интерфейса.

• Контроллеры, которые обрабатывают входящие запросы, выполняют операции


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

Каждая порция архитектуры MVC четко определена и самодостаточна; такое по­


ложение вещей называют разделением обязанностей. Логика, которая манипулиру-
Глава 3. Паперн MVC, проекты и соглашения 69
ет данными в модели, содержится только в модели. Логика, отображающая данные,
присутствует только в представлении. Код, который обрабатывает пользовательские
запросы и ввод. находится только в контроллере. Благодаря ясному разделению
между порциями приложение будет легче сопровождать и расширять на протяже­
нии его времени существования вне зависимости от того, насколько большим оно
станет.

Понятие моделей
Модели (Мв МVС) содержат данные, с которыми работают пользователи. Сущест­
вуют два обширных типа моделей: модели представлений, которые выражают сами
данные, передаваемые из контроллера в представление. и модели предметной облас­
ти, которые содержат данные в предметной области наряду с операциями. транс­
формациями и правилами для создания, хранения и манипулирования данными, все
вместе называемыми логикой моделей.
Модели - это определение "вселенной", в которой функционирует приложение.
Например. в банковском приложении модель представляет все аспекты банковской
деятельности, поддерживаемые приложением, такие как расчетные счета, главная

бухгалтерская книга и кредитные лимиты для клиентов. равно как и операции, ко­
торые могут применяться для манипулирования данными в модели. подобные вне­
сению денежных средств и списанию их со счетов . Модель отвечает также за сохра­
нение общего состояния и целостности данных - например, удостоверяясь, что все
транзакции внесены в главную книгу, а клиент не снимает со счета больше денеж­
ных средств, чем имеет на то право, или больше. чем находится в распоряжении
самого банка.
Для каждого компонента в паттерне MVC будет описано, что он должен включать,
а что не должен.

Модель в приложении, построенном с использованием паттерна МVС, доЛJ1Сна:

• содержать данные предметной области;

• содержать логику для создания, управления и модификации данных предмет­


ной области;

• предоставлять чистый АРI-интерфейс, который открывает доступ к данным мо­


дели и операциям с ними.

Модель не должна:

• показывать детали того, ка~< осуществляется получение или управление дан­

ными модели (другими словами, подробности механизма хранения данных не


должны быть видны контроллерам и представлениям);

• содержать логику. которая трансформирует модель на основе взаимодействия с


пользователем (поскольку это работа контроллера):

• содержать логику для отображения данных пользователю (т.к. это работа


представления).

Преимущества обеспечения изоляции модели от контроллера и представлений за­


ключаются в том, что вы можете гораздо легче тестировать логику (модульное тести­
рование описано в главе 7) и проще расширять и сопровождать приложение в целом.
70 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

Совет. Многих разработчиков, только приступивших к ознакомлению с паттерном MVC, при­


водит в замешательство идея помещения логики в модель данных из - за их уверенности

в том, что целью паттерна MVC является отделение данны х от логики . Это заблуждение:
цель паттерна MVC - разделение приложения на три функциональных области, каждая
из которых может содержать и логику, и данные. Цель не в том, чтобы устранить логику
из модели. Наоборот, цель в том, чтобы гарантировать наличие в модели только логики,
предназначенной для создания и управления данными модели.

Понятие контроллеров
Контроллеры являются "соединительной тканью " паттерна MVC, исполняя роль
каналов между моделью данных и представлениями. Контроллеры определяют дейс­
твия. предоставляющие бизнес-логику, которая оперирует на модели данных и обес­
печивает представления данными, подлежащими отображению для пользователя.
Контроллер, построенный с применением паттерна MVC, должен:

• содержать действия. требующиеся для обновления модели на основе взаимо­


действия с пользователем.

Конт роллер не должен:

• содержать логику, которая управляет внешним видом данных (это работа


представления);

• содержать логику, которая управляет постоянство м данных (это работа


модели).

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

• содержать логику и разметку, необходимые для показа данных польз ов ателю.

Представления не должны:

• содержать сложную логику (ее лучше поместить в контролл е р):

• содержать логику. которая создает, сохраняет или манипулирует моделью пред­

метной области.

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


ваться умеренно. Помещение в представление чего угодно кроме вызовов простейших
методов или несложных выражений затрудняет тестирование и сопровождение при­
ложения в целом.

Реализация MVC в ASP.NET Core


Как подразум евает само название, реализация ASP.NET Core MVC адаптиру­
ет абстрактный п атте рн MVC к ми ру разработки ASP.NET и С#. В инфраструктуре
ASP.NET Core MVC контроллеры - это классы С#, обычно производные от класса
Microsoft . AspNetCore . Mvc . Contro ll er. Каждый открытый метод в производном от
Con t rol le r классе является методом действия. который ассоциирован с каким-то URL.
Глава 3. Паттерн MVC, проекты и соглашения 71
Когда запрос посьmается по URL, связанному с методом действия, операторы в данном ме­
тоде действия выполняются, чтобы провести некоторую операцию над моделью предмет­
ной области и затем выбрать представление для отображения клиенту. Взаимодействия
между контроллером, моделью и представлением проиллюстрированы на рис. 3. 1.

НТТР
Запрос ------------~ _• Постоянство

Модель (обычно с помощью


Контроллер
реляционной
Ответ Представление . -1 - - - - - - - - 1 - - базы данных)
Модель
.___ _ _ ____. представления .__ _ _ ____.

Рис. 3.1. Взаимодействия в приложении MVC

В инфраструктуре ASP.NET Core МVС применяется механизм визуализации, из­


вестный как Razor, который является компонентом, ответственным за обработку
представления для генерации ответа браузеру. Представления Razor - это НТМL­
шаблоны, содержащие логику С#, которая используется для обработки данных мо­
дели с целью генерации динамического содержимого, реагирующего на изменения в

модели. Работа Razor рассматривается в главе 5.


Инфраструктура ASP.NET MVC не налагает никаких ограничений на реализацию
модели предметной области. Вы можете создать модель с применением обычных
объектов С# и реализовать постоянство с использованием любых баз данных, инф­
раструктур объектно-реляционного отображения или других инструментов работы с
данными, поддерживаемых в .NET.

Сравнение MVC с другими паттернами


Разумеется, MVC- далеко не единственный архитектурный паттерн программно­
го обеспечения. Есть много других паттернов подобного рода, и некоторые из них
являются или, по крайней мере, были чрезвычайно популярными. О паттерне MVC
можно узнать много интересного, сравнивая его с альтернативами. В последующих
разделах кратко описаны разные подходы к структурированию приложения и приве­

дено их сравнение с МVС . Одни паттерны будут близкими вариациями на тему MVC,
в то время как другие - совершенно отличаться.

Я не утверждаю, что МVС - идеальный паттерн во всех ситуациях. Я сторонник


выбора такого подхода . .который лучше всего решает имеющуюся задачу. Как вы
вскоре увидите, есть ситуации, когда не.которые конкурирующие паттерны в равной
степени полезны или лучше MVC. Я призываю делать информированный и осознан­
ный выбор паттерна . Сам факт чтения вами этой книги наводит на мысль, что в опре­
деле нной мере вы уже склонны применять паттерн MVC, но вс егда полезно сохранять
максимально широкую точку зрения.

Паттерн ин теллектуального пользовательского интерфейса


Один из наиболее распространенных паттернов проектирования известен как ин­
теллектуальный пользовательский интерфейс (Smart UI). Большинству программис­
тов приходилось создавать приложение с интеллектуальным пользовательским ин­

терфейсом на том или ином этапе своей профессиональной деятельности - меня это
определенно касается. Если вы использовали Windows Forms или ASP.NET Web Forms,
то сказанное относится и к вам.
72 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

При построении приложения с интеллектуальным пользовательским интерфейсом


разработчики конструируют интерфейс, часто перетаскивая набор компонентов или
элементов управления на поверхность проектирования или холст. Элементы управ­
ления сообщают о взаимодействии с пользователем, инициируя события для щелч­
ков на кнопках. нажатий клавиш на клавиатуре, перемещений курсора мыши и т.д.
Разработчик добавляет код реакции на эти события в набор обработчиков событий.
которые являются небольшими блоками кода, вызьmаемыми при выдаче специфичес­
кого события в определенном компоненте. В конечном итоге получается монолитное
приложение, показанное на рис. 3.2. Код. который поддерживает пользовательский
интерфейс и реализует бизнес-логику. перемешан между собой без какого-либо раз­
деления обязанностей. Код, который определяет приемлемые значения для вводимых
данных и запрашивает данные или модифицирует, например, расчетный счет поль­
зователя, оказывается размещенным в небольших фрагментах, связанных друг с дру­
гом в предполагаемом поряДitе поступления событий.

__ ._ Постоянство
Запрос ~ Интеллектуальный
(обычно с помощью
пользовательский
реляционной
Ответ интерфейс
- - базы данных)

Рис. 3.2. Паттерн интеллектуального пользовательского интерфейса

Интеллектуальные пользовательские интерфейсы идеальны для простых проектов,


т.к. позволяют добиться неплохих результатов достаточно быстро (по сравнению с раз­
работкой MVC, которая, как показано в главе 8, требует определенных начальных за­
трат, прежде чем будут доставлены результаты). Интеллектуальные пользовательские
интерфейсы также подходят для построения прототипов пользовательских интерфей­
сов. Их инструменты визуального конструирования могут оказаться по-настоящему
удобными, и если вы обсуждаете с заказчиком требования к внешнему виду и потоку
пользовательского интерфейса, то инструмент интеллектуального пользовательского
интерфейса может быть быстрым и удобным способом для генерации и проверки раз­
личных идей.
Самый крупный недостаток интеллектуальных пользовательских интерфейсов
связан с тем, что их трудно сопровождать и расширять. Смешивание кода модели
предметной области и бизнес-логики с кодом пользовательского интерфейса при­
водит к дублированию, когда один и тот же фрагмент бизнес-логики копируется и
вставляется для поддержки вновь добавленного компонента. Нахождение всех дубли­
рованных фрагментов и применение к ним исправления может быть непростой за­
дачей. Добавление новой функции может оказаться практически невозможным без
нарушения работы каких-то существующих функций. Тестирование приложения с
интеллектуальным пользовательским интерфейсом также может быть затруднено .
Единственный способ тестирования - эмуляция взаимодействия с пользователем ,
что является далеким от идеала и трудно реализуемым фундаментом для обеспечения
полного покрытия тестами.

В мире МVС интеллектуальный пользовательский интерфейс часто называют ан­


типаттерном, т.е. чем-то таким, чего следует избегать любой ценой . Эта антипатия
возникает (по крайней мере, частично) оттого, что к инфраструктуре MVC обращают­
ся в поисках альтернативы после множества не слишком успешных попыток разра-
Глава 3. Паттерн MVC, проекты и соглашения 73
ботки и сопровождения приложений с интеллектуальным пользовательским интер­
фейсом , которые попросту вышли из-под контроля.
Тем не менее, полный отказ от паттерна интеллектуального пользовательского
интерфейса будет заблуждением . В нем не все настолько плохо, и с данным подхо­
дом связаны положительные аспекты. Приложения с интеллектуальным пользова­
тельским интерфейсом быстро и легко разрабатывать. Производители компонентов
и инструментов проектирования приложили немало усилий, чтобы сделать процесс
разработки приятным занятием, поэтому даже совершенно неопытный программист
за считанные часы может получить профессионально выглядящий и достаточно фун­
кциональный результат.
Наибольшая слабость приложений с интеллектуальным пользовательским интер­
фейсом - низкое удобство сопровождения - не проявляется при мелких объемах
разработки. Если вы создаете несложный инструмент для небольшой аудитории, то
приложение с интеллектуальным пользовательским интерфейсом может оказаться
идеальным решением. Просто при этом не гарантируется увеличение сложности, под­
держиваемое приложением MVC.

Архитектура "модель-представление"
В приложении с интеллектуальным пользовательским интерфейсом проблемы со­
провождения обычно возникают в области бизнес-логики. которая настолько рассеяна
по приложению, что внесение изменений или добавление новых функций становится
мучительным процессом. Улучшить ситуацию помогает архитектура "модель-пред­
ставление'', которая выносит бизнес-логику в отдельную модель предметной облас­
ти. Все данные, процессы и правила концентрируются в одной части приложения
(рис. 3.3).

Запрос
_ ._ Постоянство
Пользовательский 1-------1~
(обычно с помощt
интерфейс Модель
реляционной
Ответ (представление) - - базы данных)

Рис. З.З. Архитектура "модель-представление"

Архитектура "модель-представление'" может быть значительным усовершенствова­


нием по сравнению с монолитным интеллектуальным пользовательским интерфей­
сом (скажем, ее гораздо легче сопровождать). но возникают две проблемы. Первая:
поскольку пользовательский интерфейс и модель предметной области тесно пере­
плетены, модульное тестирование любой из этих частей может оказаться затрудни ­
тельным . Вторая проблема проистекает из практики, а не из определения паттерна.
Обычно модель содержит большой объем кода доступа к данным (не обязательно. но в
большинстве случаев) , а это означает, что модель данных содержит не только бизнес ­
данные, операции и правила.

Классическая трехуровневая архитектура

Чтобы решить проблемы, присущие архитектуре "модель-представление'", трехзвен­


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

предметной области и помещает его в новый компонент, который называется уровн.ем


доступа к данным (data access layer - DAL). Сказанное иллюстрируется на рис. 3.4.
74 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

Постоянство
Запрос Пользовательский 1 - - - - . - i Уровень - - •(обычно
интерфейс Модель доступа с помощью

(представление) к данным __ реляционной


Ответ
базы данных)

Рис. 3.4. Трехуровневая архитектура

Трехуровневая архитектура является наиболее широко используемым паттерном


в бизнес-приложениях. Она не устанавливает никаких ограничений на способ реали­
зации пользовательского интерфейса и обеспечивает хорошее разделение обязаннос­
тей , не будучи излишне сложной. Кроме того, ценой некоторых усилий уровень DAL
может быть создан так, чтобы модульное тестирование проводилось относительно
легко. Можно заметить очевидные сходства между классическим трехуровневым при­

ложением и паттерном МVС. Ра зница в том, что когда уровень пользовательского ин­
терфейса напрямую связан с инфраструктурой графического пользовательского ин­
терфейса типа "щелчок-событие" (такой как Windows Forms или ASP.NET Web Forms),
то выполнение автоматизированных модульных тестов становится практически не­

возможным. А поскольку часть пользовательского интерфейса трехуровневого при­


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

В худшем сценарии отсутствие принудительных ограничений, накладываемых


трехуровневой архитектурой на уровень поль зовательско го интерфейса, означает,
что многие такие приложения в итоге оказываются тонко замаскированными при­

ложениями с интеллектуальным пользовательским интерфейсом без какого-либо ре­


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

к тому же и чрезмерно сложно.

Разновидности MVC
Основные принципы проектирования приложений МVС были уже описаны, осо­
бенно те из них, которые применимы к реализации ASP.NET Core МVС. Другие реа­
лизации интерпретируют аспекты паттерна MVC иначе, дополняя, подстраивая или
ка~<-то еще адаптируя его для соответствия области охвата и целям своих проектов .
В последующих разделах мы кратко рассмотрим две наиболее распространенных ва­
риации на тему MVC. Понимание этих разновидностей не имеет особого значения
в случае работы с ASP.NET Саге MVC. Данная информация включена ради полноты
картины, потому что такие термины будут употребляться в большинстве обсуждений
паттернов проектирования ПО .

Паттерн "модель·nредставление-презентатор"

Паттерн "модель-представление-презентатор" (model-view-presenter - МVР) яв­


ляется разновидностью МVС и разработан для того, чтобы облегчить согласование с
поддерживающими состояние платформами графического пользовательского интер­
фейса, такими как Windows Forms или ASP.NEТ Web Forms. Это достойная по пытк а из­
влечь лучшее из паттерна интеллектуального пользовательского интерфейса, избежав
проблем, которые он обычно привносит.
Глава 3. Паттерн MVC, проекты и соглашения 75
В этом паттерне презентатор имеет те же обязанности, что и контроллер МVС, но
он также более непосредственно связан с представлением, сохраняющим информа­
цию о состоянии, напрямую управляя значениями, которые отображаются в компо­
нентах пользовательского интерфейса в соответствии с вводом и действиями пользо­
вателя. Существуют две реализации этого паттерна.

• Реализация пассивного представления, в которой представление не содер­


жит никакой логики. Такое представление служит контейнером для элементов
управления пользовательского интерфейса, которыми напрямую управляет
презентатор .

• Реализация координирующего контроллера, в которой представление может от­


вечать за определенные элементы логики презентации, такие как привязка дан­

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

Отличие между этими двумя подходами касается уровня интеллектуальности


представления . В любом случае презентатор отделен от инфраструктуры графичес­
кого пользовательского интерфейса , что делает логику пр е зентатора более простой и
подходящей для модульного тестирования.

Паттерн "модель-представление-модель представления"

Паттерн "модель-представление-модель представления" (model-view-view model -


МWМ) - это последняя разновидность МVС. Он появился в Microsoft и используется
в инфраструктуре Windows Presentation Foundation (WPF). В паттерне МWМ модели
и представления играют те же самые роли, что и в MVC. Разница связана с присутс­
твующей в МWМ концепцией модели представления, которая является абстрактным
представлением пользовательского интерфейса. Как правило, модель представле­
ния - это класс С#, который открывает доступ к свойствам для данных, подлежащих
отображению в пользовательском интерфейсе, и операциям с данными. инициируе­
мым из пользовательского интерфейса. В отличие от контроллера МVС модель пред­
ставления МWМ не имеет ни малейшего понятия о существовании представления
(или любой конкретной технологии пользовательских интерфейсов). Представление
МWМ применяет средство привязки WPF, чтобы установить двунаправленное соеди­
нение между свойствами , доступ к которым открывают элементы управления в пред­
ставлении (вроде пунктов раскрьmающегося меню или эффекта от щелчка на кнопке),
и свойствами . доступ к которым открывает модель представления.

На заметку! В MVC также используется термин модель представления, но он относится к


простому классу модели, предназначенному только для передачи данных из контроллера

в представление, как противоположность моделям предметной области, которые являют­


ся сложными представлениями данных, операций и правил.

Проекты ASP.NET Core MVC


При создании нового проекта ASP.NET Core МVС среда Visual Studio предлагает
на выбор несколько вариантов начального содержимого, которое желательно иметь
в проекте. Идея в том, чтобы облегчить разработчикам-новичкам процесс обучения
и применить сберегающий время передовой опыт при описании распространенных
средств и задач. Я не поклонник такого подхода в отношении заготовленных про­
ектов или нода. Намерения обычно хороши, но исполнение никогда не приводит в
76 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

восторг. Одной из характеристик, которая мне больше вс его нравит ся в ASP.NET и


MVC, является высочайшая гибкость в подгонке платформы под мой стиль р а зработ­
ки. Процессы, классы и представления, которые создает и наполняет среда Visual
Studio, вызывают у меня чувство , что я вынужден работать в стиле кого-то другого .
К тому же я нахожу содержимо е и конфигурацию слишком обобщенными и прими­
тивными, чтобы приносить хоть какую-нибудь пользу. Разработчики в Microsoft не
могут знать, какой вид приложения необходим, поэтому они охватьmают все основы,
но настолько обобщенным путем. что в итоге я просто избавляюсь от всего ст андар­
тного содержимого.

Моя рекомендация (всем, кто по недоразумению спросил) заключается в том, что­


бы начинать с пустого проекта и добавлять нужные папки , файлы и пакеты. Вы не
только узнаете больше о способе работы МVС, но и будете обладать полным контро­
лем над тем , что содержит ваше приложение .

Но мои предпочтения н е должны формировать вашу практику разработки. Вы мо­


жете счесть шаблоны бол е е удобными, чем я. особенно если явля етесь новичком в
разработке ASP.NET и е ще не выработали стиль, который устраивает лично вас. Вы
может е такж е признавать шаблоны проектов пол е зным ресурсом и источником идей,
хотя должны проявлять осмотрительность и не добавлять функциональность к прило­
жению до того , как полностью поймете, каким образом она работает.

Создание проекта
Когда вы впервые создаете новый проект ASP.NET Core, то имеете три базовых от­
правных точки. из которых можно выбирать: шаблон Empty (Пустой), шаблон Web API
и шаблон Web Application (Веб-приложение), как показано на рис. 3.5.

r---------------------.,
Stftct а tempfote:

ASP.NEТ Core Te mplates 1' /


1 А project templ•I• for cre•t•ng an ASP.NEТ Coro
•ppltcat1on wrth ""ampfe ASP.NET MVC Vil!\.,; and
Controllers. This ttmplate can afso Ье used for RESTTul
I ~ ~ ij 1' НТТР servtces.
1 Empty Web API Web
Applicatton
1 Ш!!!.т.ш
1
1
1 l~c-h~
an-g-
eA_ut
_h_e_
.nt-ic-
al-
io~
n1
i ______________________
L Authe:nticotio n: No AuthentkatIOn
1

<;!) Microsoft Azure


G О Host in the cloud

~OK~,, ~J
~-5:_rvice --]

Рис. 3.5. Шаблоны проектов ASP.NET Core

Шаблон проекта Empty содержит связующие механизмы для инфраструктуры


ASP.NET Core, но не включает библиотеки или конфигурацию, требующиеся прило­
жению МVС. В состав шаблона проекта Web API входят инфраструктуры ASP.NET Core
и MVC, а также пример приложения. который демонстрирует получение и обработ ку
Глава 3. Паттерн MVC, проекты и соглашения 77
запросов Ajax от клиентов. Шаблон проекта Web Application содержит инфраструк­
туры ASP.NET Core и MVC с примером приложения, иллюстрирующим генерацию
НТМL-содержимого. Шаблоны Web API и Web Application могут конфигурироваться с
различными схемами для аутентификации пользователей и авторизации их доступа
к приложению.

Шаблоны проектов могут производить впечатление , что для создания определен­


ного вида приложения ASP.NET вы обязаны следовать специфическому пути, но это
не так. Шаблоны - это просто разные отправные точки для получения той же самой
функциональности, и вы можете добавлять в проекты, созданные с помощью любого
шаблона , любую желаемую функциональность . Скажем, в главе 20 объясняется, как
им еть дело с запросами Ajax, а в главах 28-30 - что делать с аутентификацией и ав­
торизацией, причем все примеры будут начинаться с шаблона проекта Empty.
Таким образом, реальная разница между шаблонами проектов связана с началь­
ным набором библиотек, конфигурационных файлов, кода и содержимого, добавля­
емого средой Visual Studio при создании проекта. Существует много отличий между
самым простым (Empty) и самым сложным (Web Application) проектами, как можно
видеть на рис. 3.6, где показано окно Solution Explorer после создания проектов с ис­
пользованием каждого шаблона. Для случая с шаблоном Web Application пришлось
привести снимки окна Solution Explorer с разными открытыми папками, потому что
единственный список файлов не уместился на печатной странице.
Дополнительные файлы, которые шаблон Web Application добавляет в проект, вы­
глядят устрашающе, но часть из них - всего лишь заполнители или реализации, ил­

люстрирующие общие функциональные возможности .

.l - Solutionlttm:
• At<Ounl
IJ tJ!r>b•l.J~Ofl , 5:1. ..,,,~ @1 Cofll11mEm.11l.c\htmt
t> J- P109ff11ts !!!) (ictvnaltoginConfirm!bon.c1h!ml с- b1(m1llog1nCo"Г.rmtti0nVir ...Mod!!I.«
•• Rtfrrtncн с.• for9otP1s~rcМrwModtl.н
8 Ь1:ttl\lllogin FJИu1c.cshtml
}' PIOp('lties ." -V.~001 ~for9ctPшwaid.cshtml
<• loginVie1.~.'\odrl.cs
tJ t.\of\<hStШn91.j1on !:> • ·• DrprndtN:iti В ForgotP1иwordCo r1firmation .c1'11ml
с• Rrgist rt'lreY<Modd.c s
..r.- Co nt10Jlt." 8 Locl:out.,shtml
с• Rt1t!Pauwo1dVitwМodrl.{~
~ -·1001 с• Acc"untCont1olltr.cs t• !.tndCodrV-IC!'WМodrl . п
В login.rshtml
• • Otptnckntit:s t" HomeControllrцs с- \lf1ify(ode\'~Modt1.ts
[!!) д.f9•Sttr.cshtml
Со Pн19rem.cs с• М.n19rCon11ollcr.c~ • ;,,..· M1rцg rV1rwModrh
@) P.tu,1P1mwo1d.c.111tml
.l rJp1ojcrtJи111 t • AddPhontNumbr!VitwModr Lu
n pro;нtJocli:.j~Qn
• "" D1t1 0 Rt~ttP,шwo1dConfirn1•tion.csh tml
• ....• м.g1111cns
В SrndCodt.uhtml
с- 01n9eP1нwo1dVlrwModrl .< 1
.D P•ojt(t_Rt•dm~html t> с- OOOOOOOOOOOOOO.Crt1trldf.nlit)·St
8 'hrifyCodc.c1html
с• C onfigurrTwo F1ct oJV1e1.~ModrJ.c 1
С• St1r11tp.H С" Applк.11tionObCon!txtModf.!Srit С" f1ctcrV1ewMQdrl.ci
• loo.· Homt
у wtb.c~fig с- Applic.t1ionDbContm.cs с- lnduVitwMode!.ts
/!) AЬouttthtml
ь illl! t-.tud~lt ео J. l..,..9d.vyi11}Vif.'WМ0Ucl.~ }
@j Cont1ct.0Nml
• Strvicн G) lndu.cshlml С* Rtm0Vtlog1nVi,...Modrl,CJ
Р l[m1i1Srndt1.o с• SdP•иwoнfVitwf"1odt~C1
;..· M1"1gr
t• ISmsSt"dr:цs С" Vt•ily?hontNumbr<V1ewModt!.c.t
@1 AddPhontN11mЬtt.cshtml
с• Meщ9tScnAct,.ci С" Applktlil)r!Ultf.U
121 Ctш19rPt~nчo1d.<Ui1ml
Jt 81 Vi~
0 lndu.иhl ml
lJ 1ppsrtting1j10" @l№rцgtlogi n,.<thtml
61 Ьundlteonfig .jso n В SftP•1swo1d. нhtml
с- P1ogr1m.c1 0 VtrifyPhc~umbtr.csh1ml
" IJ project.j1or1 • ....- Sh"rd
.!~' Projr:cLR"dmc.html е .tl)'O\ILt~htmt
e< S1artup .cs В .L oginP11rti•l.ohtm! /> • ir"•9н
f) wtt. conl19 В .V11l1d1t1onXript~Patti11Ltshtml /1 $ j'
[!) (rfor.cshtml • .... r1 ь
@l .Viewimportм~htnil " "1i boot1t1J p
@} .ViewScer1.c1html !:> ~ j qutty

1> О jqUt"'f·v.tГod1li11n·unoЫru1N't
!J _1dtrtncn.j~
f1viton .ico

Рис. 3.6. Стандартное содержимое , добавляемое в проект шаблонами Empty


и Web Application
78 Часть 1. Введение в инфраструктуру ASP.NET Саге MVC

Одни дополнительные файлы настраивают MVC или конфигурируют ASP.NET.


Другие представляют собой библиотеки клиентской стороны , которые будут вклю­
чаться в состав НТМL-разметки, генерируемой приложением. Список файлов в на­
стоящий момент может выглядеть огромным, но к концу книги вы будете поним ать,
что делает каждый из них .
Независимо от того , какой шаблон применяется для создания проекта , существу­
ют общие папки и файлы, имеющиеся во всех шаблонах . Одни элементы в проекте
играют специальные роли, которые жестко закодированы в инфраструктуре ASP.NET
или МVС либо каком-то инструменте из числа поддерживаемых Visual Studio. Другие
связаны с соглашениями об именовании, которые используются в большинстве про­
ектов ASP.NET или MVC. В табл. 3.1 описаны важные файлы и папки, с которыми вы
столкнетесь в проекте ASP.NET Core MVC; часть из них не присутствует в проекте по
умолчанию , но будет представлена в последующих главах.

На заметку! Все папки и файлы, описанные в табл . 3.1, находятся в папке s r c , которая
является местоположением, где Visual Studio создает проект ASP.NET Core MVC внутри
решения .

Таблица 3.1. Сводка по элементам проекта MVC

Папка или файл Описание

/Area s Области - это способ разбиения крупных приложений на


мелкие порции. Области рассматриваются в главе 16
/Dependencies Элемент зависимостей предоставляет подробные сведения
обо всех пакетах, от которых зависит проект. Диспетчеры
пакетов , применяемые Visual Studio, описаны в главе 6
/Component s Здесь определены классы компонентов представлений,
которые используются для отображения самодостаточных
средств, таких как корзины для покупок. Компоненты пред­
ставлений обсуждаются в главе 22

/ Controller s Сюда помещаются классы контроллеров. Это соглаше-


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

/Data Здесь определены классы контекста баз данных, хотя я


предпочитаю игнорировать это соглашение и определять их

в папке Mode ls, как будет продемонстрировано в главе 8


/Migra t ions Здесь хранятся детали схем баз данных, чтобы мож но было
обновлять развернутые базы данных. Процесс развертыва­
ния будет показан в главе 12
/ Models Сюда помещаются классы моделей представлений и моде­
лей предметной области. Это соглашение. Классы моделей
могут определяться где угодно в текущем проекте или даже
в отдельном проекте

/Vi ews Здесь хранятся представления и частичные представления,


обычно сгруппированные в папки с именами контроллеров,
к которым они относятся. Представления будут подробно
описаны в главе 21
Глава 3. Паттерн MVC , проекты и соглашения 79
Окончание табл . 3. 1
Папка или файл Описание

/Views/Shared Здесь хранятся компоновки и представления , которые не


являются специфическими для каких-то контроллеров .
Представления будут подробно описаны в главе 21

/Views/ _Viewimport s . cshtml Этот файл применяется для указания пространств имен,
которые будут включены в файлы представлений Razor, как
объясняется в главе 5. Он также используется для установ­
ки дескрипторных вспомогательных классов (глава 23)

/Views / ViewStart . cshtml Этот файл позволяет указать стандартную компоновку для
механизма визуализации Razor, как описано в главе 5
/bower. j son По умолчанию этот файл скрыт. Он содержит список паке­
тов , управляемых диспетчером пакетов Bower (глава 6)
/p r o j ect. j son В этом файле указаны базовые конфигурационные пара­
метры для проекта , в том числе применяемые им пакеты
NuGet (глава 6)
/Prog r am . cs Этот класс конфигурирует платформу, на которой размеща­
ется приложение (глава 14)
/Startup . cs Этот класс конфигурирует само приложение (глава 14)
/wwwroot Сюда помещается статическое содержимое, такое как фай­
лы CSS и изображений. Кроме того, именно сюда диспет­
чер пакетов Bower устанавливает пакеты JavaScript и CSS
(глава 6)

Соглашения в проекте MVC


В проекте MVC существуют два вида соглашений . Первый вид - это просто предпо­
ложения о то м , как про ект м ожет быть структурирован. Например , пакеты JavaScript
и CSS от независимых поставщиков, на которые вы полагаетесь , общепринято поме­
щать в папку www r oot/lib . Им енно там ожидают обнаружить их другие разработчш<И
МVС , и сюда их будет устанавливать диспетчер пакетов. Но вы вольны п е р еименовать
папку 1 ib или вообще удалить е е и хранить пакеты в другом месте. Это не помешает
инфраструктуре MVC выполнить ваше приложение при условии, что элеме нты script
и l i nk в представлениях ссылаются на местоположение , куда вы пом е стили пакеты.
Второй вид соглашений вытекает из принципа соглашения по конфигурач,ии (или
соглашения над конфигурач,ией, если делать акцент на пр еимуществе соглашения пе­
ред конфигурацией), который был одним из главных аспектов, обеспечивших попу­
лярность платформе Ruby оп Rails. Соглашение по конфигурации означает, что вы не
должны явно конфигурировать, с1<ажем, ассоциации м ежду контроллер ами и их пред­
ставлениями. Вы просто следуете определенному соглашению об именовании для сво­
их файлов - и все работает. Когда приходится иметь дело с соглашени ем такого вида,
снижается гибкость в отношении изменения структуры проекта. В последующих раз­
делах объясняются соглашения, которые используются вместо конфигурации.

Совет. Все соглашения могут быть изменены путем замены стандартных компонентов MVC
собственными реализациями . В книге будут описаны различные способы делать это , что
поможет объяснить, как работают приложения MVC, но с рассматриваемыми здесь согла­
шениями придется иметь дело в большинстве проектов.
80 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

Следование соглашениям для классов контроллеров


Классы контроллеров имеют имена, которые заканчиваются на Controller, на­
пример, ProductController, AdminController и HomeController. При ссылке на
контроллер в другом месте проекта, скажем, в случае применения дескрипторного

вспомогательного класса, указывается первая часть имени (такая как Product), а ин­
фраструктура MVC автоматически добавляет к этому имени слово Controller и на­
чинает поиск класса контроллера.

Совет. Это поведение можно изменить, создав соглашение для моделей, как будет описано
в главе 31.

Следование соглашениям для представлений


Представления и частичные представления помещаются в папку /Views/ ИмяКонт ­
роллера. Например, представление, ассоциированное с классом ProductController,
должно находиться в папке /Views/Product.

Совет. Обратите внимание, что часть Controller имени класса в имени папки внутри Views
не указывается, т.е. используется /Views/Product, а не /Views/ProductController.
Поначалу такой подход может показаться нелогичным, но он быстро войдет в привычку.

Инфраструктура МVС ожидает, что стандартное представление для метода дейс­


твия должно иметь имя этого метода. Например, представление, ассоциированное с
методом действия по имени List (),должно называться
List . cshtrnl. Таким обра­
зом, ожидается, что для метода действия
List () класса ProductController стан­
дартным представлением будет /Views/Product/List. cshtrnl. Стандартное пред­
ставление применяется при возвращении результата вызова метода View ( ) в методе
действия, примерно так:

return View() ;

Можно указать имя другого представления:

return View("MyOtherView");

Обратите внимание, что мы не включаем в представление расширение имени фай­


ла или путь. При поиске представления инфраструктура МVС просматривает папку,
имеющую имя контроллера, а затем папку /Views/Shared. Это значит, что представ­
ления, которые будут использоваться более чем одним контроллером, можно помес­
тить в папку /Views/Shared и MVC успешно найдет их.

Следование соглашениям для компоновок


Соглашение об именовании для компоновок предусматривает снабжение имени
файла префиксом в виде символа подчеркивания (_J и размещение файлов компо­
новки в папке /Views/Shared. Стандартная компоновка применяется по умолчанию
ко всем представлениям через файл /Views/ _ ViewStart . cshtrnl. Если вы не хотите,
чтобы стандартная компоновка применялась к представлениям, то можете изменить
Глава 3. Паттерн MVC, nроекты и соглашения 81
настройки в файле _ViewStart.cshtml (либо вообще удалить его ) , указав другую
компоновку в представлении, например:

@{
Layout = "-1 _ MyLayout. cshtml";

Или же можете отключить компоновку для заданного представления:

@{
La yout = null;

Резюме
В этой главе был представлен архитектурный паттерн МVС и его сравнение с не­
Сl{Олькими другими паттернами, с которыми вы могли сталкиваться или слышать о

них ранее. В следующей главе объясняется структура проектов MVC в Visual Studio и
рассматриваются важнейшие средства языка С#, которые используются при разра­
ботке веб-приложений МVС.
ГЛА В А 4
Важные
функциональные
возможности языка С #

в этой главе будут описаны функциональные возможности С#, используемые при


разработке веб - приложений, 1юторые не очень хорошо понимают или часто
путают. Однако настоящая книга не посвящена языку С#, так что для каждой воз ­
можности будет приводиться только краткий пример , поэтому вы можете прорабо ­
тать примеры из остальных глав книги и задействовать нужные возможности в своих
проектах . В табл . 4.1 приведена сводка для данной главы.

Таблица 4. 1. Сводка по главе

Задача Решение Листинг

Избегание обращения к свойствам Используйте n u ll-условную 4.6-4.9


по ссылкам nul l операцию

Упрощение свойств С# Используйте автоматически реа- 4.10-4.12


лизуемые свойства

Упрощение формирования строк Используйте интерполяцию строк 4.13


Создание объекта и установка его свойств Используйте инициализатор 4.14-4.17
за один шаг объекта или коллекции

Добавление функциональности в класс, Используйте расширяющий 4.18-4.25


который не может быть модифицирован метод

Упрощение использования делегатов Используйте лямбда - выражение 4.26-4.33


и однострочных методов

Применение неявной типизации Используйте ключевое слово var 4.34


Создание объектов без определения типа Используйте анонимный тип 4.35 , 4.36
Упрощение использования асинхронных Используйте ключевые слова 4.37-4.40
методов a sync и awa i t
Получение имени метода или свойства клас­ Используйте выражение nameof 4.41, 4.42
са без определения статической строки
Глава 4. Важные функциональные возмож ности языка С# 83

Подготовка проекта для примера


Для этой главы создайте в Visual Studio новый проект по имени LanguageFeatur es ,
используя шаблон ASP.NET Core Web Application (.NET Core) (В е б-приложение ASP.NET
Core (.NЕТ Core)). Снимите отметку с флажка Add Application lnsights to Project (Добавить
в проект службу Application Insights) и щелкните на кнопке ОК (рис. 4.1).

! !; Recent ··1 - -
" S.Ort Ьу: ~efau!t_
·- -
__ ".
." .::::
! ~ - · ~::J
-· -· - -
~ ~,;_c!:'_J~sblled !:m!:!~ts.\~ tr/"fJ.
·-
i
~-·!r" !f\rt.Jllr:d
ij ASP.NП \Чf!Ь Application (.NЕТ Fran\~ork} VisualC: Туре: Vi1ui1I с;

.с Templistes Projec t t empl.ttes for cre11tin9 ASP.NET


.с Visu1t!C• Core •pplic11tions for Windo\>6, linux and
0) Х using .NП Core.
f itJindo~vs
wеь ASP.NET Core V./eb Application ( .N П Framtwoik) VisualC~ ' ApplkottIOn lnsJ9hts
.NE1 Core О Add Applit~tion ln~ight!; to proje<:t
And1oid Optimize performonce •nd monitot
Cloud usage in ycur live opplicaticn,
Exten~ibllity

iOS
Reporting
Silverlight
Help ntt undtrs~nd Appl1cttio.n lr1s!ghts
\ ~ Onliм Pri11;,cy Stat~.ment

1
Locatiorn \ Br~:;:J 1
Solutionnгme Li11n9 u:g~Fe0Jtur~ ., _ _ C.reatedl1eф;iryforsolutlon'

L~-'---·-·--"-· ----~--~-~--_:_--~-----·~-~~~:~~~~-·-·~-Add:~"~j~~~::r J!__:~~ !j


Рис. 4.1. Выбор типа проекта

Когда отобразятся различные конфигурации проекта ASP.NET, выберите шаблон


Empty (Пустой), как показано на рис. 4.2, и щелкните на кнопке ОК, чтобы создать
проект.

1 S•l«t • lemplate:
с - ~- ~--- -----~- -----------"'--- ----1 An empty p,oject template fo r cr9tin9 an ASP.NП Co re 1
1 j1 AS P.NEТ Core Tempiates 1 app!kэtion. This templi!!te does not have any content in
it.

~ 1 ~
'

111 ij
1 Web AP I \'leb ,. 1
I Application
1
1 1, 1 _________ l
j j l~~~~-~~~)-t~~:.~~~~

i__ ··-· --·- ·--··-~--~- -- ·--·-·~--·~ ----·· ----~-J ~~~~~~~~n-----·---


~ Microsoft Azur~
1
1 0 О Hort in the cloud

lf дрр S~~~--·:·:
1
'-'--·---~·-·j
1

L ----·--·---------------- - - - - ·---···-------------------DC1~ 1
1
__________ J

Рис. 4.2. Выбор начального содержимого проекта


84 Часть 1. Введени е в инфрастру ктуру ASP.NET Core MVC

Включение ASP.NET Core MVC


Шаблон проекта Empty создает проект, который содержит минимал ьную конфи­
гурацию ASP.NET Core без какой-либо поддержки MVC. Это значит, что содержимое­
заполнитель, добавляемое шаблоном Web Application (Веб-приложени е), отсутствует,
но танже означает необходимость в выполнении ряда дополнительных шагов для
включения МVС, чтобы заработали такие ср едства , как контроллеры и пр едставле­
ния . Здесь мы внесем изменения, требуемые для внлючения МVС в проекте, но по1<а
не будем вдаваться в детали каждого шага . Первый шаг предусматривает добавле­
ние сборок .NET для МVС , что делается в разделе dependencies файла proj ect . j son
(листинг 4 . 1) .

Листинг 4.1. Добавление сборок MVC в файле proj ect. j son

" dependencies ": {


"Microsoft . NETCore . App" :
"version ": 11
1 . 0 . 0 11 ,
" type ": "platform"
},
"Microsoft . AspNetCore . Diagnostics ": "1. 0 .0",
"Microsoft . AspNetCore . Server . IISintegration ": " 1 . 0 . 0 ",
"Microsoft.AspNetCore . Server . Kestre l": " 1.0.О ",
"Microsoft.Extensions . Logging . Console ": " 1.0.0 ",
"Microsoft .AspNetCore .Mvc": "1.0.0"
} 1

В разделе dependencies файла proj ect. j son перечислены сборки, которые тре­
буются для про екта. Мы добавили сборку Microsoft. AspNetCore. Mvc, содержащую
классы МVС. Обратите внимание на добавление запятой в конце строки, предшес­
твующей Microsoft . AspNetCore . Mvc. Файлы конфигурации JSON чувствительны
к корректному форматированию, а о добавлении запятой легко забыть и тем самым
вызвать ошибку.

Совет. Каждая сборка указывается с номером версии. Вы должны удостовериться , что все
сборки с указанными версиями нормально работают вместе . Во время редактирования
файла proj ect. j son среда Visual Studio предоставит списо к доступны х верси й сборок .
Простейший подход заключается в том, чтобы указанная вами версия для Microsoft .
AspNetCore . Mvc совпадала с версией существующих сборо к в разделе dependenc ies,
к оторый был добавлен Visual Studio, когда создавался проект.

На следующем шаге инфраструктуре ASP.NET Core сообщается о необходимости


использования MVC, что делается в классе Startup (листинг 4.2).

Листинг 4.2. Включение MVC в файле Startup . cs


using Microsoft.AspNetCore . Builder;
using Microsoft.AspNetCore . Hosting ;
using Microsoft.AspNetCore . Http ;
using Microsoft . Extensions.Dependencyinjection;
Глава 4. Важные функциональные возможности языка С# 85
using Mi crosoft . Extensions . Logg i ng ;
narnespace LanguageFeatures {
puЫic class Star tu p {

puЫic void Config ureServices (I Se r vi ce Collection services ) {


services.AddМvc();

puЫic void Configure(IApp l i cationBu i l de r ар р, I Host i ngEnvironrnen t env ,


ILogge r Factory loggerFac t ory) {
app.UseMvcWithDefaultRoute();

Конфигурировани е приложений ASP.NET Core МVС будет объясняться в главе 14, а


пока достаточно зн ать, что два оператора, добавленные в листинге 4.2, обеспечивают
б а зовую н а стройку MVC с примен ением стандартной конфигур ации и соглаш е ний .

Создание ком понентов приложения MVC


Имея настро енную инфраструктуру МVС, можно добавлять 1<0мпоненты приложения
МVС , которые будут исполь зоваться для демонстрации важных языковых средств С# .

Создание модели
Н ачн ем с создания простого класса модели, чтобы иметь какие-то данные , с кото­
ры м и можно работать. Добавьте папку по имени Models и создайте в ней файл класса
Product . cs с определением, приведенным в листинге 4.3.

Листинг 4.3. Содержимое файла Product. cs из папки Models


narnespace LanguageFeatures . Models {
puЬlic cla s s Product {

puЫic string Narne { get ; set ; }


puЫic decirnal? Price { get ; set ;
puЬlic static Product[] GetP r oducts()
Product kayak = new Pr oduct {
Narne = " Kayak ", Pr i ce = 275М
};
Product l ifejacket = new Prod uct
Narne = "Lifejacket ", Price = 48 . 95М
};
return new Product[] { kayak , lifejacket , null };

В ю~ассе Products определены свойства Name и Pr i c e, а также статич е ский метод


по имени GetP r oducts () , который возвращает м ассив элементов Product . Один из
эле м е нтов , с оде ржащихся в возвращаемом из метода GetProducts () массив е, уста­
новл ен в null .
86 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

Создание контроллера и представления


В примерах этой главы мы применяем простой контроллер для демонстрации р а з­
личных языковых средств . Создайте папку Controllers и добавьте в не е файл класса
по имени HomeContro ll e r . cs , содержимое которого показано в листинге 4.4 . В слу­
чае использования стандартной конфигурации MVC инфраструктура будет по умол ­
чанию отправлять НТГР-запросы контроллеру Home.

Листинг 4.4. Содержимое файла HomeController . cs из папки Controllers


using Microsoft . AspNetCore . Mvc ;
namespace Language Feat u res . Contro ll e rs
p u Ьlic cla s s HomeCo ntro l ler : Control l er

puЬlic Vi e wRes ult I ndex()


ret ur n Vi ew (n ew str in g [] { "С#", "Langua ge ", " Fe a t u res " }) ;

Метод действия Index ( ) сообщает инфраструктуре MVC о необходимости визуа ­


лизировать стандартное представление и передает ей массив строк, который должен
быть включен в НТМL- разметку, отправляемую клиенту . Чтобы создать соответствую­
щее пр едставление, добавьте папку Vi ews/ Home (сначала создав папку Views , а зате м
внутри нее паш<у Home) и поместите в нее файл представления по имени Index . cshtml
с содержимым, приведенным в листинге 4.5.

Листинг 4.5. Содержимое файла Index. cshtml из папки Views/Home


@model I E n um eraЫe<string>

@{ Layout = null;
< ! DOC TYPE html >
<html>
<head>
<meta name =" v iewp o rt " co nt e nt= " width=devi ce - width " / >
<t itl e >La ng ua ge Features</ t it l e>
</head>
<body>
<u l >
@foreach (str ing s in Mode l ) {
<li >@s</ l i>

</ul>
</body>
</html >

Запустив пример приложения путем выбора пункта Start Debugg ing (Запустит ь от ­
ладку) в меню Debug (Отладка) , вы увидите вывод , предст авленный на рис . 4.3.
Глава 4. Важные функциональные возможности языка С# 87

D language features Х

____? ~athost:sз._29_0 _ __

. с~
"
• La11guage
• Feanю~s

_J
Рис. 4.3. Запуск пример приложения

Поскольку выводом во вс ех примерах в данной главе является текст, отображаемые


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

С#
Language
Features

Использование null - условной оп е раци и


С помощью null -условной операции можно более элегантно обнаруживать зна­
чения null. Во вр е мя раз работки пр иложений МVС встр ечается много проверок на
предмет null, т.к. нужно выяснять , содержит ли запрос специфический заголовок
или значение либо содержит ли модель определенный элемент данных. Традиционно
работа со значениями null требовала явных проверок, которы е могли становиться
утомительны м и и подверженными ошибкам , когда требовалось инспектировать и
объект, и его свойства. За счет применения null -условной операции такие проверки
будут намного легч е и компактне е (листинг 4.6).

Листинг 4.6. Обнаружение значений null в файле HomeController. cs


using Microsoft . AspNetCore . Mv c ;
using System.Collections.Generic;
using LanguageFeatures.Models;
name space LanguageFeatures . Controlle rs
pu Ы ic class HomeCo nt roll er : Co nt ro l le r

puЬlic ViewResult Index() {


List<string> results = new List<string>();
foreach (Product р in Product.GetProducts())
string name = p?.Name;
decimal? price = p?.Price;
results .Add (string. Format ("Name: {0}, Price: {1}", name, price));

return View(results);
88 Часть 1. Введение в инфраструктуру ASP.NET Саге MVC

Статический метод GetProducts (), определенный в классе Product, возвраща­


ет массив объектов, который инспектируется в методе действия Inde x () контрол­
лера с целью получения списка значений Name и Price. Проблема в том, что как
объект в массиве, так и значения его свойств могут быть null, т.е. нельзя прос­
то ссылаться нар. Narne или р. Price внутри цикла foreach, не получив исключе­
ние NullReferenceException. Во избежание этого используется null-условная
операция:

string name = p?.Name;


decimal? price = p?.Price;

null-условная операция обозначается знаком вопроса(?). Если значение р равно


null, то переменная name также будет установлена в null. Если значение р не равно
null, то переменной name будет присвоено значение свойства Person. Name . Такой же
проверне подвергается свойство Price. Обратите внимание. что переменная. ноторой
выполняется присваивание с применением null-условной операции, должна быть в
состоянии иметь дело со значениями null, поэтому переменная price объявлена с
десятичным типом, допускающим null (decimal ?).

Связывание в цепочки null-условных операций


Для навигации по иерархии объектов null-условные операции могут связывать­

ся в цепочки и превращаться в по-настоящему эффективный инструмент для упро­


щения нода и обеспечения безопасной навигации. В листинге 4. 7 I{ классу Product
добавлено свойство с вложенными ссылками, что создает более сложную иерархию
объектов.

Листинг 4. 7. Добавление свойства в файле Product. cs


namespace Languag e Feat ures.Models {
puЬlic class Product {

puЬlic string Narne { get; set ; }


puЫic decimal? Price { get ; set ;
puЫic Product Related { get; set; }
puЫic stati c Product[J GetProducts()
Product kayak = new Product {
Name = "Kayak" , Price = 275М
};

Product lifejacket = new Product


Name = " Lifejacket" , Price = 48 . 95М
};

kayak.Related = lifejacket;
return new Product[] ( kayak, lifejacket, null };

Каждый объект Product имеет свойство Related, которое может ссылаться на дру­
гой объект Product . В методе GetProducts () мы устанавливаем свойство Related
Глава 4. Ва ж ные фун к циональные возмо ж ности языка С# 89
для объекта Produ c t , представляющего каяк . В листинге 4.8 показано, как можно
со единить вместе nu ll-условные опер ации для навигации по свойствам объектов. не
вы з ывая исключ е ние .

Листинг 4.8. Обнаружение вложенных значений null в файле HomeController. cs


using Microsoft . AspN e tCore . Mvc;
using System . Collect i o n s . Gene r ic ;
using LanguageFeatures . Models ;
namespace Language Featu r es . Cont r o ll ers
puЫic c l ass HomeCo nt r oll e r : Co nt r o ller

puЫic ViewResul t Inde x() {


List<string> resu l ts = new List<st r ing>() ;
foreach (P r oduct р in Product . GetProducts())
string name = p? . Name ;
dec i mal? price = p? . Price;
string relatedName = p?.Related?.Name;
resul ts .Add (string. Format ( "Name: {О}, Price: {1}, Related: {2}",
name, price, relatedName));

r etu r n Vi ew(re sul ts) ;

n ull-условную операцию можно применять :к каждой части цепочки свойств,


например:

string relatedName = p?.Related?. Name;

В таком случа е переменная re la tedName получит значение nu ll, когда значение


null и м еет р или р . Rela ted. Иначе relatedName будет присвоено знач ение свойства
р . Rela ted . Name. Запустив пример приложения, вы увидите в окне браузера следую­
щий вывод:

Name : Ka yak , Pr i ce : 275 , Re l ated : Lifej ac ket


Name : Lifejacket , Pr i ce : 48 . 95 , Relat ed:
Name : , Price : , Related :

Ко мби ниро в ание null-условной операции


и оп ерации объеди нения с null
Для установки альтернат ивного зн ачения, представляющего null, удобно комби­
ниров ать nu l l -условную оп е рацию (один знак вопроса) и операцию объедин е ния с
nul l (два знака вопроса), как демонстрируется в листинге 4.9.

Листинг 4.9. Сочетание операций работы с null в файле HomeController. cs


using Microsoft . As pNetCore . Mvc ;
us i ng System . Col l ect i ons . Ge ne ri c ;
using LanguageFeatu r es . Models ;
90 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

namespace LanguageFeatures.Controllers {
puЬl i c class HomeController : Con tr oller

puЫic ViewResult Index() {


List<string> results = new List<string>();
foreach (Product р in Product . GetProducts( ) )
string name = р? .Name ?? "<No Name>";
decimal? price = p? . Price ?? О;
string relatedName = р? .Related? .Name ?? "<None>";
results.Add(string . Format( "Name: (0 ), Price : (1) , Related : {2} ",
name , price , relatedName)) ;

return View(results);

null-условная операция гарантирует, что при навигации по свойствам объектов


не возникнет исключение Nul lReferenceExcep tion , а операция объединения с null
обеспечивает отсутствие значений null в результатах, отображаемых в браузере. Если
вы запустите пример приложения, то увидите в окне браузера следующий вывод:

Name : Kayak , Price : 275 , Related : Lifejacket


Name : Lifejacket, Price : 48 . 95 , Re l ated : <None>
Name: <No Name>, Price : О, Related : <None>

Использование автоматически
реализуемых свойств
В языке С# поддерживаются автоматически реализуемые свойства, которые мы
применяли при определении свойств класса Person в предыдущем разделе:

namespace LanguageFeatures .Models {


puЫic class Product {

puЫic string Name { get; set; }


puЫic decimal? Price { get; set;
puЬlic Product Related { get; set; }
puЬlic static Product(] GetProducts()
Product kayak = new Product {
Name = "Kayak ", Price = 275М
};
Product lifejacket = new Product
Name = " Lifejacket ", Price = 48 . 95М
};

kayak . Related = lifejacket ;


return new Product[J { kayak , lifejacket , nul l };
Глава 4. Важные функциональные возможности языка С# 91
Данное средство дает возмож ность определять свойства . н е реали зуя блоки кода
get и set. Средство автоматически реализуемых свойств позволяет трактовать сле­
дующее опр еделение свойства:

puЫic string Name { get ; se t; }

как экви в ал ентное приведенному ниже коду:

puЫic st r ing Name {


get { return name ;
set { name = val u e ; }

Ср едств а подобного типа и з вестны как "синтаксический сахар", который делает


р аботу с С# более приятной (за счет устранения в этом случа е избыточного кода , дуб­
л ируем ого в итоге для 1шждого свойства), однако существенно н е изм е няет поведение
яз ы ка. Те р мин " саха р " может показаться уничижит ельным, но л юбы е улучшения. ко ­
торые облегчают н ап исание и сопровождение кода . приносят пользу - особенно в
кру пных и сл ожных про е ктах.

И спользо ва ни е и ни циал и заторов


автомат и чески реализуемых свойст в
Автоматич е ски р е ализуемые свойства поддерживаются, н ачиная с ве р сии С # 3.0.
В последней версии С# доступны инициализаторы для автоматич е ски р еализуемых
свойств . которые позволяют устанавливать начальны е знач ения. не требуя прим ене­
ния конструкторов (листинг 4 . 10) .

Листинг 4.1 О. Использование инициализатора автоматически реализуемого


свойства в файле Product. cs
n a mespace LanguageFeatures . Models {
puЫic c l ass Product {

puЫic str ing Name { get ; s et ; }


puЬlic string Category { get; set; "Watersports";
puЬl i c decimal? Pr i ce { ge t; se t; }
puЫi c Product Related { get ; set ; }
puЫic stat i c Product[ J GetP r oducts ( )
Product kayak = new Product {
Name = "Kayak" ,
Category = "Water Craft",
Price = 275М
};
Product l i fejacke t = new Product {
Name = " Li fejacket ", Pr i ce = 48 . 95М
};
ka y ak . Re l ated = lif ejacke t;
return new Produc t [] { kayak , life jacket , null } ;
92 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

Присваивание значения автоматически реализуемому свойству не препятствует


применению установщика для изменения свойства в более позднее время, а всего
лишь приводит в порядок код для простых типов с конструкторами. которые содер­

жат список присваиваний свойств, обеспечивающих наличие стандартных значений.


В приведенном примере инициализатор присваивает свойству Category значение
11
Wa tersports 11 • Начальное значение может быть изменено, что и делается при со­
здании объекта когда ему указывается значение
11
kayak. Water Craft 11 •

Создание автоматически реализуемых свойств только для чтения


Можно создать свойство только для чтения, используя инициализатор и опуская
ключевое слово set из определения автоматически реализуемого свойства, 1юторое
имеет инициализатор (листинг 4.11).

Листинг 4.11. Создание свойства только для чтения в файле Product. cs


namespace LanguageFeatures.Models {
puЬlic class Product {

puЫic string Name { get ; set ; }


puЫ i c string Category { g et; set; 11
Watersports" ;
puЫic decimal? Price { get ; set ; }
puЬlic Product Re l ated { get ; set ; }
puЬlic bool InStock { get; } = true;
puЫic static Product [ J GetProducts()
Product kayak = new Product
Name = " Kayak 11 ,
Category = 11 Wate r Craft ",
Price = 275М
};
Product l ifejacket = new Product {
Name = 11 Li fe j acket 11 , Price = 48. 95М
};

kayak.Related = l ifejacket ;
return new Product[] { kayak , l ifejacket , null } ;

Свойство I nS tock инициализируется значением true и не может быть изменено;


тем не менее, ему можно присвоить значение внутри конструктора типа, кmt показа­

но в листинге 4 . 12.

Листинг 4.12. Присваивание значения свойству только для чтения в файле Product. cs

namespace LanguageFeatures . Models {


puЫic class Product (

puЫic Product(bool stock = true)


InStock = stock;
Глава 4. Важ ные функциональные возмож ности языка С# 93
puЬl i c string Name { ge t; set; }
puЫic string Category { ge t; set ; " Wate r spo r ts ";
puЫic decima l ? Price { get ; set ; }
puЫic Product Related { ge t; s et ; }
puЫic Ьооl InStock { get; }

puЫic s t atic Pr o d uct[ ] Get Pro duc t s( }


Product kayak = new Product {
Name = " Kayak ",
Category = " Water Craft ",
Pri ce = 275М
};

Product lifejacket = new Product (false)


Name =
"Lifejacket",
Price = 48.95М
};

kayak . Re l ate d = l ifeja cket ;


return new Pr o du ct [ J { kaya k , l i f ej ac ket, nu l l };

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


допускающего только чтение, и если значение не пр едоставлено, тогда он устанавли­

вает свойство в стандартное значение tru e . Посл е установки конструктором значе­


ния свойства оно не может быть изменено.

И с п ольз о ва ние интерполяции строк


Традиционным инструментом С# для образования строк, содержащих значения
данных, является метод str ing. Fo r mat ( }. Вот пример применения этого приема в
контроллер е Horne :

resul ts . Add ( string. Format ( "Name: {О}, Price: { 1}, Related: { 2}",
name, price, relatedName) ) ;

В С# 6.0 появилась подде ржка другого подхода , называемого интерполяцией строк,


который позволяет избежать необходимости гарантировать, что ссылки {О} в шаб­
лоне строки соответствуют переменным, указанным в качестве аргументов. Взамен
инте рполяция строк использует имена переменных напрямую (листинг 4.13).

Листинг 4.1 З. Применение интерполяции строк в файле HomeController. cs


using Microsoft . AspNetCore . Mvc ;
using Sy stem . Col l e c tions . Ge n e r ic ;
using LanguageFeat ur es .M o d e ls;
namespace LanguageFeat u res . Controllers
puЫic class HomeContro l ler : Contro ll er

puЬli c ViewRe s ult Index() {


Lis t<st ri ng> r e sults = n e w Li st< s t rin g>( );
94 Часть 1. Введение в инфрастру ктуру ASP.NET Core MVC

foreach (Product р in Product . GetProducts())


string name = p?.Name ?? " <No Name> ";
decimal? price = p? . Price ?? О;
string relatedName = p? . Related? . Name ?? " <None>";
results . Add($"Name : {name}, Price: {price}, Related: {relatedName}");

return View(results) ;

Интерполируемая строка снабжается префиксом в виде символа $ и содержит


"дыры", которые представляют собой ссылки на значения, содержащиеся внутри фи­
гурных скобок ( { и }). Когда строка оценивается, "дыры" заполняются текущими зна­
чениями указанных переменных и констант.

Среда Visual Studio обеспечивает поддержку средства IntelliSense для создания


интерполированных строк и предлагает список доступных членов, когда набирается
символ {; это помогает минимизировать количество опечаток, а результатом оказы­
вается формат строки, который легче понять.

Совет. Интерполяция строк поддерживает все спецификаторы формата, которые доступ­


ны для метода string . Format (} . Спецификаторы формата включаются в виде части
"дыры", поэтому $ " Pri ce : {price : С2 }" сформатирует значение price как дене ж ное
значение с двумя десятичными цифрами .

Использование инициализаторов
объектов и коллекций
При создании объекта в статическом методе GetProducts () класса Product при­
менялся иниц,иализатор объекта. который позволяет создавать объект и указывать
значения его свойств за один шаг, например:

Product kayak = new Product


Name = "Kayak",
Category = "Water Craft",
Price = 275М
} ;

Это еще одна форма "синтаксического сахара", делающая язык С# легче в исполь­
зовании. Без такого средства пришлось бы вызывать конструктор Product и затем
применять вновь созданный объект для установки всех его свойств:

Product kayak = new Product() ;


kayak.Name = " Kayak ";
kayak . Category = "Water Craft ";
kayak.Price = 275М ;
Глава 4. Важ ные функциональные возмож ности язы ка С# 95
Свя з анное с ним средство - ин.ич,иализатор колле кции - позволя ет создавать
колл е 1щию и указ ывать ее содержимое з а один шаг. Без такого инициализатора со­
здани е , к примеру. м ассива потребовало бы указания размера и элементов массива
по отдельности, как показ ано в листи н ге 4.14 .

Листинг 4.14. Инициализация массива в файле HomeController. cs

using Microsoft . AspNetCore . Mvc ;


using System . Co l lections . Gener i c ;
using LanguageFeatures . Models ;
namespace LanguageFeature s. Control l ers
puЫic class HomeController : Controlle r

puЫic Vi ewResult Index() {


string[] names = new string[З];
names [О] "ВоЬ";
names [ 1 ] = "Joe" ;
names [2] = "Alice";
return View("Index", names);

Инициализатор коллекции дает возм ожность ука з ать с одержимое массива как
часть е го конструирования , что неявно предоставля ет компилятору ра зме р м ассива

(листинг 4.15).

Листинг 4.15. Использование инициализатора коллекции в файле HomeController. cs

using Microsoft . Asp NetCo re. Mvc ;


using System.Co l lections . Ge neric ;
us i ng LanguageFeatures . Model s ;
namespace LanguageFeature s. Control l ers
puЫic class HomeController : Controller

puЬlic ViewResult Inde x () {


return View("Index", new string[J { "ВоЬ", "Joe", "Alice" }) ;

Эл ем е нты массива задаются м ежду символами { и }, что позволяет получить более


кратко е определ е ние коллекции и делает возможным определять коллекцию внутри

вы з ов а м етода. Код в листинге 4.15 дает тот же р е зультю~ что и код в листинге 4.14,
и если вы запустит е пример приложения, то получите в окне браузера следующий
вывод :

ВоЬ
Joe
Alice
96 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

Использование инициализатора индексированной коллекции


В С# 6 улучшен способ применения инициализаторов коллекций для создания кол­
лекций , использующих индексы, таких как словари. В листинг е 4.16 приведен код
метода действия Index (), переписанный для определения коллекции с помощью под­
хода С# 5 к инициализации словаря.

Листинг 4.16. Инициализация словаря в файле HomeController. cs


using Microsoft .AspNetCore . Mvc ;
using System . Col l ections . Generic ;
using LanguageFeatures . Models ;
namespace LanguageFeatures . Cont rollers
puЫic class HomeController : Controller
puЫic ViewResult Index() {
Dictionary<string, Product> products = new Dictionary<string,
Product> {
{ "Kayak", new Product { Name "Kayak", Price= 275М } } , =
{ "Lifejacket", new Product{ Name = "Lifejacket", Price = 48. 95М }

};
return View("Index", products.Keys);

Синтаксис для инициализации такого типа коллекции слишком сильно полагается


на символы { и }, особенно когда значения коллекции создаются с применением иници­
ализаторов объе ктов. В С# 6 предлагается более естественный подход к инициализации
индексированной коллекции, который согласован со способом извлечения или модифи­
кации значений после того, как коллекция была инициализирована (листинг 4.17).

Листинг 4.17. Использование синтаксиса инициализатора коллекции С# 6


в файле HomeController. cs
using Microsoft.AspNetCore.Mvc;
using System . Co ll ections.Gener ic;
using LanguageFeatures . Models ;
namespace LanguageFeatures.Contr oll ers
puЬlic c l ass HomeController : Controlle r
puЫ i c ViewResult Index() {
Dictionary<string, Product> products = new Di ctionary<string , Product> {
[ "Kayak"] = new Product { Name = "Kayak", Price = 275М } ,
[ "Lifejacket"] = new Product { Name = "Lifejacket", Price = 48. 95М
};
return View( " Index ", products.Ke ys) ;

Результат остается прежним , т.е. создание словаря, ключами ноторого являются


Kayak и Lifejacket, а значениями - объекты Product, но элементы создаются с
применением системы обозначений для индексов, используемой в других операциях
Глава 4. Важные функциональные возможности языка С# 97
с коллекциями. Запустив пример приложения, вы увидите в окне браузера следую­
щий вьmод:
Kayak
Lifejacket

Использование расширяющих методов


Расширяющие методы - это удобный способ добавления методов в 1Uiaccы, вла­
дельцем 1<оторых вы не являетесь и не можете модифицировать напрямую. В листин­
ге 4.18 приведено определение 1Uiacca ShoppingCart , добавленного в папку Models в
виде файла ShoppingCart . cs, который представляет коллекцию объектов Product.

Листинг 4.18. Содержимое файла ShoppingCart. cs из папки Мodels

using System . Co ll ections.Generic ;


namespac e LanguageFeatures . Models
puЫic class ShoppingCart {
puЫic IEnumeraЬle<P r od uc t> Products { get ; set; }

Это простой класс, который действует в качестве оболоч1ш для коллекции


List объектов Product (в данном примере необходим лишь эл е ментарный класс).
Предположим , что нам требуется возможность определения общей стоимости объек­
тов Product в классе ShoppingCart , но мы не можем изменить сам класс, возможно

потому, что он поступил от третьей стороны, и мы не располагаем его исходным ко­


дом. Для добавления нужной функциональности можно применить расширяющий ме­
тод . В листинге 4.19 показан класс MyExtensionМethods, также добавленный в папку
Models в виде файла MyExtensionМet h ods . cs.

Листинг 4.19. Содержимое файла МyExtensionМethods. cs из папки Models


namespace LanguageFeatures . Mode l s {
puЫic static c l ass MyExtens i onMethods {
puЫic static decimal Tota lPr ices(t his ShoppingCart cartParam) {
decimal total = О;
foreach (Product prod in cartParam . Products)
total += prod?.Price ? ? О ;
return total ;

Ключ е во е слово this, расположенное перед первым параметром, помечает


TotalPrices () ка~< расширяющий метод. Первый параметр указывает .NЕТ, к какому
классу может применяться расширяющий метод - I< Shopp ingCart в данном случае.
На экземпляр класса ShoppingCart, к которому применен расширяющий метод. мож­
но ссылаться с использованием параметра cartParam. Расширяющий метод прохо­
дит по объектам Product
ShoppingCa r t и возвращает сумму значений их свойств
в
Product . Price. В листинге 4.20 демонстрируется применение расширяющего метода
в методе действия контроллера Horne.
98 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

На заметку! Расширяющие методы не позволяют нарушать правила доступа, которые клас­


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

Листинг 4.20. Применение расширяющего метода в файле HomeController. cs


u sing Micr oso ft . AspNetCore . Mvc ;
using Syst e m. Collections . Generic ;
using Language Features . Mode ls ;
namespace LanguageFeatures . Control l ers
puЫic clas s HomeController : Co n tro l l e r

puЫic ViewResult I ndex() {


ShoppingCart cart
= new ShoppingCart { Products = Product.GetProducts() };
decimal cartTotal =
cart.TotalPrices();
return View (" Index", new string [] { $" Total: { cartTotal: С2}" } ) ;

Вот ключевой оператор:

d ecimal cartTotal = cart . Tota l Prices() ;

М ет од TotalPrices () вызывается на объект е ShoppingCart, как е сли бы он был


частью класса ShoppingCart, хотя он является расширяющим методом, которы й оп­
редел ен в совершенно другом класс е . Среда .NET будет обнаруживать расширяющи е
классы , е сли они находятся в области действия текущего класса , т. е. являются частью
того же с амого про с транства им е н ил и пространства име н, которо е ук а зано в оп е р а ­

тор е using. Запустив пример приложения, вы увидите в окне брауз е ра следующий


вывод:

Total : $323 . 95

Применение расш иря ющих методов к интерфейсу


Можно такж е создавать расширяющие методы, которые применяются к интерфей ­
су , что позволит вы з ывать таки е расширяющие методы для вс ех к л ас с ов , р е ализую­

щих этот интерф ейс . В листинге 4.21 прив еден код кл асса ShoppingCart, модифици­
рованный для реализации интерфейса I E nume r aЫ e<P r oduct>.

Листинг 4.21. Реализация интерфейса в файле ShoppingCart. cs


using System.Collections;
using System . Collection s.Generic ;
namespace LanguageFeatur e s . Models
puЬlic class ShoppingCart : IEnumeraЫe<Product>
puЫic I En u m eraЫe<Pro d uc t > Pr oduct s { g et; set ;
Глава 4. Важные функциональные возможности языка С# 99
puЫic IEnumerator<Product> GetEnumerator ()
return Products.GetEnumerator();

IEnumerator IEnumeraЬle.GetEnumerator()
return GetEnumerator();

Теперь расширяющий метод можно изменить так, чтобы он работал с интерфей­


со м IEnumeraЫe<Pro du ct> (листинг 4.22).

Листинг 4.22. Модификация расширяющего метода в файле МyExtensionМethods. cs

using System.Collections.Generic;
namespac e Language Fea tures . Mo de l s
puЫic static c l a s s MyEx t ens i o nMethod s
puЫic static decimal TotalPrices (this IEnumeraЫe<Product> products)
decimal total = О ;
fo r each (Product pr od in pr od u c ts)
total += prod?.Price ?? О;

return total ;

тип первого параметра был изменен на I EnumeraЫe<Pro du ct>, а это значит, что
цикл fo r e a ch в теле метода работает непосредственно с объектами Prod uct . Переход
н а использование упомянутого интерфейса означает, что мы мож ем подсчитать об­
щую стоимость объектов Produ ct , перечисляемых посредством любого интерфейса
IEnumeraЫe<Produc t > . что включает не только экз емпляры Shoppi ngCart , но также
массивы объе ктов Produ c t (листинг 4.23).

Листинг 4.23. Применение расширяющего метода к массиву в файле HomeController. cs

using Microsoft . As p NetCore . Mvc ;


using System . Collections . Generic ;
using LanguageFeatures . Mode l s ;
namespace LanguageFe atures . Co ntro ll ers
puЫic c l ass HomeCo nt rol l e r : Cont ro ller

puЫic ViewResu l t Index() {


ShoppingCart cart
= new Shoppin g Cart { Produ cts = Pr o du ct . GetP r oducts() } ;
Product [] productArray = {
new Product {Name = "Kayak", Price = 275М},
new Product {Name = "Lifejacket", Price = 48.95М}
} ;
decimal cartTotal = cart.TotalPrices();
decimal arrayTotal = productArray.TotalPrices();
100 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

return View("Index", new string[]


$ "Cart Total: { cartTotal: С2} " ,
$ "Array Total: { arrayTotal: С2}" } ) ;

Если вы запустите проект, то увидите показанные ниже результаты, которые де­


монстрируют. что расширяющий метод возвращает один и тот же результат, незави­
симо от способа перебора объектов Product:
Cart Total : $323 . 95
Array Total: $323 . 95

Создание фильтрующих расширяющих методов


Последний аспект расширяющих методов, о котором необходимо упомянуть -
возможность их использования для фильтрации коллекций объектов. Расширяющий
метод, который оперирует на интерфейсе IEnumeraЫe<T> и также возвраща ет
IEnumeraЫe<T>. может задействовать ключевое слово yield, чтобы применить кри­
терий отбора к эл ементам в источнике данных с целью ген е рации сокращенного на­
бора результатов. В листинге 4.24 представлен такой метод, который добавляется в
класс MyExtensionMethods .

Листинг 4.24. Добавление фильтрующего расширяющего метода


в файле МyExtensionМethods . cs

using System . Collections . Generic ;


namespace LanguageFeatures.Models {
puЫic static class MyExtensionMethods
puЫic static decimal TotalPrices(this IEnumeraЫe<Product> products) {
decimal total = О;
foreach (Product prod in products)
total += prod?.Price ?? О ;

return total ;

puЫic static IEnumeraЫe<Product> FilterByPrice(


this IEnumeraЫe<Product> productEnum, decimal minimumPrice)
foreach (Product prod in productEnum) {
if ( (prod?. Price ?? О) >= minimumPrice) {
yield return prod;
}

Расширяющи й метод по имени Fil te rByPrice () принимает дополнительный


параметр. который позволяет фильтровать товары, так что в результате возвраща­
ются объекты Product, значение свойства Price которых совпадает или пр евыша­
ет значение, указанное в параметре. Использование этого метода демонстрируется в
листинге 4.25.
Глава 4. Важные функциональные возможности языка С# 101
Листинг 4.25. Применение фильтрующего расширяющего метода
в файле HomeController. cs
using Microsoft . AspNetCore.Mvc;
using Sys tern . Collections . Generic ;
using LanguageFeatures . Models ;
narnespace LanguageFeatures . Controllers
puЫic class HorneController : Controller

puЫic ViewResult Index() {


Product[J productArray = {
new Product {Narne "Kaya k", Price = 275М} ,
new Product {Narne "Lifejacket ", Price = 48 . 95М),
new Product {Name =
"Soccer ball", Price 19.SOM}, =
new Product {Name = "Corner flag", Price = 34. 95М}
};

decimal arrayTotal = productArray.FilterByPrice(20) .TotalPrices();


return View("Index", new string[] { $ 11 Array Total: {arrayTotal:C2}" }) ;

При вызове метода FilterByPrice () на массиве объектов Product метод


TotalPrices () получает и использует для подсчета суммы только те из них, которые
стоят больше $20. Запустив пример приложения, вы увидите в окне браузера следу­
ющий вьmод:

Total : $358 .90

Использование лямбда-выражений
Лямбда-выражения - это средство, которое служит причиной многочисленных
заблуждений и не в последнюю очередь из-за того, что упрощаемое им средство само
вызывает путаницу. Чтобы понять решаемую задачу, рассмотрим расширяющий ме­
тод FilterByPrice (), который был определен в предыдущем разделе. Метод реали­
зован так, что он может фильтровать объекты Product по цене, а это значит. что если
вы захотите фильтровать объекты по названию, то вам придется создать второй ме­
тод наподобие приведенного в листинге 4.26.

Листинг 4.26. Добавление фильтрующего метода в файле МyExtensionМethods . cs


using Systern.Collections.Generic;
namespace LanguageFeatures . Models
puЫic static class MyExtensionMethods
puЫic static decirnal TotalPrices(this IEnurneraЫe<Product> products) {
decirnal total = О ;
foreach (Product prod in products)
total += prod?.Price ?? О ;

return total ;
102 Часть 1. Введение в инфраструктуру ASP. NET Соге MVC

puЫic static IEnumeraЫe<Product> Fi l te r ByPrice(


this IEn umeraЬle<Pro d uct> productEnum , decimal minimumPrice) {
foreach (P r oduct prod in p r oductEnum) {
i f ( (prod? . Price ? ? 0) >= mi n i mum Price) {
yield return prod ;

puЫic s ta tic IEnumeraЫe<Product> Fil terByName (


this IEnumeraЫe<Product> productEnum, char firstLetter) {
foreach (Product prod in productEnum) {
if (prod?.Name?[O] == firstLetter) {
yield return prod;

В листинге 4.27 демонстрируется прим ен е ние в контролл ер е обоих фильтрующих


мет одов для с оздания двух ра з ных итоговых сум м .

Л истинг 4.27. Использование двух фильтрующих методов в файле HomeController. cs


using Mi crosoft . AspNetCore . Mvc ;
using System . Col l ections . Generic ;
using LanguageFeat u res . Mode l s ;
namespace LanguageFeatures . Con t rollers
puЫic cla s s HomeContro l ler : Co n troll er

puЫ i c ViewResult Index() {


Produ c t[] p r oductArray = {
new Product {Name " Kayak ", Pr ice = 275М} ,
new Produ ct {Name " Lifejac ket ", Price = 48 . 95М} ,
new Product {Name " Soccer ba l l ", Price 19 . 50М) ,
new Product {Name " Corner flag ", Price = 34 . 95М}
};

decirnal priceFilterTotal = productArray.FilterByPrice(20) .TotalPrices();


decimal nameFilterTotal = productArray.FilterByName('S') .TotalPrices();
return View( 11 Index 11 , new string[] {
$ 11 Price Total: {price Fil terTotal: С2} 11 ,
$ 11 Name Total: {nameFil terTotal: С2} " } ) ;

П ервый фильтр отбирает все товары с ценой $20 и выше , а второй фильтр - то ­
в ары с названиям и , н а чинающимися н а букву S. После запуска прим ер а п рил о жен ия
вы ув идите в окн е браузера сл едующий вывод :

Price Total : $358 . 90


Name Total : $19 . 50
Глава 4. Важ ные функциональные возмо ж ности языка С# 103

Определ ение функций


Описанный выше процесс можно повторять до бесконечности и создавать филь­
трующие м етоды для каждого интересующего свойства и сочетания свойств. Более
элегантный подход предусматривает отделение кода, обраб атывающего перечисле­
ние, от критерия отбора. В С# это делается легко, поскольку функции разрешено пе­
р едавать нак объекты . В листинге 4.28 показан единственный расширяющий метод,
1юторый фильтрует перечисление объектов Product , но делегирует отдельной функ­
ции решени е о том, каки е из них должны быть включ е ны в р езультат.

Листинг 4.28. Создание универсального фильтрующего метода


в файле МyExtensionМethods . cs

using System . Collections . Generic ;


using System;
namespace LanguageFeatures . Mode ls
puЫic static class MyExtensionMe t hods
puЫic static decimal TotalP r ices(th is IEnumeraЫe<Product> products)
decimal total = О;
foreach (Product prod i n p r oducts)
total += prod? . Price ?? О;
}.
return total;

puЬlic static IEnumeraЫe<Product> Filter(


this IEnumeraЬle<Product> productEnum,
Func<Product, bool> selector) {
foreach (Product prod in productEnum)
if (selector(prod)) {
yield return prod;

Вторым аргументом метода Fi l ter () является функция, которая принимает объ­


ект Product и возвращает значение типа boo l. М етод Fil ter () вызывает эту фун­
кцию для каждого объекта Product и включает его в результат. если функция воз­
вращает true . Для применения метода Fil ter () можно указать метод или создать
автономную функцию (листинг 4.29).

Листинг 4.29. Использование функции для фильтрации объектов Product


в файле HomeController. cs

using Microsoft . AspNetCore . Mvc ;


us i ng System . Collections .Ge neric;
using LanguageFeatures . Models ;
using System;
namespace Language Featu res . Contro l ler s
puЫic class HomeController : Controller
104 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

bool FilterByPrice(Product р) {
return (p?.Price ?? 0) >= 20;

puЫic ViewResult Index() {


Product [ ] productArray = {
new Product {Name " Kayak ", Pri ce = 275М} ,
new Product { Name " Lifej acket ", Price = 4 8 . 95М} ,
new Product {Name " Soccer ball" , Price 19.SOM} ,
new Product {Name " Corner flag", Price = 34 . 95М}
};

Func<Product, bool> nameFilter = delegate (Product prod) {


return prod?. Name? [О] == 'S' ;
} ;

decirnal priceFilterTotal = productArray


.Filter(FilterByPrice)
. TotalPrices () ;
decirnal nameFilterTotal = productArray
.Filter(narneFilter)
. TotalPrices О ;
return View( " Index ", new string [] {
$ "P rice To ta l: {priceFi l terTota l: C2} ",
$ " Name Total : {nameFilterTotal : C2} " }) ;

Ни один из подходов не идеален. Определение методов вроде FilterByPrice ()


засоряет определение класса. Создание объекта Func<Product, boo l > устраняет
данную проблему. но сопряжено с неудобным синтаксисом, который труден для вос ­
приятия и сопрово:нщения. Именно эту задачу решают лямбда-выражения, позволяя
определять функции более элегантным и выразительным способом, как показано в
листинге 4.30.

Листинг 4.30. Использование лямбда-выражений в файле HomeController. cs

using Microsoft.AspNetCore .Mvc;


using System . Collections .Generic ;
using Languag eFeat ures.Models;
using System ;
namespace Lang u ageFeatures .Co nt rol ler s
puЫic c l ass HomeContro l ler : Con t rolle r

puЫic ViewResult Index() {


Product [] productArray = {
new Product { Name " Kayak ", Price = 27 5М} ,
new Product {Name " Life j acket ", Price = 48 . 95М} ,
new Product {Name " Soccer ball ", Price 1 9.50М} ,
new Product {Name " Corner flag ", Price = 34 . 95М}
1;
dec i mal priceFilterTotal = productArray
.Filter(p => (p? . Price ?? О) >= 20)
. Tota lP rices(} ;
Глава 4. Важ ные функциональные возмож ности языка С# 105
decimal nameFilterTotal = productArray
.Filter(p=>p?.Name?[O] == 'S')
.TotalPrices();
return View( " Index ", new string[]
$"Price Total: {priceFilterTotal : C2} ",
$ "Name Total : {nameFilterTotal : C2}" }) ;

Лямбда-выражения выделены полужирным. Пар аметры выражаются без указания


типа, который будет выведен автоматически. Символы => можно читать как "направ­
ляется в" и связывают параметр с результатом лямбда-выражения. В рассматривае­
мом примере параметр Product по имени р направляется в результат bool, который
будет равен true, если значение свойства Price эквивалентно или превышает 20 в
первом выражении или если значение свойства Name начинается с буквы S во втором
выражении. Этот код работает тем же самым образом, что и отдельный метод и деле­
гат в виде функции, но он короче и для большинства людей легче в восприятии.

Другие формы лямбда-выражений

Представлять логику делегата посредством лямбда-выражения вовсе не обязательно . С тем


же успехом можно вызвать метод, подобный следующему:

prod => EvaluateProduct(prod)


Если требуется лямбда - выражение для делегата, который имеет несколько параметров, то
параметры должны быть заключены в круглые скобки:

(prod, count) => prod . Price > 20 && count >О

И, наконец, если в лямбда-выражении необходима логика, которая требует более одного


оператора, то ее можно реализовать, используя фигурные скобки ( { }) и завершая блок опе­
ратором return:
(prod , count) =>
// ... несколько операторов кода ...
return r esult ;

Вы не обязаны применять лямбда-выражения в коде, но они служат изящным способом вы­


ра же ния слож ных функций в читабельной и ясной манере. Вы будете часто встречать лямб ­
да-выражения в примерах, приводимых в этой книге.

Использование методов и свойств в форме лямбда-выражений


В С# 6 поддержка лямбда-выражений была расширена так, что их можно при­
менять для реализации методов и свойств. Во время разработки приложений MVC,
особенно при написании контроллеров, часто появляются методы, которые содержат
единственный оператор, выбирающий данные для отображения и пр едставление для
визуализации. В листинге 4.31 приведен код метода действия Index (), переписан­
ный в соответствии с таким общим шаблоном.
106 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

Листинг 4.31. Создание общего шаблона действия в файле HomeController. cs

using Microsoft . AspNetCore . Mvc ;


using System . Col l ections . Generic ;
using LanguageFeatures . Models ;
using System;
using System.Linq;
namespace LanguageFeatures.Controllers
puЫic c l ass HomeController : Controlle r

puЬlic ViewResult Index() {


return View(Product.GetProducts() .Select(p => p?.Name));

Метод действия Index () получает от статического метода Product. GetProducts ()


коллекцию объектов Product и с помощью LINQ строит проекцию значений свойств
Name. Затем эта проекция применяется в качестве модели представления для стан­
дартного представления. Запустив пример приложения, вы увидите в окне браузера
следующий вывод:

Kayak
Lifejacket
В окне браузера также будет присутствовать пустой элемент списка, потому что
метод GetProducts () включает в свой результат ссылку null, но в настоящем разде ­
ле главы это неважно.

Когда тело метода состоит из единственного оператора, его можно переписать в


виде лямбда-выражения (листинг 4.32).

Листинг 4.32. Представление метода действия как лямбда-выражения


в файле HomeController. cs
using Microsoft . AspNetCore . Mvc;
using System . Collect io ns.Generic ;
using Lang uage Features.Mode l s ;
using System;
using System . Linq ;
namespace LanguageFeatures.Control l ers
puЫic class HomeController : Controller

puЫic ViewResul t Index () =>


View{Product.GetProducts{) .Select{p => p?.Name));

Лямбда-выражения для методов позволяют опустить ключевое слово return и ис­


пользуют символы => (направляется в) для связывания сигнатуры метода (включ ая
аргум енты) с его реализацией. Метод Index (), показанный в листинге 4.32, работает
точно так же, как метод Index () из листинга 4.31, но выражается более лаконично.
Тот же самый базовый подход можно также применять для определения свойств .
В листинге 4.33 демонстрируется добавление в класс Product свойства, которое ис­
пользует лямбда-выражение.
Глава 4. Важ ные функциональные возмож ности языка С# 107
Листинг 4.33. Представление свойства как лямбда-выражения в файле Product. cs

namespace LanguageFeatures.Models {
puЫic class Product {
puЫic Product(bool stock = true)
InStock = stock ;

puЫic string Name { get ; set ; }


puЫic string Category { get ; set; " Watersports ";
puЫic decima l ? Price { get ; set ; }
puЬlic Product Related { get ; set; }
puЫic bool InStock { get; }
puЫic bool NameBeginsWi thS => Name? [О] 'S' ;
puЬlic static Product[] GetProducts() {
Product kayak = new Product {
Name = " Kayak ",
Category = " Water Craft ",
Price = 27 5М
};
Product lifejacket = new Product(false) {
Name = " Lifejacket ",
Price = 48 . 95М
};
kayak . Related = lifejacket ;
return new Product[ ] { kayak, lifejacket, null );

Использование автоматического
выведения типа и анонимных типов
Ключевое слово var языка С# позволяет определять локальную п еременную без
явного указания ее типа, как показано в л истинге 4.34. Такой прием называется
выведением типа или неявной типизацией.

Листинг 4.34. Использование выведения типа в файле HomeController. cs


using Microsoft.AspNetCore . Mvc ;
using System . Col l ections . Generic ;
using LanguageFeatures . Mode ls;
using System ;
using System.Linq;
namespace LanguageFeatures.Controllers
puЫic class HomeContro l ler : Controller

puЫic ViewResult Index() {


var names =
new [] { "Kayak", "Lifejacket", "Soccer ball" } ;
return View(names);
108 Часть 1. Введение в инфраструктуру ASP. NET Core MVC

Речь идет вовсе не о том, что переменная myVar ia Ыe не имеет типа; мы всего
лишь предложили компилятору самостоятельно вывести тип из кода. Компилятор ис­
следует объявление массива и решает, что он является строковым. Выполнение при­
мера дает следующий вывод:

Kayak
Li fejacket
Soccer ball

Использование анонимных типов


Комбинируя инициализаторы объектов и выведение типов, можно создавать про­
стые объекты модели представления, которые удобны для передачи данных между
контроллером и представл ением. без необходимости в определении класса или струк­
туры (листинг 4.35).
Листинг 4.35. Создание анонимного типа в файле HomeController. cs
using Microsoft . AspNetCore . Mvc ;
using System . Co l lect i ons.Generic ;
using LanguageFea tures . Models ;
using System ;
us ing System.Linq ;
namespa ce LanguageFeatures.Controllers
puЫic class HomeControlle r : Cont r ol l e r

p u Ыic ViewResult Index() {


var products = new [] {
new { Name = "Kayak", Price = 275М } ,
new { Name = "Lifejacket", Price = 48. 95М } ,
new { Name = "Soccer Ьall", Price = 19.SOM},
new { Name = "Corner flag", Price = 34. 95М }
};
return View(products.Select(p => p.Name));

Каждый объект в массиве product s относится к анонимному типу. Это не значит.


что объекты являются динамическими в том смысле, в каком считаются динамически­
ми переменные JavaScript. Это просто означает, что определение типа будет создано ав­
томатически компилятором. Строгая типизация по-пр ежнему обеспечивается. Скажем.
вы можете получать и устанавливать только те свойства, которые были определены в
инициализаторе. Запустив пример. вы увидите в окне браузера следующий вывод:

Kayak
Lifejacket
Soccer ball
Corner f lag
Компилятор С# генерирует класс на основе имени и типов параметров в инициа­
лизаторе. Два анонимно типизированных объекта, которые имеют те же самые имена
и типы свойств, будут относиться к одному и тому же автоматически сгенерированно­
му классу. В результате все объекты в массиве products получат один и тот же тип,
поскольку они определяют те же самые свойства.
Глава 4. Важ ные функциональные возмож ности языка С# 109

Совет. Для определения массива анонимно типизированных объектов должно применять­


ся ключевое слово var , т.к. тип не будет создан до тех пор , пока код не скомпилирует­
ся , и его имя не известно. Все элементы в массиве анонимно типизированных объектов
обязаны определять те же самые свойства , иначе компилятор не сможет выяснить тип
массива .

В целях демонстрации изменим вывод в листинге 4.36, чтобы отображать имя


типа вместо значения свой ства Name.

Листинг 4.36. Отображение имени анонимного типа в файле HomeController. cs


using Microsoft . AspNetCore . Mvc ;
using System . Col lections.Generic;
using LanguageFeatures . Models ;
using System ;
using System.Linq ;
namespace LanguageFeatures . Controllers
puЫic class HomeController : Contro l ler

puЬlic ViewResult Index() {


va r products = new [] {
new { Name " Kayak ", Price = 275М } ,
new { Name " Lifejacket ", Price = 48.95М } ,
new { Name " Soccer ball ", Price 19.50М },
new ( Name " Corner flag ", Price = 34 . 95М }
};

return View(products.Select(p => p.GetType() .Name));

Всем объектам в массиве был назначен один и тот же тип, что можно увидеть ,
запустив пример. Имя типа не выглядит дружественным к пользователю, но оно и
не рассчитано на непосредственное использ ование, к тому же в вашем случае может

оказаться другим:

<>f~AnonymousType0'2
<>f~A nonymousType0 ' 2
<>f~AnonymousType0 ' 2
<>f~AnonymousTy pe0' 2

Использование асинхронных методов


Одни м из недавних крупных добавлений к языку С# явля ется улучшение спосо­
ба работы с асинхронными методами. Асинхронные методы осуществляют возврат и
выполняют работу в фоновом режиме с уведомлением о ее завершении, по зволяя коду
в это время заниматься другими действиями. Асинхронные методы - важный инс­
трумент при устранении узких мест в коде; они позволяют приложениям извл екать

преимущества от наличия нескольких процессоров и процессорных ядер. выполняя

работу в параллел ьном режиме.


В инфраструктуре МVС асинхронные методы могут применяться для увеличения
общей прои зв одительности приложения, предоставляя серверу большую гибкость от-
11 О Часть 1. Введение в инфраструктуру ASP.NET Core MVC

носительно того, как запросы планируются и выполняются. Для вьmолн ения работы
асинхронным образом используются два ключевых слова С# - async и await.
Для целей данного раздела в пример проекта понадобится добавить новую сбор1\у
.NET. чтобы можно было делать асинхронные НТТР-запросы. В листинге 4.37 показа­
но добавление, произведенное в разделе dependenci es файла proj ect. j son.

Листинг 4.37. Добавление ссылки на сборку в файле project. json

"dependencies ":
"Microsoft . NETCore.App" :
" ve r sion ": " 1 . О . 0 ",
" type " : " platform "
}'
"M icrosoft . AspNetCore . Diagnostics ": " 1. О. О",
"Mi crosoft .AspNetCore.Serve r.I ISintegrat ion": "1 . 0 . О ",
"Mi crosoft.AspNetCore .Server.Kestrel ": "1. 0 . 0 ",
"Microsoft.Extens i ons . Logg ing .Conso l e ": "1. 0.О ",
"M icrosoft.AspNetCore . Mvc ": "1 . 0 .О",
"System.Net.Http": "4 .1. 0"
}'

После сохранения файла pro j ect. j son среда Visual Studio загрузит сборку System.
Net. Http и добавит ее в проект. В главе 6 процесс будет описан более подробно.

Работа с задачами напрямую


Язык С# и платформа .NET предлагают великолепную поддержку для асинхрон­
ных методов, но код быстро становится многословным, а разработчики , не привык­
шие к параллельному программированию , зачастую не могут справиться с необыч­
ным синтаксисом. В качестве примера в листинге 4.38 приведен метод по имени
GetPageLength (), который определен в классе MyAsyncMethods, добавленном в пап­
ку Mode ls в виде файла MyAsyncMethods . cs .

Листинг 4.38. Содержимое файла МyAsyncМethods. cs из папки Мodels


using System . Net .H ttp ;
using System . Threading.Tasks;
namespace LanguageFeatures.Models
puЫic class MyAsyncMethods {
puЫic static Task<long?> GetPageLength()
HttpClient client = new HttpClient() ;
va r httpTask = client.GetAsync("http://apress.com");
11 Во время выполнения НТТР-запроса
//можно было бы делать другую работу .

return httpTask.ContinueWith((Task<HttpResponseMessage> antecedent) => {


return antecedent . Resu l t . Content . Headers.ContentLength;
}) ;
Глава 4. Важные функциональные возможности языка С# 111
Метод использует объект System. Ne t . Ht tp. HttpC li e nt для запрашивания со­
держимого домашней страницы издательства Apress и возвращает его длину. Работа,
которая будет выполняться асинхронно , представлена в .NET как объект Ta sk .
Объ е кты Task строго типизируются на основе результата , выдаваемого фоновой ра­
ботой . Таким образом , при вызове метода HttpClient . GetAs yn c () получается объ­
ект Task<HttpResponseMes sage> . Он сообщает о том, что запрос будет выполнен в
фоновом режиме и его р езультатом будет объект HttpRespons eMe ssage.

Совет. Когда мы употребляем понятия вроде фоновый режим, то опускаем массу деталей,
чтобы подчеркнуть только ключевые аспекты, которые важны для мираMVC . Поддержка
.NET для асинхронных методов и параллельного программирования в целом превосход­
на, и ее рекомендуется внимательно изучить, если вы хотите создавать по-настоящему

высокопроизводительные приложения, которые могут извлекать преимущества от много­

процессорного или многоядерного оборудования . Вы увидите , как MVC облегчает созда­


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

Самой непонятной для большинства программистов частью явля ется продолже­


ние, пр едставляющее собой механизм , с помощью которого указывается то, что долж­
но произойти, когда фоновая задача зав е ршится . В приведенном примере применяет­
ся метод ContinueWi th () для обработки объекта HttpResponseMe s sag e , получаемого
из метода Ht t pCli ent . GetAsync () . Это делается с использованием лямбда-выраже­
ния , которо е возвраща ет значение свойства, содержащего длину полученного от веб­
се рвера Apress содержимого. Вот код продолжения :

return httpTask . ContinueWith((Task<HttpRespons eMessag e > anteceden t ) => {


return a n tecedent . Res ul t . Content .He aders . Con tent Len g t h ;
}) ;

Обратите внимание , что ключевое слово r e turn встречается два раза. Именно
эта часть вызывает путаницу . Первое применение ключевого слова return указыва­
ет, что во звращается объект Ta s k<Http Re spons eMessage> , который при завершении
задачи возвратит (второе ключевое слово r e t u r n) длину из заголовка Conte n t Le ng th.
Заголовок ContentLength возвращает р е зультат long ? (тип long , допускающий зна­
чения nul l), т.е . результатом метода Ge tPage Length () является Ta s k< l ong?> :

puЫic static Task<long?> GetPageLength() {

Не переживайте , если все сказанное выглядит для вас бессмысленным - в этом вы


не одиноки. Как раз по такой причине в Microsoft и решили добавить в С# ключевые
сло в а , упрощающие р а боту с асинхронными методами.

Применение ключевых сло в async и awai t


Разработчики из Microsoft ввели в язык С# два ключевых слова, которые специаль­
но призваны облегчить использование асинхронных методов, подобных HttpClient .
GetAsync () . Ими являются async и a wa i t ; в листинге 4.39 показано, как с их помо­
щью упростить наш пример метода.
112 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

Листинг 4.39. Применение ключевых слов async и awai t в файле МyAsyncМethods. cs

using System.Net .H ttp;


using System.Threading.Tasks;
namespace LanguageFeatures.Models
puЫic class MyAsyncMethods {
puЫic async static Task<1ong?> GetPageLength()
HttpClient client = new HttpClient();
var httpMessage = await client .GetAsync("http://apress.com");
return httpMessage.Content.Headers.ContentLength;

Ключевое слово awai t используется при вызове асинхронного метода. Оно сооб­
щает компилятору С# о том, что необходимо подождать результата Task, который
возвращается методом GetAsync (), и затем заняться выполнением остальных опе­
раторов в том же методе.

Применение ключевого слова awai t означает. что мы можем трактовать результат


метода GetAsync (), как если бы он был обычным методом, и просто присвоить воз­
вращаемый им объект HttpResponseMessage какой-нибудь переменной. Еще лучше
то, что затем можно использовать ключевое словоreturn традиционным образом для
выдачи результата из другого метода - значения свойства ContentLength в рассмат­
риваемом случае. Это намного более естественный подход, к тому же нам не придется
беспокоиться по поводу метода ContinueWith () и многократного при менения юпоче­
вого слова return.
При использовании ключевого слова awai t потребуется также добавить к сигнатуре
метода ключевое слово async, как бьmо сделано в рассмотренном выше примере. Тип
результата метода не изменяется - метод GetPageLength () по-прежнему возвращает
Task<long?>. Причина в том, что ключевые слова await и async реализованы с при­
менением ряда искусных трюков компилятора, которые позволяют использовать более
естественный синтаксис, но не изменяют того, что происходит внутри методов, к кото­
рым они применены. Программист, вызывающий метод GetPageLength () , по-прежне­
му имеет дело с результатомTask<long?>, т.к. все еще существует фоновая операция,
которая выдает значение long, допускающее null; хотя, конечно же, программист мо­
жет также предпочесть пользоваться ключевыми словами awai t и async.
Такой шаблон повсеместно соблюдается в контроллере MVC, что позволяет легко
писать асинхронные методы действий (листинг 4.40).

Листинг 4.40. Определение асинхронных методов действий в файле HomeController. cs

using Microsoft . AspNetCore.Mvc;


using System.Collections . Generic ;
using LanguageFeatures.Models;
using System;
using System.Linq;
using System.Threading.Tasks;
namespace LanguageFeatures.Controllers
puЫic class HomeController : Controller
Глава 4. Важные функциональные возможности языка С# 113
puЬlic async Task<ViewResul t> Index () {
long? length =
await MyAsyncMethods.GetPageLength();
return View(new string[] { $ 11 Length: {length}" }) ;

Тип результата метода действия Index () изменен на Task<ViewResul t>. Это со­
общает MVC о том, что метод действия будет возвращать объект Tas k, который по
завершении выдаст объект ViewResul t, а тот предоставит детали представления,
подлежащего визуализации, и требующиеся ему данные . К определению метода было
добавлено ключевое слово async, что позволило использовать 1wючевое слово awai t
при вызове метода MyAsyncMethods. GetPathLength (). О продолжениях позаботят­
ся инфраструктура MVC и платформа .NET, а результатом будет код, который легко
писать, читать и сопровождать. Запустив приложение, вы увидите вывод, похожий
на показанный ниже (хотя наверняка с отличающейся длиной, т.к. содержимое веб­
сайта Apress часто изменяется):

Length : 62164

Получение имен
Во время разработки веб-приложений есть много задач, в которых необходимо
ссылаться на имя аргумента , переменной, метода или класса. Распространенными
примерами являются ситуации с генерированием исключения или созданием ошибки
проверки достоверности при обработке пользовательского ввода. Традиционный под­
ход предполагал применение строкового значения с жестко закодированным именем

(листинг 4.4 1).


Листинг 4.41. Использование жестко закодированного имени в файле HomeController. cs

using Microsoft . AspNetCore . Mvc;


using System . Collections.Generic;
using LanguageFeat ur es . Model s ;
using System ;
using System.L inq ;
namespace LanguageFeatures . Controllers
puЫic class HomeController : Co ntro ller
puЬlic ViewRes ul t I ndex() {
var products = new [ ] {
new { Name = "Kayak", 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(p => $"Name: {p.Name}, Price: {р. Price} "));

Вызов метода Se le ct ( ) из LINQ генерирует последовательность строк, каждая из


которых содержит жестко закодированную ссылку на свойства Name и Price. Запуск
приложения дает следующий вьmод в окне браузера:
114 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

Name: Kayak , Price: 275


Name: Lifejacket , Price: 48.95
Name: Soccer ball, Price: 19 . 50
Name: Corner flag, Price : 34 . 95
Проблема этого подхода в том, что он предрасположен к ошибкам, которые обус­
ловлены либо неправильно набранным именем, либо некорректно обновленным име­
нем в строке после рефакторинга. Результат может вводить в заблуждение , что осо­
бенно проблематично для сообщений, отображаемых пользователю. В С# 6 появилось
выражение nameof, благодаря которому ответственность за формирование строки
имени возлагается на компилятор (листинг 4.42).

Листинг 4.42. Использование выражений nameof в файле HomeController. cs

using Microsoft.AspNetCore.Mvc;
using System . Collections . Generic ;
using LanguageFeatures . Models ;
using System ;
using System . Linq ;
namespace LanguageFeatures.Contro l lers
puЬlic class HomeC ontroller Contro l ler
puЫic ViewResult Index() {
var products = new [] {
new { Name " Kayak ", Price = 275М ),
new { Name "Lifejacket ", Price = 48.95М } ,
new { Name "S occer ball" , Price 1 9 . SOM } ,
new { Name "Corner flag ", Price = 34.95М )
};
return View(products.Select(p =>
$" {nameof (p.Name)}: {p.Name}, {nameof (р. Price)}: {р. Price} "));

Компилятор обрабатывает ссылку наподобие р . Name таким образом, что только


последняя часть включается в строку, порождая тот же самый вывод, как и в преды­
дущем примере . Среда Visual Studio располагает поддержкой средства IntelliSense для
выражений nameof , поэтому вы будете снабжены подсказками при выборе ссылок, а
выражения корректно обновятся в случае рефакторинга кода. Поскольку за работу с
nameof отвечает компилятор, применение недопустимой ссылки приводит к ошибке
на этапе компиляции , что не позволит некорректным или устаревшим ссылкам ус­

кользнуть от глаз.

Резюме
В настоящей главе был дан обзор основных языковых средств С#, которые дол­
жен знать результативный программист приложений MVC. Язык С# является гибким
в достаточной мере для того , чтобы обычно существовало несколько способов р еше­
ния любой задачи, но есть средства, с которыми вы будете чаще всего встречаться во
время разработки веб-приложений и видеть повсюду в при ме рах , рассматриваемых в
данной книге. В следующей главе будет представлен механизм визуализации Razor и
приведены объяснения , как его использовать для генерации динамического содержи­
мого в веб-приложениях MVC.
ГЛАВА 5
Работа с Razor

в приложении ASP.NET Core MVC для выпуска содержимого, отправляемого кли­


ентам , используется компонент, который называется мехш-шзмом визуалu­
за4uu. Стандартным механизмом визуализации является Razor, и он обрабатывает
аннотированные НТМL-файлы, производя поиск инструкций, которые вставляют ди­
намическое содержимое в вывод, отправляемый браузеру.
В этой главе дается краткое введение в синтаксис Razor, так что вы сможете опоз­
нать выражения Razor, когда столкнетесь с ними. Мы не собираемся превращать гла­
ву в исчерпывающий справочник по Razor; считайте ее в большей степени ускорен­
ным курсом по синтаксису. Особенности Razor будут раскрыты в последующих главах
книги при рассмотрении других средств МVС. В табл. 5.1 приведена сводка , позволя­
ющая поместить Razor в контекст.

Таблица 5.1. Помещение Razor в контекст

Вопрос Ответ

Что это такое? Razor - это механизм визуализации, отвечающий за встраивание


данных в документы HTML
Чем он полезен? Возможность динамической генерации содержимого являет-
ся неотъемлемой частью процесса написания веб-приложения .
Механизм визуализации Razor предоставляет средства, которые
упрощают работу с остальной инфраструктурой ASP.NET Core MVC с
применением операторов С#

Как он используется? Выражения Razor добавляются к статической НТМL-разметке в


файлах представлений. Эти выражения оцениваются для генерации
ответов на клиентские запросы

Существуют ли какие­ Выражения Razor могут содержать почти любые операторы С#. Иногда
то скрытые ловушки может быть трудно решить, должна ли логика принадлежать представ­
или ограничения? лению или контроллеру, что способно разрушить принцип разделения
обязанностей, который является центральным в паттерне MVC

Существуют ли В главе 21 объясняется , как написать собственный механизм визу­


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

Изменился ли он по Механизм визуализации Razor работает в значительной степени


сравнению с версией таким же образом, как и в MVC 5, но с рядом удобных улучшений.
MVC5? Файл импортирования представлений используется для указания
пространств имен, в которых будет производиться поиск типов при
обработке представления , а также для определения мест, где нахо­
дятся дескрипторные вспомогательные классы (глава 23)
116 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

В табл. 5.2 приведена сводка по главе.

Таблица 5.2. Сводка по главе

Задача Решение Листинг

Доступ к модели представления Используйте выражение @Model для опре­ 5.6, 5.15,
деления типа модели и выражение @rnodel 5.18
для доступа к объекту модели

Использование имен типов без Создайте файл импортирования 5.7, 5.8


их уточнения с помощью про­ представлений
странств имен

Определение содержимого, Применяйте компоновку 5.9-5.11


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

Указание стандартной Используйте файл запуска представления 5.12-5.14


компоновки

Передача данных из контролле- Применяйте объект ViewBag 5.16,5.17


ра представлению за предела­

ми модели представления

Выборочная генерация Используйте условные выражения Razor 5.19, 5.20


содержимого

Генерация содер жимого для Применяйте выражение @foreach 5.21, 5.22


каждого элемента в массиве механизма Razor
или коллекции

Подготовка проекта для примера


Для демонстрации работы механизма визуализации Razor мы создали проект
ASP.NET Core Web Application (.NET Core) (Веб-приложение ASP.NET Core (.NET Core))
по имени Razor, используя шаблон Empty (Пустой), как делали это в предыдушей гла­
ве . Отредактировав раздел dependencies файла proj ect . j son, мы добавили сборку
МVС (листинг 5.1).

Листинг 5.1. Добавление сборки MVC в файле proj ect. j son

11
dependenci е s 11 : {
11
Microsoft . NETCore.App 11 :
11
version 11 : 11 1 . 0.0 11 ,
11
type 11 : 11 platform 11
},
11
Mi crosoft . AspNetCore.Diagnostics 11 :
11
1.0.0 11 ,
11
Microsoft .AspNetCore.Server.IISintegration 11 : 11 1 . 0 . 0 11 ,
11
Microsoft.AspNetCore . Server . Kestrel ": "1.0 . 0 ",
11
Microsoft .Ex tensions.Logging .Console 11 : " 1.0.0 11 ,
"Microsoft . AspNetCore.Mvc 11 :
11
1.0.0 11
},
Глава 5. Работа с Razor 117
После сохранения изменений , внесенных в pr oj ect . j son, среда Visual Studio
добавит в проект сборку Microsoft. AspNetCo re. Mvc . Далее мы включаем инфра­
структуру MVC с ее стандартной конфигурацией в файле Startup . cs (листинг 5.2).

Листинг 5.2. Включение инфраструктуры MVC в файле Startup. cs


using Microsoft.AspNetCore . Builder ;
using Microsoft . AspNetCore.H osting ;
using Microsoft.AspNetCore .H ttp;
using Microsoft . Extensions .Dependency inj ection ;
using Microsoft . Extens i ons .Logging ;
namespace Razor {
puЬlic class Startup
puЬlic void ConfigureServices( I ServiceCollection servi ces) {
services.Adc!Мvc();

puЫic void Configure(IApplicationBuilder арр , IHostingEnvironment env ,


ILoggerFactory loggerFactory) {
app.UseMvcWithDefaultRoute();

Определение модели
Затем мы созда ем папку Models и добавляем в нее файл класса по имени
Product . cs с приведенным в листинге 5.3 определением простого класса модели.

Листинг 5.3. Содержимое файла Product. cs из папки Models


namespace Razor . Models {
puЫic class Product {
puЫic int ProductID { get; set ;
puЫic string Name { get; set ; }
puЬlic string Description { get ; set;
puЬlic decimal Price { get ; set;
puЫic string Category { set ; get ; }

Создание контроллера
Стандартная конфигурация, установленная в файле Startup . cs , следует соглаше­
нию MVC относительно отправки запросов контроллеру по имени Ноте по умолчанию.
Мы создали папку Controllers и добавили в нее файл класса HomeController. cs ,
поместив в него простое определение контроллера (листинг 5.4).
118 Часть 1. Введение в инфрастру ктуру ASP.NET Core MVC

Листинг 5.4. Содержимое файла HomeController. cs из папки Controllers


using Microsoft . AspNetCore.Mvc ;
using Razor . Models ;
namespace Razor.Contro l lers {
puЬl i c class HomeController : Contro ller

p uЫ ic ViewRes ult Index() {


Product myP rod u ct = new Product {
ProductID = 1,
Name = "Kayak",
Description = " А boat f or one person ",
Category = " Watersports ",
Price = 275 М
};

r etu rn View(myP roduct);

В контроллере Ноте определен метод действия по имени Index () , в котором со ­


здается объект Product с заполнением его свойств. Объект Product передается ме­
тоду View () ,так что он используется как модель во время визуализации представле ­
ния. При вызове метода View () имя файла представления не указывается , поэтому
будет применяться стандартное представление для метода действия.

Создание представления
Чтобы создать стандартное представление для метода действия Index () , мы со ­
здаем папку Views/ Home и добавляем в нее файл типа MVC View Page (Стр аница пр ед ­
ставления MVC) по имени Index . cs html, куда помещаем содержимое, показанное в
листинге 5.5.

Листинг 5.5. Содержимое файла Index. cshtml из папки Views/Home


@model Razor . Models . Product
@{
Layout = nul l;

< ! DOCTYPE html >


<h tm l >
<head>
<meta name= " viewpo rt" content= " wid th =device - width " />
<tit le > Index </ title>
</hea d >
<body>
Conte nt will go here
</body>
</html >
Глава 5. Работа с Razor 119
В последующих разделах мы рассмотрим различные части представления Razor и
продемонстрируем разнообразные вещи. которые с ним можно делать. При изучении
Razor полезно помнить. что представления существуют для выражения пользователю
одной или более частей модели - и это означает генерацию НТМL-разметки. которая
отображает данные, извлеченные из одного или множества объектов. Если не забы­
вать, что мы всегда пытаемся строить НТМL-страницу, которая может быть отправ­
лена клиенту. тогда вся активность механизма визуализации Razor обретает смысл.
Запустив приложение. вы увидите простой вывод, приведенный на рис. 5.1.

[J lndex

1--~ С [ф localhost:60753
L o11tent \vill go 11е1·е - - - - - -

Рис. 5.1. Выполнение примера прило жения

Работа с объектом модели


Давайте начнем с самой первой строки в файле представления Index. cshtml:

@model Razor.Models .Product

Выражения Razor начинаются с символа@. В данном случае выражение @model


объявляет тип объекта модели, который будет передаваться представлению из метода
действия. Это позволяет ссылаться на методы, поля и свойства объекта модели пред­
ставления посредством @Model, как показано в листинге 5.6 , в котором приведено
простое дополнение к представлению Index.

Листинг 5.6. Ссылка на свойство объекта модели представления в файле Index. cshtml
@mode l Razor.Models.Product
@{
Layout = null;

< !DOCTYPE html>


<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>Index</title>
</head>
<body>
@Model.Name
</body>
</html>
120 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

На заметку! Обратите внимание, что тип объекта модели представления объявляется с ис­
пользованием @model (со строчной буквой m), а доступ к свойству Name производится с
применением @Model (с прописной буквой М). Поначалу зто может немного запутывать,
но со временем станет вполне привычным .

Запустив приложение, вы увидите вывод, представленный на рис. 5.2.

[j lndox х

"·· С [QS-;-~~ 1110~1:w7sз --~--------==*l

~
-------·· ' . . ·-·-- -
Kayak
---· ·------.·~------·--·-·--··~-·---·-w-·--w-·--·---*-"•-...,•-•

Рис. 5.2. Результат чтения значения свойства внутри представления

Представление, которое использует выражение @model для указания типа, назы­


вается строго типизированным представлением. Среда Visual Studio способна при­
менять выражение @mode l для открытия окна со списком предполагаемых имен чле­
нов, когда вы вводите @Mode l с последующей точкой (рис. 5.3).

Razor - 1:1 Х

, ~d el R~:tor ./-lodels . Product


l+-
@{
L~yo u t ~ null;

<! ООСТУРЕ html >


~
B <html>
. B <t,ead >
·i <me ta nэrr.e•"viewport" contenta'·\-..idth=device·\"lidth'' />
i <title>l ndex</title>
• l </head >
B <body >
i l!t'lodel .J11
1</Ьоdу> .,,, Category
[ </html> -" Oe<cription
Ф Equols
_ _,.._ _ _ ___tl· Ф GetType
100% - ~
Gt:tHashCode i
Ф

"' ---~­
,,, Price
-" ProductlD
,Ф . ToS.tring

Рис. 5.3. Среда Visual Studio предлагает список предполагаемых имен членов
на основе выражения @Model

Список предполагаемых имен членов, отображаемый Visual Studio, помогает из­


бегать ошибок в представлениях Razor. При желании вы можете игнорировать эти
предположения, и тогда Visual Studio будет подсвечивать проблемные имена членов,
Глава 5. Работа с Razor 121
чтобы вы внесли исправления, как поступает в обычных файлах классов С#. Пример
подсветки проблемы можно видеть на рис. 5.4, где мы пытается сослаться на свойство
@Mode l . No tARe alProperty. Среда Visual Studio обнаруживает, что класс Produc t,
указанный в качестве типа модели, не содержит такого свойства, и подсвечивает
ошибку в редакторе .

. 1· , r.
FJ<head)
l <rneta nan;e ="vie1~port " conten t " "1~idtl1=device-widt h " />

l <title>I nd ex</title>
</head>
Э <Ьоdу>
@f'tode l . ~e~y,,

· l </body>
</html>
~...; ,..," ".~, ....
~
.м. ~
r
Рис. 5.4. Среда Visual Studio сообщает о проблеме с выражением @Mode l

Использование файла импортирования представлений


При определении объекта модели в начале файла Index . cshtml необходимо было
включать пространство имен, которое содержит класс модели, например:

@model Razor.Models.Pr oduct

По умолчанию все типы, на которые производится ссьmка в строго типизирован­


ном представлении Razor, должны уточняться своими пространствами имен. Это не
особо крупная проблема, когда есть только ссылка на объект модели, но понимание
представления может значительно затрудниться при написании более сложных вы­
ражений Razor, таких как рассматриваемые позже в настоящей главе.
Добавив в проект файл импортuрования представлений. можно указать набор про­
странств имен, в которых должен осуществляться поиск типов. Файл импортирования
представлений размещается в папке Vi e ws и имеет имя _ View impo rt s . c sht ml.

На заметку! Файлы в папке Vi ew s, имена которых начинаются с символа подчеркивания


( ), пользователю не возвращаются , что позволяет с помощью имен файлов проводить
различие между представлениями, подлежащими визуализации, и поддерживающими их

файлами. Файлы импортирования представлений и компоновки (будут описаны вскоре)


имеют имена, начинающиеся с символа подчеркивания.

Чтобы создать файл импортирования представлений, щелкните правой кнопкой


мыши на папке View s в окне Solution Explorer, выберите в контекстном меню пункт
Addc::>New ltem (Добавить<=:> Новый элемент) и укажите шаблон MVC View lmports Page
(Страница импортирования представлений МVС) из категории ASP.NET (рис. 5.5).
Среда Visual Studio автоматически назначит файлу имя_Vi ew i mp o r t s. cshtml,
а щелчок на кнопке Add (Добавить) приведет к его созданию. Поместите в файл выра­
жение, показанное в листинге 5.7.
122 Часть 1. Введение в инфраструктуру ASP.NET Саге MVC

ASP.NEТ
• Тур~: ASP.N~
MVC View l •yout Page
Clitnt·side
MVC Vie\v lmpc
Code
MVC у;..,, St4rt Page ASP.NEТ t
t> Online
J'!

·.
ASP . NП Ccnfiguration File ASP .NEТ

~е to 90 online and find templates.

Nыn<: _Viewlmports.c shtn' I

Рис. 5.5. Создание файла импортирования представлений

Листинг 5.7. Содержимое файла_Viewimports. cshtml из папки Views


@using Razor . Mode ls

Пространств а имен , в которых должен проводиться поиск классов, пр именя емых


в представлениях Razor, указываются с использованием выражения @using, за кото ­
рым следует пространство имен. В листинге 5. 7 добавлена запись для пространства
имен Razor . Models , содержащего класс модели в прим ере приложения .
Теперь, когда пространство имен Ra z or . Models включено в файл импортирова ­
ния представле ний, можно удалить пространство имен из файла Index . cshtml (лис­
тинг 5.8) .

Листинг 5.8. Ссылка на класс модели без пространства имен в файле Index. cshtml
@model Product
@{
Layout = null ;

< ! DOCTYPE html>


<html>
<head>
<meta name= "v i ewpo r t " content=" wi dth=de v i ce - width " />
<t i tle> I ndex</title>
</ head>
<body>
@Mo del . Name
</body>
</html >
Гл ава 5. Работа с Razor 123

Совет. Выражение @using м ожн о также до бавлять в отдельные файлы представ л ений, ч то
позволит применять внутри них типы без указания пр о ст ранства имен.

Работа с компоновками
Ниже приведено еще одно важное выражение Razor из файла представления
Index. cshtml:

@{
Layout null;

Это пример блока кода Razor, который позволяет включать в представление опе­
раторы С#. Блок кода открывается посредством @{ и закрывается с помощью }, а
содержащиеся в нем операторы оцениваются при визуализации представления .

Показанный выше блок кода устанавливает значение свойства Layou t в null.


Представления Razor компилируются в классы С# внутри приложения MVC, а в ба­
зовом классе, который они используют, определено свойство Layout. В главе 21 объ­
ясняется, нак все это работает, а пока достаточно знать, что результатом установки
свойства La yout в null является сообщение инфраструктуре МVС о том, что наше
представление является самодостаточным, и оно будет визуализировать все свое со­
держимое, которое необходимо возвратить клиенту.
Самодостаточные представления хороши для простых примеров приложений, но
реальный проект может включать десятки представлений. и некоторые представле­
ния будут иметь общее содержимое. Дублированное общее содержимое в представ­
лениях становится трудным в управлении, особенно если нужно внести изменение и
отследить все представления, подлежащие модификации.
Более эффективный подход предусматривает использование компоновки Razor, яв­
ляющейся шаблоном, который хранит общее содержимое и может быть применен к
одному или большему числу представлений. Когда вы вносите изменение в компонов­
ку , оно автоматически воздействует на все представления, которые ее используют.

Создание компоновки
Компоновки обычно совместно используются представлениями , применяемыми
множеством контроллеров, и хранятся в папке по имени Views/Shared, которая
входит в перечень местоположений, просматриваемых Razor в попытках найти файл.
Чтобы создать компоновку. создайте папку Views/Shared, щешшите на ней правой
кнопкой мыши и выберите в контекстном меню пункт AddФNew ltem (ДобавитьФНовый
элемент). Укажите шаблон MVC View Layout Page (Страница компоновки представле­
ний МVС) в категории ASP.NET и введите _BasicLayo ut . cshtml в качестве имени
файла (рис. 5.6). Щелкните на кнопке Add (Добавить) для создания файла. (Подобно
файлам импортирования представлений имена файлов компоновок начинаются с
символа подчеркивания .)
В листинге 5.9 показано начальное содержимое файла _Basi c Layout . cshtml, до­
бавленное средой Visual Studio при создании файла.
124 Часть! . Введение в инфраструктуру ASP.N ET Саге MVC

" lnstall<d f'Se;rc~' ln<talled Templ•lf


ASP.Nff
Client-s id• ~·
fl;
MVC Controller Closs ASP . NEТ
Туро: AS P . NEТ

MVC Vi.,,v Loyout Pog


Code
~· Web API Controller Cla1S ASP.N EТ
fl- Online
fl;
i


. МVС View lmpor1s Poge ASP.NET


fl; Ritzor Tag H ~ l pe r ASP.NET
"'
Cljclc here to go onlioe itnd find tempi<!t es.

_Bo sicl"yout.csl,tml

Рис. 5.6. Создание компоновки

Листинг 5.9. Начальное содержимое файла _BasicLayout. cshtml


<!DOCTYPE html>
<html>
<head>
<meta name="viewport " content="width=device-width" />
<title >@ViewBag.Title </title>
</head>
<body>
<div>
@RenderBody ()
</di v>
</body>
</html>

Компоновки - это специализированная форма представлений; в листинге 5.9


выражения @ выделены полужирным . Вызов метода @RenderBody () вставляет в
разметку компоновки содержимое представления. указанное с помощью метода

действия. Другое выражение Razor в компоновке обращается к свойству по имени


ViewBag . Ti tle для установки содержимого элемента ti t le . Объ ект ViewBag явля­
ется удобным средством, которое позволяет передавать значения данных внутри при­
ложения - в рассматриваемом случае между представлением и его компоновкой . Вы
увидите, как это работает, когда компоновка будет применена к представлению.
Элементы HTML в компоновке будут применены к любому представлению. которое
использует компоновку , предоставляя шаблон для определения общего содержимого.
В листинге 5.1 О к компоновке добавлена простая разметка, чтобы ее эффект как шаб­
лона был очевидным.
Глава 5. Работа с Razor 125
Листинг 5.10. Добавление содержимого в файл_BasicLayout. cshtml
< !DOCTYPE html>
<html>
<head>
<meta name= "viewport " content= "width=device - width" />
<title>@ViewBag .Ti tle</title>
<style>
#mainDiv {
padding: 20рх;
border: solid medium Ыасk;
font-size: 20 pt
}
</style>
</head>
<body>
<hl>Product Informa tion</hl>
<div id="mainDiv">
@RenderBody ()
</div>
</body>
</html >

Здесь был добавлен элемент заголовка, а также стиль CSS для стилизации со­
держимого элемента di v, в котором находится выражение @RenderBody (), прос­
то ради прояснения, какое содержимое поступает из компоновки, а KaJ{Oe - из

представления.

Применение компоновки
Чтобы применить компоновку к представлению, понадобится установить значение
свойства Layou t и удалить НТМL-разметку, которая теперь будет предоставляться
компоновкой, такую как элементы html, head и body (листинг 5.11).

Листинг 5.11. Применение компоновки в файле Index. csh tml


@model Product
@{
Layout = "_BasicLayout";
ViewBag .Title = "Product Name";

Product Name: @Model.Name

Свойство Layout указывает имя файла компоновки, который будет использовать­


ся для представления, без расширенияcshtml . Механизм Razor будет искать указан­
ный файл компоновки в папках /Views/Home и Views/Shared.
Кроме того, выполнено присваивание свойства ViewBag. Ti tle . Оно будет приме­
няться компоновкой для установки содержимого элемента t i t 1 е при визуализации
представления.

Трансформация пр едставления значительна даже для такого простого приложе­


ния. Компоновка располагает всей структурой, требуемой для любого НТМL-ответа ,
оставляя представлению только заботу о динамическом содержимом, которое отоб-
126 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

ражает данные пользователю. Когда инфраструктура MVC обрабатывает файл


Index. cshtml, она применяет компоновку для создания унифицированного НТМL­
ответа (рис. 5.7).

, -> С ' CJ localhost:6063 '1 := J


. - - - - - - - - -- - - ---

1 P1·oduct Information

Ргоdнсt Nаше: Kayak

Рис. 5. 7. Результат применения компоновки к представлению

Использование файла запуска представления


Осталась еще одна небольшая шероховатость, которую необходимо устранить -
мы должны указывать файл компоновки для применения в каждом представлении.
Следовательно, если нужно переименовать файл компоновки, то придется отыскать
все ссылающиеся на него представления и внести изменение, что будет чреватым
ошибками процессом и противоречит лейтмотиву, пронизывающему разработку при­
ложений MVC - легкости сопровождения.
Решить упомянутую проблему можно с использованием файла запуска представ­
ления. При визуализации представления инфраструктура МVС ищет файл по имени
_ ViewStart . cshtml. Содержимое этого файла будет трактоваться так, если бы оно
присутствовало внутри самого файла представления , и данное средство можно при­
менять для автоматической установки свойства Layout .
Чтобы создать файл запуска пр едставления, щелкните правой кнопкой мыши на
папке Views, выберите в контекстном меню пункт AddqNew ltem (ДобавитьqНовый
элемент) и укажите шаблон MVC View Start Page (Файл запуска представления МVС) в
категории ASP.NET (рис . 5.8).
Среда Vlsual Studio автоматически назначит файлу имя _ViewStart.cshtml;
щелчок на кнопке Add (Добавить) приведет к созданию файла с начальным содержи­
мым, приведенным в листинге 5.12.

Листинг 5.12. Начальное содержимое файла ViewStart. cshtml


@{
Layout "_Layout ";

Для применения компоновки ко всем представлениям в приложении значение,


присваиваемое свойству Layout, изменено, как показано в листинге 5.13.
Глава 5. Р абота с Aazor 127

Client~side
MVC View l"yout Page ASP.NEТ
. Тур е: ASP.N ET
MVC View St•rt Page
Code

~ Online
ji" MVC View lmports Page ASP.NEO
#

t.; Razo r Т119 Helper ASP.NET


,5
Middleware Class ASP.NEТ
~


t.; Startup class ASP.NEТ

у1] ASP.NEO Configurotion File ASP.NEТ

Cli<:k lч~re !О go online § ПО [ind A~ml2; 1 c1t~ .

№me: _ViewStart.cshtml

Рис. 5.8. Создание фай л а запуска представления

Листинг 5.1 З. Применение стандартного представления в файле_ViewStart. cshtml


@{
Layout = "_BasicLayout";

Поскольку файл з апуска представления содержит значение для свойства Layout,


можно удалить соответствующее выражение из файла Iпd e x . c shtml (листинг 5.14).

Листинг 5.14. Обновление файла Index. csh tml, позволяющее отразить


использование файла запуска представления

@model Product
@{
ViewBag . Ti t le = " Pr oduct Name";

Produc t Name: @Model.Name

Специально указывать, что должен применяться файл запуска представления, не


требуется . Инфраструктура MVC автоматически обнаруживает данный файл и ис­
пользует его содержимо е . Преимущество отдается значениям , определяемым в фай­
ле представления, что делает переопределение файла запуска представления очень
простым.

Можно также применять несколько файлов запуска представлений , чтобы устано­


вить стандартное поведение для разных частей приложения . Механизм Razor ищет
самый близкий файл запуска для обрабатываемого представления, а это значит, что
стандартные настройки можно переопределять, добавляя файл запуска представле­
ния в папку View s /Home или Views / Share d.
128 Часть 1. Введение в инфрастру ктуру ASP.NET Core MVC

Внимание! Важно понимать разницу между отсутствием свойства Layout в файле пред­
ставления и его установкой в nu ll. Если представление является самодостаточным, и
вы не хотите применять компоновку, то установите свойство Layout в n ul l. Если же
просто опустить свойство Layou t , то инфраструктура MVC будет считать, что компоновка
вам необходима, и она должна использовать значение, которое найдет в файле запуска
представления.

Использова ни е в ы ражен и й Razor


Теперь , когда вы видели основы представлений и компоновок, давайте переклю­
чимся на другие виды выр ажений, поддерживаемые Razor, и посмотрим , к а к их при­
менять для создания соде ржимого представлений. В хорошем приложе нии MVC им е ­
ется четкое разделение между ролями метода действия и представления . Роли просты
и кратко описаны в табл. 5.3.

Таблица 5.3. Роли, исполняемые методом действия и представлением


Компонент Что делает Чего не делает

Метод действия Передает представлению объект Не передает представлению


модели представления сформатированные данные

Представление Использует объект модели пред­ Не изменяет ни одного аспе кта в


ставления для отображения со­ объекте модели представления
держимого пользователю

Далее в книге мы будем неоднократно возвращаться к этой теме. Чтобы извлечь


максимум из MVC , необходимо соблюдать и обеспечивать разделение между разными
частями приложения. Вы увидите , что механизм Razor позволяет делать очень мно­
гое , включая применение операторов С# , но вы не должны использов ать Razor для
выполнения бизнес-логики или манипулирования объектами моделей предметной
области.
В качестве простого примера в листинге 5.15 демонстрируется добавлени е нового
выраж ения в представление I n d ex.

Листинг 5.15. Добавление выражения в файл Index. csh tml


@mode l Produ ct
@{
ViewBag . Title = " Product Name ";

<p>Product Name: @Model.Name</p>


<p>Product Price: @($"{Model.Price:C2}")</p>

Значени е свойства Price можно было бы сформатировать в м етоде действия и п е ­


редать его представлению . Это бы работало, но такой подход р азрушает пр еи мущест­
во шаблона МVС и сокращает возможность реагировать на изменения в будущем. Как
уже упоминалось, мы возвратимся к данной теме снова , но вы должны з апо м нить ,
что инфраструктура ASP.NEТ Core MVC не принуждает правил ьно применять паттерн
МVС и нужно учитывать влияние решений, принимаемых во время проектирования
и написания кода .
Глава 5. Работа с Razor 129

Обработка или форматирование данных

Ва жн о отличать обработку данных от их форматирования. Представления форматируют дан­


ные , поэтому в предыдущем разделе представлению передается объект Product, а не
отобража емая стро ка с форматированными свойствами этого объекта. За обработку данных,
в ключая выбор объе ктов данны х для отображения, отвечает контроллер , который будет об­
ращаться к модели с целью получения и модификации требующихся данны х. Иногда трудно
понять, где проходит черта между обработкой и форматированием, но в качестве эмпири ­
ческого правила рекомендуется занять осторожную позицию и выносить из представления в

контроллер все кроме просте й ших выраже ний.

Вставка значений данных


Самое простое, что можно делать с помощью выражения Razor - вставлять значе­
ния данных в разметку. Наиболее распространенный способ сделать это предполага­
ет использование выражения @Model . Представление Index уже содержит примеры
применения такого подхода, подобные показанному ниже:

<p>Product Name : @Model.Name</p>

Вставлять значения можно также с использованием объекта ViewBag, кото­


рый применялся для установки содержимого элемента t i tle в компоновке . Объект
ViewBag мож но использовать для передачи данных из контроллера в представление,
дополняющее модель (листинг 5 .1 6) .

Листинг 5.16. Применение ViewBag в файле HomeController. cs


using Microsoft . AspNetCore .Mvc;
using Razor.Models;
namespace Razor . Controlle rs {
puЬlic class HomeController : Controller

puЬlic ViewResult Index() {


Product myProduct = new Product {
ProductID = 1 ,
Name = "Kayak",
Description = " А boat for one person ",
Category = "Watersports ",
Price = 275 М
};

ViewBag. StockLevel = 2;
return View(myProduct);

Свойство ViewBag возвращает динамический объект, который может использо­


ваться для определения произвольных свойств. В листинге 5.16 было определено и
установлено в 2 свойство по имени StockLevel. Так как объект ViewBag является
динамическим, объявлять имена свойств заранее необязательно, но это означает, что
130 Часть 1. Введение в инфраструктуру ASP.NEТ Саге MVC

для свойств ViewBag среда Visual Studio не в состоянии предоставлять предполагае­


мые варианты автозавершения.

Знание того, когда применять ViewBag, а когда расширять модель - вопрос опы­
та и сложившихся предпочтений. Мой персональный стиль заключается в том, что­
бы использовать объект ViewBag только для предоставления визуальных подсказок
о способе визуализации данных и не применять его для значений данных, которые
отображаются пользователю. Но зто просто то, что подходит лично мне. Если вы хо­
тите использовать ViewBag для данных, отображаемых пользователю, тогда обращай­
тесь к значениям с помощью выражения @ViewBag (листинг 5.17).

Листинг 5.17. Отображение значения ViewBag в файле Index. cshtml


@model Product
@{
ViewBag.Title = " Product Name ";

<p>Product Name : @Model.Name</p>


<p>Product Price: @($ " (Model.Price:C2} " )</p >
<p>Stock Level: @ViewBag.StockLevel</p>

Результат можно видеть на рис. 5.9.

~ Product N"m~ Х

С ~- localho;GO~ - ---- - - - - - - - · - - _j] ~-1


1

1 P1·oduct I11fo1·matio11
1

1 Product Name: Kayak 1

1 ::::::::~~е2 $275 00 1

iL_..._- -__- - - - -_--__-_-_=:_-:_-_-_-_-_-_-_-__-_-_-_- _-_::_- -._-:-:-:-:-:-:..._,


1

Рис. 5.9. Применение выражений Razor для вставки значений данных

Установка значений атрибутов


Во всех рассмотренных до сих пор примерах устанавливалось содержимое элемен­
тов, но выражения Razor можно использовать также для установки значений атри­
бутов элемента . В листинге 5.18 демонстрируется применение выражений @Model и
@ViewBag для установки содержимого атрибутов в элементах представления Index.
Глава 5. Работа с Razor 131
Листинг 5.18. Использование выражений Razor для установки значений атрибутов
в файле Index. cshtml
@model Product
@{
ViewBag . Title = " Product Name ";

<div data-productid="@Model.ProductID"
data-stocklevel="@ViewBag.StockLevel">
<p>Product Name : @Model . Name</p>
<p>Product Pr i ce : @($ "{ Model . Price : C2 }" )</p>
<p>Stock Level : @ViewBag . StockLevel</p>
</div>

Выраж е ния Razor пр и меняются для установки знач е ний некот орых атрибутов
данных в эл ементе di v.

Со в ет. Атрибуты данны х , которые представляют собой атрибуты с именами, начинающимися


на da ta - , в течение многих лет были неформальным способом создания специальных
атрибутов и затем сделаны частью формального стандарта HTML5. Чаще всего они ис­
пользуются так , что код JavaScript может находить специфические элементы, или так , что
стили CSS могут применяться более ограниченным образом.

Запустив прим е р приложения и прос м отрев НТМL-разм етку , отправл е нную бр ау­
зе ру, вы увидите, что механизм Razor установил значения атрибутов :

<div data-stocklevel="2" data-productid="l">


<p>Product Name : Kayak</p>
<p>Product Price : $2 7 5 . 00</р>
<p>Stock Level : 120</р>
</div>

И с п ольз о в ан и е усло вны х операторо в


М ехани зм Razor спо собен обр абатывать условны е операторы, что означает воз­
м ож ность настрой ки вывода из представления на основ е значений данных представ ­
л ения . Такой при е м я вляется наиболее важной частью Razor и позволя ет создавать
сложн ы е и и зм е нчивые компоновки , которые по-прежн е му остаются простыми в

пони м ании и сопровождении. В л и стинг е 5. 19 прив еде но обновленное содержи м о е


пр едставл ен ия Index, включающее условный оператор .

Листинг 5.19. Применение условного оператора Razor в файле Index. cshtml


@model Product
@{
ViewBag . Title = " Produc t Name ";

<div data - product i d= " @Model . Pr odu ctID "


data-stocklevel= " @Vie wBag . StockLeve l" >
<p>Product Name: @Model. Na me</p>
<p>Product Price : @($ " {Model . Price : C2 }" )</p>
<p>Stock Level :
132 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

@s witc h ( (int)Vi ewBag . StockLevel )


case О :
@: Ou t of Stock
brea k;
case 1 :
case 2 :
case 3 :
<b>Low Stock (@Vie wBag . StockLeve l)</b>
b re ak ;
defaul t:
@: @ViewBag. StockLeve l in Stock
brea k;

< /р >
</ div>

Чтобы начать условный оператор , перед условным ключевым словом С# (в этом


примере switch) помещается символ @ . Блок кода завершается закрывающей фигур­
ной скобкой (}) подобно обычному блоку ~юда С#.

Совет. Обратите внимание, что для использования внутри оператора sw i tch значение
свойства Vi ewBag . ProductCount должно быть приведено к типу int. Причина в том,
что Rаzоr-выра жение @sw i tch не может оценивать динамическое свойство - необходи ­
мо приведение к специфичному типу, чтобы было известно , как выполнять сравнения.

Внутри блока кода Razor в вывод представления можно включать НТМL- элементы и
значения данных , просто определяя НТМL-разметку и выражения Razor, например :

<Ь>Low Stock (@ViewBag.StockLevel)</b>

Помещать элементы или выражения в кавычки либо отмечать их каким-то спе­


циальным образом не требуется - механизм Razor будет интерпретировать это как
вывод, подлежащий обработке. Тем не менее, если нужно вставить в представление
литеральный текст, когда он не содержится в НТМL-элементе, то об этом понадобится
сообщить Razor, добавив к строке префикс:

@: Out of Stock

Символы @: пр едотвращают обработку механизмом Razor данной строки как опе­


ратора С#, что является стандартным поведением в отношении те кста . Результат вы­
полнения такого условного оператора показан на рис. 5.10.
Условные операторы играют важную роль в представлениях Razor, т.к. они позво ­
ляют варьиров ать соде ржимое на основе значений данных, которые пр едставл ение
получает от метода действия. В качестве дополнительной демо н страции в листин­
ге 5.20 приведено представл е ние Index . c s h t ml с добавленным оператором if - еще
одним распространенным условным оператором.
Глава 5. Работа с Razor 133

С) ProdU<t N""' Х

1- С (~ l~~~~~t~60i_S3 -==--=·=-=--·=--==='---~
1 P1·oduct Info1·mation

1
1 Prodнct Nаше: Kayak
1 P1·od.нct PI"ice: $275.00

1 Stock Level: Lo\V Stock (2)

Рис.
l_- - --------------------------------
5. 1о. Применение оператора sw i tch в представлении Razor

Листинг 5.20. Использование оператора if в файле Index. cshtml


@model Product
@{
ViewBag.Title = " Product Name ";

<div data - productid= " @Model . ProductID "


data - stocklevel= " @ViewBag . StockLeve l" >
<p>Product Name : @Model . Name</p>
<p>Product Price : @($ "{ Model.Price : C2} ") </p>
<p>Stock Level :
@if (ViewBag.StockLevel == 0)
@: Out of Stock
} else if (ViewBag. StockLevel > О && ViewBag. StockLevel <= 3) {
<b>Low Stock (@ViewBag.StockLevel)</b>
else {
@: @ViewBag.StockLevel in Stock
}
</р>
</div>

Этот условный оператор выдает те же самые результаты, что и оператор swi tch ,
но мы просто хотели проде монстрировать применение условных операторов С# в
представлениях Razor. В главе 21, где представления рассматриваются более подроб­
но, будут даны объясн ения, как все это работает.

Проход по содержимому массивов и коллекций


При разработке приложения МVС часто необходимо выполнять проход по содер­
жимому массива или друrой разновидности коллекции объектов с генерацией подроб­
ной информации для каждого объекта. Чтобы продемонстрировать, как это делается,
134 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

в листинге 5.21 показан модифицированный метод действия I ndex () в контроллере


Home , 1<0торый теперь передает представлению массив объектов Pr odu ct .

Листинг 5.21. Использование массива в файле HomeController. cs

using Mi crosoft . As pNetCo re. Mvc ;


us i ng Raz or. Mode ls;
namespace Razor . Controllers {
pu Ы ic c l a s s HomeCont r ol l er : Con troll e r

pu Ыi c I Actio nRes ult Index () {


Product [] array = {
new Product {Name = "Kayak", Price = 275 М},
new Product {Name "Lifejacket", Price = 48. 95 М},
new Product {Name "Soccerball", Price 19.50М},
new Product {Name "Corner flag", Price = 34.95 М}
} ;
return View(array);

Метод действия Index ( ) создает объект Product [] , который содержит простые


значения данных, и передает его методу Vi ew () для визуализации с применени ем
стандартного представления. В листинге 5.22 изменен тип модели для пр едставления
Index и с помощью цикла for e ach производится проход по объектам в массиве.

Листинг 5.22. Проход по массиву в файле Index. csh tml

@mode l Product[J
@{
ViewBag .T it l e "P roduct Name ";

<taЬle>
<thead>
<tr><th>Name</th><th>Price</th></tr>
</thead>
<tЬody>
@foreach (Product р in Model)
<tr>
<td>@p.Name</td>
<td>@($"{p.Price:C2}")</td>
</tr>
}
</tЬody>
</taЬle>

Оп е ратор @f orea c h выполняет проход по содержимому массива объектов моде­


ли и генерирует для каждого элемента массива строку в таблице. Вы видите, что в
цикле fo r each создана локальная переменная по имени р , на свой ства которой осу­
ществляется ссылка с использованием выражений Razor вида @р . Name и @р . Price .
Результат приведен на рис. 5.11 .
Глава 5. Работа с Razor 135

['j Product №me Х

. С Гф lo-ca-111-os-t.60
- 75-=3=====
-- -- - ~-==---=----=:-:-_-:::::-::;::_-:::;:::::::-_-:-::::::::-:..-._-________- - - -

Product Info1·mation

Name Price
Kayak $275.00
Litej acket $48.95
Soccer ball $ 19.50
Согпе1· flag $34.95

----------------
Рис. 5.11. Применение Razor для прохода по массиву

Резюме
В этой главе был предложен обзор механизма визуализации Razor и показано, как
его использовать для генерации НТМL-разметки. Мы взглянули, каким образом ссы­
латься на данные, передаваемые из контроллера, через объект модели представления
и объект Vi ewBag, а также продемонстрировали применение выражений Razor для
настройки ответов пользователю на основе значений данных. В оставшихся главах
книги вы увидите много разных примеров использования Razor, а в главе 21 найдете
подробное обсуждение функционирования механизма визуализации МVС. В следую­
щей главе будут описаны некоторые средства, предлагаемые Visual Studio для работы
с про ектами ASP.NET Core MVC .
ГЛАВА 6
Работа с Visual Studio

в
этой главе рассматриваются основные средства, предоставляемые Visual Studio
для разработки проектов ASP.NET Core MVC . В табл. 6.1 приведена сводка по
главе.

Таблица 6. 1. Сводка по главе

Задача Ре шение Листинг

Добавление к проекту пакетов Отредактируйте раздел зависимостей 6.1-6.6


.NET (dependencies) файла proj ect . j son
или воспользуйтесь инструмен том NuGet

Добавление к проекту пакетов Создайте файл bower. j son и добавь­ 6.7, 6.8
JavaScript или CSS те требующиеся пакеты в его раздел
dependencies
Просмотр результатов изменения Используйте модель итеративной 6.9-6.11
представления или класса разработки

Отображение детальны х сообще­ Используйте страницы исключений 6.12


ний в браузере разработчика

Получение детальной информа­ Используйте отладчик 6.13


ции и контроля над выполнением

приложения

Повторная загрузка одного или Используйте средство Browser Link 6.14-6.16


большего числа браузеров с при­ (Ссылка на браузер)
менением Visual Studio
Со кращение количества НТТР­ Используйте расширение Bundler & Minifier 6.17-6.28
запросов и ширины полосы про­ (Упаковщик и минификатор)
пускания, требуемой для файлов
JavaScript и CSS

На заметку! Ка к объяснялось в главе 2, в Microsoft заявили , что в будущих выпусках Visual


Studio изменят инструменты , которые применяются для создания приложе ний ASP. NEТ
Core. Это означает, что приводимые в данной главе инструкции могут устареть . Проверяйте
веб-сайт издательства на предмет пересмотренных инструкций, которые я напишу, когда
новые и нструменты станут стабильными .
Глава 6. Работа с Visual Studio 137

Подготовка проекта для примера


Для ц елей настоящей главы мы создали новый проект ASP.NET Саге Web
Application (.NET Саге) (Веб-приложени е ASP.NET Core (.NET Core)) по имени
WorkingW it hVisualStudi o, и спол ьзуя шаблон Empty (Пустой). Мы добавили сборку
МVС з а счет редактирования раздела dependenc i es файла proj ect. j son (листинг 6.1).

Листинг 6.1. Добавление сборки MVC в файле proj ect. j son

"dependenc i es " : {
"Microsoft . NETCore . App":
" version ": 11
1 . 0.0 ",
"type ": "platform "
}'
"Mi c r osoft . AspNetCo r e . Diagnostics": "1. О . О ",

"Microsoft . AspNetCore . Server .I ISi ntegra t ion ": " 1 . 0 . 0",


"Microsoft . AspNetCore . Server . Kestre l": "1. 0 . 0 ",
"Microsoft . Ext ens i ons .Logging .Conso l e ": "1. 0 . 0 ",
Мicrosoft . AspNetCore. Mvc" : 11 1. О. О 11
11

},

З атем мы включили инфраструктуру MVC с ее стандартно й конфигурацией в фай ­


ле Startup . cs (листинг 6.2).

Листинг 6.2. Включение инфраструктуры MVC в файле Startup. cs


using Microsoft . AspNetCore . Bui lde r ;
using Microso f t . AspNe t Co r e . Host i ng ;
using Microsoft . AspNetCo r e . Http ;
using Mi crosoft .Extens i ons .Depe ndenc yinj ecti on ;
using Microsoft . Extensions . Logg i ng ;
namespace Worki ngWi thVi sualStud i o
puЫic class Sta r tup {
puЫ i c void ConfigureSe r vices (I Servi ceCo l lect i on se r v i ces) {
services.AddМvc();

puЫic void Configu r e (IAppl i cati onBuilder ар р, IHost ingEnv i ro nment env ,
ILoggerFact ory l oggerFact ory) {
app.UseMvcWithDefaultRoute();

Созда н ие м одел и
Со здайте папку Mode l s и добавьте в нее файл класса по имени Product . cs с оп­
редел е ни ем, показанным в листинге 6.3.
138 Часть 1. Введение в инфраструктуру ASP. NЕТ Саге MVC

Листинг 6.3. Содержимое файла Product. cs из папки Models


namespace Worki ngWithVisualStudio . Models
puЫic class Product {
puЫic string Name { get ; set; }
puЬlic decimal Price { get ; set;

Чтобы создать простое хранилище объектов Product, добавьте в папку Models


файл 1<ласса по имени SimpleReposi tory . cs и поместите в н е го опр еделе ние, при­
веденное в листинге 6.4 .

Листинг 6.4. Содержимое файла SimpleReposi tory. cs из папки Models

using System . Collections . Generic ;


namespace WorkingWithVisualStudio.Models
puЫic class SimpleRepository {
private static SimpleRepository sharedRepository = new SimpleRepository( ) ;
private Dictionary<string , Product> products
= new Dictionary<string , Product>();
puЬlic static SimpleRepository SharedRepository => sharedRepository;
puЫic SimpleRepository() {
var initialitems = new [] {
new Product { Name " Kayak ", Price = 275М } ,
new Product ( Name "Lifejacket ", Price = 48. 95М } ,
new Product { Name " Soccer ball ", Price 19. 50М } ,
new Product { Name " Corner flag ", Price = 34 . 95М }
};
foreach (var р in initialitems) {
AddProduct(p);

puЫic IEnumeraЫe<Product> Products => products.Values;


puЫic void AddProduct(Product р) => products.Add(p.Name, р) ;

Класс SimpleReposi tory хранит объекты модели в памяти, т.е. любые внесенные
в модель изменения утрачиваются, когда приложение останавлива ется или переза­

пускается . Для примеров этой главы непостоянного хранилища вполне достаточно,


но такой подход не может применяться во многих реальных про ектах; в главе 8 рас­
сматривается пример создания хранилища, которое запоминает объекты модели на
постоянной основе, используя реляционную базу данных.

На заметку! В листинге 6.4 определено статическое свойство по имени SharedRepos i tory,


предоставляющее доступ к единственному объекту SimpleReposi tory, который может
применяться повсюду в приложении . Это не является установившейся практикой, но я
хочу продемонстрировать распространенную проблему, с которой вы столкнетесь при
разработке приложений MVC; в главе 18 будет описан более эффективный способ работы
с разделяемыми компонентами.
Глава 6. Работа с Visual Studio 139

Создание контроллера и представлени я


Добавьте в проект папку Control l er s и поместите в нее ф айл класса по имени
HorneController . cs, в котором определен контролл е р. показанный в листинге 6.5.

Листинг 6.5. Содержимое файла HomeController. cs из папки Controllers


using Microsoft . AspNet Core . Mvc ;
using WorkingWithVis ualStud i o . Model s;
narnespace Wo r ki ngW i thVis ua l Studio . Cont r o l lers
puЬlic class HorneCont r ol l er : Cont r oll er {
puЫic IAc t ionResu lt Inde x( )
=> View(SirnpleRepository . SharedRepository . Pr oducts) ;

Зд е сь имеется един ственный метод действия Index () ,который получает все объ­
екты модели и передает их методу Vi ew () для визуализации стандартного представ­
ле ния. Чтобы добавить это пр едставление, создайте папку Views/Home и поместите
в не е файл представления по имени I n dex . csh tml, содержимое которого приведено
в ли стинге 6 .6.
Листинг 6.6. Содержимое файла Index. cshtml из папки Views/Home
@model IE n u rne r aЫe< W o r k i ngW i t hVisu a l S tud i o.M od els. Prod u c t >

@{ Layout = null ;
< !DOCTYPE htrnl>
<html>
<head>
<meta name= "v iewport " c ont ent=" width=device - wi dth " />
<title>Working with Vis ual St udi o</t i t le >
</head>
<body>
<tаЫе>

<thead>
<tr><td>Narne</td><td >P r ice </td></ t r>
</thead>
<tbody>
@fo r each (var р i n Mode l )
<tr>
<td>@ p.Narne</td>
<td>@p . Pr i ce</td>
</tr>

</tbody>
</tаЫе>
</body>
</html>

Представление включает т аблицу, в которой с помощью Rаzоr-цикла f o r each со­


здаются строки для всех объектов модули, причем каждая строка содержит ячейки
для значений свойств Name и Pr i c e. Запустив пример приложения, вы увидите ре­
зультаты. п01ш занные на рис. 6.1.
140 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

--·- ----------------·1
С l.~~~~~ost:612~-------
-- ------ -------· ---···----·--~
----------~- ~- -~

Nаще Price
Kayak 275
Lifejacket 48.95
Soccer ba ll 19.50
Сошеr flag 34.95

Рис. 6.1. Выполнение примера приложения

Выбор исполняющей среды .NET


При создании нового проекта ASP.NET Core предлагается выбрать один из двух шаблонов проек­
тов с похожими названиями :ASP.NET Core Web Application (.NET Core) (Веб-приложение ASP.
NET Core (.NET Core)) и ASP.NET Core Web Application (.NET Framework) (Веб-приложение
ASP.NET Core (.NЕТ Framework)). Оба шаблона можно использовать для создания приложений с
применением инфраструктуры ASP.NET Core MVC, а отличаются они тем, какая исполняющая сре­
да .NET выполняет код .

.NET Core - это небольшая оптимизированная исполняющая среда, первоначально созданная


специально для ASP.NET, но теперь она играет более широкую роль для других типов приложений
.NET. Она была спроектирована как межплатформенная и предоставляет возможности для раз­
вертывания приложений ASP.NET за пределами традиционного набора платформ Windows, а так­
же в легковесные облачные контейнеры, подобные Docker. Исполняющая среда .NET Core будет
поддерживать Windows , macOS, FreeBSD и Linux; она спроектирована как модульная и включает
только сборки, которые требуются приложению, что дает в результате меньший и более простой
отпечаток для развертывания. Кроме того, АРl-интерфейс .NET Core API также имеет меньшие
размеры, поскольку из него изъяты средства, которые являются специфичными для Windows и
не могут поддерживаться на других платформах .

В краткосрочной перспективе выбор исполняющей среды для проектов будет управляться ис­
пользуемыми инструментами и библиотеками . У независимых поставщиков уйдет некоторое
время на обновление своего программного обеспечения для работы с .NET Core и доведение
его до уровней стабильности, требуемых для производственного применения. Если вы зависи­
те от пакета либо инструмента, которому необходима полная платформа .NET Framework (или
у вас есть унаследованная кодовая база, обновить которую невозможно), тогда при создании
проектов ASP.NEТ вам придется использовать вариант ASP.NET Core Web Application (.NET
Framework). Вы по-преж нему можете применять все средства, описанные в настоящей книге, и
единственное отличие будет связано с исполняющей средой, которая запускает код.

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

новые зависимости от .NET Framework, если их можно избежать, и при выборе новых инструмен­
тов и библиотек понадобится учитывать возможность поддержки ими .NET Core. Дополнительные
сведения о .NЕТ Core доступны по адресу https: / /docs .rnicrosoft. corn/en-us/
dotnet/articles/welcorne.
Глава 6. Работа с Visual Studio 141

Управление программными пакетами


Проектам ASP.NET Core МVС требуются два разных вида программных пакетов.
В последующих разделах будут описаны эти типы пакетов вместе с инструментами,
которые среда Visual Studio предлагает для управления ими.

Инструмент NuGet
Среда Visual Studio предоставляет графический инструмент для управления паке­
тами .NET, который включается в проект. Чтобы открыть этот инструмент, выберите в
меню Tools~NuGet Package Manager (Сервис~Диспетчер пакетов NuGet) пункт Manage
NuGet Packages for Solution (Управл ение пакетами NuGet для решения) . Инструмент
NuGet откроется и отобразит список уже установленных пакетов (рис. 6.2).

Bro'.V$e ~ Updates ConsoHdate Manage Packages for Solulio n


St.tr t h lt tft "[) . о

- Mltro,oft.As pNetCote.Dlllgnostlcs Ьу Mlooio11.o\lp№tC01t.O!•gn1a1ш


8'о ASPJ~fТ Co1t middfcw1rt for U(1:pt>0n hcrtdГ.ntJ, e:ctci1tion df1pl.y p•ljt\, 111d
v l .0.0
."8 Microso r.t.AspNetCore.Mvc

Vщlon(,) · 1
j
5
di.1gn"'1k~ 1nfo11Ntion. Jrn:lvdes devtloptr U<tp!!On P'!i" m1ddJtw~te, tнq}tiOl'I h•n~.
. 10
' 0
P1ojtct :_---~-:i
Ut\Vlor1cincJт'MhVi~u 1.0.О 1

Mlcrosoft.AspNetCore.Sc:rver.llSJntegratlon bylA1c10'.0fl.AsJJfle1Co1t.Sm·t _..i .o.o


1 1 1
t!) ASP.NП Co•t c.cmpotм:nti fc1 wo1 l~n9with1~ h~ д!pNt1CcrtM<1dul•.

1
18
~
Mlcrosoft.AJpNe1Core.S.rvtr.llSlntegration.Tools ЬуМЮ> v!Л.O·prlE'М"NHin11
llS 1rittg1.1tion yublish t.:io~ for .NfТ Cort ClL Cant11n' tht dctntl·puhll,h·
------- __ :J1
lllt1~•1t 111 comn11rn:1 lc1 pЩ:lnking wdi 1ppl1c.1Ucrn !о bt hoottd ustng ltS. lм t.llled: 1 .!1.О

M/crosoft.Asp№tCore.Scrvcr.Кo1trel ЬyM•c101oh.д1ptj(1Ca•c.StNcr.K~t1tl vl.Cl.O


• /\SP.Шf Ccrt ~tsщl cro~~·pl1tfc1mwct- ltNtt.

1111 Mlcrosoft.Extenslons. Logglng.Con,olc Ьу M1crosott.Ьl:tn11on1.. t o99•n9.Con1 ... 1.0.0


-О Con1cl t to99trptov1dtrнт1pltmtnatю11 fo1 Mkro1011 .(;..tcn1iont.L0991n9
O t:Кrlpllon

ASP.IJE T CottMYC i1.i web fщТitio1cnl: t~t 9r1n


111 MlcroJoft. NEТCore.App ЬуМюоw.t ~ 1.0.0 you1 p o,.."ttfu~ p1lltt,.,1·b.1~«iw1ytobuild

~ д tti cf .NET lJll"~ INI olt' mduded ln the dt!1i.rlt .NП Cort f IK411on modrL dyntmit .... 11ь1rtei 1r.d wtb AP! ~. ASP . NH (Ot( мvс "

Рис. 6.2. Использование диспетчера пакетов NuGet

На в1шадке lпstalled (Установленные) приводится сводка по пакетам, которые уже


установлены в проекте. Вкладку Browse (Обзор) можно применять для нахождения и
установки новых пакетов, а вкладку Updates (Обновления) - для получения списка

пакетов, для которых были выпущены свежие версии.

Список и местоположение пакетов NuGet


Инструмент NuGet управляет содержимым раздела зависимостей (dependencies)
файла project . j son , который создается Visual Studio при настройке нового проек ­
та, даже когда используется шаблон Empty.

На заметку! В Microsoft намерены изменить инструментальную оснастку для ASP.NET в бу­


дущих выпусках Visual Studio. Одно из изменений, которое было анонсировано (но не ре­
ализовано), связано с тем, что файл proj ect. j son больше не будет применяться для
управления пакетами NuGet. Проверяйте веб-сайт издательства на предмет обновлений
для этой книги, когда компания Microsoft выпустит новые версии .
142 Часть 1. Введение в инфрастру кту ру ASP.NEТ Core MVC

Другие разделы файла proj ect. j son будут описаны в главе 14, но если вы вни­
мательно изучите список пакетов, отображаемый инструментом NuGet, то заметите,
что он соответствует элементам раздела dependencies, которые для примера проек­
та выглядят следующим образом:

11
dependen cies 11
: {

Microsoft . NETCore.App ":


11

"version "1.0.0",
11
:

"type": "platform"
}'
"Microsoft . AspNetCore . Diagnostics ": "1.0.0 11
,

"Microsoft . AspNetCore.Server . IISintegration" : "1. 0 . О",


"M icrosoft.AspNetCore . Server.Kestrel ": " 1.0 . 0 ",
"Microsoft . Extensions . Logging . Console": 1.0.0 11 11
,

Microsoft . AspNetCore.Mvc
11
1.0.0" 11
:
11

} '
Каждый пакет указывается с использованием его имени и обязательного номера
версии. Некоторые пакеты, такие как Microsoft . NetCore . Арр в примере про екта,
имеют дополнительную конфигурационную информацию, как объясняется в главе 14.
Ср еда Visual Studio отслеживает содержимое файла proj ect . j son, т.е. вы можете до­
бавлять или удалять пакеты , редактируя файл напрямую, что и делается повсеместно
в книге , т.к. это помогает гарантировать получение ожидаемых результатов , если вы

прорабатываете примеры.
Когда вы применяете NuGet для добавления в проект какого-то пакета, он автома­
тически устанавливается наряду со всеми пакетами, от которых зависит. Исследовать
пакеты NuGet и их зависимости можно, открыв в окне Solution Explorer элемент
References (Ссылки), который содержит записи для всех пакетов NuGet в файле
proj ect . j son. В результате развертывания записи пакета отображаются пакеты, от
которых он зависит (рис . 6.3).

Solution Explorer • CJ х

Ф Ф_@ I ~ -1. ~-r~ ! ~-


3..:~г:!:~о~:ХУ~!~:.:) _ ·--
•·1 Referenc~
• ili .NEТCoroApp,Vorsi on:v1 .0
t> ·~ Microsoft.AspNetCore.Oiagnortics (1.0.0)

~ ·~ MicrosoftAspNetCo r e.Мvc.ApiExplorer (1.0.0)


·~ Mic rosoft.AspNetCore.Мvc . Cors (\.О.О)
·~ Microsoft.AspNetCore.Mvc.DataAnnot"tions (1.0.0)
t> ·~ Microsoft.Asp NetCore.Mvc.FormottersJson (1.0.0}
t> ·~ Microsoft.AspNetCore.Mvc.loc"lization (1.0.0)
·~ M i crosoft.Asp№tCore.Мvc . Rdzor (1.0.0)
0

1lj Microsoft.AspN<Kore.Mvc.TogHelpers (1.0.0)


·~ Micros: oft.AspNetCore. Мvc .ViewFeature.s
(1.0.0)
·~ Microsoft.Extensions.Caching.Memory {1.0.0)
·~ Microsoft.Extensions.Oependencylnjection (1.0.0)
•·• Microsoft.Asp№tCore.Mvc . dl l
~
0

fl! Microsoft .AspNetCore.Se.rver.llSlnt~ration (1.0.0)


·~ Microsoft .Asp№tCor~Server.Kestrel (1.0.0)
'llj Microsoft.Extensions.Logging.Console (1.0.0)
il} Microsoft.NEТCore.App {1.0.0}
0

·~ Microsoft,Visual Studio.\Чeb.Browserlink. l oader (14.0.0)

Рис. 6.3. Раздел References окна Solution Explorer


Глава 6. Работа с Visual Studio 143
На рис. 6.3 Mi cro s o ft. AspNetCor e . Mvc в лис­
видно, что при добавлении пакета
тинге 6.1 инструмент NuGet загружает и устанавливает его и множество других паке­
тов, которые требуются для разработки приложений MVC.

Инструмент Bower
Пакет клиентской стороны включает содержимое, которое отправляется клиенту ,
такое как файлы JavaScript, стили CSS либо изображения . Для управления этими
пакетами можно привычно пользоваться инструментом NuGet, но ASP.NEТ Core MVC
полагается на новый инструмент под названием Bower. Инструмент с открытым ко­
дом Bower был разработан за пределами Microsoft и мира .NET и широко применялся
в разработке веб-приложений, отличных от ASP.NET. В действительности Bower стал
настолько успешным , что некоторые популярные пакеты клиентской стороны рас­
пространялись только через Bower.

Список пакетов Bower


П акеты Bower bower . j s on . Чтобы создать этот файл , щелк ­
указываются в файле
ните правой кнопкой мыши на элементе проекта Wo rki ngW i thVisua l Studio в окне
S ol u ti oп Explo re r, выберите в контекстном меню пункт Add<=:>New lte m (Добавить<=:> Новый
элемент) и укажите шаблон элемента Bowe r C o п figurat io п File ( Файл конфигурации
Bower) из категории Clie пt - s i d e (Клиентская сторона), как показано на рис. 6.4.

Sort Ь у: Def•ult
ASP.NET • Туре: Client-side
TypeScript JSON Configur•tion File Client-sido
(lient-side
Bower Configuration
Code and .bo1.;verrc .

~ Online
n рП1 Co11figuration File С lie n\ · •ide

rJs Gulp Configurotion File Client-side

rJS Grunt Configur•tion File ClienHide

п JSON Schemo File Client-side

гJs
JSXFile Client-side
<iiil>J
(lick here to go opline •nd find !empla\б.

Рис. 6.4. Созда н ие фай ла кон фи гу р ации Bower

На заметку! Дл я з а груз ки п акето в кл ие н тс ко й сторо н ы Bower используе т инстр у м е нт g i t.


Чт об ы обеспечит ь кор ректн ую р аб оту Bower, пот ребуетс я з ам е нить верс и ю gi t из Visual
Studio, как бы ло о п и с а н о в гла в е 2.
144 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

Среда Visual Studio установит b ow er. j s o n в качестве имени и после щелч­


ка на кнопке Add (Добавить) добавит в проект файл со стандартным содержи м ым
(листинг 6. 7).

Совет. По умолчанию Visual Studio скрывает файл b owe r . j s on; чтобы его было видно, нуж­
но щелкнуть на кнопке Show All Files (Показать все файлы) в верхней части окна Solution
Explorer.

Листинг 6. 7. Стандартное содержимое файла bower . j s on

"name ": "asp .n et ",


"p rivate ": t ru e ,
"depende nc ie s ":
}

В листинге 6.8 демонстрируется добавление в файле b ower . j s on пакета клиент­


ской стороны, что делается включением записи в раздел dependenc i es с примене­
нием такого же формата , как в файле p r o j ec t. j so n.

Совет. Хранилище пакетов Bower находится по адресу h ttp: //bower . io/ sea r ch, где
можно искать пакеты для добавления в свои проекты.

Листинг 6.8. Добавление пакетов в файле bower. j son

"name ": "as p.net ",


"p rivate ": t r ue ,
"depende nc i e s": {
"bootstrap": "3.3.6"

Выделенная полужирным строка обеспечивает добавление в пример проекта СSS­


пакета Bootstrap. При редактировании файла b ower. j s on среда Visual Studio предло­
жит список имен пакетов и перечислит доступные версии пакетов (рис. 6.5).

l>(J \'Jork1n9WithVosuolStud•o - bower.1son' - С1 Х

NuGet • Solution •
Schema: http://json.schem astore.org/ bower
в{ - - -
\ "narne" : "asp . net" ,
"p1· ivate" : t г ue ,
1
8 "dependen<ies" : {
/ "bootstrap" : " ~
f} } ! f..-3.-3.б----./ т~e lot~t ~i~Ы•v•rsion-;r th• ~.~k•g•
" ш л з.З . б

~ - 3.З . б

Рис. 6.5. Перечисление доступных версий пакетов клиентской стороны


Глава 6. Работа с Visual Studio 145
На момент написания главы последней версией пакета bootst r ap была 3.3.6.
Однако обратите внимание, что Visual Studio предлагает три варианта: 3. 3 . 6, л 3. 3 . 6
и - 3. 3. 6. Номера версий в файле b owe r . j son м огут указываться в виде диапазо­
на разнообразными способами, наиболее полезные из которых описаны в табл. 6.2.
Самый безопасный способ указания пакета предусматривает использование явного
номера версии. Это гарантирует, что вы всегда будете работать с той же самой верси­
ей до тех пор, пока пр еднамере нно не обновите файл b ower . j son для запроса другой
в е рсии.

Таблица 6.2. Ра спространенные форматы для номеров версий в файле Ьower. j son
Формат Описание

3.3.6 Выраже ние номера версии напрямую приведет к установке пакета с точно
совпадающим номером версии, например , 3.3. 6

* Применение символа звездочки позволяет инструменту Bower загружать и


устанавливать любую версию пакета

>3 . 3 . 6 Снабжение номера версии префиксом > или >= позволяет инструменту
>=3 . 3 . 6 Bower устанавливать любую версию пакета, которая больше либо больше или
равна заданной версии

<3.3.6 Снабжение номера версии префиксом < или <= позволяет инструменту
<=3 . 3 .6 Bower устанавливать любую версию пакета, которая меньше либо меньше
или равна заданной версии

-3 . 3 . 6 Снаб жение номера версии префиксом в виде тильды (символ - ) позволяет


инструменту Bower устанавливать версии , даже если номер уровня исправ­
лений (последнее число в номере версии) не совпадает. Например, указание
-3 . 3. 6 позволяет инструменту Bower устанавливать версию 3.3.7 или 3.3.8
(которая будет исправлена до версии 3.3.6), но не версию 3.4.0 (которая бу ­
дет новым младшим выпуском)

лз . 3 . 6 Снабжение номера версии префиксом в виде символа л позволяет инстру­


м енту Bower устанавливать версии, даже если номер младшего выпуска (вто­
рое число в номере версии) или номер исправления не совпадает. Например,
указание л 3 . 3.О позволит Bower устанавливать версии 3.3.1 , 3.4.0 и 3.5.0,
но не версию 4.0.0

Совет. Для примеров в настоящей книге мы создали и отредактировали файл bower . j son
напрямую . Редактировать файл очень просто , к тому ж е это способствует получению
предсказуемых результатов в случае проработки примеров. Среда Visual Studio также
предоставляет графический инструмент для управления пакетами Bower, который можно
открыть, щелкнув правой кнопкой мыши на проекте WorkingWithVisualStudio в окне Solution
Explorer (родительский элемент файла b ower. j son ) и выбрав в контекстном меню пункт
Manage Bower Packages (Управление пакетами Bower) .

Среда Visual Studio отслеживает изменения в файле bower . j son и автоматически


задействует инструмент Bower для загрузки и установки пакетов. Посл е сохранения
изменений, внесенных в файл согласно листингу 6.8, среда Visual Studio загрузит па­
кет Bootstrap и установит его в папку wwwroot/ lib (рис. 6.6).
146 Часть 1. Введение в инфраструктуру ASP.NET Соге MVC

1> bootstrap
t> jquery

Рис. 6.6. Добавление в проект пакетов клиентской стороны

Подобно NuGet инструмент Bower управляет зависимостями пакетов, добавляемых


в проект. Некоторые расширенные средства пакета Bootstrap завис ят от JavaScript-
библиoтeки jQuery, отчего на рис . 6.6 показаны два пакета. Для просмотра списка па­
кетов и их зависимостей необходимо развернуть элемент Depeпdeпcies (Зависимости)
в окне Solution Explorer (рис. 6.7).

"'1:1 х

... ii.J Bower


~ ~ bootstrap (3.3. б)
~ jquery (2.2.4)

Рис. 6. 7. Исследование пакетов клиентской стороны и их зависимостей

Что произошло с инструментами NPM и Gulp?


На начальных стадиях разработки ASP.NET Соге в Microsoft адаптировали два других популярных
инструмента с открытым кодом извне экосистемы .NET - NPM и Gulp . Средство NPM представ­
ляет собой диспетчер пакетов для инструментов разработки , который выполняется JаvаSсriрt­
механизмом Node.js , а Gulp является основанным на JavaScript инструментом запуска задач,
который позволяет писать сценарии для выполнения задач разработки , таки х как объединение
и минификация файлов .

Перед самым выпуском ASP.NET Саге 1.0 в Microsoft внесли изменение в ядро и упомянутые инс­
трументы больше не задействуются автоматически в шаблонах проектов MVC. Одна из наиболее
распространенных задач , где использовался инструмент Gulp , теперь выполняется расширением
Visual Studio, которое описано в разделе " Подготовка файлов JavaScript и CSS для развертыва­
ния " далее в главе .

Среда Visual Studio по-прежнему поддерживает инструменты NPM и Gulp, и они все еще могут
применяться для проектов, которые имеют дело со сложным компонентом клиентской стороны .
Это может быть полезно, поскольку существуют удобные инструменты и пакеты , которые доступ­
ны толь ко через NPM и могут настраиваться исключительно с использованием Gulp. За деталями
обращайтесь в мою книгу Pro Client Development for ASP.NEТ Саге MVC Developers.
Глава 6. Работа с Visual Studio 147

Итеративная разработка
Разработка веб-приложений часто может быть итеративным процессом, когда вы
вносите небольшие изменения в представления или классы и запускаете приложение,
чтобы протестировать результат. Инфраструктура МVС и среда Visual Studio на пару
поддерживают такой итеративный подход, делая наблюдение за влиянием изменений
быстрым и легким.

Внесение изменений в представления Razor


Во вр е мя разработки изменения, внесенные в представления Razor, вступают в
силу, как только поступает Н1ТР-запрос от браузера. Чтобы увидеть это в работе, за ­
пустите приложение, выбрав пункт Start Debugging (Запустить отладку) в меню Debug
(Отладка) , и после открытия вкладки браузера, отображающей данные, внесите в
файл Index. cshtrnl изменения, приведенные в листинге 6.9.

Листинг 6.9. Внесение изменений в файле Index. cshtml

@rnodel IEnurneraЬle<WorkingWithVisualStudio.Models.Product>
@{ Layout = null; }
< ! DOCTYPE html>
<htrnl>
<head>
<meta name =" v i ewport " content="width=device- width " />
<title>Wo rking with Vi s ual Studio</title>
</head>
<body>
<hЗ>Products</hЗ>
<tаЫе>
<thead>
<tr><td>Name</td><td> Pri ce</ td> </ tr>
</thead>
<tbody>
@foreach (var р in Model)
<tr>
<td>@p .Name</td>
<td>@($"{p.Price:C2}")</td>
</tr>
}
</tbody>
</tаЫе>
</body>
</html>

Сохраните изменения в представлении Index и перезагрузите текущую веб-стра­


ницу с помощью кнопки обновления в браузере. Изменения в представлении (добав­
ление заголовка и форматирование свойства Price объекта модели как денежного
значения) оказьmают воздействие и отображаются в браузере (рис. 6.8).

Совет. Процесс подготовки к использованию представлений Razor рассматривается в главе 21.


148 Часть 1. Введение в инфраструктуру ASP.NET Саге MVC

С !. <D localhost.:61207 j : 1
- - - - --==-----==-------===--·--::::::::......·-·-!
*
P1·od11cts !
Nаше Price
Lifejacket S48.95
Soccer ball S19.50
С ошеr flag $34.95 1
1
L --- ·---·--__)
1

Рис. 6.8. Результаты внесения изменений в представление

Внесение изменений в классы С#


Для классов С#, включая контроллеры и модели, способ обработки изменений за·
висит от того, как запускается приложение. В последующих разделах объясняются
два доступных подхода, которые инициируются посредством разных пунктов в м е ню

Debug и кратко описаны в табл. 6.3.

Таблица 6.3. Пункты меню Debug

Пункт меню Описание

Start Without Debuggiпg Классы в проекте компилируются автоматически, когда посту­


(Запустить без отладки) пает НТТР-запрос, делая возможной более динамичную практи­
ку разработки . Приложение запускается без отладчика , что не
позволяет взять под контроль выполнение кода

Start Debuggiпg При таком стиле разработки вы должны явно компилировать


(Запустить отладку) проект и перезапускать приложение, чтобы изменения вступили
в силу. Во время выполнения к приложению присоединен отлад­
чик, позволяя инспектировать его состояние и анализировать

причины возникновения любых проблем

Автоматическая компиляция классов


В течение нормальной стадии разработки быстрый итеративный цикл позволя­
ет видеть результаты изменений немедленно независимо от того, связаны они с до­
бавлением нового действия или с изменением способа, которым выбираются данные
модели представления. Для разработки такого вида Visual Studio поддерживает об­
наружение изменений , как только получен НТГР-запрос из браузера, и автоматичес­
кую перекомпиляцию классов. Чтобы посмотреть, как это работает, выберите пункт
Start Without Debugging (Запустить без отладки) в меню Debug (Отладка) среды Visual
Studio. После отображения данных приложения в браузере внесите в контроллер Home
изменения, показанные в листинге 6.10.
Глава 6. Работа с Visual Studio 149
Листинг 6.1 О. Фильтрация данных модели в файле HomeCon troller. cs
using Microsoft . AspNetCore . Mvc ;
using WorkingWithVisualStudio . Models ;
using Systern.Linq;
narnespace WorkingWithVisua l Studio . Cont r ol l ers
puЫic class HorneController : Controller {

puЫic IActionResult Index()


=> View(SirnpleRepository.SharedRepository.Products
.Where(p => p.Price < 50));

Изменения связаны с применением LINQ для фильтрации объектов Product, что­


бы представл ению пер едавались только те из них , свойство Price которых имеет
значение меньше 50 . Сохраните изменения, произведенные в файле класса контрол­
лера, и обновите окно браузера, не останавливая или не перезапуская приложение в
Vlsual Studio. Отправленный брауз ером НТГР-запрос инициирует процесс компиля­
ции, и приложение будет запущено повторно с использованием модифицированного
класса контроллера, генерируя результаты, в которых из таблицы исчез товар Kayak
(рис. 6.9).

С) Working with Vi~ual S с Х

11 С [Ф localhost:6120:__ _ _ _ _ _ _ _~ :
----· ---·· - ---------- -- -----·---·------··-- -~-

P1·oducts

Nаше Price

1
Lifejacket $48.95
Soccer ball $19.50
Соше.1' flag S34.95
'------------------------
Рис . 6.9.
_J
Автоматическая компиляция классов

Средство автоматической компиляции удобно, когда все идет по плану . Его недо­
статок в том, что ошибки этапа компиляции и времени выполнения отображаются в
браузере, а не в Visual Studio, затрудняя выяснение причины возникновения пробле­
мы . В качестве примера в листинге 6.11 демонстрируется добавление ссылки null в
коллекцию объектов моделей внутри хранилища.

Листинг 6.11. Добавление ссылки null в файле SimpleReposi tory. cs

using Systern . Collections . Generic ;


narnespace WorkingWithVisualStudi o . Models
puЫic class SirnpleRepository {
private static SirnpleRepository sharedRepository = new SirnpleRepository();
150 Часть 1. Введение в инфраструктуру ASP.NEТ Саге MVC

private Dictionary<string , Product> products


= new Dictionary<string , Product>();
puЫic static SimpleRepository SharedRepository => sharedRepository ;
puЫic SimpleRepository() {
var initialitems = new[] {
new Product { Name " Kayak ", Price = 275 М ) ,
new Product { Name "L ifejacket ", Price = 48 . 95 М },
new Product { Name " Soccer ball", Price 19. 50 М } ,
new Product { Name "Corner flag", Price = 34 . 95 М )
};
foreach (var р in initialitems) {
AddProduct(p) ;

products. Add ( "Error" , null) ;

puЬlic IEnumeraЬle<Product> Products => products . Values ;


puЫic void AddProduct(Product р) => products . Add(p . Name, р);

Средство InteШSense среды Visual Studio будет подсвечивать места в коде, где при­
сутствуют проблемы с синтаксисом. но проблема вроде ссьmки null не будет обнару­
жена вплоть до запуска приложения. Перезагрузка страницы в браузере приведет к
тому, что класс SimpleReposi tory скомпилируется, а приложение запустится зано­
во. Когда инфраструктура МVС создает экземпляр класса контроллера для обработки
НТГР-запроса от браузера, конструктор HomeController создаст экземпляр класса
SimpleReposi tory, 1шторый в свою очередь попытается обработать ссылку null,
добавленную в листинге 6.11.
Ссылка null приведет к возникновению проблемы, но сущность проблемы не будет
очевидной, потому что браузер не отобразит какое-то полезное сообщение (а браузер
Chrome вообще не выведет никаких сообщений. отобразив взамен пустую вкладку).

Включение страниц исключений разработчика


Во время процесса разработки при возникновении проблемы может быть удобно
отображать более полезную информацию в окне браузера. Это можно делать, включив
страницы исключений разработчика, что требует изменения конфигурации в классе
Startup, как показано в листинге 6. 12.
Роль класса Startup подробно объясняется в главе14, а пока достаточно знать,
что вызов расширяющего метода UseDeveloperExceptionPage () устанавливает
описательные страницы ошибок.

Листинг 6.12. Включение страниц исключений разработчика в файле Startup. cs


using Microsoft.AspNetCore . Builder ;
using Microsoft.AspNetCore .Hosting ;
using Microsoft . AspNetCore .Http;
using Microsoft . Extensions.Dependencyinjection;
using Microsoft.Extensions .L ogging ;
namespace WorkingWithVisualStudio {
Глава 6. Работа с Visual Studio 151
puЫic class Startup {
puЫic void ConfigureServices(ISe rviceCollection services) {
services.AddMvc();

puЫic void Configure(IApplicat ionBuilder арр , IHostingEnvironment env ,


ILoggerFactory loggerFactory) {
app.UseDeveloperExceptionPage();
app . UseMvcWithDefaultRoute();

Если вы перезагрузите окно браузера, то автоматически запускаемый процесс


компиляции перестроит приложение и выдаст более полезное сообщение об ошибке
(рис. 6.1 О).

D lnt<rмl Server Error х

1-- --~ ~;~-~host:12~.~~=---=·


Ап unhandled exception occшred while processing the request.
NL1 llRefe renceExceptioп: Ol)jec 1·eference r.ot set to an instance of an object.
<lndex>b_ O_O in HomeCon t ro ll e r. c:.. line 10
I~

• Que1y Cookies Heade1s

NullRefereпceException: Object refere1кe rюt set to ап instance of an object.


<lr.·:le<> b_O_C •n Hon1eCont:roller . cs
10. . \olhere(p •> р.Р гiс е < 50)) ;

f/ cv~№xt

~l o·,el·J ext

L
1r, Index . cs ht ml

··--·--···---:!
17 . @foгeac h (va 1· р in Hodel) (

-~-~~·---·· -------·
Рис . 6.1 О. Страница исключения разработчи ка

Отображаемого в браузере сообщения об ошибке может быть достаточно для вы­


явления простых проблем, особенно из-за того, что итеративный стиль разработки
означает, что причиной, скорее всего, стали самые последние изменения. Но для уст­
ранения более сложных проблем - и для проблем, которые не становятся очевидны­
ми немедленно - требуется отладчик Visual Studio.

Использование отладчика
Среда Visual Studio также поддерживает выполнение приложения MVC с примене­
нием отладчика, который позволяет останавливать приложение для инспектирования
его состояния и пути прохождения запроса по коду. Это требует другого стиля раз­
работки , поскольку модификации классов С# не будут применены до тех пор, пока
приложение не запустится заново (хотя изменения, вносимые в представления Razor,
по-прежнему вступают в силу автоматически) .
152 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

Такой стиль разработки не настолько динамичен, как использование средства


автоматической компиляции, но отладчик Visual Studio великолепен и может предо­
ставлять намного более глубокое проникновение в суть способа работы приложения.
нежели то, что возможно с помощью сообщений, отображаемых в окне браузера .
Чтобы запустить приложение с применением отладчика, выберите пункт Start
Debugging (Запустить отладку) в меню
Debug (Отладка) среды Visual Studio. Перед
запуском приложения среда Visual Studio скомпилирует классы С# в проекте, но
вы можете также вручную скомпилировать свой код, используя пункты меню Build
(Постро ение).
Пример приложения все еще содержит ссылку null, т.е. необработанное исключе­
ние Null Refere n ceExcept i o n, которое генерируется в классе S i mpleReposi tory ,
прервет приложение и передаст управление выполнением отладчику (рис. 6.11).

Ho meCont r oПtr.cs ~ Х

~\Vo;.kin9WrthVi$u11IStu~~~p-p,Version_:l~ \4/01ki~~i!_~ViwalStud_i~~~~~m~~~!.~.----·~-----·-,.:
B u ~ing ~lic"osoft.д!ipfle-t'Core.Мvc ; li'.
using Worki n gWithVis ualStudio .М<Юtl::o ; r,--
. ....,.....,.._.,,,...~--:-""::..,,......,,.., __~ -~.-.----;i
l using Syste•.li nq; /-1.'..-~~~.!!;~ «-~c,epttono<tut~-· _.·_. ---~---21
j &:ctpdon thtown: "Systtm.Nul\Refe1ence.f.'щ1ption' in 1
1 89 n1и1esp\1Ьli
pace Wo гkingWithVi:иJ 1йStudi o . Cont rollers {
c class 11-)!l'eCм·uol.t~r : Contrclle { /
,/ l Wo ikingW! tliViм'5 tud'ю.dlr
1
J /' / Additi"onal lnfo~tion: OЬjr.ct rr.ferencl! oot sd to an inst.sn<.t of an objC!.ct.'.
puЬlic !1\tticnRc!.iJlt Index(} · -· -·- --"-------. ...,.~- --~ - -- - ..... ··-- ·• -·
•> Vitw( 51ro:rlt'1'to;>cs 1 tor}·· Sh!l':'C!~R!Po~J.~y.Products ! ~~~:~~-
· - - - - - - - - - - - - ----··-··--
. lihere(p •> P. · Pric~ < S!) ); / f.bШ.!.1?..~~!f!1i!'.!A.!.ti~..o..ЧLts.ti.t!'...Yl!.~.!to!! HM!~9Jht..~.~~:;.!bl ~
1
t} 1
. Ustt lhe • nt O\'м kt)'lf'.Old tc ctt<!tt dn cbjкt 1 nst •nt~

'1

lJ_~_!t'~~~~~lp fo~-:~~~.~~~-·---···-·----~···--··-=-~
1

~rch for n1ore Н~р 01'il1ne...


1 "- - - --· -- - -- -- - •• " --· -- . " _" -.
1 b c ~ptlon ~ t1 ings-:
! f2J Bruk \.\th~ n tJш rxc.ttp ti 11 м t).•pe i: thгa 1vn
1 д,..;;;- - -- . -- - .. ----- -·-·- "_,_ _, -·. "----,
1 v""o",;1 ". j
о/о . " -i (О р/ e.~ce ptщn detщ l to the cl1pbocr1d
100 1
" _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _8811!!_ O~nc:c-~~=~tin9~----------------------__J

Рис. 6.11. Возникновение необработанного исключения

Совет. Если отладчик не перехватывает исключение, тогда выберите пyнктWiпdowsQException


Settiпgs (ОкнаQНастройки исключений) в меню Debug (Отладка) среды Visual Studio и
удостоверьтесь , что в списке Common Language Runtime Exceptions (Общие исключе­
ния языка во время выполнения) отмечены флажки для всех типов исключений.

Настройка точки останова

Отладчик вовсе не показывает основную причину проблемы, а только место, где


она проявилась . Подсвечиваемый Visual Studio оператор указьшает, что проблема слу­
чилась при фильтрации объектов с применением LINQ, но для получения деталей и
выявления лежащей в основе причины потребуется проделать небольшую работу.
Точка останова - это инструкция, которая сообщает отладчику о необходимости
остановить выполнение приложения и передать управление программисту. Вы може­
те исследовать состояние приложения, посмотреть, что произошло, и при желании

возобновить выполнение.
Глава 6. Работа с Visual Studio 153
Чтобы создать точку останова. щелкните правой кнопкой мыши на операто­
ре кода и выберите в контекстном меню пункт Breakpointqln se rt Breakpoint (Точка
остановаqВставить точку останова). В целях демонстрации поместите точку останова
на метод AddProduct (} в классе SimpleReposi tory (рис. 6.12).

o<J V-/ork1n9V11thVi~ualStud10 - Г-'J Х

[!) V/orkingWrthVisuaLSt~_dio"NEТCoreApp, Ver5ion •] ~ \t./crlungW1thViiu~IStudio.Mode:ls. Simple:Reposi • [ 19 Simpl~('.poяtoty{)


1
'$ puЫic S!mploRopository() {
vа г i niti.s lit ems • new[ ] {
1 ne\'l Pr·oduct { l~ a~ • "Kayak" 1 Price " 27S~' } , '
li n e't'1

new
Product { tlame .- "Li fe jacket" 1 Pric e • 48.95М },
11ew P r od tн:t { N am~ • "Soc.ce1· ball" , Price'"' 19.50!'1 } ,
P rodщ:t { fJame • ' ' C oгner flag" , Ргiсе '"' 34.9SМ }

1
~ 1
};
1 foreach (var р in initi.s lit ems ) {
AddProduct(p) ;

products . Add( "Error" , null); •


11

ll,
puЫic П1\ul!'. eг aЫ~ < Product > Products "'> products . Vii!l lues ;

• puЫic 11o id AddP roduc t( Produtt р) ""> P:ffAМфhfdl!фW3J;

100 8.'7 ..
Рис. 6.12. Создание точки о стано ва

Выберите пункт меню Deb ugc::>Start Debu gging (ОтладкаqЗапустить отладку).


чтобы запустить приложение, используя отладчик, или Debugc::>Restart (Отладка~::>
Перезапустить), если приложение уже выполняется. Во время обработки начального
НТГР-запроса от браузера будет создан экземпляр класса SimpleRepository. а вы­
полнение кода достигнет точки останова, где возникнет пауза.

В этот момент вы можете применять пункты меню Debug среды Visual Studio или
элементы управления в верхней части окна для управления выполнением приложения
либо использовать разнообразные представления отладчика, доступные через меню
DebugqWindows (ОтладкаqОкна), чтобы инспектировать состояние приложения.

Просмотр значений данных в редакторе кода

Наиболее распространенное применение точек останова связано с отслеживанием


дефектов в коде. Прежде чем можно будет исправить дефект, вы должны выяснить ,
что происходит, и одним из самых полезных средств, предлагаемых Visual Studio,
является возможность просмотра и наблюдения за значениями переменных прямо в
редакторе кода.

Если вы наведете курсор мыши на аргумент р в методе AddProduct (), подсве­


ченном отладчиком, то появится всплывающее окно, которое показывает текущее

значение р (рис . 6. 13). Рассмотреть всплывающее окно может быть затруднительно,


поэтому на рис. 6.13 показана также его увеличенная версия.
Результат может не выглядеть особо впечатляющим, т.к. объект данных определен
в том же конструкторе, что и точка останова, но это средство работает для любой пе­
ременной. Вы можете исследовать переменные, чтобы увидеть значения их свойств и
полей. Правее каждого значения находится небольшая кнопка с изображением кан­
целярской кнопки, которую можно использовать для наблюдения за значением пере­
менной, когда выполнение кода продолжится .
154 Часть 1. Введение в инфраструктуру ASP. NEТ Core MVC

Simpl•Repos~ory. cs
IOJ" w~rkingWit hVis~11IStu dio .. NETCoreApp.Version " ~ Wo~ki~gWitl1VisuolStudio.Modei~:SimpleReposi . ...
nl!\'l Pl"'Qduct { Name • "Corner flag " , Price • 34.9S.Ч }
};
foreaich (vl!lr р in initi alitems ) {
+ AddProd uc1:( p); '"s-,,_-p----{W-or-ki-
n9-l~-it-
hV-is-u•-IS-tu-d-io-.M-o-de-ls-.P-ro-d-uc-.t}
1
pr-od ucts .Add( "Err ci r " 1 rшl l) ; 1" p.Name Р '" "Koyak"
#11 p.P rice 275

Gliil р {WorkingWrthVisualStudio.Mod els.Product}


JI p.Name Р • " Кауаk"
JI p.Price 275

Рис. 6.1 З. Инспектирование значения данных

Наведите курсор мыши на переменную р щелкните на кнопке с изображением


канцелярской кнопки, чтобы закрепить ссылку на Product . Разверните закреплен­
ную ссылку. чтобы также закрепить свойства Name и Price, получив в итоге резуль­
тат, представленный на рис. 6. 14.

lf!J Work1n9WithVisuolStud10"NEТCort.App, Vers1on ~ \ otk1n g\V1thV1"u"JStud 10.Modf:ls.S1 1npl~Repos1 rФ SimpleR~positoryO


new Product { Name • "Corner flag" ) Price • 34. 95r>t }

Рис. 6.14. Закрепление значений в редакторе кода

Выберите пункт Continue (Продолжить) в меню Debug (Отладк а) среды Visual


Studio, чтобы продолжить выполнение приложения . Поскольку приложение выпол­
няет цикл foreach , снова произойдет пауза, когда точка останова встретится опять.
Закрепленные значения показывают, как изменяется объект, присвоенный пер емен ­
ной р . и его свойства (рис. 6.15) .
Глава 6. Работа с Visual Studio 155

G~ р {WorkingWithVisualStudio.Models.Product}
JI p.Name Р " "lifejacket"
/1 p.Price 48.9S
Рис. 6.15. Наблюдение за изменением состояния
с применением за к реплен н ых значений

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


Связанным средством является окно Locals (Локальные) , которое открывается пу­
тем выбора пункта меню Debugr::>Wiпdows r::> Locals (Отладкаr::> Окнаr::>Локальные). В окне
Locals отображаются значения данных похожим на закрепленные значения образом, но
6.16) .
здесь присутствуют все локальные объекты, относящиеся к точке останова (рис.

locals • 1:1 х

; Туре_
1
Name Value __ • _ ··-
" oi this {WorkingWithVisualStudio.Models.SimpleRepository) WorkingWithVisualStudio.Models.SimpleRepos
" 1' Producl< Count = 2 Syste m.Colfecticns.Generic .I EnumeraЫe<Work
~ oi 10) {WorkingWithVisualStudio.Models.Product) WorkingWitl1VisualStudio.Models.Product
~ oi [1] {\"lorkingWithVisualStudio.Mod<Ols.Product} Wo rkingWithVisualStudio.Models.Product
~ oi Raw Vie\v
~ !M!!fi!!.ijЩi!!Wi Sy;tem.Colfectrons.Generic.D1ct1щшy<5tring, i
~ "1: Static members
{WorkingWithVisualStudio.Models.Product}
" " р WorkingWit hVisualStudio.Model<.Product
1' Nan10 · soccer Ьа11 · Q. • string
1' Price 19.SO decimal

Рис . 6.16. Окно Locals

Каждый раз, когда вы выбираете пункт Continue, выполнение приложения возоб­


новляется и в циюrе foreach обрабатывается новый объект. Продолжая поступать
так, вы увидите ссылку null в окне Locals и в закрепленных значениях, отображае­
мых внутри редактора кода . Применяя отладчик для управления выполнением при­

ложения, вы можете отслеживать его продвижение по коду и получить представление

о том, что происходит.

Устранить проблему со ссылкой null можно было бы за счет очистки коллекции


объектов Product, но альтернативный подход предусм атривает увеличение надеж­
ности контроллера, как показано в листинге 6.13, где для проверки на предмет зна­
чений null используется null -условная операция (описанная в главе 4).
Листинг 6.13. Исправление проблемы со ссылкой null в файле HomeController. cs

using Microsoft . AspNetCore . Mvc ;


using WorkingWithVisualStudio . Models;
using System . Linq ;
namespace Wo rkingWithVisualStudio.Controllers
puЬlic class HomeController : Controller {
puЫic IActionResult Index()
=> View(SimpleRepository.SharedRepository . Products
.Where(p => p?.Price < 50));
156 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

Отключить точку останова можно, щелкнув правой кнопкой мыши на операто­


ре кода, где она находится, и выбрав в контекстном меню пункт Breakpoint~Delete
Breakpoint (Точка останова<=:> Удалить точку останова). Перезапустив приложение, вы
увидите простую таблицу с данными (рис. 6.17).

[:) Working with Visu•I Stu, Х

· С [Ф~·;h:;s~;~1 207 ·- -------·------;-]


·----·- ------ -· -~· ·-·-
P1·o<l11cts

Nаше Price
Lifejackcr $48. 95
Socce1· bal\ $19.50

L Сошеr flag $34.95


----· --·---------------1
Рис. 6.17. Устранение дефекта

По сравнению с задачами, которые требует реальное выявление ошибок, эта про­


блема была решена просто, однако отладчик Visual Studio чрезвычайно эффективен, и
с помощью многочисленных доступных представлений приложения и управления вы ­
полнением вы можете легко углубляться в детали для выяснения, что пошло не так.

Использование средства Browser Link


Средство Browser Link (Ссылка на браузер) позволяет упростить процесс разра­
ботки за счет помещения одного или большего числа браузеров под контроль Visual
Studio. Оно особенно полезно, если нужно видеть влияние изменений в некотором
диапазоне браузеров. Средство Browser Link работает с или без отладчика, но оно
наиболее полезно в случае применения средства автоматической компиляции клас­
сов. потому что появляется возможность модифицировать любой файл в проекте и
видеть влияние изменения, не переходя в окно браузера и не перезагружая страницу
вручную.

Настройка средства Browser Link


Включение средства Browser Link означает добавление в проект еще одной сборки
и изменение его конфигурации. В листинге 6.14 демонстрируется добавление сборки
Mi cro s oft . VisualStudio. Web . Browse rLink. Loader в раздел dependencies фай­
ла proj ect . j son.

Листинг 6.14. Добавление сборки Browser Link в файле proj ect. j son

"dependencies ": {
"Microsoft .N ETCore.App ":
"version ": " 1 . 0 . О ",
" type ": "platform "
}'
"Microsoft . AspNetCore.Diagnostics ": " 1 . 0 . 0 ",
Глава 6. Работа с Visual Studio 157
"Microsoft .AspNetCore.Server . IISintegration ": "1 . 0 . О" ,
"Microsoft . AspNetCore . Server. Kes trel " 1 . О. О
11
:
11
,

"Microsoft .Extens i ons .Logging.Console ": "1. 0 .О",


"Microsoft . AspNetCore . Mvc ": "1. О . О ",
11
Мicrosoft.VisualStudio.Web.BrowserLink.Loader": "14.0.0"
}'

В листинге 6.15 показано соответствующее изменение в классе Startup.

Листинг 6.15. Включение средства Browser Link в файле Startup. cs


using Microsoft . AspNetCore . Builder;
using Microsoft . AspNetCore . Hosting ;
using Microsoft . AspNetCore . Http ;
using Microsoft.Extensions .Dependencyin je ction ;
using Microsoft . Extensions .Logging ;
namespace WorkingWithVisualStudio
puЫic class Startup {

puЫic void ConfigureServices(IServiceCollection services) {


services.AddMvc(} ;

puЫic void Configure(IApplicationBuilder арр, IHostingEnvironment env ,


ILoggerFactory loggerFactory) {
app . UseDeveloperExcept i onPage();
app.UseBrowserLink();
app.UseMvcWithDefaultRoute();

Использование средства Browser Link


Чтобы понять, как работает средство Browser Link, выберите пункт Start Without
Debugging (Запустить без отладки) в меню Debug (Отладка) среды Visual Studio. Среда
Visual Studio запустит приложение и откроет новую вклад1<у в окне браузера для отоб­
ражения результатов . Просмотрев НТМL-разметку , отправленную браузеру, вы заме­
тите, что она содержит дополнительный раздел, подобный приведенному ниже:

< !DOCTYPE html>


<html>
<head>
<meta name= viewport content= "width=device-width " />
11 11

<title>Working with Visual Studio</title>


</head>
<body>
<h3>Products</h3>
<tаЫе>
<thead>
<tr><td>Name</td><td>Price</td></tr>
</thead>
<tbody>
158 Часть 1. Введение в инфраструктуру ASP.NET Соге MVC

<tr><td>Lifejacket</td><td>$48 . 95</td></tr>
<tr><td>Soccer ball</td><td>$19 . 50</td></tr>
<tr><td>Corner flag</td><td>$34 . 95</td></tr>
</tbody>
</tаЫе>
< ! - - Visual Studio Browser Link -->
<script type="application/json"
id=" browserLink initializationData">
- -
{"requestid":"9eOOcбfB05Bf436981Be7ba315c9bdde",
"requestмappingFromServer" : false}
</script>
<script type="text/javascript"
src="http://localhost:56147/e7bB5fe070c5419Ba04ld57c363cee40/browserLink"
async="async"></script>
< ! -- End Browser Link -->
</ body>
</html >
Среда Visual Studio добавляет в НТМL-разметку , посылаемую бр аузеру, п а ру эле ­
ментов script , которые применяются для открытия долговечного НТГР-подключения
к серверу приложений, чтобы ср еда Visual Studio могла обеспечить принудител ьную
перезагрузку страницы браузером. (Если вы не видите элементы script, тогда удос­
товерьтесь в том, что отмечен пункт ЕnаЫе Browser Link (Включить Browser Link) в
меню, показанн ом на рис. 6. 18.) В листинге 6.16 приведе но изменение, внесенное в
представл ение Index, которое проиллюстрирует результат использования средства
Browser Link.

р t:! х
.1ninistrator)
"ools Test Дnalyze Window Help Adam Freeman • 11
Any CPU . ~ ' • llS Express • ·~~ • '[ ·ff ~-;j[__~ i:f!.L'~"~J~_!_.:!.:i! .:L .
....____. G Refresh Linked Browsers ~ Ctrl+Alt.-Enter .
. S~@} BromerlinkOashboard
, ~ ЕnаЫе Browser Link
1" ЕnаЫе CSS Auto·Sync

"
- -- -"...,.---...
= Solution ltems
61 global.json

Рис. 6.18. Применение средства Browser Link для перезагрузки браузера

Листинг 6.16. Добавление временной отметки в файле Index. csh tml


@model IEnumeraЫe<WorkingWithVisualStudio.Models.Product >
@{ Layout = nu ll; }
< !DOCTYPE html>
<html>
<head>
<meta name= " viewport " content= "width=device - width " />
<title>Working with Vi sua l Studio</tit l e>
</head>
Глава 6. Работа с Visual Studio 159
<body>
<h3>Produc ts </h3 >
<p>Request Time: @DateTime .Now. ToString ("HH:mm: ss") </р>
<tаЫе>
<thead>
<tr><td>Name</td><td>Pri ce</td></ tr >
</thead>
<tbody>
@foreac h (var р in Model )
<tr>
<td>@ p. Name</td>
<td>@( $" {p . Price : C2) " )</td>
</tr>

</tbody>
</tаЫе>
</body>
</html>

Сохраните изменение в файле представления и выберите пункт Refresh Linked


Browsers (Обновить связанные браузеры) в меню средства Browser Link внутри панели
инструментов Visual Studio (см. рис. 6.18). (Если средство Browser Link не работает,
попробуйте перезапустить Visual Studio и повторить попытку.)
Код JavaScript, встроенный в отправляемую браузеру НТМL-разметку, будет пере­
загружать страницу, де монстрируя результат добавления , которым является появле­
ние простой временной отметки. Каждый раз , когда выбирается пункт меню Refresh
Linked Browsers, браузер будет делать новый запрос к серверу. Результатом запроса
будет визуализация представления Index и генерирование новой НТМL-страницы с
обновленной временной отметкой.

На заметку! Элементы scr i p t, относящиеся к средству Browser Link, встраиваются толь­


ко в успешные ответы. Это значит, что если во время компиляции класса , визуализации
представления Razor или обработки запроса возникает исключение , то подключение меж­
ду браузером и Visual Studio утрачивается и вам придется перезагружать страницу, ис­
пользуя сам браузер, пока проблема не будет устранена.

Использование нескольких браузеров


Средство Browser Link может применяться для отображения прилож ения в не­
скольких браузерах одновременно, что удобно, когда нужно уладить вопросы несходс­
тва реализаций между браузерами (особенно при реализации специальных таблиц
стилей CSS) или увидеть, как приложение визуализируется набором настольных и
мобильных браузеров .
Чтобы отобрать браузеры, которые будут использоваться, выберите пункт Browse
With (Просмотреть с помощью) в меню, связанном с кнопкой llS Express в панели инс­
трументов Visual Studio (рис. 6.19).
Среда Visual Studio отобразит список известных ей браузеров. На рис. 6.20 пока­
заны браузеры, установленные в моей системе; часть из них установлена вместе с
Windows (lnternet Explorer и Edge), а часть - лично мною по причине их широкого
распространения.
160 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

'' ~D fr ~Q;ii~k L~unf


'

llSExpr~s

./ 115 Express
WorkingWithVi.sualStudio

Web Bro,vstr (Google Chrome Canary)


Brows.Wrth".
th • dev ice-..,1idth" />
'title >

Рис. 6.19. Выбор множества браузеров

Browsers (select one or more):

Google Ch rome Ca nary


lnternal Web Browser

[---· --···---]
......----
Set as Default
.;,.~~
Opera lnternet Browser

Ptogram:

Size of browser window:

Рис. 6.20. Отбор браузеров из списка

Среда Visual Studio ищет общие браузеры во время процесса установки, но с по­
мощью кнопки Add (Добавить) вы можете указывать браузеры, которые не были об­
наружены автоматически. Вы можете также настроить инструменты тестирования от
независимых поставщиков наподобие Browser Stack, которые запускают браузеры на
расположенных в облаке виртуальных машинах, так что вам не придется управлять
крупной матрицей операционных систем и браузеров во время тестирования.
На рис 6.20 выбраны три браузера: Chrome, Internet Explorer и Edge. Щелчок на
кнопке Browse (Обзор) приводит к запуску всех трех браузеров и заrрузки в них URL
примера приложения (рис . 6.21).
Чтобы посмотреть , какими браузерами управляет средство Browser Link, выберите
пункт Browser Link Dashboard (Инструментальная панель Browser Link) в меню средс­
тва Browser Link; откроется окно Browser Link Dashboard (Инструментальная панель
Browser Link) , представленное на рис. 6.22. Инструментальная панель показывает
URL, отображаемый каждым браузером , и позволяет обновлять каждый браузер по
отдельности.
Глава 6. Работа с Visual Studio 161

EJ Working with Visua1 Stu1 Х


11\ttp.://locilhost:60)18/ ~ 'Norr.in9 ""ilh Visu1'.I Stvd- Х ·

I ~· -) () 1 ~
l0<.1 ••::_ •i} ..O:..~~~~ait1~~~~~18~==~:::.~-=--=---=-~ S
P1·oducts 1
R•questTime: 17:5 1:26 Pi·odurts
Name Price
1 Nasne Price 1 ReqнestT11110 : 1 7 : 5 1 .26
Lifejacket !:48.95 11

Soccer ball .Е19. 50 Lif•jocket f 4&. 95 1 №111е Price


' Corner ftag 1:34.95 \ Soccer ball П 9 . 50 L1f<;ncket S4S .95
L ·-. ___ --·----- ___ __j Corner flag 04.95 Sосш bnll $19.50
j Сошеr flag $34.95
1
t--·" - -------------·---------·~ --~-- · - - - - - - -

Рис. 6.21. Работа с несколькими браузерами

Browser Link Dash board


.А \1,'o rkingWithVisualStudio (0 co nnections)
.А Connections
1·..z o cur re"t ~on n ec 1o ns
View in Brow~er

.А U nkno\'IП (З con nections)


.А Connections
Chrome • http:/ /localhost6051 8f
Microsoft Edge ... htt p;//l o c a l hostб0518/
Mozilla ... htt p:// localhost60518/

Learn n1ore about Bro\'ISEI Link

Рис. 6.22. Окно Browser Link Dashboard

Подгото вка ф айло в JavaScript


и CSS дл я раз в ерты в ани я
При создании клиентской части веб-приложения вы обычно будете создавать ряд
специальных файлов JavaScript и CSS, которые дополняют аналогичные файлы в па­
кетах, установленных инструментом Bower. Такие файлы требуют специальной обра­
ботки с целью оптимизации их доставки в производственную среду, чтобы миними­
зировать количество НТГР-запросов и долю полосы пропускания , необходимую для
доставки файлов клиенту. В этом разделе будет описано расширение Visual Studio,
которо е Microsoft пр едоставляет для выполнения указанной задачи .

В ключение доставки статического содержимого


Инфраструктура ASP.NET Core располагает поддержкой для доставки статических
файлов из папки wwwroot клиентам , но когда проект создается с применением шаб­
лона Empty, упомянутая поддержка по умолчанию отключена. Чтобы включить подде­
ржку статических файлов , в раздел dep e nde nci es файла p ro j ect . j s on потребуется
добавить новый пакет (листинг 6. 17).
162 Ч асть 1. Введение в инфраструктуру ASP.NET Core MVC

Листинг 6.17. Добавление пакета в файле project. json

"dependencies ": {
"Mic ros oft . NETCore . App ":
"version ": " 1 . 0 . 0 ",
" type ": "platform "
}'
"Microsoft . AspNetCore . Diagnostics ": " 1 .0.0",
"Microsoft . AspNetCore . Server.IISintegration ": " 1.0.0 ",
"Microsoft . AspNetCore . Server . Kestrel ": " 1 . 0 . 0 ",
"Microsoft . Extensions . Logging . Consol e ": " 1 . 0 .О",
"Microsoft . AspNetCore . Mvc ": " 1 . О . О ",
"Microsoft . VisualStudio . Web .B rowserLink . Loader": " 14 . 0 . 0 ",
"Microsoft . AspNetCore.StaticFiles": "1.0.0"
}'

Пакет Microsoft . AspNetCore . Stat icFiles содержит функциональность для


обработки статических файлов, которая должна быть включена в кла с се Startup,
как показано в листинге 6.18.

Листинг 6. 18. Включение поддержки статических файлов в файле S tartup . cs

using Microsoft . AspNetCore.Builder ;


usi ng Microsoft . AspNetCore .Hosting ;
using Microsoft . AspNetCore . Http;
using Microsoft . Extensions.Dependencyinjection ;
using Microsoft.Extensions.Logging;
namespace Worki ngWithVisualStudi o
puЫ i c class Startup {
puЫic void ConfigureServices(I ServiceColl ec tion services) {
services . AddMvc() ;

puЬlic void Configure(IApplicationBui l der арр , IHostingEnvironment env ,


ILoggerFactory l oggerFactory) {
app . UseDeveloperExceptionPage() ;
app . UseBrowserLink();
app.UseStaticFiles();
app .UseMvcWithDefaultRoute() ;

Добавление в проект статического содержимого


Чтобы продемонстрировать процесс пакетирования и минификации, пон адобится
добавить в проект какое-нибудь статическое содержимое и обеспечить возможность
его доставки клиенту. Для начала создайте папку wwwroot/css, где будут храниться
специальные файлы CSS. Затем добавьте файл по имени first . css, используя шаб ­
лон элемента Style Sheet (Таблица стилей), как показано на рис. 6.23.
Глава 6. Работа с Visual Studio 163

ASP.NEТ

Cli,e.nt-~e

Code

~ Online

TypeSc:tipt JSX File Cli~nt·side.

TypeScript JSON Configuraticn File Client·side-

Bo\VtrConfigu rc!tion file Cl1ent-side

npm Con figuration Fi!e ( lient·side

N.ame first.css

Рис. 6.23. Создание таблицы стилей CSS

Поместите в файл f i r s t . сs s стили CSS из листинга 6.19.

Листинг 6.19. Содержимое файла f1rs t. css из папки wwwroot/ cs s

hЗ {
font - size: 18 pt ;
font - family: sans - serif ;

tаЫе , td {
border : 2рх solid Ыасk;
border - collapse : collapse ;
padding : 5 рх ;
font - family: sans - serif ;

Повторите процесс для создания в папке wwwroot/css еще одной таблицы стилей
по имени second. css с соде ржимым , приведенным в листинге 6.20.

Листинг 6.20. Содержимое файла second. css из папки wwwroot/css

р {
font - family: sans -se rif ;
font - size: 10 pt ;
color: darkgreen ;
background-color : antiquewhite ;
border : lpx solid Ыасk ;
padding : 2рх ;

Специальные файлы JavaScript хранятся в папке wwwroot/j s . Создайте эту папку


и с применением шаблона элемента JavaScript File (Файл JavaScript) добавьте в нее
файл third . j s (рис . 6.24).
164 Часть 1. Введение в инфраструктуру ASP.NET Соге MVC

ASP.NEl
~client·sid~
" Туре: Client·side f'
А script file co ntai ning JlJvllScript со
(ode

~ Onlinf

Type:Script File Clien t·si d ~

TypeScript JSX File Client· side

f;J TypeSc ript JSO N Configuration File Client· side

f;J Bower Configurat1on File C l i ent - ~ ide

Click here. to $19 011line and find t~


1
Nllme: third.js {
-----------~-~-----·--"-~-·----ц
Рис. 6.24. Создание файла JavaScript

Поместите в новый файл простой код JavaScript, представленный в листинге 6.21.


Листинг 6.21. Содержимое файла third. j s из папки wwwroot/ j s
document . addEventListener( " DOMContentLoaded ", function () {
var element = document . createElement("p " ) ;
element.textContent = "This is the element from the third .js file ";
document . querySelector( "body " ) . appendChild(element) ;
) ) ;

Нам необходим еще один файл JavaScript. Создайте в папке wwroot/js файл по
имени fourth. j s с содержимым , показанным в листинге 6.22.
Листинг 6.22. Содержимое файла fourth. j s из папки wwwroot/ j s
document.addEventListener("DOMContentLoaded", function () {
var element = document . createElement ( "р");
element. tex tContent = "This is the element fr om the fourth. j s file ";
document . querySelector( "body " ) . appendChi l d(element) ;
}) ;

Обновление представления
Последний подготовительный шаг связан с обновлением представления
Index. cshtml для использования новых таблиц стил ей CSS и файлов JavaScrlpt
(листинг 6.23).

Листинг 6.23. Добавление элементов script и link в файле Index. csh tml
@model IE numeraЬle<WorkingWithVisua l Studio . Models . Produc t >
@{ Layout = null; )
Глава 6. Работа с Visual Studio 165
< !DOCTYPE html>
<html>
<head>
<meta name="viewport" content= "width=device - width" />
<title>Working with Visual Studio</title>
<link rel="stylesheet" href="css/first.css" />
<link rel= "stylesheet" href="css/second.css" />
<script src="js/third . js"></script>
<script src="js/fourth. js"></script>
</head>
<body>
<h3>Products </h3>
<p>Request Time : @DateTime . Now . ToString("HH:mm:ss")</p>
< tаЫе>
<thead>
<tr><td>Name</td><td>Price</td></tr>
</thead>
<tbody>
@foreach (var р in Model)
<tr>
<td>@p . Name</td>
<td>@($ " {p.Price : C2} ")</td>
</tr>
}
</tbody>
</tаЫе>
</body>
</html>

Запустив пример приложения, вы увидите содержимое, приведенное на рис. 6.25.


Существующее содержимое было стилизовано посредством таблиц стилей CSS, а код
JavaScript добавил новое содержимое.

D \'Jorking V1ith Vi'u•I Stu· Х

____
---· ___с_ СФ:~~~:;~~~~=~-~-::..---::::~~-=--=====----_-::::' ~-----~~- :=-~~--
!
\ Products
l /Request Time: 17'29:48

Name Price
Lifejacket $48.95
Soccer ball $19.50

1 ~T,-hl-sl_s_th-e-el-em_en_t_r o-,n-th-e th-lr-o.J-s-fi~---------------------~1 1


1
1
Corner flag $34.95

~his is the elemer.1 fron1 the fourth.js file .

Рис . 6.25. Выполнение примера приложения


166 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

Пакетирование и минификациS1 в приложениS1х MVC


В данный момент есть четыре статических файла, и браузер должен делать четы­
ре запроса, чтобы получить эти статические файлы. При доставке клиенту каждый
такой файл отнимает больше полосы пропускания. чем должен. поскольку содержит
пробельные символы и имена переменных, которые являются содержательными для
разработчика, но не имеют никакого значения для браузера .
Объединение файлов одного и того же типа называется пакетированием.
Уменьшение размеров файлов называется минификацией . Обе задачи выполня­
ются в приложениях ASP.NET Core MVC с помощью расширения Bundler & Minifier
(Упаковщик и минификатор) для Visual Studio.

Установка расширения Visua/ Studio


Первый шаг заключается в установке расширения. Выберите пункт меню
Toolsc::>Extensions and Updates (СервисqРасширения и обновления) и щелкните на кате­
гории On line (Онлайновые), чтобы отобразить галерею доступных расширений Visual
Studio. В поле поиска справа вверху введите строку Bund ler & Mini fie r (рис . 6.26).
Найдите расширение Bundler & Minifler и щелкните на кнопке Download (Загрузить).
чтобы добавить его в Visual Studio. Завершите процесс установки и перезапустите
Visual Studio.

~ lnstolled Bundler & Minifier х •


" o~itne:·.
,,. '' Created Ьу: Mods Kristensen
• Visual Studio G•lfery
~ Controls 1 Version: 2. 1.255
Oo>Vnloads: 144S40
~

~
~ Templates Web E.ssentinls 2015.3 Rating: ., (42 Votes)
• Tools Adds many us~ u l features to \/i<J ual Studio for we b More lnformat1on '
!Seatc ~ults develop•"· Requires Visu•I Studio 2015 Report Exten:-.ion to Mкrosoft
" 1

~
~ Samples Gallery

1 ~,,,
Web Exton.slon Pack
1
Р Updotes (2) The e a ~1est way to set up Visual Studto for tl't ultimtte
we:b dNelopment experienc e.

~
Web An;ilyzer
Provid•s static analysis dir~ctly in Visual Studio for

~
JavdScript, Type:Sc ript, JSX. CSS .Jnd more

..t;J lmng" Sprites ,,.


у

-··-··
..:...~....._._.".:....._:....:;...~".-; .... ~~--·....;,._.' -·~·~·---,.,.:_ ... __
_;..;.,'
Close
................-:.._'~..... "-..;.·-~.:;·-· :.;_,-..:.~:..~-~-· ~-----'
' '
Рис. 6.26 . Поиск расширения Visual Studio

пакетирование и минификация файлов


После установки расширения и перезапуска Visual Studio вы можете выбирать
множество файлов одного типа, пакетировать их вместе и минифицировать их содер­
жимое. В качестве примера выберите файлы first. css и second. css в окне Solution
Explorer, щелкните на них правой кнопкой мыши и выберите в контекстном меню пункт
Bundler & MinifierqBundle and Minify Files (Упаковщик и минификаторqПакетировать и
минифицировать файлы). как показано на рис . 6.27.
Глава 6. Работа с Visual Studio 167

Solution Explorer • r:3 Х

О'IФ ~ ! Ф·~@!@ с+ Open


. .
Se6rch Solut•on Ex~or (C trl•:.!._ Opon With".
Ф ~JWroot Hide from Solution Exp lorer

{} Cle•nup Selocted Code


.@] fors\.css. ·,_.,. ,"
~ second.;;s
~
-Coll.apse Recurs1vely
. - -
" -.i js -·Rt1n Web Code:. Analysi.s
.
rr fourth.js Bundler & Minifitr
--..~-

rr thiгd .js .... NEW Solution Explore.r V1e.;,v


lib
J(, Cut Ctrl+X
~--------< Q1 Сору Ctrl+C
Х D•lete Del
Rfnan1t

1' Properties Aft•Enter

Рис. 6.27. Пакетирование и минификация файлов CSS

Сохраните выходной файл как bundle. c ss и расширение обработает файлы CSS.


В окне Solution Explorer отобразится новый элемент b undl e . cs s, который можно рас­
крыть и увидеть минифицированный файл по имени bund l e . min. css . Открыв ми­
нифицированный файл, вы заметите, что содержимое обоих отдельных файлов CSS
было объединено, а все пробельные символы удалены. Работать с этим файлом на­
прямую вы вряд ли захотите, однако он меньше по размеру и требует только одного
НТТР-подключенил для доставни стилей CSS клиенту.
Повторите процесс с файлами third. j s и fo u rth . js , чтобы создать новые фай­
лы bundle . js и bundle . mi n . js в папке www r oot/j s.

Внимание! Удостоверьтесь, что выбираете файлы в порядке, в котором они загружаются


браузером , для того чтобы предохранить порядок следования стилей или операторов
кода в выходном файле. Таким образом, например, выбирайте файл thi rd. j s перед
файлом four th. j s , чтобы обеспечить выполнение кода в правильном порядке.

В листинге 6.24 показано представление I ndex . csht ml, в котором элементы link
для отдельных файлов заменены одним таким элементом , запрашивающим пакетиро­
ванные и минифицированные файлы.

Листинг 6.24. Применение пакетированных и минифицированных файлов


в файле Index. cshtml
@model IE n ume r a Ыe <Wo rkin g W i thVisu a l S tudi o . Mo d e ls. Produc t >
@{ Layout = null ; }
<!DOCTYPE html>
<html>
<head>
<met a name= "viewpor t" cont ent=" wi dt h=devi ce - width " />
<title>Working with Visual Studio</ti tl e>
<link rel="stylesheet" href="css/bundle.min.css" />
<script src="js/bundle .min. js"></script>
</head>
<body>
<hЗ>P r oducts</hЗ>
<p>Request Ti me : @Dat e Ti me . Now.T oS t r i ng ("HH: mm : s s" )</p>
168 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

<tаЫе>
<thead>
<tr><td>Name</td ><td >P ri ce</ td></ tr>
</thead>
<tbody >
@f o r each (var р in Model)
<tr>
<td>@ p.Name</td>
<td>@($ "{p. Price: C2 }" )</ td>
</tr>

</tbody>
</tаЫе>
</body>
</html>

После запуска приложения вы не заметите никаких визуальных отличий, но оно


использует пакетированные и минифицированные файлы, пр едоставляя браузеру все
стили и код, которые были определены в отдельных файлах.
По мере выполнения оп е раций пакетирования и минификации расширени е в едет
учет обработанных файлов в файле по имени b un d l econfig . j son, находяще мся в
корневой папке проекта. Вот конфигурация. которая была сгенерирована для ф айлов
в примере приложения:

"o utp utFileN a me": "wwwroo t/ c s s /bundle. c ss",


"inp utFile s": [
" wwwroot/ c ss/fir st . css ",
" wwwroo t /css/second . cs s"
]
} 1

{
"outputFil eName ": " wwwroot/ j s/bundl e .j s ",
" inputFiles ": [
" wwwroo t / j s/t hird .js",
" wwwroot/ j s / fo u r t h .j s "

Расширение автоматически отслеживает входные файлы на предмет изменений и за­


ново гене рирует выходные файлы, когда происходят изменения. гарантируя отражение в
пакетированных и минифицированных файлах результатов любого редактирования. Для
демонстрации в листинге 6.25 приведено изменение. внесенное в файл th i rd . j s.

Листинг 6.25. Внесение изменения в файл third. j s


d ocume nt . addEvent Lis t ener (" DOMConte nt Loade d", f unct ion () {
var e l eme nt = document . create El eme nt ( "р") ;
element.textContent =
"This is the element from the (modified) third . js file";
document . querySelector( "body " ) . append Ch il d(e l ement) ;
}) ;
Глава 6. Работа с Visual .Studio 169
После того как файл сохранен, расширение заново сгенерирует файл bundle . min. j s.
Перезагрузив страницу в браузере , вы увидите изменение (рис . 6.28).

Name Price
Lifejacket $48.95
Soccer ball $19.50
Corner flag $34.95

Рис. 6.28. Обнаружение изменений в пакетированных и минифицированных файлах

Резюме
В этой главе была рассмотрена структура проектов МVС, описаны две доступные
исполняющие среды .NET и обсуждены средства, которые Visual Studio предлагает для
разработки веб-приложений, включая автоматическую компиляцию классов, средство
Browser Liпk, а также пакетирование и минификацию. В следующей главе объясняет­
ся. как проекты ASP.NET Core МVС подвергаются модульному тестированию .
ГЛАВА 7
Модульное тестирование
приложений МVС

в настоящей главе будет показано, как проводить модульное тестирование при­


ложений MVC. Модульное тестирование - это вид тестирования, при котором
индивидуальные компоненты изолируются от остальной части приложения, позволяя
тщательно проверить их поведение . Инфраструктура ASP.NET Core МVС была спроек­
тирована так, чтобы сделать легкой задачу создания модульных тестов, и среда Visua]
Studio предлагает поддержку для широкого диапазона инфраструктур модульного
тестирования. В главе объясняется , как настроить проект модульного тестирования,
каким образом установить одну из самых популярных инфраструктур тестирования
и что собой представляет процесс написания и прогона тестов. В табл. 7.1 приведена
сводка по главе.

Таблица 7.1. Сводка по главе

Задача Решение Листинг

Создание модульного теста Создайте проект модульного тестирования, 7.1-7.8


установите тестовый пакет и добавьте клас-
сы, которые содержат тесты

Изоляция компонентов для Используйте интерфейсы для разделения 7.9-7.16


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

Прогон тех же самых тестов Используйте параметризованный модуль­ 7.17-7.19


xUnit.net с разными значениями ный тест либо получайте тестовые данные
данных из метода или свойства

Упрощение процесса создания Используйте инфраструктуру имитации 7.20-7.22


фиктивных тестовых объектов
Глава 7. Модульное тестирование приложений MVC 171

Проводить ли модульное тестирование?

Наличие возможности легко выполнять модульное тестирование является одним из преиму­


ществ использования инфраструктуры ASP.NET Core MVC, но эта процедура не для всех, и я
не намерен притворяться, что дело обстоит иначе.
Мне нравится модульное тестирование, и я применяю его в своих проектах, но далеко не во
всех и не настолько согласованно, как вы могли ожидать. Я предпочитаю концентрироваться
на написании модульных тестов для средств и функций, о которых знаю , что они будут труд­
ны в реализации и, скорее всего, станут источником ошибок после развертывания. В таких
ситуациях модульное тестирование помогает структурировать мои мысли о том , как лучше

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

Тем не менее, модульное тестирование - это инструмент, а не догма, и только лично вы


знаете, в каком объеме должно проводиться тестирование. Если вы не находите модульное
тестирование полезным или располагаете другой методологией, которая вам больше под­
ходит, то не должны думать, что вы обязаны использовать модульное тестирование просто
потому, что это модно. (Однако если вы не располагаете более эффективной методологией
и вообще не выполняете тестирование, то вероятно даете возможность пользователям об­
наруживать имеющиеся ошибки, что редко оказывается идеальным решением . Вы не обяза­
ны применять модульное тестирование, но действительно должны проводить тестирование
какого-то вида.)

Если вы не сталкивались с модульным тестированием ранее, я предлагаю опробовать его и


посмотреть, как оно работает. Если вы не являетесь поклонником модульного тестирования,
то можете пропустить данную главу и перейти к чтению главы 8, где будет начато построе­
ние более реалистичного приложения MVC.

П одготовка проекта для примера


В этой главе мы продолжим пользоваться проектом Wor k i ngW i thVisu al S t udi o ,
который был создан в главе 6. Здесь мы добавим в него поддержку для создания но­
вых объ е ктов Pr oduc t в хранилище.

Включение встроенных дескрипторных вспомогательных классов


В данной главе применяется один из встроенных дескрипторных вспомога­
тельных классов для установки атрибута href элемента а. Работа дескрипторных
вспомогательных классов подробно объясняется в главах 23-25, а здесь мы просто
должны включить их. Создайте файл импортирования представлений, щелкнув пра­
вой кнопкой мыши на папке Vi ews, выбрав в контекстном меню пункт Addi::>New
ltem (Добавить~::> Новый элемент) и указав шаблон элемента MVC View lmports Page
(Страница импортирования представлений МVС) из категории ASP.NET. Среда Visual
Studio автоматически назначит файлу имя _ Vi e wi mp ort s. cs html, а щелчок на
кнопке Add (Добавить) приведет к его созданию. Поместите в файл содержимое, по­
казанное в листинге 7. 1.

Листинг 7.1. Содержимое файла_Viewimports. cshtml из папки Views


@addTagHelpe r *, Mi cro s oft. As pNe t Co r e . Mvc .TagHe lp e r s
172 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

Этот оператор включает встроенные дескрипторные вспомогательные классы. в


том числе тот, который вскоре будет использоваться в представлении Index . Можно
было бы добавить операторы using для импортирования пространств имен из проек­
тов, но представления не являются важной частью рассматриваемого в данной главе
примера приложения, так что ссылка на типы моделей с их пространствами имен не
вызовет проблем.

Добавление действий к контроллеру


Первый шаг связан с добавлением действий к контроллеру Home, который будет
визуализировать представление для ввода данных и для получения этих данных от

браузера (ли стинг 7.2). Действия следуют тому же шаблону, который применялся в
главе 2 и подробно обсуждается в главе l 7.

Листинг 7.2. Добавление методов действий в файле HomeController. cs


usin g Microsoft . AspNetCore . Mvc;
using WorkingWithVisualStudio . Mode ls;
using System.Linq;
namespace WorkingWithVisua lStudio.Controllers
puЫic class HomeController : Controller {

SimpleRepository Repository = SimpleRepository.SharedRepository;


puЬlic IActionResul t Index () => View (Reposi tory. Products
.Where (p => p?.Price < 50));
[HttpGet]
puЬlic IActionResul t AddProduct () => View (new Product () ) ;
[HttpPost]
puЫic IActionResult AddProduct(Product р)
Repository.AddProduct(p);
return RedirectToAction("Index");

Создание формы для ввода данных


Чтобы снабдить пользователя возможностью создания нового товара, мы создадим
представление Razor по имени AddProduct . cshtml в папке Views/Home . Соглашения
относительно имени файла и его местоположения соответствуют стандартному пред­
ставлению, которое визуализирует метод AddProduct () контроллера Home . В лис­
тинге 7.3 приведено содержимое нового представления, которое полагается на пакет
Boostrap, добавленный к проекту с использованием Bower в главе 6.

Листинг 7.3. Содержимое файла AddProduct. cshtml из папки Views/Home


@model WorkingWithVisualStudio.Models.Product
@{ Layout = null; }
< !DOCTYPE html>
<html>
<head>
Глава 7. Модульное тестирование приложений MVC 173
<meta name= " viewport " conten t ="width=de vi ce - wi dth " />
<title>Working with Visual Studio</title>
<link rel= " stylesheet " href= " /liЬ/bootstrap/dist/css/bootstrap . min . css " />
</head>
<body class= "panel - body " >
<hЗ>Create Product</hЗ>
<form asp- action= "AddProduct " method= "post " >
<div c l ass= " 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>
<button type="submit" class =" Ьtn Ьtn -pr imary " > Add </button>
<а asp-action= " Index " class= " Ьtn Ьtn -de fault " >Cancel</a>
</form>
</body>
</html>

Представление содержит НТМL-форму, которая применяет НТТР-запрос POST


дл я отправки значений Name и Price действию AddProduct контроллера Ноте .
Соде ржимое стилизовано с использованием пакета CSS из Bootstrap.

Обновление представления Index


Последний подготовительный шаг предусматривает обновление представления
Index , чтобы включить в него ссылку на новую форму (листинг 7 .4). Кроме того, поя­
вилась возможность удалить файлы JavaScript, используемые в предыдущей главе, и
заменить специальные таблицы стилей CSS стилями Bootstrap, которые применяют­
ся к элементам HTML в представлении.

Листинг 7.4. Обновление содержимоrо в файле Index. cshtml


@model IEnumeraЫe<WorkingWithVisualStudio . Models.Product>
@{ Layout = null ; }
< ! DOCTYPE html>
<html>
<head>
<meta name:o "viewport " content= "width=device - width " />
<title>Worki ng with Visua l Studio</title>
<link rel="stylesheet" href="/lib/bootstrap/dist/css/bootstrap.min.
css" />
</head>
<body class="panel-body">
<hЗ class="text-center">Products</hЗ>
<tаЫе class="taЫe taЬle-bordered taЫe-striped">
<thead>
<tr><td>Name</td><td>Price</td></tr>
</thead>
<tbody>
@foreach (var р in Model) {
174 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

<t r>
<td >@p .Name</td>
<t d >@($ " {p .Pri ce :C 2} " )</td>
</ t r>
}
</tb o dy >
</ t а Ые >
<div class="text-center">
<а class="Ьtn Ьtn-primary" asp-action="AddProduct">
Add New Product
</а>
</div>
</body >
</ html>

Запустив пример приложения, вы увидите по-новому стилизованное содержимое


и кнопку Add New Product (Добавить новый товар) , щелчок на которой приводит к
открытию формы для ввода данных. Отправка формы добавит в хранилище новый
объект Produ c t и п е р енаправит браузер для отображения первоначального представ­
ления приложения (рис. 7.1).

Рис. 7 .1. Выполнение примера приложения

Совет. Не забывайте, что в этом примере объекты хранятся только в памяти, т.е . любые со­
здаваемые товары при перезапуске приложения будут утеряны.

Модульное тестирова н ие приложений MVC


Модульные тесты используются для проверки поведения отдельных компонентов и
функциональных ср едств в приложении, а инфраструктуры ASP.NET Core и ASP.NET
Core MVC спроектированы так, чтобы максимально облегчить настройку и запуск
модульных тестов для веб-приложений. В последующих разделах объясняется, как
настроить модульное тестирование в Visual Studio, и демонстрируется написани е
м одульных тестов для приложений МVС. Кроме того, будут представлены пол езные
инструменты , которые делают модульное тестирование проще и надежн ее .
Глава 7. Модульное тестирование приложений MVC 175
Доступен целый ряд разных пакетов для модульного тестирования. В книге при­
меня ется один из них, который называется xUnit.net; он выбран из-за его хорошей
инте грации сVisual Studio, к тому же этот пакет использовался командой разработ­
чиков Microsoft при написании модульных тестов для ASP.NET. В табл. 7.2 приведена
сводка, позволяющая поместить xUnit.net в контекст.

Таблица 7.2. Помещение xUnit.net в контекст

Вопрос Ответ

Что это такое? xUnit.net - это инфраструктура модульного тестирования, которая мо­
жет применяться для тестирования приложений ASP.NET Core MVC

Чем она полезна? xUпit.пet является хорошо написанной инфраструктурой модульного


тестирования, которая легко интегрируется со средой Visual Studio
Как она Тесты определяются как методы, аннотированные с помощью атрибута
используется? Fac t или Theo r y. Внутри тела метода используются методы класса
Assert для сравнения ожидаемого результата теста с тем, что получи­

лось в действительности

Существуют ли Главный просчет при модульном тестировании связан с недостаточной


какие-то скры­ изоляцией тестируемого компонента . За дополнительными сведениями
тые ловушки или обращайтесь в раздел "Изолирование компонентов для модульного
ограничения? тестирования" далее в главе. Самой крупной проблемой, специфичной
для xUпit.пet, является нехватка документации. Кое-какая базовая ин­
формация доступна на веб-сайте http: //xuni t . gi thuЬ . i o, но рас­
ширенное применение требует метода проб и ошибок

Существуют ли Доступно множество инфраструктур модульного тестирования.


альтернативы? Двумя популярными альтернативами являются MSTest (производства
Microsoft) и NUпit

Изменилась ли она Инфраструктура ASP.NET Core MVC делает легким проведение модуль­
по сравнению с ного тестирования , но вовсе не требует и не навязывает его использо­
версией MVC 5? вание, а также не регламентирует применение конкретных инструмен­

тов тестирования . Вы вольны использовать любые инструменты или не


выполнять тестирование вообще

На заметку! В области модульного тестирования практически все является предметом пер­


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

тесты в том же самом проекте или даже в том же самом файле класса. Я описываю здесь
распространенный подход , которому следую сам, но если он не кажется вам правильным,
то вы должны поэкспериментировать с различными стилями тестирования, пока не под­

берете для себя подходящий.

Создание проекта модульного тестирования


Для приложения ASP.NEТ Core обычно создается отдельный проект Visual Studio, со­
держащий модульные тесты, каждый из которых определя ется как метод в классе С# .
Применение отдельного проекта означает, что приложение можно развернуть, не раз­
вертывая одновременно тесты.
176 Часть 1. Введение в инфраструктуру ASP.NET Соге MVC

Соглашение предусматривает назначение проекту модульного тестирования име­


ни <ИмяПриложения> . Tests test, находящей­
и создание его в папке под названием
ся на том же уровне, что и папка src. Для приложения
WorkingWithVisualStudio
именем проекта модульного тестирования будет WorkingWithVisualStudio . Tests .
На рис. 7.2 показана обычная структура проекта ASP.NET Core в Visual Studio, кото­
рый содержит модульные тесты.

Решение _____. Папка src f----+


Проект
WorkingWithVisualStudio WorkingWithVisualStudio

-- Папка test ,_______.... Проект

WorkingWithVisualStudio . Tests

Рис. 7.2. Обычная структура проектов при модульном тестировании

Создание такой структуры требует небольшой работы из-за особенностей распре­


деления средой Visual Studio содержимого решения по файлам на диске.
Первый шаг заключается в использовании проводника файлов или окна командной
строки для создания папки test внутри папки решения WorkingWithV isualStudio,
чтобы она оказалась на одном уровне с существующей папкой src.

Совет. В Visual Studio имеется шаблон проекта Unit Test (Модульный тест), однако он не
настроен для применения с .NET Саге и не поддерживает средства, подобные файлу
proj ect . j son.

Щелкните правой кнопкой мыши на элементе решения WorkingWi thVisua lStudio


в окне Solutioп Explorer среды Visual Studio ( элемент верхнего уровня, охватываю­
щий все остальное), выберите в контекстном меню пункт Addc::>New Solution Folder
(Добавить~:::> Новая папка решения) и установите имя новой папки в test . (Вы не смо­
жете добавить папку решения при выполняющемся отладчике; выберите пункт Stop
Debugging (Остановить отладку) в меню Debug (Отладка) и попробуйте заново.)
Щешшите правой кнопкой мыши на элементе папки test в окне Solution Explorer
и выберите в контекстном меню пункт Addc::>New Project (Добавитьс::>Новый проект).
Выберите шаблон Class Library (.NET Core) (Библиотека классов (.NET Core)) из катего­
рии lnstalledc::>Visual C#c::>.NET Core [Установленныес::>Visuаl C#c::>.NET Core), установите
имя проекта в WorkingWi thVisualStudio. Tests, а в поле Location (Местоположени е)
введите путь к папке test (рис. 7.3).
Щелкните на кнопке ОК, чтобы создать проект. В результате структура проектов,
отображаемая в окне Solution Explorer, будет соответствовать структуре папок проек­
тов в файловой системе.

Конфигурирование проекта модульного тестирования


Чтобы подготовить проект модульного тестирования, откройте файл proj ect . j son
из проекта WorkingWi thVisualStudio . Tests и измените его содержимое так , как
показано в листинге 7.5.
Глава 7. Модульное тестирование прило жений MVC 177

·-·· •• • Г.- - ' " .


." • i ;;· l.: = LSearc~ 1~

Туре: Vis~
дprojectt/

i
11
t; 1NindO\ЧS
\.\/еЬ

~ЕТС?.~
1@} Sii Co n1ole App lication (.NЕТ Core) Visual (# Core class;-

1
ASP.NEТ Core Web Application (. NЕТ Co re) Visual (:<
1 Android
Cloud
1.1;
1 E:<tensibllity
1
1 iOS
1 Repor1ing
1
Silv~rlight
1
1 Test
i
1 WCF

1 ~ Online
! м,mе
1
f . Lo<:atio,ru

Р ис. 7.3. Создание проекта для тестов

В нимание! Будьте внимательны, чтобы внести изменения в файл p r o j ect . j son внутри
прое кта модульного тестирования, а не проекта главного приложения.

Листин г 7.5. Содержимое файла project. json из проекта


WorkingWithVisualStudio.Tests

"version ": " 1 . 0 . 0 - * ",


"testRunner": "xunit",
"dependenc i es ": {
"Microsoft.NETCore.App":
"type ": "platform",
"version": "1.0.0"
}'
"xuni t" : "2 . 1 . О" ,
"dotnet-test-xunit": "2.2.0-preview2-build1029"
),
" f r ameworks ": {
"ne tcoreappl.0":
"imports ": [ "dotnet5. 6", "portaЫ e -net 4 5+win8"]

Эта конфигурация сообщает Visual Studio о том, что требуются три пакета. Пакет
Microsoft . NETCore . Арр пр едоставля ет АРl-инт е рфейс .NET Core. Пакет xuni t
соде рж ит инфр а ст руктуру тестирования , а пакет dotnettest - x un i t обеспечива -
178 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

ет интеграцию между xUnit.net и Visual Studio. На момент написания книги пакет


dot net - test - xuni t , подде рживаемый для приложен ий .NET Core, был доступен в

виде пр едварительно й верси и, и при чтении этой главы вы можете обнаружить более
ПОЗДНЮЮ версию .

Когда вы сохраните изменения , вн е сенные в файл proj ect . j son , среда Visual
Studio з агрузит и устан овит NuGеt-пакеты xUnit.net вм есте с их зави с имостями . Вс е
это мож е т потребовать определенного времени, поскольку зависимостей очень много .
Процесс создания проектов модульного тестирования для приложений ASP.NET Core
МVС в будущих выпусках Visual Studio, скорее всего, будет упроще н , по этому описан­
ные здесь дополнительные шаги больше не понадобятся.

Добавление ссылки на проект приложения


Для т ого чтобы появилась возможность тестирования кл а ссов в приложении, не­
обходимо добавить ссылку на проект приложения в файл proj ect . j son про екта мо­
дульного тестирования (листинг 7.6).

Листинг 7.6. Добавление ссылки на проект приложения в файле project. j son


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

"ve rsion ": "1 . 0 . 0- *",


"t e stRunner ": "xunit ",
"de pende nc i es ": {
"Microsoft . NETCore . App ":
"type ": "platform ",
"version ": " 1 . 0 . 0"
} '
"xunit ": " 2.1 . О ",
"dotnet - test - xun i t ": "2 . 2 . 0- pr eview2 - bui l dl029 ",
"WorkingWithVisualStudio": "1 . 0.0"
}'
" framewo r ks": {
" netcoreappl . 0 ":
" imports ": ["dotnet5 . 6", " portaЬle - net45+win8 " ]

Написание и выполнение модульных тестов


Теперь, когда все подготовительны е шаги завершены, можно приступать к написа­
нию ряд а тестов. Первым делом добавьте в про ект Wo r kingWi thVisualStudio . Tests
файл класса по им ени ProductTes ts . c s с определением, предст авленным в листин­
ге 7.7. Это простой класс , но он содержит все, что требуется для начала модульного
те стирования.

На заметку! В методе CanChangeP r oductPrice () умышленно допущена ошибка, кото­


рая будет устранена позже в данном разделе .
Глава 7. Модульное тестирование приложений MVC 179
Листинг 7.7. Содержимое файла ProductTests. cs
using WorkingWithVisualStudio.Models ;
using Xunit ;
namespace WorkingWithVisualStudio .Tests
puЫic class ProductTests {
[Fact]
puЫic void CanChangeProductName()
11 Организация
var р = new Product { Name = "Test ", Price lOOM };
11 Действие
p.Name = "New Name ";
11 Утверждение
Assert . Equal ("Ne w Name ", p . Name) ;

[Fact]
puЫic void CanChangeProductPrice()
11 Организация
var р = new Product { Name = "Test ", Price lOOM };
11 Действие
р . Price = 200М ;

11 Утверждение
Assert . Equal(lOOM , p.Price) ;

В классе ProductTests присутствуют два модульных теста, каждый из ко­


торых проверяет определенное поведение класса модели Product из проекта
WorkingWi thVisualStudio . Проект тестирования может содержать много классов,
которые способны включать много модульных тестов .
По соглашению имя тестового метода описывает то, что делает тест, а имя клас­
са - то, что подвергается тестированию . Это облегчает структурирование тестов в
проекте и упрощает понимание того, какими будут результаты всех тестов, когда они
выполняются средой Visual Studio. Имя ProductTests у1шзывает, что класс содер­
жит тесты для класса Product, а имена методов говорят о том , что они проверяют
возможность изменения названия и цены объекта Product.
Атрибут Fact применяется к каждому методу, указывая на то , что метод является:
тестом. Внутри тела метода модульный тест следует шаблону , который называется
орган.изация/действие/утвержден.ие (arrange/act/assert -А/А/А). Организация от­
носится: к настройке условий для: теста , действие - к выполнению теста, а утверж­
дение - к пров е рке того, что результат оказался тем, который ожидали.
Разделы организации и действия этих тестов представляют собой обычный код
С#, но раздел утверждения обрабатывается: инфраструктурой xUnit.net, которая пре ­
доставляет класс по имени Assert , чьи методы используются для проверки , является
ли результат действия тем, что ожидался.

Совет. Атрибут Fact и класс Asset определены в пространстве имен Xuni t , для которого
дол жен быть предусмотрен оператор using в каждом тестовом классе.
180 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

Методы класса Assert определены как статичес1ше и применяются для выполн е­


ния разных видов сравнений меж,ду ожидаемыми и действительными р е зул ьтатами .
В табл. 7.3 описаны распространенные методы класса Assert .
Таблица 7.3. Часто используемые методы класса Assert из инфраструктуры xUnit.net

Имя Описание

Equal(expected , result) Этот метод добавляет утверждение о том , что резуль­


тат равен ожидаемому исходу. Существуют перегру­
женные версии данного метода для сравнения различ­

ных типов и для сравнения коллекций. Имеется также


версия, которая принимает дополнительный аргумент
в форме объекта, который реализует интерфейс
IEqua li tyCornparer<T> для сравнения объектов

NotEqual(expected , result) Этот метод добавляет утверждение о том, что резуль­


тат не равен ожидаемому ис х оду

True(result) Этот метод добавляет утверждение о том, что резуль­


тат равен true
False(result) Этот метод добавляет утвер ждение о том, что резуль­
тат равен false
IsType(expected, result) Этот метод добавляет утверждение о том, что резуль­
тат принадлежит указанному типу

IsNotType(expected, resu l t) Этот метод добавляет утверждение о том, что резуль­


тат не принадле жит указанному типу

IsNull (result) Этот метод добавляет утверждение о том , что резуль­


тат равен null
IsNotNull(result) Этот метод добавляет утвер ждение о том , что резуль­
тат не равен null
InRange(result, low , high) Этот метод добавляет утверждение о том , что резуль­
тат находится между l ow и high
NotinRange(result, low ,high ) Этот метод добавляет утверждение о том, что резуль­
тат не находится между l ow и high
Throws(exception,expression) Этот метод добавляет утверждение о том, что указан­
ное выражение генерирует исключение заданного типа

Каждый метод класса Assert позволяет выполнять разные типы сравнения


и генерирует исключение, е сли результат оказыва ется н е тем, 1юторый ожидался.
Исключение применяется для указания на то, что тест не прош ел. В тестах из лис­
тинга 7.7 метод Equal () используется для определения, корректно ли изменилось
знач ение свойства:

Assert .Equal ( " New Name ", p.Name) ;

Прогон тестов с помощью окна Test Explorer


Среда VisuaJ Studio предлагает поддержку для поиска и прогона мо,дульных тестов
посредством окна Test Explorer (Проводник тестов), которое доступно через пункт меню
TestqWiпdowsqTest Explorer (ТестqОкнаqПроводник тестов) и показано на рис. 7.4.
Глава 7. Модульное тестирование прилож ений MVC 181

Test Explorer
Test Explorer • !:! х
Searcn
П: . . а · Sea.:_:h Р·

Ruo А!! 1 Run". • 1 Pi•y11>t: All Test.< •


... Failed Tests (1)
Q T!!sts.Produc tTtsts .CгnCh.!11gePtoductPri ct l lms
... Passed Tests (1)
(J Tests.ProductТest.<.CanC l1o ngoProduct№me 3 ms

Sun1mary
Last Te:st Ru n Failed (Тotel Run Timl! О 00:04)

О Foiled
1 Ttst
О 1 Test Po"<d

Рис. 7.4. Окно Test Explorer в Visual Studio

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

Выполните тесты, щелкнув на пункте Run All (Выполнить все) в окне Test Explorer.
Ср еда Visual Studio прим енит xUnit.net для прогона тестов в проекте и отобразит ре ­
зул ьтаты . Как отм е чалось, тест CanChangeProduct Pr i ce () содержит ошибку. кото­
рая приводит к тому , что те ст не проходит. Проблема связан а с аргументом метода
Assert . Equa l () , из-за чего прои сходит сравнение с исходным значением свойс ­
тва Price , а не со значением , на которое оно изменилось. В листинге 7.8 проблема
устранена.

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

Листинг 7.8. Исправление теста в файле ProductTests. cs

using WorkingWithVis ualStud i o . Mode l s ;


using Xunit ;
namespace Worki ngWithVisualStudio .Tests
puЬlic class Produc t Tests {
[Fact]
puЫic void CanChangeProductName()
11 Организация
var р = new Product { Name = " Test ", Price lOOM };
11 Действие
p . Name = "New Name ";
11 Утверждение
Assert . Equal( " New Name ", p . Name) ;
182 Часть 1. Введение в инфраструктуру ASP.NET Соге MVC

[Fac t]
p uЫi c vo id Ca nCh a ng e Pro du c tPri c e ( )
11 Ор га н иза ц и я
va r р = n ew Pr oduct { Name = " Tes t", Pr i ce l OOM };
11 Де йс т ви е
p .P rice = 200 М;

11 У т вержд е н и е
Assert.Equal(200M, p.Price);

При наличии большого количества тестов выполнение их всех может занять неко­
торое время. Таким образом, чтобы можно было работать быстро и итеративно, в окне
Test Explorer предлагаются различные варианты для выбора подмножества тестов,
подлежащих прогону. Самым полезным подмножеством является набор тестов , кото­
рые не прошли (рис. 7.5). Запустите исправленный тест снова, и в окн е Test Explorer
будет показано , что не прошедшие тесты отсутствуют.

'Test Explorer
._,

(f: '"'1 ~Jt!-~~~~


~ . - .

Run All 1 Ruo". ? 1 Playl«t: All Te.t'


г--~·---~~·-----
?
.
J. Failed Т( Run FAited Tt<Sts ~
О Testsj Run Not Run Теsн 11 " Р Tosts (2)
(j Tests.ProductTe-s1s.(.anChan9ePr~ductPrice: Sms
" Р•s.М \ Run P•ssed Test•
& Te~ts{ Repti.11 L.ist Run Ctrl... R. l

Summary
St1mmary
l a5t Test Run Failed otJI Run iin~e 0:00:04)
l ast T~ t Ru n Passe:d (Tot.al Run Time 0:00:02)
f.З; 1 Test Fi!ifed
$ 1 TostPaшd О 1 TestPaщd

Рис . 7.5. Избирательный прогон тестов

И золирование компонентов для модульного тестирова н и я


Написание модульных тестов для классов моделей вроде Prod uct особой сложнос­
тью не отличается. Класс Produ c t не только прост, он также самодостаточен , т.е . при
вьmолнении какого-то действия над объектом Pro du c t можно иметь ув е ренность в
том, что тестируется функциональность, предоставляемая классом Product .
С другими компонентами в приложении МVС ситуация сложнее , потому что между
ними е сть зависимости. Следующий набор опр еделяемых тестов будет оп ерировать
на контроллере, исследуя последовательность объектов Pr odu c t, которая пер едается
между контроллером и представлением .
Глава 7. Модульное тестирование прило жений MVC 183
При сравнении объектов, являющихся экземплярами специальных классов, пона­
добится использовать метод Assert. Equal () из xUnit.net, который принимает ар­
гумент, реализующий интерфейс IEquali tyComparer<T>, так что объекты можно
сравнивать. Сначала необходимо добавить в проект модульного тестирования файл
класса по имени Comparer . cs и поместить в него определения вспомогательных
классов, приведенные в листинге 7.9.

Листинг 7.9. Содержимое файла Comparer. cs из проекта


WorkingWithVisualStudio.Tests
using System ;
using System . Co ll ections.Generic ;
namespace WorkingWithVisualStudio . Tests
puЫic class Comparer {
puЫic static Compa rer<U > Get<U>(Func<U, U, bool> func) {
return new Comparer<U>(func) ;

puЬlic class Comparer<T> : Comparer, IEqualityComparer<T>


private Func<T , Т , bool> comparisonFunction;
puЫic Comparer(Func<T , Т , bool> func) {
comparisonFunction = func;

puЫic bool Equa l s(T х , Ту)


return comparisonFunction(x, у);

puЫic int GetHashCode(T obj)


return obj . GetHashCode() ;

Показанные классы позволят создавать объекты IEquali tyComparer<T> с при­


менением лямбда - выражений вместо определения нового класса для каждого типа
сравнения, которое нужно предпринимать . Это не является жизненно необходимым,
но упростит код в классах модульных тестов и сделает его легче для понимания и

сопровождения.

Теперь, когда можно легко делать сравнения, давайте рассмотрим проблему зави­
симостей между компонентами в приложении.
Добавим в проект WorkingWithVisualStudio. Tests новый файл класса по имени
HomeControllerTests . cs и поместим в него определение модульного теста, пред­
ставленное в листинге 7.10.

Листинг 7.10. Содержимое файла HomeControllerTests. cs из проекта


WorkingWithVisualStudio.Tests
using Microsoft . AspNetCore . Mvc;
using System.Collections . Generic;
using WorkingWithVisualStudio.Controllers;
using WorkingWithVisualStudio . Models ;
using Xunit;
184 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

narnespace WorkingWithVis ua lStudio.Tests


puЫic class HomeControllerTests {

[Fact]
puЫic void IndexActionModelisCornplete()
11 Ор гани за ци я
var contro l ler = new HorneController();
11 Действие
var rnode l = (controller.Index() as ViewResult)? . ViewData.Model
as IE nurneraЫe <Pro duct > ;
11 Утверждение
Assert . Equa l (S irnple Repository . SharedRepos itory . Products , rnodel ,
Cornparer . Get<Prod uct>( (pl, р2) => pl.Narne == p2 .Narne
&& pl. Price == р2. Price)) ;

Модульный тест в листинге 7.10 проверяет, что метод действия Index () переда­
ет представлению все объекты в хранилище. (Пока что не обращайте внимания на
раздел действия: класс ViewResul t и роль, которую он играет в приложениях MVC,
будут объясняться в главе 17. В настоящий момент достаточно знать. что здесь полу­
чаются данные модели, возвращаемые методом действия Index () .)
Запустив тест, вы увидите, что он не проходит, указывая на отличие между набо­
ром объектов в хранилище и набором объектов, 1шторые возвратил метод Index () .
Но когда дело доходит до выявления причины, из-за чего т ест не прошел, возникает
проблема: предполагается, что тест действует на контроллере Home, однако класс кон­
троллера зависит от класса SimpleRepository. Это затрудняет выяснение. отра зил
ли тест проблему с 1шассом, на 1\Оторый он нацелен, или же проблему в другой части
приложения.

Пример приложения достаточно прост, чтобы можно было легко выявить пробле­
му, всего лишь взглянув на код классов HomeCont r o lle r и SimpleRepository. В ре­
альном приложении визуальный осмотр не настолько прост, т.к. цепочка зависимос­
тей способна затруднить понимание того, что привело к отказу в прохождении теста.
Обычно хранилище будет полагаться на какую-то разновидность системы постоян­
ного хранения. подобную базе данных, а также библиотеку, которая предоставляет
к ней доступ. Модульный тест может взаимодействовать со всей цепочкой сложных
компонентов, любой из которых может вызвать проблему.
Модульные тесты эффективны, когда они нацелены на небольшие части приложе­
ния, такие как отдельный метод или 1шасс. Нам необходима возможность изоляции
контроллера Home от остальной части приложения, чтобы можно было ограничить
область действия теста и исключить влияние со стороны хранилища .

Изолирование компонента
Ключом к изолированию компонентов является использование интерфейсов С# .
Чтобы отделить контроллер от хранилища, добавьте в папку Mode l s новый файл
класса по имени IReposi tory . cs с определением интерфейса, показанным в лис­
тинге 7.1 1.
Глава 7. Модульное тестирование приложений MVC 185
Листинг 7.11. Содержимое файла IReposi tory. cs из папки Models
us i ng System . Collec t ions . Gener i c ;
namespace WorkingWithVisualStudio.Models
puЬlic i nterface IRe pository {
IEnurneraЫe<Product> Products { get ; }
void AddProduct(Product р);

С этим интерфейсом не связано ничего примечательного (за исключением того. что


в н ем н е определен полный набор операций, который обычно будет нужен в веб-при­
ложении; более реалистичный и завершенный пример можно найти в главе 8). Тем не
менее, добавление интерфейса вроде IReposi t ory позволяет легко изолировать ком­
понент для тестирования. Первым делом нужно обновить класс SimpleReposi to ry,
чтобы он реали зовывал новый интерфейс (листинг 7.12).

Листинг 7.12. Реализация интерфейса в файле SimpleReposi tory. cs


using Sys te rn.Co lle ctions . Gen e ric ;
narnespace WorkingWithVisualStudio . Model s
puЫic class SimpleRepository : IRepository
private static SirnpleRepository sharedRepository new
SirnpleRepository();
priva t e Dictionary<s tr ing , Product> products
= new Dictionary<str i ng , Product>() ;
puЬlic static SirnpleRepository SharedRepository => sharedRepository ;
puЫ i c SirnpleRepository() {
var in itialiterns = new(J {
new Product { Name " Kayak ", Pri ce = 275М },
new Product { Narne " Li fejacket ", Price 48 . 95М },
new Product { Narne " Soccer bal l", Price 1 9 . SOM },
new Product { Narne "C orner flag", Price 34 . 95М }
};
foreach (var р i n i nitiali tern s) {
AddProduct(p) ;

products . Add( " Error ", null);

puЫic IEnumeraЫe<Product> Products => products.Values;


puЫic void Add Pr oduct(Product р) => pr oducts . Add(p.Name, р);

Следующий шаг предусматривает модификацию контроллера, чтобы свойство,


приме няемое для ссылки на хранилище, использовало интерфейс, а не класс (лис­
тинг 7.13).
186 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

Совет. Инфраструктура ASP.NET Core MVC поддерживает более элегантный подход к реше­
нию этой проблемы, который называется внедрением зависимостей и описан в главе 18.
Внедрение зависимостей зачастую вызывает путаницу, поэтому в настоящей главе ком­
поненты изолируются более простым и ручным способом.

Листинг 7.13. Добавление свойства Reposi tory в файле HomeController. cs

using Microsoft . AspNetCore . Mvc ;


using WorkingWithVisualStudio . Mode l s;
using System .L inq ;
namespace WorkingWithVisualStudio . Controllers
puЬlic class HomeController : Controller (
puЫic IRepository Repository = SirnpleRepository.SharedRepository;
puЫic IActionResult Index() => View(Repository . Products
. Where (р => р? . Price < 50) ) ;
[HttpGet]
puЫic IActionResult AddProduct () => View () ;
[HttpPost]
puЬlic IActionResult AddProduct(Product р) (
Reposito r y . AddProduct(p) ;
return RedirectToAction("Index");

Это может выглядеть н езначител ьной модификацией, но позволяет изменять хра­


нилище , которое контроллер применяет во время тестирования, что демонстри рует

способ изоляции контроллера. В листинге 7. 14 модульные тесты контроллера обнов­


лены. так что они используют специальную версию хранилища.

Листинг 7 .14. Изоляция контроллера в модульном тесте внутри файла


HomeControllerTests.cs
using Microsoft . AspNetCore.Mvc ;
using System . Co l lections . Generic ;
using WorkingWithVisualStudio . Controllers ;
using WorkingWithVisualStudio . Models;
using Xunit ;
namespace WorkingWithVisualStudio .Tests
puЫic class HomeControllerTests (

class ModelCornpleteFakeRepository : IRepository


puЫic IEnurneraЫe<Product> Products { get; } = new Product [] {
new Product { Narne = "Pl" , Price = 2 7 SM } ,
new Product { Narne = "Р2", Price 48. 95М } ,
new Product { Narne "РЗ", Price = 19 . SOM},
new Product { Narne = "Р3", Price 34 . 95М }
};
Глава 7. Модульное тестирование приложений MVC 187
puЫic void AddProduct (Product р) {
// ничего не делать - для теста не требуется

[Fact]
puЫic void IndexAct i onMode l isComple te ( )
11 Организация
var control l er = new HomeControl l e r () ;
controller.Repository = new ModelCompleteFakeRepository();
11 Действие
va r mode l = (contr olle r . I nde x ( ) as ViewRe sult) ? . Vi e wData .Model
as IE nu meraЫe<Pro du ct> ;
11 Утверждение
Assert.Equal(controller.Repository.Products, model,
Compare r. Get<Prod uct> ( (pl, р2) => pl . Name == р2 . Name
&& pl.Price == p2 . Pr i ce)) ;

Мы определили фиктивную реализацию интерфейса IRe p osi t ory, в которой при­


сутствует только свойство, необходимое для теста, и применяются всегда согласован­
ные данные (чего может не быть при работе с реальной базой данных, особенно в
случае е е совместного использования с другими разработчиками, вносящими собс­
тв енные изменения) .
Исправл енный модульный тест по-прежнему не проходит, указывая на то, что при­
чиной проблемы явля ется метод действия Index ( ) в классе Home Con tro ller , а не
компоненты , от которых он зависит. Метод действия, вызываемый модульным тес­
том, в достаточной степени прост для того, чтобы проблема стала очевидной в ре­
зул ьт ате его осмотра:

puЫic IActionRes ul t I nde x () =>


View(Repos i tory .Product s. Whe r e(p => p . Pr i ce < 50)) ;

Проблема связана с применением м етода Whe r e () из LINQ, который исполь зуется


для фильтрации объектов Product со значениями свойства Pri ce , равным 50 или
больше . Зде сь уже есть серьезное указание на причину проблемы, но п е редовой опыт
предполагает создание теста, который подтвердит наличие проблемы, прежде чем
вносить корректирующую правку (листинг 7.15) .

Совет. В показанных тестах присутствует много дублированного кода. В следующем разделе


объясняется, как упростить тесты.

Листинг 7.15. Добавление теста в файле HomeControllerTests. cs

usi ng Microso ft.AspN etCor e .Mvc;


using System . Co ll ections . Gener i c ;
using WorkingWi t hVisualStudi o . Con trol l e r s ;
using WorkingWithVisualS t udio . Model s ;
using Xunit ;
188 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

namespace WorkingWithVisualStudio. Te sts {


puЫic class HomeControllerTests {

class ModelCompleteFakeReposito ry : IRepository


puЫic IEnumeraЫe<Product> Products { get ; } = new Product[] {
new Product { Name "Pl '', Price 275М },
new Product { Name " Р2 ", Price 48 . 95М }'
new Product { Name " РЗ ", Price 19 . SOM } 1

new Product { Name " РЗ


11
, Price 34 . 95М }
};
puЬlic void AddProduct(Produc t р) {
//ничего не делать - для теста не требуется

[Fact]
puЫic void IndexActionModelisComplete()
11 Организация
var controller = new HomeController() ;
controller . Repository = new ModelCompleteFakeRepository() ;
11 Действие
var model = (controller.Index() as ViewResult)? . ViewData.Model
as IEnumeraЫe<Product> ;
//Утверждение
Assert .Equal(controller .Repo sitory.Products , model ,
Comparer.Get<Product>( (pl, р2) => pl . Name == p2.Name
&& pl.Price == p2 . Price)) ;

class ModelCompleteFakeRepositoryPricesUnderSO IReposi tory {


puЬlic IEnumeraЫe<Product> Products { get; } new Product [ ]
new Product { Name = 11 Pl 11 , Price SM } ,
new Product { Name Р2 , Price11
4 В. 95М } ,
11

new Product { Name = 11 РЗ 11 1 Price = 19.SOM },


new Product { Name РЗ , Price11
34. 95М }
11

};
puЬlic void AddProduct(Product р)
//ничего не делать - для теста не требуется

[Fact]
puЬlic void IndexActionМodelisCompletePricesUnderSO()
/ / Организация
var controller = new HomeController();
controller.Repository = newModelCompleteFakeRepositoryPricesUnderSO();
//Действие
var model = (controller.Index() as ViewResult)?.ViewData.Model
as IEnumeraЫe<Product>;
//Утверждение
Assert.Equal(controller.Repository.Products, model,
Comparer. Get<Product> ( (pl, р2) => pl. Name == р2 . Name
&& pl. Price == р2. Price) ) ;
Глава 7. Модульное тестирование прилож ений MVC 189
Мы определили фиктивное хранилище , которое содержит только объекты Product
со значениями свойства Price меньше 50, и применили его в новом тесте. Запустив
этот тест, вы увидите, что он проходит, подкрепляя идею о том , что пробл ема связана
с применением метода Where () в методе действия Index () .
В реальном проекте выяснение причины отказа теста означает необходимость со­
гласования цели теста со спецификацией для прилож ения. Вполне может оказаться,
что метод Index () обязан фильтровать объекты Product по свойству Price , в случае
чего тест нуждается в пересмотре. Это распространенный итог, и тест, который не
прошел, вовсе не всегда указывает на присутствие реальной проблемы в приложении.
С другой стороны, если метод действия Index () не должен фильтровать объекты мо­
дели, тогда потребуется внести корректирующую правку (листинг 7.16).

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

В этой главе я придерживаюсь наиболее часто используемого стиля модульного тестиро­


вания, при котором мы пишем функцию приложения и затем тестируем ее, чтобы удосто­
вериться в том, что она работает требуемым образом. Такой стиль популярен, поскольку
большинство разработчиков думают сначала о прикладном коде, а затем о его тестирова­
нии (я определенно подпадаю в эту категорию).
Проблема такого подхода в том, что он склоняет к созданию модульных тестов только для
того прикладного кода, который было трудно писать или серьезно отлаживать , оставляя ряд
аспектов каких-то функций лишь частично протестированными или вообще без тестирования .

Альтернативным подходом является разработка через тестирование (Test-Driveп


Developmeпt - TDD) . Существует много вариаций TDD, но основная идея в том, что тесты
для функции пишутся до того, как она реализуется. Написание тестов первыми заставля ­
ет более тщательно думать о реализуемой спецификации и о том, как узнать, что функция
была реализована корректно . Вместо погружения в детали реализации подход TDD застав­
ляет заранее продумывать, чем будет измеряться успех или неудача.
Все создаваемые тесты изначально не будут проходить, т.к. новая функция еще не реали­
зована . Однако по мере добавления кода в приложение тесты постепенно будут переходить
из красного состояния в зеленое , и ко времени завершения функции все тесты окажутся
пройденными. Подход TDD требует дисциплины, но приводит к получению более полного
набора тестов и может дать в результате более надежный и устойчивый код.

Листинг 7. 16. Удаление фильтрации посредством LINQ в файле HomeController. cs


using Microsoft . AspNe tC or e.Mvc;
using WorkingWithVisualStudio . Models ;
using System . Linq ;
namespace WorkingWithVisualStudio . Controllers
puЫic class HomeController : Controller {
puЫic IRepository Repository = SimpleRepository .SharedRepository ;
puЬlic IActionResul t Index () => View (Reposi tory. Products) ;

[HttpGet]
puЫic IActionResult AddProduct() => View( new Product() ) ;
[HttpPostJ
puЫic IActionResult AddProduct(Product р) {
Repository . AddProduct(p) ;
return RedirectToAction( " Index ");
190 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

Запустив тесты снова, вы увидите, что все они проходят (рис . 7.6).

Р·

Run All 1 Run". • Playlist : All Tests •


А Passed Tests (4)
О Tests.HomeControlle rTests.lnd~xActionModell sComplete 1 ms
О Tests.HomeControllerTe.sts.lr1dexActionModellsComplete... 36 ms
() Tests . ProductTests.CanChangeProduct№me 1 ms
О Tests.ProductTests.CanCha ngeProductPrice 9ms

Summary
l ast Test Run Passed (Тotal Run Time 0:00:04)
О 4 Tesls Passed

Рис. 7.6. Прохождение всех тестов

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

Улучшение модульных тестов


В предыдущем разделе был представлен базовый подход к написанию модульных
тестов и прогону тестов в Visual Studio, а также акцентировано внимание на важ­
ности изоляции тестируемого компонента. В этом разделе рассматриваются расши­
ренные инструменты и средства, которые можно применять для написания тестов

более согласованным и выразительным образом. Если вы сильно увлечетесь модуль­


ным тестированием, то можете в итоге получить настолько много тестового кода, что

его ясность становится очень важной, особенно с учетом необходимости пересмотра


тестов для отражения изменений в приложении, вносимых во время разработки и
сопровождения.

Параметризация модульного теста


Тесты, написанные для класса HomeC o n trol l er, выявили проблему, которая
присутствовала только для определенных значений данных. Чтобы проверить это ус­
ловие , были созданы два похожих теста, каждый из которых содержал собственное
фиктивное хранилище. При таком подходе появляется дублированный код. особен­
но учитывая то, что единственное отличие между двумя тестами связано с набором
значений decimal, которые указываются для свойства Price объектов Product в
фиктивных хранилищах.
Глава 7. Модульное тестирование nриложений MVC 191
Инфраструктура xUnit.net предлагает поддержку параметризованных тестов,
когда данные, используемые в тесте, из него удаляются, так что единственный ме­
тод может применяться для множества тестов. В листинге 7.17 с помощью средс­
тва параметризованных тестов устраняется дублированный код в тестах для класса
HorneController.

Листинг 7.17. Параметризация модульного теста в файле HorneControllerTests. cs


using Microsoft . AspNetCore . Mvc ;
using System.Collections .Gene ric;
using WorkingWithVisualStudio.Controllers ;
using WorkingWithVisualStudio . Models;
using Xunit ;
namespace WorkingWithVisualStudio . Tests
puЫic class HomeControllerTests {

class ModelCompleteFakeRepository : IRepository


puЫic IEnumeraЫe<Product> Products { get; set;
puЫic void AddProduct(Product р) {
11 ничего не делать- для теста не требуется

[Theory]
[InlineData(275, 48.95, 19.50, 24.95)]
[InlineData(5, 48.95, 19.50, 24.95)]
puЫic void IndexActionМodelisComplete(decimal pricel, decimal price2,
decimal priceЗ, decimal price4)

11 Организация
var controller = new HomeController();
controller.Repository = new ModelCompleteFakeRepository
Products = new Product [] {
new Product {Name "Pl", Price = pricel } ,
new Product {Name "Р2", Price = price2 } ,
new Product {Name РЗ , Price = priceЗ } ,
11 11

new Product {Narne = "Р4", Price = price4 } ,

} ;
11 Действие
var model = (control ler.Index() as ViewResult)? . ViewData.Model
as IEnurneraЫe<Product> ;
11 Утверждение
Assert . Equal(controller . Repository .Products , model ,
Comparer .Get <Product>( (pl, р2) => pl.Name == p2.Name
&& pl . Price == р2 . Price));

Параметризованные модульные тесты помечаются атрибутом Theory вместо ат­


рибута Fact, который используется для стандартных тестов. Здесь таюке применя­
ется атрибут InlineData, который позволяет указывать значения для аргументов,
192 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

определяемых методом модульного теста. Язык С# ограничивает способ выражения


значений данных в атрибутах, поэтому мы определили четыре аргумента decimal
тестового метода и посредством атрибута InlineData предоставили для них значе­
ния. Значения decimal внутри тестового метода используются для генерации масси­
ва объектов Product, который применяется для установки свойства Products объ­
екта фиктивного хранилища.
Каждый атрибут Inline определяет отдельный модульный тест, который пока­
зан как индивидуальный элемент в окне Test Explorer среды Visual Studio (рис . 7.7) .
Запись в окне Test Explorer отображает значения. которые будут использоваться для
аргументов метода модульного теста.

T~t Explortr • 1:) х

Р·

PJJn AU 1 !Wn.• • 1 Playlis t : All Te;ts •


А Passed Tбis (4) Summa ry
last Tes:t Run Passed (Тot31 Run Т
t) 4 Tests Ptssed
О Tests.Pre>Cluct e-sts.CanCh.a1'9eProductNome 2 rns
$ Tests.Prod1.1ctT~tsC~ Cl'\an.g~ProductPricl!! 8ms

Рис. 7.7. Параметризованные тесты в окне Test Explorer среды Visual Studio

Получение тестовых данных из метода или свойства


Ограничения, налагаемые на выражение данных в атрибутах, сокращает полез­
ность атрибута I n 1 i n е Da t а. Альтернативный подход предусматривает создание
статического метода или свойства, возвращающего объект, который требуется для
тестирования. В такой ситуации нет никаких ограничений относительно того, как
определяются данные, и можно создавать более широкий диапазон тестовых значе­
ний. Чтобы продемонстрировать подход в работе, добавим в проект модульного тес­
тирования файл класса по имени ProductTestData . cs с определением , показанным
в листинге 7.18.

Листинг 7.18. Содержимое файла ProductTestData. cs из проекта


WorkingWithVisualStudio.Tests
using System . Collections;
using System.Collections.Generic;
using WorkingWithVisualStudio . Models;
namespace WorkingWithVisualStudio.Tests
puЫic class Produ c tTestData : IEnumeraЫe<object[]>

puЫic IEnumerator<object[]> GetEnumerator() {


yield return new object[] { GetPricesUnder50() };
yield return new object[] { GetPricesOverSO };
Глава 7. Модульное тестирование приложений MVC 193
IEnumerator IEnumeraЫe . GetEnumerator()
return this.GetEnumerator() ;

private IEnumeraЫe<Product> GetPricesUnder50()


decimal[] prices = new decimal[] { 275, 49 . 95М , 19.SOM, 24 . 95М };
for (int i =О ; i < prices . Length ; i++) {
yield return new Product { Name = $"P{i + 1}", Price = prices[i] };

private Product [ ] GetPricesOver50 => new Product[ ]


ne w Product { Name "Pl" , Price 5 },
new Product { Name " Р2", Price 48 . 95М } ,
new Product { Name " РЗ" , Price 19 . SOM } ,
new Product { Name "Р4", Price 24 . 95М }
};

Тестовые данные пр едоставляются через класс, который реализует интерфейс


IEnumeraЫe < obj ect [] >, возвращающий последовательность массивов объектов.
Каждый массив объектов в посл едовательн ости содержит один набор аргументов,
которые будут передаваться тестовому методу. Мы п е реопределим тестовый метод,
чтобы он принимал массив объектов Product, который добавит к тестовым данным
еще один уровень - перечисление массивов объектов Product. Такая глубина струк­
туры тестовых данных может сбивать с толку, но важно понимать, что инач е т есты
не будут работать, когда количество аргументов, которые xUпit.net пытается п е редать
тестовому методу, не соответствует сигнатуре метода.

Мне нравится имеющаяся структура классов тестовых данных, когда закрытые


методы или свойства определяют индивидуальные наборы тестовых данных, кото­
рые затем с помощью метода GetEnumerator () объединяются в последовател ьности
массивов объектов. Для иллюстрации различных при ем ов массивы объектов Product
были созданы с применением и метода, и свойства, но я пр едпочитаю использовать в
своих про ектах один подход (н а его выбор влия ет вид данных, с ноторыми проводится
тестирование). В листинге 7.19 показано, как можно прим енять класс тестовых дан­
ных с ат рибутом Theory для настройки тестов.

Совет. Если вы хотите включить тестовые данные в тот же самый класс, который определяет
модульные тесты, тогда можете использовать атрибут MemberData вместо ClassData.
Атрибут MemЬerData конфигурируется с применением строки, указывающей имя стати­
ческого метода, который будет предоставлять реализацию IEnumeraЫe<object [ ] >,
где каждый массив объектов в последовательности является набором аргументов для тес­
тового метода.

Листинг 7.19. Использование класса тестовых данных в файле


HomeControllerTests.cs
using Microsoft . AspNetCore . Mvc ;
using System . Collections . Generic ;
using WorkingWithVisualStudio . Controllers ;
using WorkingWithVisualStudio . Models;
using Xunit ;
194 Ча сть 1. Введение в инфраструктуру ASP.NET Соге MVC

namespace WorkingWithVisualStudio . Tests {


puЬlic class HomeControllerTests {

class ModelCompleteFakeRepository : IRepository


puЫic IEnumeraЫe<Product> Products { get; set;
puЫic void AddProduct(Product р) {
11 ничего неделать - для теста не требуется

[T h eory ]
[ClassData(typeof(ProductTestData))]
puЫic void IndexActionМodelisComplete (Product [] products) {
11 Организация
var controller = new HomeController();
controller . Repository = new ModelCompleteFakeRepository
Products = products
};
11 Действие
var model = (contro ll er . Index() as ViewResu l t)? . ViewData . Model
as IEnumeraЫe<Product> ;
11 Утверждение
Assert.Equal(controller . Repository . Products , model ,
Comparer . Get<Product>( (pl , р2) => pl . Name = = p2 . Name
&& pl . Price = = p2.Price)) ;

Атрибут ClassData конфигурируется с типом класса тестовых данных, которым в


рассматриваемом случае является ProductTestData. Во время выполнения тестов ин­
фраструктура xUnit.net создаст новый экземпляр класса ProductTestData, после чего
будет применять его для получения последовательности тестовых данных для теста.

На заметку! Если вы посмотрите на список тестов в окне Test Explorer, то увидите, что для
тестов IndexActi onModelisComp lete предусмотрена единственная запись, хотя
класс ProductTestData предоставляет два набора тестовых данны х . Так происходит в
ситуации , когда объекты тестовых данных не удается сериализировать, и проблему можно
решить, применив к тестовым объектам атрибут SerializaЫe.

Улучшение фиктивных реализаций


Эффективная изоляция компонентов требует фиктивных реализаций классов, что­
бы предоставить тестовые данные или проверить, ведет ли себя компонент так, как
должен. В предшествующих примерах создавался класс, реализующий интерфейс
IReposi tory . Это мог быть эффективный подход, но он приводил к созданию клас­
сов реализаций для каждого вида теста, который требовалось запускать. В качестве
примера в листинге 7.20 демонстрируется добавление теста, который проверяет, что
метод действия Index () вызывает метод Products () в хранилище толыю один раз .
(Такая разновидность теста распространена, когда имеется опасение , что компонент
делает дублирующиеся запросы 1< хранилищу, становясь причиной множества запро­
сов к базе данных. )
Глава 7. Модульное тестирование приложений MVC 195
Листинг 7.20. Доба вление модульного теста в файле HomeControllerTests. cs
using Microsoft.AspNetCore . Mvc ;
using System . Collections .Gener ic ;
using WorkingWithVisualStudio . Contro llers;
using WorkingWithVisualStudi o . Models ;
using Xunit ;
using System ;
namespace WorkingWithVisualStudio . Tests
puЫic class HomeControllerTests {

class ModelCompleteFakeRepository : IRepository


puЫic IEnumera Ы e<Product> Pr od ucts { get ; set ;
puЫic void AddP r oduct(Product р) {
11 ничего не делать - для теста не требуется

[Theory]
[ClassData(typeof(ProductTestData))]
puЫic void IndexActionModelisComplete(Product[J products) {
11 Организация
var controller = new HomeController();
controller . Repository = new ModelCompleteFakeRepository
Products = products
) ;

11 Действие
var model = (control ler.Inde x() as Vi ewResult)? . ViewData . Model
as IEnumeraЫe<Product> ;
11 Утверждение
Assert . Equal(control l er . Repository . Products , model,
Comparer . Get<Product>( (pl , р2) => pl.Name == p2 . Name
&& pl . Price == p2 .Price)) ;

class PropertyOnceFakeRepository IRepository


puЫic int PropertyCounter { get; set; } = О;
puЫic IEnumeraЫe<Product> Products {
get {
PropertyCounter++;
return ne w [] { new Product { Name 11
Pl 11 , Price 100 } } ;

puЬlic void AddProduct(Product р)


// ничего не делать - для теста не требуется

[Fact]
puЬlic void RepositoryPropertyCalledOnce() {
/ / Организация
var repo = new PropertyOnce FakeRepository();
var controller = new HomeController { Repository = repo };
196 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

/ / Действие
var resul t = controller. Index () ;
/ / Утверждение
Assert.Equal(l, repo.PropertyCounter);
}

Фиктивные реализации не всегда являются простыми источниками данных; они


также могут использоваться для оценки способа, которым компоненты выполняют
свою работу. В этом случае было добавлено простое свойство счетчика, которое уве­
личивается каждый раз, когда читается свойство Products фиктивного хранилища,
и с помощью метода Assert. Equal () выполнена проверка , что обращение 1< свойс­
тву производилось только один раз.

Добавление инфраструктуры имитации


Создание фиктивных объектов подобного рода выходит из-под контроля, и лучший
способ вернуть себе контроль - применить инфраструктуру фиктивных объектов,
также называемую ин.фраструктурой имитации. (Суще ствует формальная разница
между фиктивными и имитированными объектами, но для облегчения использования
современные инструменты тестирования размывают ее , поэтому данные термины бу­
дут применяться взаимозаменяемо.) В настоящей главе используется инфраструктура
Moq, которая описана в табл. 7.4.
Таблица 7 .4. Помещение Moq в контекст

Вопрос Ответ

Что это такое? Moq - это программный пакет для создания фиктивных реали­
заций компонентов в приложении

Чем она полезна? Инфраструктура имитации облегчает создание фиктивных ком­


понентов для изоляции частей приложения в целях модульного
тестирования

Как она используется? Moq применяет лямбда-выражения для определения функцио­


нальности фиктивных компонентов и требует определения толь­
ко тех средств, которые используются при тестировании

Существуют ли какие-то Привыкание к синтаксису может потребовать некоторых усилий.


скрытые ловушки или Документация и примеры находятся по адресу
ограничения? https : //githuЬ.com/Moq/moq4

Существуют ли Доступно несколько альтернативны х инфраструктур, в том числе


альтернативы? NSubstitute (http : / /nsubstitute . githuЬ . io ) и FakeltEasy
(http : / /fakeiteasy . github. io). Все эти инфраструктуры
предлагают похожие возможности, и выбор между ними связан
только с предпочитаемым вами синтаксисом

Изменилась ли она по Применение инфраструктуры имитации специфично для модуль­


сравнению с версией ного тестирования и не является требованием ASP.NET Core MVC
MVC 5?
Глава 7. Модульное тестирование приложений MVC 197
При написании этой главы платформа .NЕТ Core находилась на ранней стадии
своей адаптации, и основные пакеты имитации пока еще не поддерживали ее.
В Microsoft создали специальное ответвление проекта Moq и перенесли его для ра -
боты с . NЕТ Core, так что именно этот пакет будет использоваться в книге . Тем не
менее, поскольку это не главный выпуск Moq. требуется дополнительный шаг для
конфигурирования NuGet с целью загрузки пакета Microsoft.
Выберите в Visual Studio пункт меню Tools~Options (Сервис<=:>Параметры) и в от­
рывшемся диалоговом окн е перейдите в раздел NuGet Package Manager~ Package
Sources (Диспетчер пакетов NuGеt<=:>Источники пакетов), как по1шзано на рис . 7.8.
Здесь можно сконфигурировать источники пакетов .

Sear<h Optons (Ctrl • E)

1 ~ En\rironment
1 ~ Projocts ond Solution<
1 t> Sошсе Control 0 api.nuget.org
1 ~ Т oxt Ed~or https:/iopi.nuget.org/v3/ indexJ<on
t• Debugging
~ Pelfo1n11nco Т ools
1 ~ D•tabase Т ools
Graphks Di~gnostic_'5
1
1>
А NuGet Packoge M•noger
. Generol
1 P.aclc_гge-S9urCe:s-
~ SQl Server Т ools
Machine·wide pockoge

J
~ Te>:t Templating
t> Tools for Apache Cordovв Mic rosoft and . NЕТ
1 0
~ Web https://v1Vм.nu9et.org/api/v2/cural<d·feeds/n1ic rosoftdotneV
1> W~b Forms Designer 0 Microsoft Vtsual Studio Offline Packages
~ Web Performanco Т est Т ools C:\Proarom Files (xSб)\Microsoft SOКs\NuGetPockoaes\
11 1> Windows Forms D esig neг
~ 'Norkflow Designer !':{omo: /nuget.org

~ XAML Oesigntt ~ourc'"J~~~~~2_1__ _ _ _ _ _ l[J [№р~


1

l----------------------------~~~ё;~_:~_,
Рис. 7 .8. Конфигурирование источников пакетов NuGet

Щелкните на кнопке с изображением знака "плюс" зеленого цвета и введите в по­


лях Name (Имя) и Source (Источник) детальную информацию, описанную в табл. 7.5.
Таблица 7.5. Настройки, требуемые для источника пакетов NuGet

Поле Значение

Name ASP . NET Contrib


Source https : //www . myget . org/F/aspnet - contriЬ/api/vЗ/index .j son

Щелкните на кнопке Update (Обновить), чтобы задействовать значения, введенные


в полях, и затем на Rнопке ОК для закрытия диалогового окна настроек.

Совет. Применение версии Moq от Microsoft является краткосрочной мерой; вам не придется
ее использовать после того, как в результате основных усилий по разработке добавится
помержка .NET Core. Когда это произойдет, вы сможете следовать инструкциям по уста­
новке Moq, доступным по адресу h t tp : / / g i thub. com/moq/moq4.
198 Часть 1. Введение в инфраструктуру ASP.NEТ Саге MVC

В листинге 7.21 в файл proj ec t. j so n проекта модульного т естирования до­


бавляется в е рсия Moq от Microsoft (известн ая как moq . netcore) н а ряду с пакетом
System . Diagn o s tics . Trace So ur ce, от которого з ависит moq . netcore .

Листинг 7 .21. Добавление Moq в файл proj ect. j son проекта


WorkingWithVisualStudio.Tests

" ve r sion": " 1 . 0 . 0 - * ",


" tes t Runner " : " xuni t ",
" dependencies ": {
" Microso f t. NET Core . Ap p ":
"t ype ": " p l at f o r m",
" version ": " 1 . 0 . 0 "
} 1

" xu ni t ": " 2 . 1 . 0 ",


" dot n et - test - x uni t ": " 2 . 2 . 0 - p r eview2 -bui ld1029 ",
" Wo r k i ngW i thVis u alStud i o ": "1. 0 . 0 ",
"rnoq.netcore": "4. 4. О-Ьеtа8",
"Systern.Diagnostics . TraceSource": "4 . 0.0"
} 1

" f r amewor ks": {


" netcoreappl . 0 ":
" i mport s": [ " dotnetS. 6 ", " portaЬle - net45 + wi n 8 " ]

Создание имитированного объекта


Создание имитированного объ екта озн ачает необходимость сообщения Moq о том,
какого вида объ ект вас интересует, конфигурирование его поведения и примен е ни е
этого объекта к те стируемой сущности. В листинге 7.22 инфрастру~<тура Moq исполь­
зуется для з ам ены двух фиктивных хранилищ в тестах для кл асс а HomeCon t r o ller.

Листинг 7.22. Применение имитированных объектов в файле


HomeControllerTests.cs
using Microsoft . AspNetCore . Mvc ;
using System. Collections . Generic ;
using WorkingWithVi s ua l St udio . Controllers ;
u sing WorkingWithVi sualSt ud io . Mode l s ;
using Xunit ;
using System ;
using Moq;
names p ace Wo rkin gW i thVis u a l S tu d i o .Test s
puЫ i c class HomeControl l e r Tests {

[Theory ]
[Cl assDa t a( t ypeo f(P ro du ct Te stDa t a ) ) ]
puЫic void In d e xActionModelisComplete(Produ ct[] p r oducts) {
11 Орга н изация
var rnock = new Mock<IReposi tory> () ;
Глава 7. Модульное тестирование прило же ний MVC 199
mock.SetupGet(m => m.Products) .Returns(products) ;
var controller = new HomeController { Repository = mock.Object };
11 Действие
var model = (controller.Index() as ViewResult)? . ViewData . Model
as IEnumeraЬle<Produc t>;
11 Утверждение
Assert.Equal(controller.Repository . Products , model,
Comparer . Get<Product>( (pl, р2) => pl . Name == p2 . Name
&& p l. Price == р2. Price)) ;

[Fact)
puЫic void Reposito ryPropertyCalledOnce()
11 Организация
var mock = new Mock<IRepository>();
mock.SetupGet(m => m.Products)
. Returns (new [] { new Product { Name = "Pl", Price = 100 } } ) ;
var controller = new HomeController { Repository = mock.Object };
11 Действие
var result = contro ller.Index();
11 Утверждение
mock.VerifyGet(m => m.Products, Times.Once);

Использование Moq позволило удалить фиктивные реализации интерфейса


IRepository и заменить их всего несколькими строками кода. Здесь не будут рас­
сматриваться детальные сведения о разных поддерживаемых Moq средствах. но даны
объяснения способа применения Moq в примерах. Примеры и документация по Moq
доступны по адресу https : //github . com/Moq/moq4 . (При объяснении модульного
тестирования различных типов компонентов МVС в оставшихся главах книги также
будут приводиться соответствующие примеры . )
Первым делом создается новый объект Mock с указанием интерфейса, который
должен быть реализован:

var mock = new Mock< IRepository> () ;

Созданный объект Mock будет имитировать интерфейс IReposi tory. Далее оп­
ределяется функциональность, которая требуется для теста. В отличие от обычной
реализации интерфейса классом для имитированного объекта конфигурируется толь­
ко поведение, нужное для теста. В первом имитированном хранилище понадобится
реализовать свойство Products, чтобы оно возвращало набор объектов Product, ко­
торый передается тестовому методу через атрибут ClassData:

mock . SetupGet(m => m. Products) .Returns(products) ;

Метод SetupGet () используется для реализации средства извлечения для свойс­


тва. Аргументом этого метода является лямбда-выражение, которое указывает подле-
200 Часть 1. Введение в инфраструктуру ASP.NET Саге MVC

жащее реализации свойство (Products в данном примере). Метод Returns () вызы­


вается на результате, полученном из метода SetupGet (), чтобы указать результат,

который будет возвращаться, I<огда читается значение свойства. Для второго имити­
рованного хранилища применяется тот же самый подход. но указывается фиксиро­
ванное значение:

mock . SetupGet(m => m.Products)


. Returns (new [] { new Product { Name = "Pl", Price = 100 } }) ;

В классе Mock определено свойство Object, возвращающее объект, который ре­


ализует указанный интерфейс и обладает ранее определенным пов едением. В обоих
модульных тестах свойство Obj ect используется, чтобы получить хранилище для
конфигурирования контроллера:

var controller = new HomeControl l er Repository = mock.Object };

Последним примененным средством Moq была проверка того, что к свойству


Products осуществлялось толыщ одно обращение:

mock . VerifyGet(m => m.P roducts , Times . Once) ;

Метод Veri f yGet () относится к тем методам класса Mock , которые инспекти­
руют состояние имитированного объекта, когда тест завершен. В этом случае ме­
тод VerifyGet () позволяет проверить, сколько раз читалось свойство Products .
Значение Times . Once указывает, что метод VerifyGet () должен генерировать ис­
ключение, если свойство читалось не в точности один раз, и это приведет к тому, что
тест не пройдет. (Методы класса Assert , обычно используемые в тестах, генерируют
исключение, когда тест не проходит, и потому при работе с имитированными объ ек­
тами метод VerifyGet (} можно применять для замены метода класса Assert .)

Резюме
Большая часть этой главы была сконцентрирована на модульном тестировании,
которое может оказаться мощным инструментом для повышения качества кода.

Модульное те стирование не подойдет абсолютно каждому разработчику, но с ним по­


лезно поэксперим ентировать, и оно может быть полез ным, даже есл и используется
только для сложных функций или обнаружения проблем. Бьшо описано прим енени е
инфраструктуры тестирования xUnit.net, объяснена важность изоляции компонентов
для целей тестирования и продемонстрированы некоторые инструменты и при е мы,
позволяющие упростить 1юд модульных тестов. В следующей главе начнется разра­
ботка более реалистичного приложения МVС, чтобы показать, как работают вместе
различные функциональные компоненты , прежде чем погружаться в характерные де­
тали в части II настоящей книги .
ГЛАВА 8
SportsStore:
реальное приложение

в предшествующихMVC,
главах мы создавали очень простое приложение МVС. Был
описан паттерн основные средства языка С#, а также инструменты, не­
обходимые профессиональным разработчикам приложенийMVC. Наступило время
собрать все вместе и построить несложное, но реалистичное приложение электрон­
ной коммерции.
Наше приложение под названием SportsStore будет следовать классическому под­
ходу, который повсеместно используется в онлайновых магазинах . Мы создадим он­
лайновый каталог товаров, который потребители могут просматривать по категори­
ям и страницам, корзину для покупок, куда пользователи могут добавлять и удалять
товары, и форму оплаты, где потребители м огут вводить сведения, связанные с до­
ставкой. Кроме того, мы создадим административную область, которая включает в
себя средства создания, чтения, обновления и удаления (create, read, update, delete -
CRUD) для управления каталогом товаров, и защитим ее так, чтобы изменения могли
вносить только зарегистрированны е администраторы.

Цел ь этой и последУющих глав - дать вам возможность увидеть, на что похожа
реальная разработка приложений MVC, за счет создания примера приложения, кото­
рый максимально приближен к реальности. Разумеет ся, мы будем ориентироваться
на ASP.NET Core MVC, поэтому интеграция с внешними системами, такими как база
данных, предельно упрощена, а определенные части приложения, например, обработ­
ка платежей , вообще отброшены.
Построение всех уровней необходимой инфраструктуры может показаться не­
сколько медленным, но первоначальные трудозатраты при разработке приложения
MVC окупаются, обеспечивая удобный в сопровожде нии, расширяемый и хорошо
структурированный код с великолепной поддержкой модУльного тестирования.

Модульное тестирование

Я уже достаточно много говорил о легкости проведения модульного тестирования в MVC, а


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

средствами MVC.
202 Часть 1. Введение в инфраструктуру ASP.NET Саге MVC

Я понимаю, что мое мнение не является единственно верным. Если вы не хотите прибе­
гать к модульному тестированию, то меня это вполне устроит. Таким образом, когда что­
то относится исключительно к тестированию, оно будет помещаться во врезку, подобную
настоящей. Если модульное тестирование вас не интересует, то можете смело пропускать
эти врезки, и приложение SportsStore будет работать не менее успешно. Чтобы восполь­
зоваться преимуществами технологии ASP.NET Саге MVC, вовсе не обязательно проводить
какое-либо модульное тестирование, хотя поддержка тестирования, конечно же, является
основной причиной перехода на ASP.NET Саге MVC.

Большинству средств MVC, используемых в приложении SportsStore, посвящены


отдельные главы далее в книге. Вместо того чтобы повторять весь материал , я сооб­
щаю ровно столько, сколько требуется для понимания этого примера приложения, и
указываю главу, в которой можно почерпнуть более подробные сведения.
Каждый шаг, необходимый для построения приложения, будет выделен, что поз­
волит понять, каким образом средства MVC сочетаются друг с другом. Вы должны
уделить особое внимание созданию представлений . Если вы не будете в точности сле­
довать примерам, то получите несколько странные результаты.

На заметку! В Microsoft заявили , что в следующей версии Visual Studio изменят инстру ­
ментарий, применяемый для создания приложений ASP.NET Core MVC. Проверяйте
веб-сайт издательства на предмет обновлений, которые появятся после выпуска новых
инструментов.

Начало работы
Если вы планируете писать код приложения SportsStore на своем компьютере
во время изучения материала этой части книги, то вам придется установить Visual
Studio и удостовериться в том, что установлен вариант LocalDB, который требуется
для постоянного хранения данных.

На заметку! Если вы просто хотите работать с проектом, не воссоздавая его, тогда може­
те загрузить готовый проект SportsStore как часть загружаемого кода примеров для на­
стоящей книги, который доступен на веб-сайте издательства. Разумеется, вы вовсе не
обязаны повторять все действия. Я старался делать снимки экрана и листинги кода мак­
симально простыми в отслеживании на тот случай, если вы читаете эту книгу в поезде ,
кафе или где-то еще.

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


Мы будем следовать тому же самому базовому подходу, который использовал­
ся в предшествующих главах и заключается в том, чтобы начать с пустого проек ­
та и добавлять в него все необходимые конфигурационные файлы и компоненты .
Выберите в меню File (Файл) среды
Visual Studio пункт NewqProject (СоздатьqПроект)
и укажите шаблон проекта ASP.NET Саге Web Application (.NET Саге) (Веб-приложение
ASP.NET Core (.NET Core)), как показано на рис. 8.1. В качестве имени проекта введите
Spo r ts Sto r e и щелкните на кнопке ОК.
Глава 8. SportsStore: реальное приложение 203

"'1emp14tts
4
m ASP.NEТ V/eb Applicdtion (.NfТ FramбVork) Visu.:il (# ТУР": ;
Proj«
"' Vi~ual С '*
"Windo~

i.'!•l>•
11 @ ASP.NEТ Core Wtb Applkation (.NЕТ frameowork}
Corea: ,
ondO~

9Appl~
.NfТ Core 0 Ad~
дndroid Optt
Cloud uУф

f!. Online

SportsS-tore
~ c:\p_r~~ct~ ~.
Solution name: ,Sport sSl:ore

Рис. 8. 1. Выбор типа проекта

Выберите шаблон Empty (Пустой), как проиллюстрировано на рис. 8.2, и щелкните


на кнопке ОК, чтобы создать проект Sp or ts Store .

S•l•d • l•mplat<:
1
г~;;:; C:reT;,;; i"- - - ---·-··------1 An empty project template for crtating "" ASP.NП Со1е 1
~pp l i c.5tfo n . This; template does not h~e any content in J

li
j1
~
Web API
m
App~::i;on
1
j
~ 1

1 i 1
1! 1 1
1i ~ г~~~~-~~~c;titJ~J 1
1

11_ ------ ------- ____________" ___ 11 Authentication: No Authentkatlon


~ Mic rosoft Azure

1 G О Host in the cJoud


1
1 !,Арр Se~;, ...._..:]
ОК
1

[
--- -----· ------
' ] j_ C..nceJ 1 j
Рис. 8.2. Выбор шаблона проекта

Добавление пакетов NuGet


Шаблон проекта Empty устанавливает базовые средства ASP.NET Core, но требу­
ются дополнительные пакеты, чтобы предоставить функциональность, обязатель­
ную для приложений МVС. В листинге 8.1 приведены изменения, внесенные в файл
p r oj ect . j son, которые добавляют пакеты, необходимые для того , чтобы начать раз­
работку приложения SportsStore.
204 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

Листинг 8.1. Добавление пакетов NuGet в файле proj ect. j son

" dependencies ": {


" Microsoft . NETCore . App ":
" version ": " 1 . 0 . 0 ",
" type ": " platform "
} '
" Microsoft .AspNetCore . Diagnostics " : " 1 . О . 0 ",
" Microsoft . AspNetCore . Server .I IS i ntegration ": "1. 0 . 0 ",
" Microsof t . AspNetCore . Server . Kestrel ": " 1 . 0 . О ",
" Microsoft.Extensions . Logging . Console ": " 1.0 . 0 ",
"Мicrosoft. AspNetCore. Razor. Tools": {
"version": 11 1. О . 0-preview2-final",
"type": "Ьuild"
} '
"Мicrosoft. AspNetCore. StaticFiles": "1. О. О",
"Мicrosoft.AspNetCor e.Mvc": "1. О. 0"
) '
" tools ": {
"Мicrosoft.AspNetCore.Razor.Tools": "1.0 . 0-preview2-final 11 ,

" Microsoft . AspNetCore . Server .I ISintegration.Tools":


" 1.0.0-preview2-final "
) '
" frameworks ": {
" netcoreappl . 0 ":
" imports ": [ " dotnetS . 6 ", " portaЫe - net45+win8 " ]

) '
" buildOptions ":
" emitEntryPoint ": true ,
" preserveCompilationContext ": true
} '
" runtimeOptions ": {
" configProperties ":
" System . GC . Server " : true

}'
" puЬlishOptions ":
" include ": ["wwwroot ", " web .config " ]
) '
" scr ip ts ": {
" postpuЫish ": [ " dotnet puЫish - iis -- puЫish - folder
% puЬlish : OutputPath % -- framework % puЫish : FullTargetFramework % "
}

Пакеты, добавленные в раздел dependencies файла proj ect . j son, предостав­


ляют самую базовую функциональность, требующуюся для начала разработки при­
ложения MVC. По мере разработки приложения SportsStore будут добавляться и дру­
гие пакеты, но пакеты в разделе dependencies являются хорошей отправной точкой
(табл. 8.1).
Глава 8. SportsStore: реальное приложение 205
Кроме пакетов в разделе dependencies в листинге 8.1 показано добавление
в раздел tools файла proj ect . j son, которое конфигурирует пакет Microsof t.
AspNetCore . Razor . Tools для применения в Visual Studio. Он включает средство
IntelliSense для встроенных дескрипторных вспомогательных классов, используемых
для создания НТМL-содержимого, которое приспособлено под конфигурацию прило­
жения MVC.

Таблица 8.1. Дополнительные пакеты NuGet в файле proj ect. j son


Имя Описание

Microsoft . AspNetCore. Mvc Этот пакет содержит инфраструктуру ASP. NET Core
MVC и предоставляет доступ к основным средс­
твам , таким как контроллеры и представления Razor
Microsoft .AspNetCore. StaticFiles Этот пакет обеспечивает поддержку для обслужи­
вания статических файлов, таких как файлы изоб­
ражений , JavaScript и CSS, из папки wwwroot
Microsoft . AspNetCore. Razor . Tools Этот пакет предлагает инструментальную подде­
ржку для представлений Razor, в том числе средс­
тво lntelliSense для встроенных дескриnторных
вспомогательных классов, которые применяются в

представлениях внутри приложения SportsStore

Создание структуры папок

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


поненты, требуемые для приложения MVC: модели, контроллеры и представления.
Чтобы создать папки , описанные в табл. 8.2, щелкните правой кнопкой мыши на эле­
менте проекта SportsStore в окне Solution Explorer (элемент внутри папки src), вы ­
берите в контекстном меню пункт Addi:::>New Folder (Добавить~:::> Новая папка) и введите
имя папки. Позже потребуются дополнительные папки, но создаваемые сейчас папки
отражают главные части приложения МVС и для начала их вполне достаточно.

Таблица 8.2. Папки, требующиеся для проекта SportsStore


Имя Описание

Models Эта папка будет содержать классы моделей

Contro llers Эта папка будет содержать классы контроллеров


Views Эта папка будет содержать все, что относится к представлениям,
в том числе индивидуальные файлы Razor, файл запуска представле­
ния и файл импортирования представлений

Конфигурирование приложения
Приложение ASP.NET Core МVС полагается на несколько конфигурационных фай­
лов . Имея установл енные пакеты NuGet, понадобится отредактировать класс Startup,
чтобы сообщить ASP.NET о том. что они должны использоваться (листинг 8 .2).
206 Часть 1. Введение в инфраструктуру ASP.NET Соге MVC

Листинг 8.2. Включение средств в файле Startup. cs


using Microsoft . AspNetCore . Builder ;
using Microsoft . AspNetCore . Hosting ;
using Microsoft.AspNetCore.Http;
using Microsoft . Extensions . Dependencyinjection ;
using Microsoft .Extensions.Logging ;
namespace SportsStore {
puЫic class Startup {
puЫic void ConfigureServices(IServiceCollection services) {
services.AddМvc();

puЬl i c void Configure(IApplicationBuilder арр ,


IHostingEnvironment env , ILoggerFactory loggerFactory) {
app.UseDeveloperExceptionPage();
app.UseStatusCodePages();
app.UseStaticFiles();
app.UseМvcWithDefaultRoute();

Метод ConfigureServices применяется для настройки разделяемых объектов ,


которые могут использоваться повсеместно в приложении через средство внедрения

зависимостей, которое рассматривается в главе 18. Метод AddMvc (), вызываемый


внутри метода ConfigureServices (), является расширяющим методом, который
настраивает разделяемые объекты, применяемые в приложении MVC.
Метод Configu re () используется для настройки средств, которые получают и
обрабатывают НТГР-запросы . Каждый метод. вызываемый в методе Configure (),
представляет собой расширяющий метод, который настраивает средство обработки
НТГР-запросов (табл. 8.3).

Таблица 8.3. Начальные методы для настройки средств, вызываемые в классе Startup

Метод Описание

UseDeveloperExceptionPage() Этот расширяющий метод отображает детали исключе­


ния , которое произошло в прило ж ении , что полезно во

время процесса разработки . Он не дол жен быть вклю­


чен в развернутых приложениях; в главе 12 будет пока­
зано , как отключить данное средство

UseStatusCodePages() Этот расширяющий метод добавляет простое сообще ­


ние в НТТР-ответы, которые иначе бы не имели тела,
такие как ответы 404 - Not Found (404 - не найдено)

UseStat i cFiles() Этот расширяющий метод включает поддержку для


обслуживания статического содержимого из папки
wwwroot
UseMvcWithDefaultRoute() Этот расширяющий метод включает инфраструктуру
ASP.NET Core MVC со стандартной конфигурацией
(которая позже в процессе разработки будет изменена)
Глава 8. SportsStore : реальное прило ж ение 207

На заметку! Класс Startup - важное средство ASP.NEТ Саге . Он будет подробно описан
в главе 14.

Дале е понадобится подготовить приложение для представлений Razor. Щелкните


правой кнопкой мыши на папке Views, выберите в контекстном меню пyнкт AddqNew
ltem (Добавить q Новый элемент) и укажите шаблон MVC View lmports Page (Страница
импортирования представлений MVC) из категории ASP.NET (рис. 8 .3).

Se.шhlnS1all.

~
1
AS!'.NEТ
Cl1enHide
l!il" MVC l•yovt P•ge
V.<w ASP.NEТ • Туре: ASP~
MVCView~
Code r:s:
12.1 MVC View Page Stм ASP.NE1
'1 Online
1 ~

~
~ z.c r Т119 H r lpe1 ASP.NEТ f
1
~· Middlew11re (iass AS P.N П
(
1 "
1
fq

~· Startup c l 11~s ASP.NE1

"1
!ti
vГJ ASP.NEТ Conliguration File д\Р.NП

1 ы.-~ (li<> he<tlo go 011!one •nd l•ndtempl1tr1


f

c-~---v~~-~=:_~~------------- J
Рис . 8.3. Создание файла импортирования представлений

Щелкните на кнопке Add (Добавить), чтобы создать файл _ Vi ew i mp orts . c s htm l


и приведите его содержимое в соответствие с листинго м 8.3.

Листинг 8.3. Содержимое файла_Viewimports. cshtml из папки Views


@using Sports Store . Models
@addTagHelper * , Microsoft . AspNetCo r e . Mvc .TagHe l pers

Оператор @using позволит применять типы и з пространства имен Spor ts St ore .


Model s в представлениях, не ссылаясь на это пространство имен. Оператор
@addTagHelper включает встроенные дескрипторны е вспомогательные классы , ко­
торые будут использоваться позже для создания элементов HTML, отражающих кон­
фигурацию прил ожения SportsStore.

Создание проекта модул ьного тестирования


Создание про екта модульного тестирования требует выполнения процесса, кото­
рый был описан в главе 7. С помощью проводника файлов создайте папку по име ­
ниte s t на том же уровне, что и существующая папка s r c, внутри папки решения
SportsStore .
Возвратит есь в Visual Studio , щелкните правой кнопкой мыши на элементе ре­
шения SportsStore (элемент верхнего уровня в окне Solution Explorer), выберите в
контекстно м меню пункт Addq New Solution Folder (Добавить q Новая папка решения)
и у становите имя новой папки в t est .
208 Часть 1. Введение в инфраструктуру ASP.NET Соге MVC

Щелкните правой кнопкой мыши на папке test в окне Solution Explorer и выбери­
те в контекстном меню пункт Add~New Project (Добавить~Новый проект). Выберите
шаблон Class Library (.NET Core) (Библиотека классов (.NET Core)) из категории
lnstalled~Visual C#~.NET Core (УстановленныеqVisuаl C#q .NET Core), как показано
на рис. 8.4, и установите имя проекта в SportsStore. Tests .

Console Applic'1 tion (.NЕТ Core} Visua\ C::


\Neb
.N_Er.~le; ASP.NEТ Core WebApplit.:! tion (.N П Core) VisuaJ (:;
Android
C1oud
E.(tensiЬility

iOS
Reporting
Silverlight
Test
WCF

t- Online

Name: S_po 11 sSto 1 ~ Tбts

Loc~tion: !c:~roj!d,!~P~~~~l~~t,.~~~t,.,.~ ~:~.~

Рис. 8.4. Создание проекта модульного тестирования

Щелкните на кнопке Browse (Обзор) и перейдите в папку test. Щелкните на кноп­


ке ОК, чтобы выбрать эту папку, и затем щелкните на ОК, чтобы создать проект мо­
дульного тестирования.

После создания проекта модульного тестирования отредактируйте содержащийся


в нем файл proj ее t . j son согласно листингу 8.4, и добавьте пакеты, необходимые
для тестирования и создания имитированных объектов.

На заметку! Пакет rnoq. ne tcore, который применяется в листинге 8.4, требует изменения
конфигурации Visual Studio, как было описано в разделе "Добавление инфраструктуры
имитации" главы 7. Если вы не прорабатывали примеры из главы 7, то должны внести это
изменение в конфигурацию прямо сейчас.

Листинг 8.4. Содержимое файла proj ect. j son из проекта модульного тестирования

"version ": "1.0.0- *",


"testRunner ": "xunit",
"dependencies": {
"Micros oft.NETCore.App":
"type ": "pl atform ",
"ver sion ": "1.0.0"
},
"xuni t": "2. 1. О ",
"dotnet -test-xunit ": " 2 . 2.0 -previ ew2 -build l02 9",
"moq.net core ": "4.4. 0-beta8 ",
Глава 8. SportsStore: реальное приложение 209
"S ystem .Diagnostics . TraceSource" : "4.0.0",
"SportsSto re ": "1. 0.0"
) 1

"frameworks ": {
"ne t coreappl.O":
"imports": ["dot net5.6", "portaЫe-net 4 5+win8"]

Проверка и запуск приложения


Solution bp lortt

~Q ~ j Ф·:< iJ' jj''"-


Проекты приложения и модульного тестирова­
ния созданы, сконфигурированы и готовы к раз­
"~~~~1.~t~~!~:~f~~_!:~>- - ~~--·-· .....-· ~-· _ ... _
Р·
__,__

"' ~~ Solution lttms


работке. Окно Solution Explorer должно содержать lJ glob•l.j•on
элементы, показанные на рис. 8.5. Если вы види­ " ._ ~1(

• iJ Sport<Store
те другие элементы или элементы расположены ~ }' Propert;e.
не в тех позициях, то возникнут проблемы, поэ­ ~ •·8 Referenc6
0 1 ~V\W/fOOt
тому уделите время проверке, что все элементы • ·• Depмdtncie:-;;
,_ Controllers
присутствуют и находятся на своих местах.
~ Models
Выбрав в меню Debug (Отладка) пункт Start " ~ Vit'\'JS
(21 _Vie"нtmports.c-;h tn1I
Debugging (Запустить отладку) или Start Without с• Progrмц s
Debugging (Запустить без отладки) , если вы пред­ ~ 6J prcject.json
,!) Project_Readme.html
почитаете итеративный стиль разработки, опи­
с• Startup.cs
санный в главе 6, вы увидите страницу ошибки v') v1eb.config
!i.. tt.st
(рис. 8.6). Сообщение об ошибке отображается
.jf

" ~S Sport.sStore.Tms
из-за того, что в настоящий момент в приложе­ flc /' Propertie\
i. •·1 Referencб
нии нет контроллеров для обработки запросов; с- Cl г~s 1 .cs.

эту проблему мы вскоре решим. ~ /J project.j•on

Начало работы с моделью Рис. 8.5. Окно Solution Explorer для


проектов приложения SportsStore
предметной области и модульного тестирования

Все проекты начинаются с модели предмет-


ной области, которая является центральной частью приложения МVС. Поскольку мы
строим приложение электронной коммерции, то наиболее очевидная модель, кото­
рая необходима, касается товара. Добавьте в папку Models файл класса по имени
Product . cs с определением, приведенным в листинге 8.5.
Листинг 8.5. Содержимое файла Product. cs из папки Models
namespace SportsStore . Models {
puЫic class Product {
puЫic int ProductID { get ; set;
puЫic string Name { get ; set; }
puЫic string Description { get ; set;
puЬlic decimal Price { get; set; }
puЫic string Category { get ; set ; }
210 Часть 1. Введение в инфраструктуру ASP.NET Соге MVC

lnt~rn"1 S e rv~r Error Х

----~~ : 1

Statu s Code : 404; Not Fou nd____ ----,


_ _ _ _ _ _ _ _J

Рис. 8.6. Выполнение приложения SportsStore

Создание хранилища
Нам нужен какой-то способ получения объектов Pr oduct из базы данных. Как
объяснялось в главе 3, модель включает в себя логику для сохранения и извлечения
данных из постоянного хранилища. Пока можно не беспокоиться о том, как будет
реализовано постоянство данных, но необходимо начать проце сс определения ин­
терфейса для него . Добавьте в папку Model s новый файл интерфейса С# по имени
I ProductRepository . cs и поместите в него определение, показанное в листинге 8.6.

Листинг 8.6. Содержимое файла IProductReposi tory. cs из папки Models


usin g Systern . Co l lect i ons . Ge ne ric ;
narne spac~ Spor ts Store . Mode ls {
puЫ ic interface IProduc t Repos i tory
IEnurnera Ы e<Product> Product s { get;

Этот инте рфейс использует I E n urneraЫe < T > , чтобы позволить вызывающе­
му коду получать последовательность объектов Product , нич его не сообщая о том,
как или где хранятся либо извлекаются данные. Класс, зависящий от интерфейса
IPr odu ctRepos i tory , может получать объекты Pro du ct, ничего не зная о том, от­
куда они поступают или каким образом класс реализации будет их доставлять. В про­
цессе разработки мы еще будем возвращаться к интерфейсу IProdu c tReposito r y,
чтобы добавлять в него нужные средства .

Создание фиктивного хранилища


Теперь, когда определен интерфейс, можно было бы р е ализовать м еханизм пос­
тоянства и привязать его к базе данных, но сначала необходимо добавить ряд дру­
гих частей прилож ения. Для этого мы создадим фиктивную реализацию интерф е йса
IP rodu ctRepos i t o ry , которая будет замещать хранилище данных до тех пор, пока
мы им не займемся . Чтобы со здать фиктивное хранилище , доб авьте в папку Models
файл класса по имени FakeProductRepo si tor y . cs и поместите в него определение,
представленное в листинге 8.7.

Листинг 8. 7. Содержимое файла FakeProductReposi tory. cs из папки Models


us i ng System. Collections . Ge ner i c ;
namespace SportsStore . Model s {
puЬl i c class FakeProductRepo si to ry I Pr oduc t Repos i tory {
Глава 8. SportsStore: реальное приложение 211
puЫic IEnumeraЫe<Produc t> Products => new List<Product>
new Product { Name "Foo tbal l", Price = 25 } ,
new Product { Name "Surf board", Price = 179 } ,
new Product { Name "Runn ing shoes", Price = 95 }
};

Класс FakeProductReposi tory реализует интерфейс IProductReposi tory, воз­


вращая фиксированную коллекцию объектов Product в качестве значения свойства
Products.

Регистрация службы хранилища


В МVС делается особый акцент на применении слабо связанных компонентов, что
означает возможность вносить изменения в одну часть приложения, не внося соответс­

твующие изменения где-то в другом месте. Такой подход трактует части приложения
как службы, которые предоставляют функциональные средства, используемые другими
частями приложения. Класс, предоставляющий службу, впоследствии может быть мо­
дифицирован или заменен, не требуя внесения изменений в классы , которые его задейс­
твуют. Более подробно это объясняется в главе 18, а в случае приложения SportsStore
необходимо создать слу;н:бу хранилища, которая позволит контроллерам получать ре­
ализующие интерфейс I ProductRep os i tory объекты, не зная, какой класс применя­
ется. В итоге появится возможность начать разработку приложения с использованием
простого класса FakeProductRepository, созданного в предыдущем разделе, и позже

заменить его реальным хранилищем, не внося изменения во все классы, которым ну;кен

доступ в хранилище. Слу;кбы регистрируются в методе ConfigureServices () класса


Startup; в листинге 8.8 определена новая службы ДJIЯ хранилища.

Листинг В.В. Создание службы хранилища в файле Startup. cs


using Microsoft . AspNetCore . Builder;
using Microsoft.AspNetCore . Hosting;
using Microsoft . AspNetCore . Http ;
using Microsoft . Extensions.Dependencyinjection ;
using Microsoft . Extensions.Logging;
using SportsStore.Models;
namespace SportsStore {
puЫic class Startup {
puЫic void Conf igureServi ces(IServiceCo llect ion services) {
services.AddTransient<IProductRepository, FakeProductRepository>();
services . AddMvc() ;

puЫic void Configure(IApplicationBuilder арр ,


IHostingEnvironment env, ILoggerFa ctory loggerFactory) {
app . UseDeveloperExceptionPage();
app.UseStatusCodePages() ;
app .Us eStaticFiles() ;
app . UseMvcWithDefaultRoute();
212 Часть 1. Введение в инфраструктуру ASP.NET Соге MVC

Добавленный в метод Conf i gur e Ser vi ce s ( ) оператор сообщает инфраструкту­


ре ASP.NET о том, что когда компоненту наподобие контроллера н е обходима р е али­
зация интерфейса IProductRepository , он а должна получить экземпляр класса
FakeProductReposi tory . Метод Ad d Tr ansi e nt () указывает, что каждый раз. когда
требуется реализация интерфейса IProd uctRepository , долже н создаваться новый
объект FakeProductRepos i tory . Не беспокойте сь, есл и смысл кода пока не особен ­
но понятен; вскоре вы увидите, как он вписывается в приложение. а детали того, что

происходит, будут представлены в главе 18.

Отображе н ие спис ка то в аров


Оставшуюся часть главы можно было бы посвятить построению модели предм ет­
ной области и хранилища, вообще не касаясь других частей приложения. Однако та ­
кой подход выглядит довольно скучным, поэтому мы пойдем другим путем и присту ­
пим к интенсивному применению MVC, вернувшись к добавлению средств модели и
хранилища , когда они понадобятся .
В этом разделе нам предстоит создать контроллер и метод действия , который мо­
жет отображать сведения о товарах в хранилище. Пока ими будут только данные из
фиктивного хранилища, но позже мы решим эту задачу. Мы также подготовим на­
чальную конфигурацию маршрутизации, чтобы инфраструктур а MVC з нал а , каким
образом сопоставлять запросы в приложении с контроллером , который будет создан.

Использование формирования шаблонов MVC в Visual Studio


Везде в книге контроллеры и представления MVC создаются за счет щелчка правой кнопкой
мыши на папке в окне Solution Explorer, выбора в контекстном меню пункта Add<=:>New ltem
(Добавить~Новый элемент) и указания шаблона элемента в открывшемся диалоговом окне
Add New ltem (Добавление нового элемента). Существует альтернативный прием , называ ­
емый формированием шаблонов (scaffolding), при котором среда Visual Studio предлагает
в контекстном меню Add (Добавить) пункты, специально предназначенные для создания
контроллеров и представлений . Выбор таких пунктов меню способствует выбору сценария
для создаваемого компонента, такого как контроллер с действиями , допускающими чтение /
запись, или представление, которое содержит форму, применяемую для создания специфи­
ческого объекта модели.
В настоящей книге формирование шаблонов не используется . Код и разметка , генериру­
емые средством формирования шаблонов, являются настолько общими, что едва ли не
бесполезны , в то время как набор поддерживаемых сценариев ограничен и не решает рас­
пространенные задачи разработки . Цель настоящей книги - не только донести до вас зна­
ния, каким образом создавать приложения MVC, но также объяснить, как все работает "за
кулисами" , и сделать это гораздо труднее, когда ответственность за создание компонентов
возлагается на средство формирования шаблонов.

Тем не менее, это еще одна ситуация, когда ваш стиль разработки может отличаться от мое­
го , и вы вполне можете предпочесть работу со средством формирования шаблонов. В таком
случае можете включить его, сделав ряд добавлений в файле p r oj ect . j son. Во-первых,
в разделе dependencies потребуется указать два новых пакета:

"depend en cies ": {


"Mic r os oft . NETCore .App ":
"v ers io n": " 1 . 0 . 0",
" type": "p l atform "
}'
Глава 8. SportsStore: реальное nрило жен и е 213
"Microsoft . AspNetCore . Di agnostics ": "1. 0 . 0",
"Microsoft . AspNetCore . Server .II Sin teg r at i o n": "1. 0 . О ",
"Microsof t. AspNetCore . Server . Kes t rel ": "1. О . О ",
"Microsoft . Extensions . Logging . Console ": " 1 . 0.0 ",
"Microsoft . AspNe t Core . Razor . Too l s ": {
"version ": "l. 0 . 0- preview2 - f i nal ",
"type ": "build "
} 1

"Microsoft . AspNetCore . Stat i c Fil es ": " 1 . О . О ",


"Microsof t. AspNetCore . Mvc ": "1. О . О ",
"Microsoft.VisualStudio.Web.CodeGeneration.Tools":
"version" : 11 1 . О. 0-preview2-final",
11
type 11 : 11 build 11
},
11
Мicrosoft.VisualStudio.Web.CodeGenerators.Mvc 11 :
11
version 11 : 11 1. О. O-preview2-final 11 ,

"type": "build"

},

Во-вторых, эти пакеты должны быть зарегистрированы в разделе tools :

" tools ": {


11
Microsoft . AspNetCore . Razor . Too l s ": "l. 0 . 0- preview2 - fina l 11 ,
"Microsoft . AspNetCore . Server . IISinteg r ation . Too l s ": " l . 0 . 0- preview2 - fina l",
"Microsoft. VisualStudio. Web. CodeGeneration. Tools 11 : {

"version 11 : 11 1 . О . O-preview2-final 11 ,
"imports 11 : [
11
portaЫe-net45+win8+dnxcore50 11
,
11
portaЫe - net45+win8"

},

После сохранения изменений и установки пакетов средой Visual Studio вы увидите новые
пункты в контекстных меню, открываемых по щелчку правой кнопкой мыши на папках в окне
Solution Explorer. Выбор этих пунктов меню будет приводить к появлению диалоговых окон,
позволяющих выбирать сценарии , которые должны применяться для создания контроллера
или представления.

Добавление контролле ра
Чтобы создать п ервый контролл ер в приложении, добавьте в папку Con t r ol lers
файл класса по им ени ProductController . c s с опр едел е нием, показанным в лис­
тинге 8.9.

Листинг 8.9. Содержимое файла ProductController. cs из папки Controllers


using Microsoft . AspNetCore . Mvc ;
using SportsStore .Mode l s ;
namespace SportsStore . Controllers
214 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

puЫic class ProductController : Controller


private IProductRepository repository;
puЫic ProductController(IProductRepository repo) {
repository = repo;

Когда инфраструктуре MVC необходимо создать новый экземпляр клас­


са ProductController для обработки НТГР-запроса, она проинспектирует
конструктор и выяснит, что он требует объекта, который реализует интерфейс
IProductRepository. Чтобы определить. какой класс реализации должен исполь­
зоваться, инфраструктура МVС обращается к конфигурации в классе Startup, ко­
торая сообщает о том, что необходимо применять класс FakeRepository, а также о
том, что каждый раз должен создаваться его новый экземпляр. Инфраструктура MVC
создает новый объект FakeReposi tory и использует его для вызова конструктора
ProductController с целью создания объекта контроллера, который будет обраба­
тывать НТТР-запрос.
Такой подход известен под названием внедрение зависимостей и позволяет объек­
туProductController получать доступ к хранилищу приложения через интерфейс
IProductRepository без необходимости в знании того, какой класс реализации был
сконфигурирован. Позже мы заменим фиктивное хранилище реальным, а благодаря
внедрению зависимостей контроллер продолжит работать безо всяких изменений.

На заметку! Некоторым разработчикам не нравится внедрение зависимостей, поскольку они


считают, что оно усложняет приложения. Я не придерживаюсь такой точки зрения, но если
вы не знакомы с внедрением зависимо стей, то рекомендую вам дождаться главы 18, пре­
жде чем п ринимать решение.

Далее добавьте метод действия по имени List (),который будет визуализировать


представление, отображающее полный список товаров из хранилища (листинг 8.1 О).
Листинг 8.10. Добавление метода действия в файле ProductController. cs

using Microsoft . AspNetCore . Mvc;


using SportsStore . Models;
namespace SportsStore.Controllers
puЫic class ProductController : Controller
private IProductRepository repository;
puЫic ProductController(IProductRepository repo) {
repository = repo;

puЬlic ViewResult List() => View(repository.Products);

Вызов метода View () подобного рода (без указания имени представления) указы ­
вает инфраструктуре MVC о том, что нужно визуализировать стандартное представ­
ление для метода действия. Передача методу View () экземпляра List<Product>
(списка объектов Product) снабжает инфраструктуру данными, которыми необходи ­
мо заполнить объект Model в строго типизированном представлении.
Глава 8. SportsStore: реальное приложение 215

Добавление и конфигурирование представления


Нам нужно создать представление для отображения содержимого пользователю,
но потребуется проделать ряд подготовительных шагов, чтобы упростить написание
представления. Сначала необходимо создать разделя емую компоновку, в которой бу­
дет определено общее содержимое, включаемое во все отправляемые клиентам НТМL­
ответы . Разделяемые компоновки - удобный способ обеспечить согласованность
представлений и наличие в них важных файлов JavaScript и таблиц стилей CSS;
их работа объяснялась в главе 5.
Создайте п апку Views /Shared и добавьте в нее новую страницу компоновки пред­
ставлений MVC по имени Layou t. cshtml, которое является стандартным именем,
назначаемым средой Visual Studio элементу такого типа. В листинге 8. 11 приведено
содержимое файла_ Layout . cshtml. В стандартное содержимое внесено одно изме­
нение , связанное с установкой внутренностей элемента ti tle в SportsStore .

Листинг 8.11. Содержимое файла _Layout. cshtml из папки Views/Shared


< ! DOCTYPE html>
<html>
<11 ead>
<meta name= "viewport " content= "width=device-width " />
<title>SportsStore</title>
</head>
<body>
<div>
@RenderBody ()
</div>
</body>
</html>

Далее пон адобится сконфигурировать прилож ение, чтобы файл _Layout . cshtml
прим енялся по ум олчанию . Это делается добавлением в папку Vie ws файла с
шаблоном MVC View Start Page (Ф айл запуска представления MVC) и именем
ViewS tar t . cshtml.
Стандартное содержимое, добавляемое Visual Studio (листинг 8.12), выбирает ком­
поновку по им ени _La yout . csh tml , которая соответствует файлу из листинга 8. 11 .

Листинг 8.12. Содержимое файла_ViewStart. cshtml из папки Views


@{
Layout " Layout ";

Теперь необходимо добавить представлени е, 1юторое будет отображаться, 1\ОГ­


да для обработки запроса используется метод действия List () . Создайте папку
Views/Product и добавьте в нее файл представления Razor по имени Li st. cshtml.
Поместите в него разметку. показанную в листинге 8. 13.
21 б Часть 1. Введение в инфраструктуру ASP.NET Соге MVC

Листинг 8.13. Содержимое файла List. cshtml из папки Views/Product


@model IEnumeraЬle<Product>
@foreach (var р in Model) {
<div>
<hЗ>@p . Name</hЗ>
@p .Description
<h4>@p .P rice .ToS tr ing( " c ") </h4>
</div>

Выражение @rnodel в начале файла указывает, что представл ение будет получать
от метода действия последовательность объектов Product в качестве данных модели.
С помощью выражения @foreach осуществляется проход по этой последовательнос ­
ти и генерация простого набора НТМL-элементов для 1шждого полученного объекта
Product .
Представлению не известно, откуда поступили объекты Product, как они были
получены или охватывают ли они все товары, известные приложению. Взамен пред­

ставление имеет дело только с тем , как отображать детали каждого объекта Product
с применением НТМL-элементов, что согласуется с принципом разделения обязаннос­
тей, который был описан в главе 3.

Совет. Значение свойства Р r i се преобразуется в строку с использованием метода


ToString ( " с "), который визуализирует числовые значения в виде денежных зна­
чений в соответствии с настройками культуры, действующими на сервере . Например ,
если в качестве настройки культуры сервера установлено en -u s, тогда вызов
( 1002 . 3) . ToString ( " с " ) возвратит значение $1 , 002. 30, но если для сервера ус­
тановлена культура en -GB, то этот же вызов возвратит значение f. 1 , 002 . 30 .

Установка стандартного маршрута


Инфраструктуре MVC понадобится сообщить, что она должна отправлять запросы,
поступающи е для корневого URL приложения (h t tp: //мой-сайт/ ), методу действия
List () класса ProductController. Это делается путем редактирования оператора
в классе Startup, который настр аивает iuraccы МVС, обрабатывающие НТТР-запросы
(листинг 8.14).
Листинг 8.14. Изменение стандартного маршрута в файле Startup. cs
using Microsoft . AspNetCore . Builder ;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions . Dependencylnjection ;
using Microsoft . Extensions . Logging ;
using SportsS tore . Models ;
namespace SportsStore {
puЫic class Startup {
puЬlic void ConfigureServices( I ServiceCollec ti on services) {
services.AddTransient<IProductRepository,
FakeProductRepository>() ;
services.AddMvc() ;
Глава 8. SportsStore: реальное приложе ние 217
puЫic void Configure(IApplicationBuilder а рр,
IHost ingEnvironment env, ILoggerFa ctory loggerFactory) {
app .Us e Develop erExc ept i on Pag e() ;
app . UseStatusCodePages();
app . UseSta t icFiles( ) ;
арр. UseМvc (routes => {
routes .мapRoute (
name: "default",
template: "{controller=Product}/{action=List}/{id?}");
}) ;

Метод Configure ( ) класса Startup применяется для настройки 1юнвейера за­


просов, состоящего из классов (известных как промежуточное программное обеспе­
чение), которые будут инспектировать НТГР-запросы и генерировать ответы. Метод
UseMvc () настраивает пром ежуточное программное обеспечение МVС, причем одним
из параметров конфигурации является схема, которая будет использоваться для со­
поставления URL с контроллерами и методами действий. Система маршрутизации
подробно рассматривается в главах 15 и 16, а пока просто следует знать, что измене­
ния, выделенные в листинге 8. 14. указывают инфраструктуре MVC на необходимость
отправки запросов методу действия List () контроллера Product, если только в URL
запроса не указано иное.

Совет. Обратите внимание, что в листинге8.14 имя контроллера указано как Pr oduct , а не
ProductController , являющееся именем класса. Это часть соглашения об именова­
нии MVC, в рамках которого имена классов обычно заканчиваются словом Controller,
но при ссылке на класс данная часть имени опускается. Соглашение об именовании и его
влияние объясняются в главе 31.

Запуск приложения
Все основные компоненты в наличии. Мы имеем контроллер с методом действия,
который MVC будет применять, когда запрашивается стандартный URL для прил о­
жения. Инфраструктура MVC создаст экземпляр класса FakeRepository, после чего
будет использовать его для создания нового объекта контроллера, обрабатывающего
запрос. Фиктивное хранилище снабдит контроллер простыми тестовыми данными,
которые его метод действия передаст представлению Razor, так что НТМL-ответ для
браузера будет включать детали каждого товара. При генерации НТМL-ответа инфра­
структура МVС объединит данные из представления, выбранные методом действия.
с содержимым разделяемой компоновки, порождая завершенный НТМL-документ,
который браузер в состоянии разобрать и отобразить. Запустив приложение, можно
увидеть результат, показанный на рис. 8. 7.
Это типовой шаблон разработки для инфраструктуры ASP.NET Core MVC.
Начальные затраты времени на необходимую настройку являются обязательными.
но затем базовые средства приложения будут собир аться очень быстро.
218 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

1·. SportsStor~ Х

f- ' С [Ф lo~host:бOOOO
1--------===----==::::::=::=.:::=--_-:::::::::::.:.. -~

Football

S25.00

JI Su1·f boa1·d

Sl79.00

l
R1шnlng slloes
$95.00
- --------- -------------------·------------
Рис. 8.7. Просмотр основной функциональности приложения

Подготовка базы данных


Мы способны отображать простое представление, содержащее сведения о товарах,
но применяем тестовые данные, которые находятся в фиктивном хранилище. Прежде
чем можно будет реализовать хранилище с реальными данными, необходимо настро ­
ить базу данных и заполнить ее данными.
Для базы данных будет использоваться SQL Server, а доступ к ней будет осущест­
вляться с применением Entity Framework Core (EF Core) - инфраструктуры объект­
но-реляционного отображения (object-relational mapplng - ORM) для Microsoft .NET.
Инфраструктура ОRМ представляет таблицы, столбцы и строки реляционной базы
данных посредством обычных объектов С#.

На заметку! Это область, где можно выбирать из широкого диапазона инструментальных


средств и технологий. Доступны не только различные реляционные базы данных, но мож­
но также работать с хранилищами объектов, хранилищами документов и рядом экзотичес­
ких альтернатив. Кроме того, существуют другие инфраструктуры ORM для .NET, каждая
из которых использует слегка отличающийся подход; такие вариации позволяют выбрать
то, что лучше всего подойдет для ваших проектов.

Инфраструктура Entity Framework Core применяется по нескольким причинам: ее


просто запустить в работу, она прекрасно интегрирована с LINQ (мне нравится ис­
пользовать LINQ) и она хорошо работает с ASP.NET Core MVC. Ранним выпускам этой
инфраструктуры были присущи мелкие недостатки, но текущие версии элегантны и
богаты возможностями.
Продукты Visual Studio и SQL Server располагают удобным средством LocalDB, ко­
торое представляет собой не требующую администрирования реализацию основной
функциональности SQL Server, специально спроектированную для разработчиков.
Благодаря этому средству можно пропускать процесс настройки базы данных на вре­
мя построения проекта , а развертывание проводить в полном экземпляре SQL Server
позже. Большинство приложений MVC развертываются в размещаемых средах, ко­
торые обслуживаются профессиональными администраторами, и наличие средства
Loca!DB означает, что конфигурирование баз данных остается в руках администрато­
ров баз данных, а разработчики приложений могут заниматься написанием кода.
Глава 8. SportsStore: реальное прило же ние 219

Совет. Если при установке Visual Studio вы не выбрали LocalDB , то придется сделать это
сейчас. Средство представляет собой часть инструментов для работы с данными или же
его можно установить как часть SQL Server.

Установка Entity Framework Core


Инфраструктура Entity Framework Core устанавливается с применением NuGet,
а в листинге 8.15 показаны требуемые добавления в раздел dependencies файла
proj ect . j son в проекте SportsStore.

Листинг 8.15. Добавление Entity Framework Core в файле proj ect. j son
внутри проекта SportsStore

"dependencies ":
"Microsoft . NETCore . App ":
"version ": "1.0 . 0 ",
"type ": "platform "
) '
"Microsoft . AspNetCore.Diagnostics ": "1. 0 . 0 ",
"Microsoft.AspNetCore . Server . I ISintegration": " 1 . 0 . 0 ",
"Microsoft . AspNetCore .Server. Kestrel ": " 1.0 . 0 ",
"Microsoft . Extensions . Logging . Con s ole ": "1. 0.0 ",
"Microsoft . AspNetCore . Razor . Tools" : (
"version ": "l.0. 0- preview2 -final ",
"type ": "build"
) '
"Microsoft . AspNetCore . StaticFiles ": " 1. О. О ",
"Microsoft . AspNetCore . Mvc ": "1. 0 .0",
"Мicrosoft.EntityFrameworkCore. SqlServer" : "1. О. 0",
"Microsoft.EntityFrameworkCore.Tools": "1.0.0-preview2-final"
) '

Базы данных управляются с использованием инструментов командной строки. ко­


торые настраиваются в разделе tools файла project . j son (листинг 8.16).

Листинг 8.16. Регистрация инструментов EF Core в файле proj ect. j son


внутри проекта SportsStore

"tools ":
"Microsoft . AspNetCore . Razor . Tools": "1. 0 . 0-preview2-final ",
"Micro sof t .AspNetCo re.Server. IISintegration.Tools ": "1. 0 .0-preview2- final",
"Мicrosoft . Enti tyFrameworkCore. Tools": {
"version": "1.0.0-preview2-final",
"imports": [ "portaЫe-net45+win8+dnxcore50", "portaЬle-net45+win8" ]

) '
220 Часть 1. Введение в инфраструктуру ASP.NET Саге MVC

После сохранения файла proj ect. j son среда Visual Studio загрузит и установит
инфраструктуру EF Core, а также добавит ее в проект.

Создание классов базы данных


Класс контекста базы данных является шлюзом между приложением и EF Core,
обеспечивая доступ к данным приложения с применением объектов моделей. Чтобы
создать класс контекста базы данных для приложения SportsStore, добавьте в папку
Models файл класса по имени ApplicationDbContext. cs с определением, приве­
денным в листинге 8 . 1 7.

Листинг 8.17. Содержимое файла ApplicationDЬContext. cs из папки Models


using Microsoft .EntityFrameworkCore ;
namespace SportsStore.Models {
puЫic class Applicat i onDbContext : DbContext {
puЫic ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base (options) {}
puЫic DbSet<Product> Products { get; set; }

Базовый класс DbContext предоставляет доступ к лежащей в основе функцио ­


налыюсти Entity Fraшework Core, а свойство Products обеспечивает доступ к объек­
там Product в базе данных. Для наполнения базы данных добавьте в папку Models
файл 1-шасса по имени SeedData. cs и поместите в него определение, показанное в

листинге 8.18.

Листинг 8.18. Содержимое файла SeedDa ta. cs из папки Model s


using System .Linq;
using Microsoft .AspNetCore.Builder;
using Microsoft . Extensions .Dependencyinjection;
namespace SportsStore.Models {
puЫic static class SeedData {
puЫic static void EnsurePopula ted(IAp plica tionBuilde r арр)
ApplicationDbContex t context = app.ApplicationServices
.GetRequiredService<ApplicationDbCon text>();
if (!context .Products .Any()) {
context.Products.AddRange(
new Product {
Name = "Kayak",
Description = "А boat for one person",
Category = "Watersports ", Price = 275 },
new Product {
Name = "Lifejacket ",
Description = "Protect ive and fashionaЫe",
Category = "Watersports ", Pric e = 48. 95m } ,
new Product {
Name = "Soccer Ball ",
Description = "FIFA-approved size and weight",
Category = "Soccer", Price = 19. 50m } ,
Глава 8. SportsStore: реальное прило ж ение 221
new Product {
Name = " Corn e r Flags ",
Descr i pt i on = "Give yo u r p l a ying f iel d а p ro f e s s ional t ouch",
Catego r y = " Socce r", Pri ce = 34. 95m } ,
new Product {
Name = " Stadi um",
Description = " Fl at - pac ked 35 , 000 - seat stadi um ",
Category = " Soc cer", Pric e = 7 9500 } ,
n e w Product {
Name = " Thinkin g С ар ",
Description = "Imp rove brai n e f fi cie n c y Ьу 7 5 %",
Category = " Ch e ss", Pr ice = 16 } ,
new Product {
Name = " Un s t eady Chair ",
Descr i ption = "S ec r etly give yo u r oppo ne nt а d is a d vanta ge",
Cate g or y = " Ch e ss", Price = 2 9. 95m } ,
ne w Produc t {
Name = " Human Ch es s Board ",
Description = " А f u n game fo r t he fa mi ly ",
Category = "Ch e s s ", Pr i ce = 75 },
new Product {
Name = " Bling - Bl ing Ki ng ",
Des c r ipt ion = "Gold-pla te d, diamond-s t udd ed King",
Ca tego ry = "Ch e ss", Pric e = 1200
}
) ;
conte x t . SaveCh anges (} ;

Статический метод Ens urePopul ated () получает аргумент типа IApp licat i o n
Bu i lder, который является нлассом, используемым в методе Con figure () нласса
Startup при регистрации классов промежуточного программного обеспечения для
обработки Н1ТР-запросов; именно здесь будет обеспечиваться наличие содержимого
в базе данных.
Метод The EnsurePopu l a t ed () получает объект Appl ic a ti o n DbCon t ex t пос­
редством интерфейса IApp lic a ti onBu i lder и применяет его для проверки , при­
сутствуют ли в базе данных какие-нибудь объекты Produc t. Если объектов нет, то
база данных наполняется с использованием коллекции объектов Product и метода
AddRange () ,после чего сохраняется с помощью метода Sav eCha n ge s () .

Создание класса хранилища


Хотя в настоящий момент может показаться иначе , но большая часть работы, тре­
буемой }J)IЯ настройки базы данных , завершена. Следующий шаг заключается в со­
здании класса, который реализует интерфейс IProdu c t Reposi tory и получает дан­
ные с применением инфраструктуры Entity Framework Core. Добавьте в папку Mod els
файл класса по имени EFPr oductRe p os i to ry. cs с определением класса хранилища,
представленным в листинге 8.19.
222 Часть 1. Введение в инфраструктуру ASP. NET Core MVC

Листинг 6.19. Содержимое файла EFProductReposi tory. cs из папки Models


using System . Co ll ect i ons . Ge neric ;
namespace Spo rts Store . Models {
puЫic clas s EFPr oductRepos i to r y : IProductRepos i tory {
pr i va t e App li cationDbContext context ;
puЫic EFP r oductRepository(Appl icat i on DbContext ctx) {
context = ctx ;

puЫ i c IEnumeraЫe<Produc t > Produ ct s => context . Products ;

По мере добавления ср едств к приложению ~тасс будет дополняться новой фун­


кциональностью, но пока что ре ализация хранилища просто отображает сво й ство
Products, определенно е в инте рфейсе IPr oductRepo s i tory, на свойство Products ,
которо е определ е но в кл асс е Appl i cat i onDbContext .

О пределение строки подключения


Строка подключе ния указывае т местоположени е и имя базы данных , а т акже
предоставля ет конфигурационные настройки, с которыми приложени е долж но под­
ключаться к серверу базы данных. Строки подключ ения хранятся в файле JSON под
названием apps ett i ng s. j son, который создан в проекте SportsSto r e с исполь зова­
нием шаблона элемента ASP.NET Configuration File (Конф и гурационный файл ASP.NET)
из раздела ASP.NET диалогового окна Add New ltem.
При создании файл а appset t ings . j son среда Visual Studlo добавля ет в н е го за ­
пол нитель для строки подключения, который необходимо привести в соотв етствие с
ЛИСТИНГОМ 8 .20.
Листинг 8.20. Редактирование строки подключения в файле appsettings. j son

" Data ": {


"Spo r tSto reP roducts ":
" Con nectionStr i ng ": "Server=( l ocaldb)\\MSSQLLoca l DB;
Database=SportsStore ; Trusted_Connection=True ; MultipleAct i veResultSets =true "
}

Внутри раздела Da ta конфигурационного файла имя строки подключ е ния уста­


навливается в SportsStoreProdu cts . Значени е элемента Conn ectionString ука­
зывает, что для базы данных по имени Sp ort s Store должно прим еняться ср едство
Loca!DB.

Совет. Строка подключения должна быть выражена в виде единственной неразрывной


строки кода, что нормально для редактора Visual Studio, но не умещается на печатной
странице и приводит к неуклю жему форматированию в листинге 8.20 . При определе­
нии строки подключения в собственном проекте удостоверьтесь, что значение элемента
Connect i onStr i ng находится в единственной строке кода.
Глава 8. SportsStore : реальное приложение 223

Конфигурирование приложения
Дал ее понадобит ся прочитать строку подключения и сконфигурировать приложе­
ние для е е исполь з ования при подключении к базе данных. Для чтения строки под­
ключения и з файла appsettings . j son требуется еще один пакет NuGet. В листин­
ге 8.21 показано из менение раздела dependencies внутри файле proj ect . j son .

Листинг 8.21. Добавление пакета в файле project. json проекта SportsStore

"dependencies ":
"Microsoft . NETCore . App ":
"version ": "1. 0 . 0 ",
" type ": "platform "
}'
"Microsoft . AspNetCore . Diagnostics ": "1. О . О ",
"Microsoft . AspNetCore . Serv er .I ISi nt egrat i on ": " 1 . 0 .0",
"Microsoft . AspNetCore . Se r ve r. Kestre l": " 1 . О . О ",
"Microsoft . Extensions . Loggi ng . Console ": " 1 . 0 . 0 ",
"Microsof t. AspNetCore . Razo r. Tools ": {
"version ": " l.0 . 0- preview2 - final ",
" type " : "build "
}'
"Microsoft . AspNetCore. Sta t icFi l es " : "1. О. О ",
"Microsoft . AspNetCore. Mvc": " 1 . О . О ",
"Microsoft . EntityFramewo r kCore . SqlServe r": " 1.0 . 0 ",
"Microsoft . EntityFrameworkCore . Tools ": " l . 0 . 0- preview2 - final ",
"Мicrosoft.Extensions.Configuration.Json": "1.0.0"
}'

Этот пакет делает возможным чтени е конфигурационных данных из файл ов JSON,


таких как appsett i ngs . j son . Чтобы применить функ циональность , предлагаемую
новы м пакетом , для чтения строки подключения из конфигурационного файла и что­
бы наст роить EF Core, в класс Startup н е обходимо внести соответствующее измене­
ние (листи нг 8.22).

Листинг 8.22. Конфигурирование приложения в файле Startup. cs


using Microsoft.AspNetCore . Builder ;
using Mic r osoft . AspNetCore . Host i ng ;
using Microsoft . AspNetCore . Http ;
using Microso f t . Extensions . Dependencyin j ect i on;
using Microsoft . Extensions . Logging ;
using Spo r tsStore . Mode l s ;
using Мicrosoft.Extensions.Configuration;
using Мicrosoft . EntityFrameworkCore;

namespace SportsStore {
puЫic clas s Startup {
IConfigurationRoot Configuration;
puЬ1ic Startup(IHostingEnvironment env) {
Configuration = new ConfigurationBuilder()
224 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json") .Build();

puЫic void Conf igu reServices(IServiceCollect ion services)


services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration["Data:SportStoreProducts:ConnectionString"]));
services.AddTransient<IProductRepository, EFProductRepository>();
services.AddMvc();

puЬlic void Configure(IApplicatio n Builder арр ,


IHostingEnvironment env , ILogger Factory loggerFactory) {
app . UseDeve l operExceptionPage() ;
app.UseStatusCodePages();
app.UseStaticFiles();
app .U seMvc(routes => {
routes.MapRoute(
name : " default ",
template : "{ controller = Product)/ {action =Li st}/{id?} " ) ;
}) ;
SeedData . EnsurePopulated(app);

Добавленный в класс Startup конструктор загружает конфигурационные на­


стройки из файла appsettings . j s on и делает их доступными через свойство по
имени Configuration. Особенности чтения и доступа к конфигурационным данным
рассматриваются в главе 14.
В метод ConfigureServices () добавле на последовательность обращений к мето­
дам, которая настраивает инфраструктуру Entity Framework Core.

services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration["Data:SportStoreProducts:ConnectionString"]));

Расширяющий метод AddDbContext () настраивает службы, предоставляемые


инфраструктурой Entity Framework Core для класса контекста базы данных. кото­
рый был создан в листинге 8.17. Как объяснялось в главе 14, многие методы, ис­
пользуемые в классе Startup, позволяют конфигурировать службы и средства про­
межуточного программного обеспечения с применением аргументов с параметр ами .
Аргументом метода AddDbContext () является лямбда-выражение , которое получает
объект options, конфигурирующий базу данных для класса контекста. В этом случае
база данных конфигурируется с помощью метода UseSqlServer (} и указания стро­
ки подключения, которая получена из свойства Configuration.
Еще одно изменение, внесенное в класс Startup, было связано с заменой фиктив­
ного хранилища реальным:

services . AddTransient<IProduc tRepository , EFProductRepository>();


Глава 8. SportsStore: реальное приложение 225
Компоненты в приложении, использующие интерфейс IProductRepository, к
которым в настоящий момент относится только контроллер Product, при создании
будут получать объект EFProductReposi tory, предоставляющий им доступ к инфор­
мации в базу данных. Подробные объяснения будут даны в главе 18, а пока просто
знайте, что результатом будет гладкая замена фиктивных данных реальными из базы
данных без необходимости в изменении класса ProductController.
Финальное изменение в 1тассе Startup касается вызова метода SeedDa ta .
EnsurePopu l ated (). который гарантирует наличие в базе данных определенной
тестовой информации и вызывается внутри метода Configure () класса Startup.
При запуске приложения метод Startup. ConfigureServices () вызывается пе­
ред методом Startup . Configure (). т. е. ко времени вызова метода SeedData .
EnsurePopulated () можно иметь уверенность в том, что службы Entity Framework
Core уже установлены и сконфигурированы.

Создание и применение миграции базы данных


Инфраструктура Entity Framework Core способна генерировать схему для базы
данных, используя классы моделей, с помощью средства, которое называется мигра­
циями. При подготовке миграции инфраструктура EF Core создает класс С#, содер­
жащий команды SQL, которые нужны для подготовки базы данных. Если необходимо
модифицировать классы моделей, тогда вы можете создать новую миграцию, которая
содержит команды SQL, требуемые для отражения изменений. Таким образом, вам
не придется беспокоиться о написании вручную и тестировании команд SQL, и вы
можете сосредоточиться на классах модели С# в приложении.
Команды EF Core выполняются с применением консоли диспетчера пакетов
(Package Manager Console), которая открывается через пункт меню Toolsq NuGet
Package Manager (СервисqДиспетчер пакетов NuGet) в Visual Studio.
Запустите в консоли диспетчера пакетов следующие команды, чтобы создать класс
миграции, 1юторый подготовит базу данных к первому использованию:

Add-Migration Initial
Когда команда завершит свое выполнение, вы увидите в окне Solution Exp lorer сре­
ды Visual Studio папку Migrations. Именно в ней инфраструктура Entity Framework
Core хранит классы миграции. Одно из имен файлов будет выглядеть как длинное
число с _ Ini tial. cs после него, и это класс, который будет применяться для созда­
ния начальной схемы базы данных. Просмотрев содержимое такого файла , вы увиди­
те, как класс модели Product использовался для создания схемы.
Запустите приведенную ниже команду, чтобы создать базу данных и выполнить
команды миграции:

Update-Database
Создание базы данных займет какое-то время, но после завершения работы ко­
манды вы сможете увидеть результат, запустив приложение. Когда браузер запраши ­
вает стандартный URL для приложения, !{онфигурация приложения сообщает МVС о
необходимости создания контроллера Product для обработки запроса. Создание кон­
троллера Product означает вызов конструктора класса ProductController, которо­
му требуется объект, реализующий интерфейс I Р roduc t Repo s i tor у, и новая конфи­
гурация указывает MVC о том, что для этого должен быть создан и применен объект
EFProductReposi tory. Объект EFProductReposi tory обращается к функциональ­
ности EF Core, которая загружает реляционные данные из SQL Server и преобразует
226 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

их в объекты Product. Вся упомянутая работа скрыта от класса ProductController,


который просто получает объект, реализующий интерфейс IProductRepository, и
пользуется данными, которые он предоставляет. В итоге окно браузера отображает
тестовую информацию из базы данных (рис. 8.8).

1 ·. \po.-ts\to« х

1 ~
С @i-1oc_a_lh-os-t.600-0-=0= = =- - - · - - -
!· -- . =-- --=::-=-=-=-----=---===--··---
Kayak
А b~ar оп<
'1

1 for p<rso11

1 5275.00

LlfejAckN
Prot~ C t1\·~ n11d f(ls luo111\ЬI~
1
1
1 S4S.95
1

\ SoccпBnll
'
L ElfЛ:<"1oe_ro ~Oя.z.~_fI04..!~~i~1t _________________________ ~-- ·--- "'

Рис. В.В. Использование хранилища в виде базы данных

Такой подход с применением Entity Framework Core для представления базы дан­
ных SQL Server в виде последовательности объектов моделей отличается простотой
и легкостью, позволяя сосредоточить все внимание на инфраструктуре ASP.NET Core
MVC. Я опустил множество деталей , связанных с функционированием EF Core, и
большое количество доступных конфигурационных параметров . Мне нравится инф­
раструктура Entity Framework Core, и я рекомендую уделить время на ее изучение.
Хорошей отправной точкой послужит веб-сайт Microsoft для Entity Framework Core,
находящийся по адресу http: / /ef. readthedocs. io .

Добавление поддержки разбиения на страницы


На рис. 8.8 видно, что представление List . cshtml отображает сведения о товарах
в базе данных на одной странице. В этом разделе мы добавим поддержку разбиения
на страницы, чтобы представление отображало на странице меньшее число товаров,
а пользователь мог переходить со страницы на страницу для просмотра всего каталог.

В метод действия List () контроллера Product необходимо добавить параметр, как


показано в листинге 8.23.

Листинг 8.23. Добавление поддержки разбиения на страницы в метод действия


List () в файле ProductController. cs
using Microsoft.AspNetCore . Mvc ;
using SportsStore.Models ;
using System.Linq;
namespace SportsStore . Controllers
puЬlic class ProductController : Controller
private IProductRepository repository;
Глава 8. SportsStore: реальное приложе ние 227
puЬlic int PageSize = 4;
puЫic ProductController(IProduc t Repository repo) {
repository = repo ;

puЬlic ViewResul t Li s t ( in t page = 1)


=> View(repository.Products
.OrderBy(p => p.ProductID)
. Skip ( (page - 1) * PageSize)
.Take(PageSize)) ;

Поле PageSize указывает, что на одной странице должны отображаться сведения


о четырех товарах. Позже мы заменим его более совершенным механизмом. В метод
List () добавлен необязательный параметр. Это означает, что в случае вызова метода
без параметра (List () )вызов обрабатывается так, словно ему было передано значение,
указанное в определении параметра (Li st ( 1) ). В результате метод действия отобража­
ет п ервую страницу сведений о товарах, когда инфраструктура МVС вызьmает его без
аргумента. Внутри тела метода действия мы получаем объекты Product, упорядочи­
ваем их по первичному ключу , пропускаем товары, которые распол агаются до начала

текущей страницы, и выбираем количество товаров , указанное в пол е PageSi ze.

Модульное тестирование: разбиение на страницы

Модульное тестирование средства разбиения на страницы можно провести , создав


имитированное х ранилище, внедрив его в конструктор класса ProductController
и вызвав метод List () , чтобы запрашивать конкретную страницу. Затем полученные
объе кты Product можно сравнить с теми, которые ожидались от тестовы х данны х в ими­
тированной реализации. Написание модульных тестов обсуждалось в главе 7. Ни же пока­
зан созданный для этой цели модульный тест, который находится в файле класса по имени
ProductControl l er Tes ts . cs, добавленном в проект Spor tsSt ore . Tests:
using System.Collections . Generic ;
using System . Linq ;
using Moq ;
using SportsStore.Controllers ;
using SportsStore .Models ;
using Xuni t ;
namespace SportsStore . Tests
puЬlic class ProductControllerTests
[Fact]
puЫic void Can Paginate()
11 Организация
Mock<IProductRepository> mock = new Mock<IProdu ctRepository>() ;
mock . Setup(m => m. Products) .Retu rns(new Product[] {
new Product {ProductID 1, Name " Pl " },
new Product { ProductID = 2 , Name " Р2 " } ,
new Product { Pr oductID = З , Name "Р З " ),
new Product {ProductID 4 ' Name " Р4 " ) ,

new Product {ProductID = 5, Name = " PS"}


}) ;
ProductController cont roller = new ProductController(mock . Object);
228 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

controller . PageSize = З;

11 Действие
IEnumeraЫe<Product> result
controller .Li st(2) . ViewData . Mode l as IEnumeraЫe<Product> ;

11 Утверждение
Product[] prodArray = result.ToArray();
Assert . True(prodArray .Length == 2);
As sert. Equal ( " Р4 ", prodArray [ О ] . Name) ;
Assert . Equal( " PS ", prodArray[l] . Name) ;

Получение данных, возвращаемых из метода действия, выглядит несколько неуклюже.


Результатом является объект ViewResul t, и значение его свойства ViewData . Model
должно быть приведено к ожидаемому типу данных . В главе 17 рассматриваются разнооб­
разные результирующие типы, которые могут возвращать методы действий, а также спосо ­
бы работы с ними.

Отображение ссылок на страницы


Запустив приложение, вы увидите, что теперь на странице отображаются четыр е
позиции каталога. Если нужно просмотреть другую страницу, в конец URL можно до­
бавить параметр строки запроса, например:

http : //localhost : 60000/ ?page=2


Вы должны указать в URL номер порта, который был назначен вашему проекту.
Используя такие строки запросов, можно перемещаться по каталогу товаров.
У пользователей нет какого-либо способа выяснить о существовании этих парамет ­
ров строки запроса, но даже если бы он был , то вряд ли бы они захотели иметь дело
с навигацией подобного вида. Взамен мы должны визуализировать в нижней части
каждого списка товаров ссылки на страницы, чтобы пользователи могли переходить
между страницами. Для этого мы реализуем дескрипторный вспомогательный класс,
который будет генерировать НТМL-разметку для требуемых ссылок.

Добавление модели представления


Чтобы обеспечить поддержку дескрипторного вспомогательного класса, мы соби­
раемся передавать представлению информацию о количестве доступных страниц,
текущей странице и общем числе товаров в хранилище. Проще всего это сделать,
создав класс модели представления, который специально прим ен яется для пер еда­
чи данных между контроллером и представлением. Создайте в проекте SportsStore
папку Models/ViewModels и добавьте в нее файл класса с содержимым, приведен­
ным в листинге 8.24.

Листинг 8.24. Содержимое файла Paginginfo. cs из папки Models/ViewModels

using System ;
namespace SportsStore . Models . ViewModels
puЫic class Paginginfo {
puЫic int Totalitems { get; set ; }
Глава 8. SportsStore : реальное прило жение 229
puЫic int ItemsPerPage { get ; s et ; }
pu Ыi cint Curre n tPage ( g et ; s e t ; }
puЫic int TotalPages =>
(int)Math . Cei l ing((decima l )To t alitems / I t e msPe r Page) ;

Добавление дескрипторного вспомогательного класса


Те п е рь , имея модел ь пр едставл е ния, можно создать дес1<рипторный вспомогатель ­

ный класс . Создайте в проекте Sport sS tore папку I nfrastr u cture и добавьте в нее
файл класса по им ени PageLinkTagHel p er . cs с определением, показанным в лис­
тинг е 8.25. Де скрипторны е вспомогательные классы являются крупной частью инф­
раструктуры ASP.NET Core MVC, и в главах 23-25 объясняется , как они работают и
каким образом их создавать.

Совет. В папку Infrastructure будут помещаться классы , которые предоставляют прило­


жению связующий код, но не имеют отношения к предметной области прилож ения .

Листинг 8.25. Содержимое файла PageLinkTagHelper. cs из папки Infrastructure

using Microsoft . AspNetCore . Mvc ;


using Microsoft . AspNetCore . Mvc .Rende r ing ;
using Microsoft . Asp NetCo r e . Mvc.Rout in g ;
using Microsoft . AspNetCore . Mvc . Vi e wFeatures ;
u sing Microsoft . AspNetCore . Ra z or . Ta g Hel p ers ;
using SportsStore . Mode l s . ViewMode l s ;
namespace SportsStore . Infrastructure
[HtmlTargetElement( " div ", Attribu t es = " page -model " )]
puЫic class PageLinkTagH el per : TagHe l per {
pr i vate IUrlHelperFactory ur l Helpe rFactory ;
puЫic PageLinkTagHelper(IUr l He l perFactory helperFactory)
u rlHe l perFacto r y = hel p erFacto r y;

[ViewContext]
[HtmlAttributeNotBound]
puЬlic ViewContext ViewContex t { get ; set ;
puЫic Paginginfo PageMode l { get ; set ; }
puЫic string PageAction { get ; set ; }
puЫic override v oid Process( Ta gHelp e r Co n t e xt co n text ,
TagHelperOutput output) (
IUrlHelper urlHelper = ur l HelperFactory.GetUrlHelper(ViewCon t ext) ;
TagBuilder res ul t = new TagBu il der( " div " ) ;
for (in t i = 1 ; i <= PageMode l . To t alPages ; i ++ ) {
TagBuilder tag = new TagBuilder ( " а " ) ;
tag . Attributes [ "href"] = ur l He l per . Action ( PageAction , new { page = i } ) ;
tag . In n erHtml . Append (i. ToS t ri n g() );
result . InnerHtml . AppendHtml(tag) ;

output . Content . AppendHtml(re s u l t . InnerHtm l ) ;


230 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

Этот дескрипторный вспомогательный класс заполняет элемент di v элементами


а , которые соответствуют страницам товаров. Сейчас мы не будем вдаваться в ка­
кие-либо детали относительно дескрипторных вспомогательных классов; достаточно
знать, что они предлагают один из наиболее удобных способов помещения логики С#
в представления. Код для дескрипторного вспомогательного класса может выглядеть
запутанным, потому что смешивать С# и HTML непросто. Но использование деСJ{рип­
торных вспомогательных классов является предпочтительным способом включения
блоков кода С# в представление, поскольку дескрипторный вспомогательный класс
можно легко подвергать модульному тестированию.

Большинство компонентов MVC, таких как контроллеры и представления , об­


наруживаются автоматически, но дескрипторные вспомогательные классы долж­

ны быть зарегистрированы. В листинге 8.26 приведено содержимое файла


_ Viewirnports . cshtrn l из папки Views с добавленным оператором, который сооб­
щает MVC о том, что дескрипторные вспомогательные классы следует искать в про­
странстве имен SportsStore. Infrastructure. Кроме того, добавлено также вы­
ражение @using, чтобы на классы моделей представлений можно было ссылаться в
представлениях, не указывая пространство имен .

Листинг 8.26. Регистрация дескрипторного вспомоi-ательного класса


в файле_Viewimports. cshtml
@using SportsStore . Models
@using SportsStore.Models.ViewModels
@addTagHelper * , Microsoft . AspNetCore . Mvc . TagHelpers
@addTagHelper SportsStore.Infrastructure .*, SportsStore

Модульное тестирование: создание ссылок на страницы

Чтобы протестировать дескрипторный вспомогательный класс PageLinkTagHelper,


вызывается метод Process () с тестовыми данными и предоставляется объект
TagHelper Output , который инспектируется на предмет сгенерированной НТМL­
разметки . Тест определен в новом файле PageLinkTagHelperTests. cs внутри проекта
SportsStore. Tests:
using System.Collections.Generic;
using System . Threading . Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore . Mvc . Routing;
using Microsoft.AspNetCore . Razor . TagHelpers;
using Moq;
using SportsStore.Infrastructure;
using SportsStore.Models . ViewModels ;
using Xunit ;
namespace SportsStore . Tests {
puЫic class PageLinkTagHelperTests

[Fact]
puЬlic void Can_Generate Page Links()
11 Организация
var urlHelper = new Mock< IUrlH e lper>() ;
Глава 8. SportsStore : реальное прило ж ение 231
urlHelper.SetupSequence(x => x . Action(It . IsAny<UrlActionContext>()))
. Returns( 11 Test/Pagel 11 )
. Returns( 11 Test/Page2 11 )
. Returns( 11
Test/PageЗ 11
) ;
var urlHelperFactory = new Mock<IUrlHelperFactory>() ;
urlHelperFactory . Setup(f =>
f . GetUrlHelper(It . IsAny<ActionContext>() ))
. Returns(urlHelper.Object) ;
PageL i nkTagHelper helper =
new PageLinkTagHelper(urlHelperFactory . Object)
PageModel = new Paginginf o {
CurrentPage = 2 ,
Totalitems = 28 ,
I t emsPerPage = 10
},
11
PageAction = Test 11
};
TagHelperContext ctx = new TagH e lperContext(
new TagHelperAttributeList() ,
new Dictionary<object , object>(), 1111 ) ;
var content = new Mock<TagHelperContent>() ;
TagHelperOutput output = new TagHe l perOutput( 11 div 11 ,
new TagHelperAttributeList() ,
(cache , encoder) => Task.FromResult(content.Object));
11 Действие
helper . Process(ctx , output) ;
11 Утверждение
Assert . Equal(@ 11 <a href= 11 " Test/Pagel "" >l</a> 11
+ @ <а href= 1111 Test/Page2 1111 >2</a> 11
11

+ @ 11
<а href= "" Test/Page3 1111
>З</a> ",
output . Content . GetCon t ent() ) ;

Сложность этого теста связана с созданием объектов, которые требуются для создания и
применения дескрипторного вспомогательного класса . Дескрипторные вспомогательные
классы используют объекты IUr lHelperFactory для генерации URL, которые указывают
на разные части приложения, и для создания реализаций этого интерфейса и связанного
с ним интерфейса IUrlHelper , предоставляющего тестовые данные , используется инф­
раструктура Moq.

Основная часть теста проверяет вывод дескрипторного вспомогательного класса с приме­


нением литерального строкового значения, которое содержит двойные кавычки. Язык С#
позволяет работать с такими строками при условии, что строка предварена символом @, а
вместо одной двойной кавычки внутри строки используется набор из двух двойных кавычек
Вы должны помнить о том , что разносить литеральную строку по нескольким строкам
1111
( ).

файла нельзя , если только строка , с которой производится сравнение, не разнесена ана­
логичным образом . Например, литерал, применяемый в тестовом методе, был размещен в
нескольки х строках из-за недостаточной ширины печатной страницы . Символ новой строки
не добавлялся, иначе тест не прошел бы.
232 Часть 1. Введение в инфраструктуру ASP.NET Соге MVC

Добавление данных модели представления


Мы пока не готовы использовать дес1(рипторный вспомогательный класс, т. к. пред­
ставление еще н е снабжено экзе мпляром класса модели представления Paginginfo.
Это можно было бы сделать с приме нением объекта Vi ewBag, но лучше пом е стить вс е
данные , подлежащие пер едаче из контроллера в представление , внутрь е динствен­

ного класса модели предст авления. Добавьте в папку Mode l s/ViewMo d e l s про е кта
Sp ortsS t o r e ф айл класса по имени P r oductsListViewModel . cs с содержимым .
приведенным в листинг е 8.27.

Листинг 8.27. Содержимое файла ProductsListViewModel. cs


из папки Models/ViewModels
us i ng Sys t em . Col l ec t ions . Ge neric ;
u s i ng Spo r tsStore . Mode l s ;
n a mespace SportsStore . Mode l s . Vi ewMode l s
p uЫic c l ass Products ListViewModel {
puЫic IEnu m eraЫe<Prod u ct> Produ cts get ; s e t ; }
p uЫic Pagingi n fo Pagingi n fo { ge t ; se t; }

Теперь можно обновить метод действия Li s t () кл асса Produc t Con troller так.
чтобы он использовал ю~асс Pr odu cts Li s tVi ewMod el для снабжения представления
сведениями о товарах, отображаем ых на ст раницах. и информацией о разбиении на
страницы (листинг 8.28).

Листинг 8.28. Обновление метода действия List() в файле ProductController.cs

using Mi crosoft . Asp NetCore . Mvc ;


u sing SportsStore . Mode l s ;
us ing System .Li n q ;
using SportsStore.Models.ViewModels;
n a mespace SportsS t ore . Con t rol l ers {
p uЬlic c l ass Prod uctControl l er : Co n trol l er
p rivate IProd u c t Repository repository ;
puЫ i c i nt PageSize = 4 ;
puЫic ProductController(I Produ ct Reposi t ory re p o) {
r epos i tory = repo ;

pu Ы ic ViewRes ul t List( in t page = 1 )


=> View(new ProductsListViewModel
Products = repository.Products
.OrderBy(p => p.ProductID)
. Skip ( (page - 1) * PageSize)
.Take(PageSize),
Paginginf o = new Paginginfo
CurrentPage = page,
ItemsPerPage = PageSize,
Totalitems = repository.Products . Count()
}
}) ;
Глава 8. SportsStore : реальное приложение 233
В не с е нн ые из м е н е ния об е спечив ают перед ачу пр е дс та вл е нию объ екта
Products ListViewModel как данных м одели .

М одульное тестиро вание:


данные разбиен и я н а страницы для модели пр едставления

Нам необходимо удостовериться в то м , что контроллер отправляет представлению коррек­


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

[Fact]
puЫic void Can_Send_ Pagination_View_Model() {
11 Организация
Mock<IProductRepository> mock = new Mock<IProduc t Repository>() ;
mock.Setup(m => m. Products) . Returns(new Product[] {
new Product {ProductID 1 , Name " Pl " ) ,
new Product {Produc t ID 2 , Name " Р2 " } ,
new Product {ProductID 3 , Name " РЗ "),

new Pr oduct {Product I D 4 , Name "Р4" } ,


new Product {Product I D 5 , Name "Р 5 " )
)) ;

11 Организация
ProductControl l er controlle r =
new ProductCon tr oller(mock . Object) { PageSize 3 );
11 Действие
ProductsListV i ewModel result =
controller . List(2) . ViewData.Model as Products ListViewModel ;
11 Утверждение
Paginginfo pageinfo = result . Pagingin f o ;
Assert . Equal(2 , pageinfo . CurrentPage) ;
Assert . Equal(З , pageinfo .I temsPerPage) ;
Assert . Equal(5 , pageinfo . Tota l items) ;
Assert . Equal(2 , pageinfo.TotalPages) ;

Потребуется также модифицировать ранее созданный модульный тест для проверки раз­
биен и я на страницы, содер жащийся в методе Can_Paginate () .Он полагается на метод
действия List () , возвращающий объе кт ViewRe s ul t , свойством Model которого явля ­
ется последовательность объе ктов Product, но мы поместили эти данные внутрь еще од­
ного типа м одели представления. Вот переделанный тест:

[E'act ]
puЫic void Can_Paginate() {
11 Организация
Mock<IProductRepository> mock = new Mock<IProduc t Repository>() ;
mock . Setup(m => m. Products) . Re t urns(new Product[J {
new Product {Product I D 1 , Name " Pl " ) ,
new Product { ProductID = 2 , Name = "Р 2 "),
234 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

new Product {ProductID = 3 , Name " Р3 " } ,


new Product {ProductID = 4 , Name "Р 4 " } ,

new Product {ProductID = 5 , Name " PS " }


}) ;
ProductContro l ler controller = new ProductController(mock . Object) ;
controller.PageSize = 3 ;
11 Действие
ProductsListViewModel result =
controller.List(2) .ViewData.Model as ProductsListViewModel;
//У твержде н ие
Product[] prodArray = result.Products.ToArray();
Assert . True(prodArray . Length == 2);
As sert.E qua l("P 4 ", prodArray(OJ . Name) ;
Assert . Equal("P5 ", prodArray[l] . Name) ;

Учитывая объем дублированного кода в двух тестовых методах, обычно следовало бы со­
здать общий метод начальной установки. Однако, поскольку модульные тесты описаны в
индивидуальных врезках вроде этой, их код представлен по отдельности, чтобы с каждым
тестом можно было ознакомиться независимо от других.

В настоящий момент представление ожидает последовательность объектов


Product , поэтому файл List . cshtml необходимо обновить, как показано в листин­
ге 8.29, чтобы иметь дело с новым типом модели представл ения.

Листинг 8.29. Обновление файла List. cshtml


@model ProductsListViewModel
@foreach (var р in Model.Products)
<div>
<h3>@p .Name</ h3>
@p . Description
<h4>@p . Pric e.ToStr ing( " c " )</h4>
</div>

Выражение @mode l было изменено для указания механизму Razor на то, что те­
перь мы работаем с другим типом данных. Цикл foreach был обновлен, чтобы источ­
ником данных стало свойство Products объекта модели.

Отображение ссылок на страницы

Наконец, все готово для добавления в представление List ссылок на страницы.


Мы создали модель представления, которая содержит информацию о разбие нии на
страницы. обновили контроллер, чтобы эта инфор мация передавалась представл е ­
нию, и модифицировали выражени е @model , приведя его в соответствие с новым
типом модели представления . Осталось только добавить НТМL- элемент, который де ­
скрипторный вспомогательный класс будет обрабатывать для создания ссылок на
страницы (листинг 8.30).
Глава 8. SportsStore: реальное приложение 235
Листинг 8.30. Добавление ссылок на страницы в файле List. cshtml
@mode l Produ cts List Vi ewMode l
@foreach (var р i n Model .P r odu cts)
<div>
<h3>@p . Name</h3>
@p . Description
<h 4>@p . Pr i ce .T oStr i ng( "c" )</h4>
</div>

<div page-model="@Model.Paginginfo" page-action="List"></div>

Запустив приложение, вы увидите новые ссылки на страницы (рис. 8.9). Стиль


отображения довольно примитивен, но далее в главе он будет улучшен. Пока важно
то, что ссыл ки позволяют перемещаться по страницам каталога и знакомиться с то­

в а рам и, выставл е нными на продажу. Когда механизм Razor обнаруживает атрибут


page - model в элементе d i v , он обращается к классу PageLinkTagHelper для пр е­
образования элемента, что и дает набор ссьmок, показанных на рис. 8.9.

1 Produ ш )( 1 Produщ Х
1
1
\ +- -' С
f- -----
D localhost:60000/'pag
+- -"
Products
_ - - -· · - - - - - -
Х ...
+- .., С С1 localhost:бOOOO/?page•З
С D locall1os t 60000/>page• - - - - - - - - - - - - - - - - - -
,..--------------
'{;;1

1
K oyok ----~- --'-----"'-'--- B ll11g- Bli11g Кlug
!
1 А boat fot онео pcr~o11 !
S t a dillШ
Gold-pla.tcd duiшoш1-snIOdcd Кшg
1 • .ОО I Flat-packcd ЗS.000-~c<ct !iiti'ldшш
5275 SI,200.00
1 1 S79,500.00
Li fфcke l lli
1 Tbluki11g Со р
j Protectt\'C' анd t"n11>!1ioщ1ЬJ 1
Iщр1·0 \·е bri'\bl cfficict1cy Ь
1 S4S.9S
j 1
Sl б.00
Soccr1· Bo ll 1
UnsleodY Ch зi1·
FIFA-npproп•d siz tшd \\'tigltt 1 .
Stc rC'tly g iп· ущ1r
1
9
Sl .SO j S29.95
1 Cш·nei· F l "s 1
Hu шn n Cl1
)ii'IY111?, fiC'ld а prof~.<.:s tOJl~I to\j
А fш1 gаш or tl1e fa1шly

1 575.00

--·-L :::J -·- 1

Рис. 8.9. Отображение ссылок для навигации по страницам

На заметку! Если вы запустите приложение с применением пункта меню Start Debuggiпg


(Запустить отладку), то можете столкнуться с сообщением об ошибке, предупреждающим
о том, что коллекция была модифицирована. Это дефект EF Core, который дол ж ен быть
исправлен к моменту выхода настоящей книги, но если он все же останется, тогда про­
стая перезагрузка окна браузера решит проблему и приведет к отображению содержимо­
го, представленного на рис . 8.9.
236 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

Почему бы просто не воспользоваться элементом управления GridView?

Если вы ранее работали с ASP.NET, то вам может показаться , что такой большой объем ра­
боты привел к довольно скромным результатам. Пришлось написать немало кода лишь для
того, чтобы получить список товаров с разбиением на страницы. Если бы мы прибегли к
услугам инфраструктуры Web Forms, то такого же результата можно было бы достичь с при­
менением готового элемента управления Gr i dVi ew или Li s tVi ew из ASP.NET Web Forms,
привязав его напрямую к таблице Pr oducts базы данных.

То, что было сделано в главе, выглядит не слишком впечатляюще, но серьезно отличается
от процесса перетаскивания какого-то элемента управления на поверхность визуального

проектирования. Во-первых, мы строим приложение с надежной и удобной в сопровожде­


нии архитектурой, которая задействует подходящее разделение обязанностей . В отличие
от простейшего использования Lis t Vie w мы не связываем напрямую пользовательский
интерфейс и базу данных, что представляет собой подход, который дает быстрые резуль­
таты, но вызовет проблемы и сложности в будущем. Во-вторых, по ходу дела мы создаем
модульные тесты, что позволяет проверять поведение приложения естественным образом,
который практически невозможен в случае применения сложного элемента управления Web
Forms. Наконец, в-третьих, не забывайте, что значительная часть главы посвящена созда­
нию инфраструктуры, на основе которой строится приложение. Например, определить и
реализовать хранилище нужно только один раз, и теперь, когда оно имеется в нашем рас­

поряжении, новые функциональные средства можно строить и тестировать легко и быстро,


как будет продемонстрировано в последующих главах.

Разумеется, ничто из сказанного отнюдь не умаляет значимость безотлагательных результа­


тов, которые способна предложить инфраструктура Web Forms, но, как объяснялось в главе 3,
такая безотлагательность имеет свою цену, которая может оказаться высокой и болезнен­
ной в крупных и сложных проектах.

Улучшение URL
Ссылки на страницы работают, но для передачи информации серверу они по-пре ­
жнему используют строку запроса, подобную следующей:

http : //localhost/?page=2
Чтобы получить бол е е привлекательные URL, необходимо создать схему. которая
следует шаблону компонуемых URL. Компонуемый URL - это URL, имеющий с мысл
для пользователя, такой как показанный ниже:

http : // l ocalhost/Page2
Инфрастру1пура МVС позволяет легко изменять схему URL в приложении, потому
что применяет средство мapшpymuзa4uuASP. NET, которо е отвечает за обработку URL
для выяснения, на какую часть приложения они указывают. Понадобится лишь доба­
вить новый маршрут при регистрации промежуточного программного обеспеч е ния
МVС в методе Configure () кл асса Star t up (листинг 8.31).

Листинг 8.31. Добавление нового маршрута в файле Startup. cs

puЫ i c void Conf i gu r e( IApp l ic a tion Bui l de r ар р,


I HostingEnvi r onment env , ILogg e r Factory l oggerFacto r y) {
a pp . UseDeve l op e rExce p t ion Pag e( ) ;
app .U seStatusCodePages() ;
Глава 8. SportsStore : реальное приложение 237
app.UseSt a ticFi l es() ;
app .UseMvc(routes => {
routes .мapRoute (
name: "pagination",
template: "Products/Page{page}",
defaults: new { Controller = "Product", action = "List" }) ;
routes.MapRoute(
name: "default",
template : " {control l er=Product}/{act ion=List}/{id?} " ) ;
}) ;
SeedData.EnsurePopulated(app);

Важно поместить этот маршрут перед стандартным маршрутом (по имени


defaul t ). который уже присутствует в методе. Как будет показано в главе 15, система
маршрутизации обрабатывает маршруты в порядке их перечисления, а нам нужно ,
чтобы новый маршрут имел преимущество перед существующим.
Это единственное изменение, которое потребуется внести, чтобы изменить схему
URL для разбиения на страницы списка товаров. Инфраструктура MVC тесно интег­
рирована с функцией маршрутизации, поэтому приложение автоматически отражает
изменение подобного рода в URL, используемых приложением , что касается и URL,
которые генерируются дескрипторными вспомогательными классами вроде применя­

емых для гене рации ссылок на страницы . Не беспокойтесь, если маршрутизация пока

не особенно понятна - она будет подробно рассматриваться в главах 15 и 16.


Запустив приложение и щелкнув на ссылке для какой-нибудь страницы, вы уви­
дите новую схему URL в действии (рис. 8.10).

Sta(liШll

Flat-pnc ke<I 35.000-seat stacl iщ11

1 ~"0.00 " ~·
....,. !11! .... .,.,,,....- ~
~..-..;...,..., _ " ' -_ _ _,,,.

Рис . 8.10. Новая схема URL, отображаемая в браузере

Стилизация содержимого
Мы построили значительную часть инфраструктуры и базовые средства прило­
жения готовы к сборке всего воедино, но пока совершенно не уделяли внимание его
внешнему виду. Хотя дан ная книга не посвящена веб-дизайну или CSS, внешний вид
приложения SportsStore настолько примитивен, что это умаляет его технические до ­
стоинства. В настоящем разделе мы постараемся исправить ситуацию. Мы собираем­
ся реализовать классическую компоновку, содержащую два столбца и заголовок, как
показано на рис. 8.11 .
238 Часть 1. Введение в инфраструктуру ASP.NET Соге MVC

Home • Product 1
·• Watersports • Product 2
• Socce.r •
• , Chess (main body)
•... .,,·

Рис. 8. 11. Целевой дизайн для приложения SportsStore

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


Для предоставления стилей CSS, которые будут применены к приложению, мы
планируем использовать пакет Bootstrap. При установке пакета Bootstrap мы будем
полагаться на встроенную в Visual Studio поддержку инструмента Bower. Выберите
шаблон элемента Bower Configuration File (Файл конфигурации Bower) из категории
Client-side (Клиентская сторона) в диалоговом окне Add New ltem, чтобы создать в
проекте SportsStore файл по имени bower. j son, как демонстрировалось в главе 6.
Добавьте в раздел dependencies созданного файла пакет Bootstrap, как показано в
листинге 8.32.

Листинг 8.32. Добавление пакета Bootstrap в файле bower. j son


внутри проекта SportsStore

" name ": " asp . net ",


"private": true,
"dependencie s": {
"bootstrap": "3. 3. 6 11

После сохранения внесенных в файл bower. j son изменений среда Visual Studio
применяет инструмент Bower для загрузки пакета Bootstrap в папку wwwroot/liЬ/
bootstrap. Пакет Bootstrap зависит от пакетаjQuегу. который также автоматически
добавится в проект.

Применение стилей Bootstrap к компоновке


В главе 5 было показано, как работают представления Razor, как они используют­
ся и как встраивать в них компоновки. Файл запуска представления, добавленный в
начале главы , указывает, что файл по имени_ Layout. cs htrnl должен выступать в
качестве стандартной компоновки, так что начальную стилизацию Bootstrap мы при­
меним именно к нему (листинг 8.33).
Глава 8. SportsStore : реальное приложение 239
Листинг 8.33. Применение СSS-файла Bootstrap к файлу _Layout. cshtml
из папки Views/Shared
<!DOCTYPE htrnl>
<htrnl>
<head>
<rneta narne ="viewport " content= " width=devi ce - width " />
<link rel="stylesheet" asp-href-include="liЬ/bootstrap/dist/css/*.min.css" />
<title>Spo rtsStore </t itle>
</head>
<body>
<div class="navbar navbar-inverse" role="navigation">
<а class="navbar-brand" href="#">SPORTS STORE</a>
</div>
<div class="row panel">
<div id="categories" class="col-xs-3">
Put something useful here later
</div>
<div class="col-xs-8">
@RenderBody ()
</div>
</d iv>
</body>
</html>

Элемент l in k имеет атрибут asp - href - include, который представляет собой


пример и спользования встроенного дескрипторного вспомогательного класса. В дан­
ном случае дескрипторный вспомогательный класс просматривает значение атрибута
и генерирует элементы link для всех файлов по указанному пути, который может
содержать групповы е символы. Это удобное средство гарантирования того, что вы
можете добавлять и удалять файлы из структуры папок wwwroot , не нарушая работу
приложения , но , как объясняется в главе 25, указание групповых символов требует
особого внимания , чтобы они соответствовали нужным файлам .
Добавление в компоновку таблицы стилей CSS из Bootstrap означает, что оп­
ределенные в ней стили можно применять в любом представлении, которое пола ­
га ется на данную компоновку. В листинге 8.34 стилизация используется в файле
List . cshtml.

Листинг 8.34. Стилизация содержимого в файле List. cshtml


@model Products ListViewMode l
@foreach (var р in Model . Products)
<div class="well">
<hЗ>

<strong>@p.Name</strong>
<span class="pull-right laЬel laЬel-primary">
@р. Price. ToString ("с")

</span>
</hЗ>
<span class="lead">@p.Description</span>
</div>
240 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

<div page-model="@Model.Paginginfo" page-action="List"


page-classes-enabled="true"
page-class="Ьtn" page-class-normal="ьtn-default"
page-class-selected= 11 Ьtn-primary 11 class= 11 Ьtn-group pull-right">
</div>

Нам необходимо стилизовать кнопки, которые генерирует класс PageLin kTagHe 1ре r ,
но жестко встраивать классы Bootstrap в код нежелательно , т.к. это затруднит пов­
торное использование дескрипторного вспомогательного класса где-то в другом месте

приложения либо изменение внешнего вида кнопок. Взамен мы определяем специ­


альные атрибуты в элементе di v, которые указывают требуемые классы. Данные ат ­
рибуты соответствуют свойствам, добавленным в дескрипторный вспомогательный
класс, которые затем применяются для стилизации создаваемых эл ементов а (лис­
тинг 8.35).

Листинг 8.35. Добавление классов к генерируемым элементам в файле


PageLinkTagHelper.cs
using Microsoft . AspNetCore . Mvc ;
using Microsoft . AspNetCore . Mvc.Rendering ;
using Microsoft . AspNetCore . Mvc . Routing ;
using Microsoft.AspNetCore.Mvc . ViewFeatures;
using Microsoft . AspNetCore . Razor .T agHe l pers ;
using SportsStore.Models . ViewMode ls;
namespace SportsStore.Infrastructure
[HtmlTargetElement("div", Attributes = " page -model " )]
puЫic c l ass Page LinkTagHe l per : TagHelper {
private IUrlHelperFactory ur lH e lperFactory ;
puЫic PageLinkTagHelper(IUrlHelperFactory helperFactory)
urlHelperFactory = he lperFac t ory ;

[ViewContext]
[HtmlAttributeNotBound]
puЬlic ViewContext ViewCo n text { get ; set ;

puЫic Paginginfo PageModel { get; set ;


puЫic string PageAction { get ; set; }
puЫic Ьооl PageClassesEnaЬled { get; set; false;
puЬlic string PageClass { get; set; }
puЬlic string PageClassNormal { get; set; }
puЬlic string PageClassSelected { get; set;
puЫic override void Process(TagHelperContext con t ext ,
TagHelperOutput output) {
IUrlHelper urlHelper = urlHelperFactory.GetUrlHelper(ViewContext);
TagBuilder result = new TagBuilder("div");
for (int i = 1 ; i <= PageModel . TotalPages ; i++) {
TagBuilder tag = new TagBuilder("a");
tag.Attributes[ " href "J = ur lHelper.Action(PageAction ,
new { page = i }) ;
Глава 8. SportsStore: реальное nриложение 241
if (PageClassesEnaЫed) {
tag.AddCssClass(PageClass);
tag.AddCssClass(i == PageМodel.CurrentPage
? PageClassSelected : PageClassNormal) ;

tag .Inne r Html. App end(i .T oS tring () );


r esu l t .Inner Html . Append Html( ta g ) ;

o ut pu t. Con tent . Appe nd Html (resu lt.I n n er Html);

Значения атрибутов автоматически используются для установки значений


свойств в дескрипторном вспомогательном классе, учитывая отображение между
форматом имени атрибута HTML (page - clas s- norma l) и форматом имени свойства
С# (PageClassNormal). Это позволяет дескрипторным вспомогательным классам по­
разному реагировать в зависимости от атрибутов НТМL-элемента, обеспечивая более
гибкий способ генерации содержимого в приложении МVС.
Запустив приложение , вы увидите , что его внешний вид улучшился - во всяком
случае, в како й -то степени (рис. 8.12).

!) Spo~•Stolf х

с Cc?..~~.:i~·_:~~Qip;;,~~![P,1g~~--=~-=.=-===·===·=:~~-~J
SPOПTS STOHE .

jPut something uselul


jt1eie later
Bling-Bling Кing
Gold·plated, diamond-studded Кing

1 ll 2

l· ~Щ···-~~~~~~~~!~':~. '
Рис. 8.12. Приложение SportsStore с улучшенным дизайном

Создание части чного представления


В качестве завершающего штриха в данной главе мы проведем рефакторинг при­
ложения , чтобы упростить пр едставление Li s t . c s html. Мы создадим частичное
представление, являющееся фрагментом содержимого , которое можно внедрять в
другое представление подобно шаблону. Частичные представления подробно рас­
сматриваются в главе 21. Они помогают сократить дублирование , когда одно и то же
содержимое должно появляться в разных местах приложения. Вместо того чтобы ко­
пировать и вставлять одинаковую разметку Razor во множество представлений, мож­
но определить ее единожды в частичном представлении. Чтобы создать частичное
представление, добавьте в папку Vi ews/S h a red файл представления Razor по имени
Produ ctSummary . cs h tml и поместите в него разметку, показанную в листинге 8 .36.
242 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

Листинг 8.36. Содержимое файла ProductSummary. cshtml из папки Views/Shared


@model Product
<div class= "well " >
<hЗ>
<strong>@Model.Name</strong>
<span c l ass= "pull-right label label - primary">
@Model . Price . ToString( "c" )
</span>
</hЗ>
<span class="lead " >@Model . Description</span>
</div>

Теперь необходимо модифицировать файл List. csh tml из папки Views/


Products, чтобы в нем применялось частичное представление (листинг 8.37).

Листинг 8.37. Использование частичного представления в файле List. cshtml


@model ProductsListViewModel
@foreach (var р in Model . Products)
@Html. Partial { "ProductSununary", р)

<div page - model="@Model . Paginginfo" page-action="List "


page - classes-enaЬled="true "
page-class="Ьtn " page - class -normal = "Ьtn- default "
page-class - selected= " Ьtn - primary"class="Ьtn-group pull-right ">
</div>

Мы взяли код разметки, который ранее размещался в цикле foreach в представ­


лении List. cshtml, и перенесли его в новое частичное представление. Обращение
к частичному представлению производится с помощью вспомогательного метода

Html . Partial () , которому в аргументах передаются имя представления и объект


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

любое представление, которое нуждается в отображении сводки о товаре. Как видно


на рис. 8. 13, добавление частичного представления не изменяет внешний вид при­
ложения; оно просто меняет место, где механизм Razor находит содержимое, которое

применяет для генерации ответа, отправляемого браузеру.

Резюме
В настоящей главе была построена основная инфраструктура для приложения
SportsStore. Пока что она не содержит достаточного набора функциональных средств,
чтобы их прямо сейчас можно было продемонстрировать пользователю , но "за кули­
сами" уже имеются зачатки модели предметной области с хранилищем товаров , кото­
рое поддерживается SQL Server и Entity Framework Core. В приложении присутствует
единственный контроллер ProductController, который может создавать разбитые
на страницы списки товаров, а также настроена ясная и дружественная к пользо ва­

телям схема URL.


Глава 8. SportsStore: реальное nриложение 243

С Spo1tsStore Х

~ с i_~-'~::'h_?:~6:_~~p:~~~?~~9~~~~~-~~-~==~===~~---------~_:_--=_~- -=~~ .::€J


SPORTS SТOilE ·
1

IPut sornething LJseful


lhere later
Kayak
А boat ror 0 11е person

Рис. 8.13. Использование частичного представления

В этой главе могло показаться , что для получения весьма незначительных выгод
пришлось провести излишне трудоемкую начальную подготовку, но в следующей
главе равновесие будет восстановлено. Теперь, когда фундаментальная структура на
месте, можно двигаться дальше и добавлять все средства, ориентированные на поль­
зователя: навигацию по категориям, корзину для покупок и начальную форму для
оплаты.
ГЛАВА 9
SportsStore: навигация

в этой главе мы продолжим построение примера приложения SportsStore. В пре­


дыдущей главе была добавлена поддержка для навигации по приложению и для
начала создания корзины для покупок.

Добавление навигационных элементов управления


Приложение SportsStore станет намного удобнее , если пользователи получат воз­
можность навигации среди товаров по категории. Это будет делаться в три этапа.

• Расширение метода действия List () в классе ProductControl l er , чтобы он


был способен фильтровать объекты Product в хранилище.
• Пересмотр и расширение схемы URL.
• Создание списка иатегорий. иоторый будет находиться в боковой панели сай­
та, подсвечивая текущую категорию и предоставляя ссылки на остальные

категории.

Фильтрация списка товаров


Мы начнем с расширения класса модели представления ProductsListViewModel,
который был добавлен в проект Spo rtsS tore в предыдущей глав е . Нам нужно обес­
печить взаимодействие текущей категории с представлением, чтобы визуализировать
боковую панель. и это хорошая отправная точка. В листинге 9.1 показаны изменения,
внесенные в файл ProductsListViewModel . cs из папки Models/ViewModels .

Листинг 9.1. Добавление свойства в файле ProductsListViewModel. cs

using System.Collections .Generic ;


using SportsStore.Models ;
namespace SportsStore . Models .ViewModels
puЫic class ProductsListViewModel {
puЫic IEnumeraЬle<Product> Products get ; set ; }
puЫ i c Paginginfo Paginginfo { get; set ;
puЫic string CurrentCategory { get; set; }
}

В класс ProductsListViewModel добавлено свойство по имени CurrentCategory.


Следующий шаг заключается в обновлении класса ProductController, чтобы метод
Глава 9. SportsStore: навигация 245
действия List () фильтровал объекты Produ ct по к атегории и использовал только
что добавленное в модель представления свойство дл я указания категории, выбран­
ной в текущи й момент. Изменения приведены в листинге 9.2.

Листинг 9.2. Добавление поддержки категорий к методу действия List ()


в файле ProductController. cs
using Microsoft . AspNetCore . Mvc ;
using SportsStore . Models ;
using System . Linq ;
us i ng SportsStore . Models . ViewMode l s ;
namespace SportsStore . Controlle r s (
puЫic class ProductController : Contro l ler
private IProductRepository repo s itory ;
puЫic int PageSize = 4;

puЫic ProductControl l er(IProductRepository repo) {


repository = repo ;

puЫic ViewResult List(string category, int page = 1)


=> View(new ProductsListVi e wMode l {
Products = repository . Products
. Where (р => са tegory == null 1 1 р. Са tegory == са tegory)
. OrderBy(p => p . Produc t ID)
. Skip( (page - 1) * PageS i ze)
. Take ( PageSize) ,
Paginginfo = new Pagingi nfo
Cu r ren t Page = p a ge,
ItemsPerPage = PageSize ,
Tota l i t ems = reposi t ory. Prod uct s. Count()
}'
CurrentCategory = category
}) ;

В этот метод действия вн е с е ны три изменения . Первое - добавлен новый пара­


метр по им е ни category. Он применяет ся вторым изменением , которое представ­
ляет собой расширение з апроса LINQ. Если значение category не равно null ,
тогда выбираются только объе кты Pr o du ct с соответствующим значение м в свойс­
Category. Последн е е, тр еть е , измен е ние ка са ется устан овки значения свойс­
тв е
тв а
Cur r entCategory, которое было добавлено в класс ProductsListViewModel .
Однако в р е зультате таких из менений значение Paginglnfo . Totalltems вычи сля ­
ется некорректно , потому что оно не принимает во внимани е фильтр по категории.
Со временем м ы вс е исправим .

Модульное тестирование: обновление существующих модульных тестов

Мы изменили сигнатуру метода действия List () , поэтому некоторые существующие ме­


тоды модульного тестирования перестали компилироваться . Чтобы решить возникшую про­
блему, в модульных тестах, которые работают с контроллером, методу действия Li s t ()
246 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

понадобится передавать в первом параметре значение null. Например, раздел действия


тестового метода Can _ Paginate () в файле ProductControllerTests . cs дол же н
выглядеть следующим образом :

[Fact]
puЫic void Can_Paginate()
11 Организация
Mock<IProductRepository> mock = new Mock<IProductRepository>() ;
mock.Setup(m => m.Products) .Returns(new Product[]
new Product {ProductID = 1, Name Pl} ,
new Product {ProductID 2, Name Р2} ,
new Product {ProductID 3 , Name РЗ},
new Product {ProductID 4 , Name Р4} ,
new Product {ProductID 5 , Name Р5)
}) ;

ProductController controller new ProductController(mock . Object) ;


controller . PageSize = 3 ;
11 Действие
ProductsListViewModel result
controller.List(null, 2) .ViewData.Model as ProductsListViewModel;
11 Утверждение
Product[J prodArray = result . Products .T oArray();
Assert.True(prodArray.Length == 2) ;
Assert . Equal(P4 , prodArray[O ] .Name);
Assert.Equal(P5 , prodArray[l] .Name) ;

Указывая null для category, мы получаем все объекты Product, которые контрол­
лер извлекает из хранилища, что воспроизводит ситуацию перед добавлением ново ­
го параметра. Такого же рода изменение необходимо внести также в тестовый метод
Can_Send_Pagination_View_Model():

[ Fact]
p u Ыic void Can Send Pagination View Model() {
11 Организация
Mock<IProductRepository> mock = new Mock<IProductRepository>();
mock . Setup(m => m.Products) .Returns(new Product[ ] {
new Product {ProductID 1 , Name Pl} ,
new Product {ProductID 2 , Name Р2},
new Product {ProductID З , Name РЗ} ,
new Product {ProductID 4 , Name Р4} ,
new Product {ProductID 5 , Name Р5}
}) ;

11 Организация
ProductController controller =
new ProductController(mock . Object) { PageSize 3 };
11 Действие
ProductsListViewModel result =
controller.List(null, 2) .ViewData.Model as ProductsListViewModel;
Глава 9. SportsStore: навигация 247
11 Утверждение
Paginginfo pageinfo = result. Paging l nfo ;
Assert . Equal(2, pageinfo . CurrentPage) ;
Assert.Equal(З , pageinfo.ItemsPerPage) ;
Assert . Equal(5 , pageinfo .T otalitem s ) ;
Assert . Equal(2 , pageinfo .T ota l Pages) ;

Когда вы примете образ мышления, ориентированный на тестирование, поддержание мо­


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

Чтобы увидеть результат фильтрации по категории , запустите приложение и вы­


берите категорию с помощью показанной ниже строки запроса, заменив номер порта
тем, который был назначен вашему проекту средой Visual Studio (позаботившись о
том, что Soccer начинается с прописной буквы S):
http://localhost : 60000/?category=Soccer
Вы увидите только товары из категории Soccer (рис. 9 . 1).
Очевидно, что пользователи не захотят переходить по категориям с прим енением
URL, но здесь было показано, что незначительные изменения в приложении МVС мо­
гут оказывать крупное влияние, если базовая структура на месте.

Put something
useful here Soccer Ball
later FIFA-approved size and VJeight

Corner Flags
Give your playing field а professlonal
touch

Stadium
Flat-packed 35,000-seat stadium

11 " j

Рис. 9.1. Использование строки запроса для фильтрации по категории


248 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

Модульное тестирование: фильтрациs~ по категории


-------
Нам необходим модульный тест для проверки функциональности фильтрации по кате­
гории, чтобы удостовериться в том , что фильтр может корректно генерировать сведения
о товарах у каз анной категории. Ни же приведен тестовый метод, добавленный в класс
ProductControllerTests:

[ Fact ]
puЬlic void Can Filter Products() {
11 Ор ган изация
- создание имитирова н ного хранилища
Mock<IProductRepository> mock = new Mock<IProductRepository>() ;
mock . Setup(m => m.Products) .Returns(new Product[] {
new Product {ProductID 1 , Name Pl , Catego r y = Catl} ,
new Produ ct {ProductID 2 , Name Р2, Category Cat2} ,
new Product {Pr oductID 3, Na me РЗ, Category = Catl},
new Product {ProductID 4, Name Р4 , Category = Cat2} ,
new Product {ProductID 5 , Name Р5 , Category = СаtЗ}
}) ;

11 Организация - создание контроллера и установка размера


// страницы в три элемента
ProductController controller = new ProductController(mock . Ob jec t} ;
controller . PageSize = З ;
11 Действие
Product[ ] resu l t =
(control l er . List(Cat2, 1) . ViewData .Model as ProductsListViewModel)
. Products.ToArray();
//Утверждение
Assert . Equal(2 , result.Length) ;
Assert . True(result[O] . Name Р2 && result[O] . Category Cat2);
Assert . True(result[l ] . Name == Р4 && result[l] . Category Cat2);

Этот тест создает имитированное хранилище, содержащее объекты Product , которые от­
носятся к диапазону категорий . С использованием метода действия List () запрашивается
одна специфическая категория, а результаты проверяются на предмет наличия корректных
объектов в пр авильном поряд ке.

Улучшение схемы URL


Мало кому понравится видеть либо иметь дело с неуклюжими URL вроде
/?category=Soccer . Для решения данной проблемы мы намерены изменить схему
маршрутизации в методе Configure () класса Startup, чтобы создать более удоб­
ный набор URL (листинг 9.3) .

Внимание ! Новые маршруты в листинге 9.3 ва ж но добавлять в показанном порядке .


Маршруты при меняются в поряд ке, в котором они определены, поэтому изменение по ­
рядка может привести к нежелательным эффектам.
Глава 9. SportsStore : навигация 249
Листинг 9.3. Изменение схемы маршрутизации в файле Startup. cs

puЬlic void Configure(IApplicationBuilder арр ,


IHostingEnvironment env , ILogger Fa ctory l oggerFactory) {
app.Use Deve l operExceptionPa ge() ;
app . UseStatusCodePages() ;
app.UseStaticFiles() ;
app . UseMvc(routes => {
routes .МapRoute (
name: null,
template: {category}/Page{page:int},
defaults: new { controller = Product, action = List }
) ;

routes.MapRoute(
name: null,
template: Page{page:int},
defaul ts: new { controller = Product, action = List, page =1 }
) ;

routes.МapRoute(
name: null,
template: {category},
defaults: new { controller = Product, action = List, page =1 }
) ;
routes .MapRoute (
name: null,
template: ,
defaul.ts : new { controller = Product, action = List, page =1 }
) ;
routes.МapRoute(name: null, template: {controller}/{action}/{id?});
) ) ;
SeedData . EnsurePopu l ated(app) ;

В табл. 9. 1 описана схема URL, которую представляют эти маршруты. Система


маршрутизации подробно объясняется в главах 15 и 16.

Таблица 9.1. Сводка по маршрутам

URL Что делает

/ Выводит первую стран и цу списка товаров всех категорий

/Page2 Выводит указанную страницу (в данном случае страницу 2 ),


отображая товары всех категорий

/Soccer Выводит первую страницу товаров указанной категории (Soccer)


/Soccer/Page2 Выводит указанную страницу (страницу 2) товаров заданной
категории (Soccer)
250 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

Система маршрутизации ASP.NET используется инфраструктурой MVC для об­


работки входящих запросов от пользователей, но также генерирует исходящие URL,
которые соответствуют схеме URL и потому могут встраиваться в веб-страницы.
Применение системы маршрутизации для обработки входящих запросов и генерации
исходящих URL позволяет гарантировать согласованность всех URL в приложении.
Интерфейс IUrlHelper предоставляет доступ к функциональности генерации
URL. Мы использовали этот интерфейс и определяемый им метод Action () в де­
скрипторном вспомогательном классе, созданном в предыдущей главе. Теперь. когда
нужно генерировать более сложные URL, необходим способ получения дополнитель­
ной информации от представления. не добавляя дополнительные свойства к дескрип­
торному вспомогательному классу. К счастью, дескрипторные вспомогательные клас­
сы обладают удобной возможностью, которая позволяет получать в одной коллекции
все свойства с общим префиксом (листинг 9.4).

Листинг 9.4. Получение значений атрибутов, снабженных префиксом,


в файле PageLinkTagHelper. cs
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
using SportsStore.Models.ViewModels;
using System.Collections.Generic;
namespace SportsStore.Infrastructure
[HtmlTargetElement(div, Attributes = page-model)]
puЫic class PageLinkTagHelper : TagHelper {
private IUrlHelperFactory urlHelperFactory;
puЫic PageLinkTagHelper(IUrlHelperFactory helperFactory)
urlHelperFactory = helperFactory;

[ViewContext]
[HtmlAttributeNotBound]
puЫic ViewContext ViewContext { get; set;

puЫic Paginginfo PageModel { get; set;


puЫic string PageAction { get; set; )
[HtmlAttributeName(DictionaryAttributePrefix = page-url-)]
puЬlic Dictionary<string, object> PageUrlValues { get; set;
= new Dictionary<string, object>();
puЫic bool PageClassesEnaЫed { get; set; } = false;
puЬlic string PageClass { get; set; }
puЫic string PageClassNormal { get; set; }
puЬlic string PageClassSelected { get; set;
puЬlicoverride void Process(TagHelperContext context,
TagHelperOutput output) {
IUrlHelper urlHelper = urlHelperFactory.GetUrlHelper(ViewContext);
TagBuilder result = new TagBuilder(div);
for (int i = l; i <= PageModel.TotalPages; i++) {
TagBuilder tag = new TagBuilder(a);
Глава 9. SportsStore: навигация 251
PageUrlValues[page] i;=
tag.Attributes[href] = urlHelper.Action(PageAction, PageUrlValues);
i f ( PageClas sesEnaЬled) {
tag . AddCssClass(PageClass);
tag . AddCssClass(i == PageModel.CurrentPage
? PageCl assSe l ected : PageClassNormal);

tag.InnerHtml . Append(i .ToString());


result .In nerHtml . AppendHtml(tag);

output . Content . AppendHtml(result .I nnerHtml);

Декорирование свойства дескрипторного вспомогательного класса посредс­


твом атрибута HtmlAttributeName позволяет указывать префикс для имен атри­
бутов элемента, которым в данном случае будет page - url-. Значение любого ат­
рибута, имя которого начинается с такого префикса, будет добавлено в словарь,
присваиваемый свойству Р age Ur 1Vа1 ue s . Затем это свойство передается методу
IUrlHelper . Action () с целью генерации URL для атрибута href элементов а , ко­
торые производит дескрипторный вспомогательный класс.
В листинге 9.5 к элементу di v добавлен новый атрибут, который обрабатывается
дескрипторным вспомогательным классом и указывает категорию, применяемую для

генерации URL. В представление был добавлен только один атрибут, но в словарь до­
бавился бы любой атрибут с тем же самым префиксом.

Листинг 9.5. Добавление нового атрибута в файле List. cshtml


@mode l ProductsListViewModel
@foreach (var р in Mode l. Products)
@Html . Partial(ProductSummary , р)

<div page-mode l =@Mode l.Paginginfo page - action=List


page - clas se s - enaЫed= tru e
page-class=btn page - class -no rmal=btn - defau lt
page-class-selected=btn-primarypage-url-category=@Model.CurrentCategory
class=btn -group pu ll- right>
</div>

До внесения этого изменения ссылки для перехода на страницы генерировались


так:

http : //<cepвep> : <пopт>/Page l

Если пользователь щелкнет на страничной ссылке подобного типа, то фильтр по


категории утрачивается и приложение отобразит страницу, содержащую товары всех
категорий.
За счет добавления текущей категории, получаемой из модели представления, вза­
мен генерируются URL следующего вида:

http : //<cepвep> : <пopт>/Chess/Pagel


252 Часть 1. Введение в инфраструктуру ASP. NEТ Соге MVC

Когда пользователь щелкает на ссьmке такого рода, текущая категория передается


методу действия List () и фильтрация сохраняется. После внесения данного измене­
ния можно посещать URL вроде /Chess или /Soccer и наблюдать, что страничные
ссылки, расположенные внизу, корре1пно включают категорию.

Построение меню навигации по категориям


Нам необходимо предложить пользователям способ выбора категории, который не
предусматривает ввод URL. Это означает, что мы должны отобразить список доступ­
ных категорий с отмеченной текущей 1<атегорией, если она есть. По мере построения
приложения список категорий будет задействован в более чем одном контроллере, по­
этому он должен быть самодостаточным и многократно используемым.
В инфраструктуре ASP.NET Core MVC поддерживается концепция компонентов
представлений, которые идеально подходят для создания единиц вроде много1<ратно
используемого навигационного элемента управления. Компонент представления -
это 1шасс С#, который предоставляет небольшой объем многократно используемой
приrтадной логики с возможностью выбора и отображения частичных представле­
ний Razor. Компоненты представлений подробно рассматриваются в главе 22.
В данном случае мы создадим компонент представления, который визуализиру­
ет навигационное меню и интегрирует его в приложение за счет обращения к этому
компоненту из разделяемой компоновки. Такой подход дает нам обычный класс С#,
который может содержать любую необходимую прикладную логику и который можно
подвергать модульному тестированию подобно любому другому классу. Это удобный
способ создания небольших сегментов приложения, сохраняя общий подход МVС.

Создание навигационного компонента представления


Создайте папку по имени Cornponents , которая по соглашению является местом
хранения компонентов представлений, и добавьте в нее файл класса под названием
NavigationMenuViewCornponent. cs с определением, показанным в листинге 9.6.

Листинг 9.6. Содержимое файла Naviga tionМenuViewComponen t. cs


из папки Componen ts
using Microsoft.AspNetCore . Mvc ;
namespace SportsStore . Components
puЫic class NavigationMenuViewComponent : ViewComponent
puЫic string Invoke() {
re tu rn Hello from the Nav View Component ;

Метод Invoke () компонента представления вызывается, когда компонент приме­


няется в представлении Razor. а результат, возвращаемый методом Invoke (), встав­
ляется в НТМL-разметку, отправляемую браузеру. Мы начали с простого компонента
представления, который возвращает строку, но вскоре заменим его динамическим
НТМL-содержимым.
Список категорий должен присутствовать на всех страницах, поэтому мы собира­
емся использовать компонент пр едставления в разделяемой компоновке. а не в отде-
Глава 9. SportsStore: навигация 253
льном представлении. Внутри компоновки компоненты представлений применяются
через выражение @awai t Component . InvokeAsync (),как показано в листинге 9.7.

Листинг 9.7. Использование компонента представления в файле _Layout.cshtml


<!DOCTYPE html>
<html>
<head>
<meta name=viewport con tent=wi dth=device-wi dth />
<l ink r el=styleshe et asp -hr ef - include=liЬ/bootstrap/dist/css/* . min .
css />
<title>SportsStore</tit le >
</head>
<body>
<div class=navbar navbar-inverse role =nav igation>
<а class =navbar -brand href=#>SPORTS STORE</a>
</div>
<div class=row panel>
<div id=categories clas s=col -xs-3>
@await Component.InvokeAsync(NavigationМenu)
</div>
<div class=col - xs-8>
@Render Body ()
</div>
</div>
</body>
</html>

Текст заполнителя Component . InvokeAsync () .


заменен вызовом метода
ViewComponent,
Аргументом этого метода является имя класса компонента без части
т.е. с помощью NavigationMenu указывается класс NavigationMenuViewComponent .
Запустив приложение, вы увидите, что вывод из метода InvokeAsync () включен в
НТМL-разметку, отправляемую браузеру (рис. 9.2).

SPORTS STORE 1

Hello from the


NavView Kayak lfifiфl
Component А boat fог one person

Рис . 9.2. Применение компонента представления


254 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

Генерация списков категорий


Теперь можно возвратиться к навигационному компоненту представления и сгене­
рировать реальный набор категорий. Построить НТМL-разметку для категорий можно
было бы вручную , как делалось в дескрипторном вспомогательном классе для стра­
ничных ссылок, но одно из преимуществ работы с компонентами представлений за­
ключается в том, что они могут визуализировать частичные представления Razor. Это
означает, что компонент представления можно использовать для генерации списка

категорий и затем применить более выразительный синтаксис Razor для визуализа­


ции НТМL-разметки, которая отобразит данный список. Первым делом нужно обно­
вить компонент представления, как показано в листинге 9.8.

Листинг 9.8. Добавление списка категорий в файле


NavigationМenuViewComponent.cs

using Microsoft . AspNetCore . Mvc ;


using System.Linq;
using SportsStore.Models;
namespace SportsStore . Components
puЫic class NavigationMenuViewComponent : ViewComponent {
private IProductRepository repository;
puЫic NavigationМenuViewComponent(IProductRepository repo)
repository = repo;

puЫic IViewComponentResul t Invoke ()


return View(repository . Products
.Select(x => x.Category)
. Distinct ()
.OrderBy(x => х));

Конструктор, определенный в листинге 9.8, принимает аргумент типа


IProductReposi t ory . Когда инфраструктуре MVC необходимо создать экземпляр
класса компонента представления, она отметит потребность в предоставленИи это­
го аргумента и просмотрит конфигурацию в классе Startup, чтобы выяснить, какой
объект реализации должен использоваться . Мы имеем дело с тем же самым ср едством
внедрения зависимостей, которое применялось в контроллере из главы 8, и результат
будет аналогичным - предоставление компоненту представления доступа к данным
без необходимости знать то, какая реализация хранилища будет использоваться. как
описано в главе 18.
В методе Invoke () с помощью LINQ выбирается и упорядочивается набор катего­
рий в хранилище, после чего он передается в качестве аргумента методу View (),к о­
торый визуализирует стандартное частичное представление Razor. Детали этого час­
тичного пр едставления возвращаются из метода с применением объекта реализации
IViewComponentResult (данный процесс будет подробно рассмотрен в главе 22).
Глава 9. SportsStore: навигация 255

Модульное тестирование: генерация списка категорий

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


относительно прост. Цель заключается в создании списка, который отсортирован в алфа­
витном порядке и не содержит дубликатов. Проще всего это сделать, построив тестовые
данные , которые имеют дублированные категории и не отсортированы долж ным образом,
передав их дескрипторному вспомогательному классу и установив утверждение, что данные

были соответствующим образом очищены . Вот модульный тест, который определяется в но­
вом файле класса по имени Na vig ationMenuViewComponen t Te sts. cs внутри проекта
SportsSto r e .Tests :
using Sys t em . Collections . Generic ;
using System . Linq ;
using Microsoft.AspNetCo r e . Mvc . ViewCompon e nts ;
using Moq ;
using Spo r t sS tore . Component s ;
us i ng Sport s Store . Models ;
using Xun i t ;
namespace SportsStore . Te st s
puЬlic class Navigation MenuV i ewCompon ent Tests
[Fact]
p uЫic void Сап Sel e c t Cate g orie s () (
11 О рган иза ция
Mock< I ProductRepos i to r y> mock = n ew Mock<IProductRep os i to r y>() ;
mock . Setup(m => m. Products) .Returns(new Product[ ] {
new Pr oduct {Produ ctID 1 , Name Pl , Category Apples },
new Product {Product I D 2 , Name Р2 , Category Apples} ,
new Product {Produc t ID 3 , Name РЗ , Category Pl ums} ,
new Pr odu c t (P r o ductI D 4 , Name Р 4 , Categor y Oran g es} ,
) ) ;

NavigationMenuViewComponent target =
new Navi gationMenuVi ewComponent(mock . Object) ;
11 Действие - п олучени е набора кате г орий
st rin g [ ] resu l .ts = ( ( IEnumeraЫe< st ring>) ( targe t. Invoke ()
as Vi ewViewCompon en t Res u lt) .V i e wDa t a . Model ) . ToA r ray() ;
11 У тве рж д е н ие
Asse rt. True(EnumeraЫe . Sequence Equ al(new s t ri n g [] { App l es ,
Or anges , Plums } , r e s u lts )) ;

Мы создаем имитированную реализацию хранилища, которая содержит повторяющиеся и


несортированные категории. Затем мы устанавливаем утверждение о том , что дубликаты
удалены и восстановлен алфавитный порядок.

Создание представления
Как будет объясняться в главе 22, при работе с представлениями, которые выби ­
раются ком понентами представлений , механизм Razor использует разнообразные со­
гл аш е ния . И стандартное имя представл е ния, и м естоположения , в которых ищется
256 Часть 1. Введение в инфраструктуру ASP.NET Саге MVC

представление, отличаются от тех, которые приняты для контроллеров. Создайте пап­


ку Views/Shared/Components/NavigationMenu и добавьте в нее файл представле­
ния по имени Default . cshtml с содержимым, приведенным в листинге 9.9.

Листинг 9.9. Содержимое файла Defaul t. cshtml из папки


Views/Shared/Components/NavigationМenu

@model IEnumeraЫe<string>

<а c lass=btn btn-Ьlock btn-default


asp-action=List
asp - co ntroller=Pr oduct
asp-route - category=>
Home
</а>
@foreach (stri ng ca tegory in Model) {
<а class=btn btn-Ыock btn -default
asp-a ction=List
asp-controller=Product
asp -route - category=@category
asp -r oute-page=l >
@category
</а>

В представлении применяется один из встроенных дескрипторных вспомогатель­


ных классов (описанных . в главах 24 и 25) для создания элементов а, атрибут href
которых содержит URL, выбирающий определенную категорию товаров.
Запустив приложение, вы увидите ссылки на категории (рис. 9.3). Щелчок на ка­
кой-то категории приводит к тому, что список товаров обновляется, отображая только
товары выбранной категории.

CJ Spo1tsStor~ Х

~ ".~ С 1t _
<D_loca
_ll10st:60000/Soccer/Pa9e1
_ _ _ _ __ _ _ _ _ ,

SРОПП.~ SЮПЕ

Home 1
1
Chess Soccer Ball
FIFA-approved size and weight 1
Soccer
1
Wa!ersports 1

1
1
.; ...J

Рис. 9.3. Генерация ссылок на категории с помощью компонента представления


Глава 9. SportsStore: навигация 257
подсветка текущей категории
В настоящий момент пользователь не располагает накой-нибудь визуальной под­
СI<азной о выбранной натегории. Хотя, основываясь на товарах в списке, можно вы­
двинуть предположение относительно 1<атегории, гораздо лучше предоставить более
наглядную визуальную обратную связь. Компоненты ASP.NET Core MVC, такие как
контроллеры и компоненты представлений, могут получать информацию о текущем
запросе, обращаясь к объекту контекста. Большую часть времени заботу о получении
объекта контекста можно поручить базовым классам, которые используются для со­
здания компонентов, подобно тому, как базовый класс Controller применяется для
создания контроллеров.

Базовый класс ViewComponent не является исключением и обеспечивает доступ


к объектам контекста через набор свойств. Одно из свойств называется RouteData
и предоставляет информацию о том, как URL запроса был обработан системой
маршрутизации.

В листинге 9 .1 О свойство Rou t е Da t а используется для доступа к данным запро­


са, чтобы получить значение выбранной в тенущий момент категории. Значение ка­
тегории можно было бы передать представлению путем создания еще одного класса
модели представления (и так бы делалось в реальном проенте), но ради разнообразия
применим объект ViewBag, ноторый был введен в главе 2.

Листинг 9.1 О. Передача выбранной категории в файле


NavigationМenuViewComponent.cs

using Microsoft.AspNetCore.Mvc ;
using System.Linq;
using SportsStore.Models ;
namespace SportsStore . Components
puЫic class NavigationMenuViewComponent : ViewComponent {
private IProductRepository repository;
puЬlic NavigationMenuViewComponent(IProductRepository repo)
repository = repo ;

puЫic IViewComponentResult Invoke() {


ViewBag.SelectedCategory = RouteData?.Values[category];
return View(repository .Products
. Select(x => x .Category)
. Distinct ()
. OrderBy(x => х));

Внутри метода Invoke () мы динамичесни создаем свойство SelectedCategory


в объекте ViewBag и устанавливаем его значение равным значению текущей катего­
рии, которое получаем через объент контекста, возвращенный свойством RouteData.
Как объяснялось в главе 2, ViewBag представляет собой динамический объент, 1юто­
рый позволяет определять новые свойства, просто присваивая им значения.
258 Часть 1. Введение в инфраструктуру ASP.NET Соге MVC

Модульное тестирование: сообщение о выбранной категории

Для выполнения проверки того , что компонент представления корректно добавил детали о
выбранной категории, в модульном тесте можно прочитать значение свойства ViewBag,
которое доступно через класс ViewViewComponentResult, описанный в главе 22 . Ниже
показан модульный тест, добавленный в класс NavigatioMenuViewComponentTests:

[Fact]
puЬlic void Indicates Selected Category()
11 Организация
string categoryToSelect = App les;
Mock<IProductRepository> mock = new Mock<IProductRepository>();
mock . Setup(m => m. Products) . Returns(new Product(] {
new Product {ProductID 1 , Name Pl , Category Apples} ,
new Product {ProductID = 4 , Name = Р2 , Category = Oranges} ,
}) ;
NavigationMenuViewComponent targe t =
new NavigationMenuViewComponent(mock . Ob j ect) ;
target . Vi ewComponentContext = new ViewComponentContext
ViewCon te xt = new ViewContext {
RouteData = new RouteData()
}
};
target .RouteData . Values(category] = categoryToSelect;
11 Действие
string result = (string) (target.Invoke() as
ViewViewComponentResult) . ViewData(SelectedCategory] ;
11 Утверждение
Assert .Equ al(categoryToSe le ct , result};

Этот модульный тест снабжает компонент представления данными маршрутизации через


свойство ViewComponentContext , посредством которого компоненты представлений
получают все свои данные контекста. Свойство ViewComponentContext предоставляет
доступ к данным контекста, специфичным для представления, с помощью своего свойства
ViewContext , которое, в свою очередь, обеспечивает доступ к информации о маршрути­
зации через свое свойство RouteData . Большая часть кода в модульном тесте связана с
созданием объектов контекста, которые будут предоставлять выбранную категорию таким
же способом, как она бы предлагалась во время выполнения приложения, когда данные кон­
текста предоставляются инфраструктурой ASP.NET Саге MVC.

Теперь , когда доступна информация о том, какая категория выбрана, можно обно­
вить пр едставление, выбираемое компонентом представления, чтобы задействовать
эту информацию, и и зм енить классы CSS, которые используются для стили зации
ссылок, сделав представление текущей категории отличающимся от остальных кате­
горий . В листинге 9 .11 приведено изменение, внесенное в файл Defaul t . cshtml.
Глава 9. SportsStore: навигация 259
Листинг 9.11. Подсветка текущей категории в файле Defaul t. cshtml

@model IEnumeraЫe< st ring>

<а class=btn btn - Ьloc k bt n- def a u l t


asp - ac t ion= Li st
asp - contro l ler= Pr oduc t
asp - ro u te - catego r y=>
Ноте
</а>

@fo r each (s t ring c a t egory in Model) {


<а class=btn Ьtn-Ыock
@(category == ViewBag.SelectedCategory? Ьtn-primary: Ьtn-default)
asp - act i on=Li st
asp -c on t roller =P rodu c t
asp - route - cate gor y=@cate gory
asp - ro u te - pa g e =l >
@category
</а>

С помощью выражения Razor внутри атрибута c lass мы применяем класс


Ьtn - pr irnary к элементу , который представляет выбранную категорию , и класс
Ьtn - default к остальным элементам . Указанные классы применяют разные стили
Bootstrap и делают активную кнопку визуально отличающейся (рис. 9.4).

D Sport1Stort Х

SPOf1fS SIORE

Home

Thinking Сар

Soccer
lmprove brain ef!lclency Ьу 75%

Waterspol1s
1

~Ch~--"'-~~j
Рис. 9.4. Подсвечивание выбранной категории

Корректировка счетчика страниц


Мы должны скорректировать ссылки на страницы , чтобы они правильно работали ,
когда выбрана какая-то кат егория. В настоящий момент количество ссьmо1< на стра­
ницы определя ется общим числом товаров в хранилище, а не количеством товаров
выбранной категории. Это значит, что пользователь может щелкнуть на ссылке для
страницы 2 категории Chess и получить пустую страницу, поскольку товаров данной
категории не хватает для заполнения второй страницы. Проблема демонстрируется
н а ри с. 9.5.
260 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

IJ SportsStore Х

f- С ~.;;;;IOOO/Chess/P•g".2

1.3
SРОПТS STOHE:

Home

Chess • '

Soccer

Wale r~ports 1
1
i
_:_ ___- - -' 1

Рис. 9.5. Отображение некорректных ссылок на страницы, когда выбрана какая-то категория

Проблему можно устранить, модифицировав метод действия List () в контролле­


ре Product так, чтобы категории принимались во внимание при разбиении на стра­
ницы (листинг 9. 12).
Листинг 9.12. Создание данных о разбиении на страницы, учитывающих категории,
в файле ProductCon troller. cs
using Microsoft.AspNetCore . Mvc ;
using SportsStore . Models ;
using System . Linq ;
using SportsStore.Models.ViewMode l s ;
namespace SportsStore . Controllers {
puЬlic class ProductController : Controller
private IProductRepository repository ;
puЬlic int PageSize = 4 ;
puЫic ProductController(IProductRepository repo) {
repository = repo;

puЬlic ViewRes ult List(string category , int page 1)


=> View(new ProductsListViewModel
Products = repository .P roducts
. Where(p => category == null 11 p . Category category)
. OrderBy(p => p .ProductID)
. Skip( (page - 1) * PageSize)
. Take(PageSize) ,
Paginginf o = new Paginginf o
CurrentPage = page ,
ItemsPerPage = PageSize ,
Totalitems = category == null ?
repository.Products.Count()
repository.Products.Where(e =>
e.Category == category) .Count()
}'
CurrentCategory = category
}) ;
Глава 9. SportsStore: навигация 261
Если категория была выбрана, тогда возвращается количество позиций в ней, а
если нет, то общее число товаров. Теперь во время просмотра товаров в какой-либо
категории ссылки в нижней части страницы корректно отражают количество товаров
в этой категории (р ис. 9.6).

С) Spott~S1 o rt Х
r: -~- ---· --- - - - - · - - - - · - - -··-··-------]
J ~ • С l-~~~~lh~:_~:::_~c!'.':~~"2.:~--- ----------·-----~-
Sf 'Olrl"!-> ~ТОНЕ

Нота

Тhinking Сар
Soccor lmprove brain efficiency Ьу 75%

Watorsports

Рис. 9.6. Отображение ссылок на страницы с учетом выбранной категории

Модульное тестирование: счетчик товаров определенной категории

Протестировать возможность генерации корректных счетчиков товаров для различных


категорий очень просто. Мы создадим имитированное хранилище, которое содержит из­
вестные данные в определенном диапазоне категорий, и затем вызовем метод действия
List (), запрашивая каждую категорию по очереди. Вот модульный тест, добавленный в
класс ProductControllerTests :

[Fact]
puЬlic void Generate_Category_Specific_Product_ Count() {
11 Организация
Mock<IProductRep os itory> mock = new Mock<IProductRepository>() ;
mock . Setup(m => m.Products) .Returns(new Product[] (
new Product {ProductID 1, Name Pl , Category Catl},
new Product {ProductID 2 , Name Р2 , Category = Cat2},
new Product {ProductID 3 , Name РЗ, Category = Catl},
new Product {ProductID 4 , Name Р4, Category = Cat2},
new Product {ProductID = 5 , Name PS , Category = СаtЗ)
}) ;

ProductController target = new ProductController(mock.Object);


target.PageSize = 3 ;
Func<ViewResult, ProductsListViewModel> GetModel = result =>
result?.ViewData?.Model as ProductsListViewModel ;
262 Часть 1. Введение в инфраструктуру ASP.NET Соге MVC

11 Действие
int? resl GetModel(target .Li st(Catl) )?.Paginginfo.Totalitems;
int? res2 = GetModel(target .List(Cat2) )?.Paginginfo.Totalitems;
int? resЗ = GetModel(target.List(CatЗ) )?.Paginginfo.Totalitems ;
int? resAll = GetModel(target.List(null))? . Paginginfo.Totalitems;
11 Утверждение
Assert.Equal(2 , resl);
Assert.Equal(2, res2) ;
Assert .Equal(l, resЗ);
Assert .Equal(5 , resAll);

Обратите внимание, что в модульном тесте также вызывается метод Lis t () без указания
категории, чтобы удостовериться в правильности подсчета общего количества товаров .

Построение корзины для покупок


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

Товары Ваша корзина Ввод информации


о доставке

Футбольный мяч 1Добавить в корзину 1 1 х Стадион $79,500.00 ". ит.д".

Угловые флажки 1Добавить в корзину 1 Всего $79,500.00


Стадион Добавить в корзину Перейти к оплате

...ii(-----11Продолжить покупку 1

Рис. 9. 7. Базовый поток корзины для покупок

Кнопка добавления в корзину (Add to cart) будет отображаться рядом с каждым


товаром в каталоге. Щелчок на ней будет приводить к выводу сводки по товарам,
которые уже были выбраны пользователем, включая общую стоимость. В этой точ­
ке пользователь может с помощью кнопки продолжения покупки (Continue shopping)
возвратиться в каталог товаров, а посредством кнопки перехода к оплате (Checkout
now) - сформировать заказ и завершить сеанс покупки.

Определение модели корзины


Начните с добавления в папку Models файла класса по имени Cart . cs с опреде­
лениями, показанными в листинге 9.13.

Листинг 9.13. Содержимое файла Cart. cs из папки Models


using System . Collections . Generic ;
using System . Linq;
Глава 9. Spo rtsStore : нав ига ция 263
narnespace SportsStore . Models {
puЫic class Cart {
private List<CartLine> lineCollection = new List<CartLine>();
puЫic virtual void Additern(Product product , int quantity)
CartLine line = lineCollection
. Where(p => p.Product . ProductID == product.ProductID)
.FirstOrDefault() ;
if (line == null) {
lineCollection . Add(new CartLine {
Product = product ,
Quantity = quantity
}) ;
else {
line.Quantity += quantity ;

puЫic virtual void RernoveLine(Product product) =>


lineCollection.RernoveAll(l => l . Product . ProductID ==
product.ProductID);
puЫic virtual decirnal CornputeTotalValue() =>
lineCollection . Surn(e => e . Product . Price * e . Quantity) ;
puЫic virtual void Clear() => lineCollection . Clear() ;
puЫic virtual IEnurneraЫe<CartLine> Lines => lineCollection;

puЫic class CartLine {


puЫic int CartLineID { get; set; }
puЫic Product Product { get; set; }

puЬlic int Quantity { get ; set; }

Класс Cart использует класс CartLine, который определен в том же самом файле
и представляет товар, выбранный пол ьзователем, а также приобретаемое его коли­
чество. Мы определили методы для добавления элемента в корзину, удаления элемен ­
та из корзины, вычисления общей стоимости элементов в корзине и очистки ко рзины
путем удаления всех элементов. Мы также предоставили свойство, которое позволяет
обратиться к содержимому корзины с применением IEnumeraЬle<CartLine>. Вс е
это легк о реализуется с помощью кода С# и небольшой доли к ода LINQ.

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

Класс Cart относительно прост, но в нем присутствует ряд важны х линий поведения , кото­
рые долж ны корректно работать. Неправильно фун к ционирующая корзина нарушит работу
всего приложения SportsStore . Мы разобьем средства на части и протестируем их по отде ­
льности. Для раз ме щения тестов в проекте SportsStore . Tests создается новый файл
по имени CartTests . cs .
Первая линия поведения относится к добавлению элемента в корзину. При первоначаль­
ном добавлении в корзину объекта Product должен быть добавлен новый экземпляр
CartLine. Ни же представлен тестовый метод вместе с определением класса модульного
тестирования.
264 Часть 1. Введение в инфраструктуру ASP. NEТ Core MVC

using System . Linq ;


using SportsStore.Models ;
using Xunit ;
namespace SportsStore . Tests
puЬl i c class CartTests {
[Fact]
puЫic void Can_Add_New_Lines() {
11 Организация - создание нескольких тестовых товаров
Product pl = new Product { ProductID = 1, Name Pl };
Product р2 = new Product { Product ID = 2 , Name = Р2 };
11 Организация - создание новой корзины
Cart target = new Cart() ;
11 Действие
target . Additem(pl , 1) ;
target . Additem(p2 , 1) ;
CartLine[] resu l ts = target .Li nes.ToAr r ay() ;
11 Утверждение
Assert . Equal(2 , results . Length) ;
Assert . Equal(pl , results[O] .Product );
Assert . Equal(p2 , results[l] .P rod uct) ;

Но если пользователь уже добавлял объе кт Produ ct в корзину, тогда необходимо уве­
личить количество в соответствующем экземпляре CartLine , а не создавать новый . Вот
модульный тест:

[Fact]
puЫic void Can Add Quantity For Existing Lines() {
11 Организация- создание нескольких тестовых товаров
Product pl = new Product { ProductID = 1, Name Pl };
Product р2 = new Product { Prod uctID = 2 , Name = Р2 };
11 Организация - создание новой корзины
Cart target = new Cart() ;
11 Действие
target.Additem(pl , 1 ) ;
target . Additem(p2 , 1) ;
target.Add i tem(pl , 10) ;
CartLine [] res ults = target .Lines
. OrderBy(c => c.Product.ProductI D) .ToAr ray() ;
11 Утверждение
Assert . Equal(2 , results.Length);
Assert . Equal(ll , results[O] .Quantity) ;
Assert . Equal(l , results[l ] . Quantity) ;

Нам также необходимо проверить, что пользователи имеют возмо ж ность менять свое
решение и удалять товары из корзины. Эта линия поведения реализуется методом
RemoveLine () . Модульный тест выглядит следующим образом :
Глава 9. SportsStore: навигация 265

[Fact]
puЫic voi d Can_ Remove Line () {
//Организация - создание нескольких тестовых товаров
Product pl new Product { ProductID 1 , Name Pl ) ;
Product р2 = new Product { Product I D = 2 , Name Р2 ) ;
Product рЗ = new Product { ProductID = 3 , Name РЗ ) ;

//Организация - создание новой корзины


Cart target = new Cart() ;
// Организация - добав лени е некоторых товаров в корзину
target . Additem(pl , 1) ;
target . Additem(p2 , 3) ;
target .Additem(pЗ , 5) ;
target . Additem(p2 , 1) ;
11 Действие
target . RemoveLine(p2) ;
11 Утверждение
Assert . Equal(O, target . Li nes . Where{c = > c . Product р2) . Count () ) ;
Assert . Equal(2 , target . Lines . Count()) ;

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

[ Fact ]
puЫic void Calculate_Cart_ Total() {
11 - создание нескольких тест овых
Организация товаров
Product pl = new Product { ProductID = 1, Name Pl , Pri ce lOOM );
Product р2 = new Produc t { ProductID = 2 , Name = Р2 , Price 50М );
/ / Организация - создание новой корзины
Cart target = new Cart();
11 Действие
target.Additem(pl , 1);
target.Additem(p2 , 1);
target.Additem(pl , 3) ;
decimal result = target . ComputeTo t a l Va lue () ;
11 Утверждение
Assert.Equa l (450M , result) ;

Последний тест очень прост. Мы дол ж ны удостовериться , что в результате оч и ст ки корзины


ее содер жим ое корректно удаляется. Ниже показан модульный тест.

[Fact ]
puЫic void Can_ Clear Contents() {
11 Организация- создание нескольких тест овых товаров
Product pl new Product { ProductID 1 , Name Pl , Pri ce lOOM } ;
Product р2 = new Product { ProductI D = 2 , Name = Р2 , Price 50М );
266 Часть 1. Введение в инфраструктуру ASP.NET Саге MVC

//Организация - создание новой корзины


Cart target = new Cart() ;
11 Организация - добавление нескольких элементов в корзину
ta r get . Additem(p l, 1);
target . Additem(p2 , 1 );
11 Действие - очистка корзины
target . Clear() ;
11 Утверждение
Assert .Equal(O , target.Lines . Count()) ;

Временами , как в данном случае , код для тестирования функциональности класса получает­
ся намного длиннее и сложнее, чем код самого класса . Не допускайте, чтобы это приводило
к отказу от написания модульных тестов. Дефекты в простых классах, особенно в тех, кото­
рые играют настолько важную роль, как Cart в приложении SportsStore, могут оказывать
разрушительное воздействие.

Создание кнопок добавления в корзину


Нам необходимо модифицировать частичное представление Views / Shared/
ProductSummary. cshtml. добавив кнопки к спискам товаров. Чтобы подготовиться
к этому, добавьте в папку Infrastructure файл класса по имени UrlExtensions .
cs с определ ением расширяющего метода, приведенного в листинге 9.14.

Листинг 9.14. Содержимое файла UrlExtensions. cs из папки Infrastructure

using Microsoft . AspNetCore.Http ;


namespace SportsStore .I nfrastructure
puЫic static class UrlExtensions {
puЫic static string PathAndQuery(this HttpRequest request) =>
request.QueryString.HasValue
? ${request . Path}{request . QueryString}
: request.Path.ToString();

Расширяющий метод PathAndQuery () оперирует над классом HttpRequest, ис­


пользуемый инфраструктурой ASP.NET для описания НТТР-запрос а. Расширяющий
метод генерирует URL, по которому браузер будет возвращаться после обновления кор­
зины. принимая во внимание строку запроса. если она есть. В листинге 9.15 к файлу
импортирования представлений добавляется пространство имен , которое содержит
расширяющий метод. так что его можно применять в частичном представлении.

Листинг 9.15. Добавление пространства имен в файле_Viewimports. cshtml

@using SportsStore.Models
@using SportsStore.Models . ViewMode ls
@using SportsStore.Infrastructure
@addTagHelper * , Microsoft . AspNetCore.Mvc . TagHelpers
@addTagHelper SportsStore.Infrastructure.* , SportsStore
Глава 9. SportsStore: навигация 267
В листинге 9.16 показано частичное представление, описывающее каждый товар,
которое обновлено для включения кнопки Add То Cart (Добавить в корзину).

Листинг 9.16. Добавление кнопки в файле ProductSummary. cshtml


@model Product
<div class=well>
<hЗ>

<strong>@Model . Name</strong>
<span class=pull-right label label - primary>
@Model.Price.ToString(c)
</span>
</hЗ>
<form id=@Model.ProductID asp-action=AddToCart
asp-controller=Cart rnethod=post>
<input type=hidden asp-for=ProductID />
<input type=hidden name=returnUrl
value=@ViewContext.HttpContext.Request.PathAndQuery() />
<span class=lead>
@Model.Description
<button type=suЬmit class=btn btn-success btn-srn pull-right>
Add То Cart
</button>
</span>
</forrn>
</div>

Мы добавили элемент form, содержащий скрытые элементы input, которые ус­


танавливают значе ние ProductID из модели представления и URL, куда браузер
должен возвращаться после обновления корзины. Элемент form и один из элементов
input конфигурируются с использованием встроенных дескрипторных вспомогатель­
ных классов, что является удобным способом генерирования форм, которые содержат
значения модели и нацелены на контроллеры и действия в приложении , как описано
в главе 24. Во втором элементе input применяется расширяющий метод, созданный
для установки URL возврата. Кроме того, добавлен элемент button, который будет
отправлять форму прило жению.

На заметку! Обратите внимание, что атрибут method элемента form установлен в post,
что инструктирует браузер относительно отправки данных формы с использованием
НТТР-метода POST . Вы можете это изменить и заставить форму применять НТТР -м етод
GET, но дол ж ны соблюдать осторожность . Спецификация НТТР требует, чтобы запросы
GET были идемпотентными, т.е . они не должны приводить к изменениям, а добавление
товара в корзину определенно считается изменением. Более подробно эта тема будет
обсуждаться в главе 16, включая объяснение того, что может произойти, если проигнори­
ровать требование идемпотентности запросов GET.

Включение поддержки сеансов


Мы собираемся сохранять детали корзины пользователя с использованием состоя­
ния сеанса, что представляет собой данные, которые хранятся на сервере и ассоции­
руются с последовательностью запросов, сделанных пользователем. Инфраструктура
268 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

ASP.NET предлагает целый ряд разных способов хранения состояния сеанса, в том
числе хранение его в памяти, что мы и будем применять. Преимуществом такого под­
хода является простота , но данные сеанса будут утеряны, когда приложение останав­
ливается или перезапускается.

Прежде всего, к приложению SportsStore понадобится добавить несколько новых па­


кетов NuGet. В листинг е 9.17 демонстрируются добавления в файле proj ect . j son.

Листинг 9.17. Добавление пакетов в файле project. j son внутри проекта SportsStore

dependencies : {
Microsoft . NETCore . App :
version : 1 . 0.0,
type : platform
} 1

Mic r osoft . AspNetCore.Diagnostics : 1.0.0,


Microsoft.AspNetCore.Server.IISintegration: 1.0. О ,
Microsoft . AspNetCore.Server.Kes t r el: 1 . 0 . 0 ,
Microsoft . Extensions . Logging . Conso le: 1 . 0 . 0 ,
Microsoft.AspNetCore.Razor.Tools : {
version : l . 0 . 0-preview2-fi nal ,
type : build
} 1

Microsoft . AspNetCore.Static Fi le s: 1.0.О ,


Microsoft . AspNetCore .Mvc : 1. 0 . 0 ,
Microsoft . EntityFrameworkCore .SqlServe r : 1.0.0,
Microsoft . EntityFrameworkCore . Tools: l . 0 . 0-preview2-final ,
Microsoft.Extensions.Configuration.Json: 1.0.0,
Microsoft.AspNetCore.Session: 1.0.0,
Microsoft.Extensions.Caching.Mernory: 1.0.О,
Microsoft.AspNetCore . Http.Extensions: 1.0.0
} 1

Включение поддержки сеансов требует добавления в класс Startup служб и про­


межуточного программного обеспечения (листинг 9.18).

Листинг 9.18. Включение поддержки сеансов в файле Startup. cs


usi ng Microsoft . AspN etCor e.Bui lder ;
usi ng Microsoft . AspNetCore . Hosting;
using Microsoft . AspNetCore . Http ;
us ing Microsoft . Extensions . Dependenc yi nject ion ;
us ing Microsoft . Extensions . Logging ;
using SportsStore . Models ;
us ing Microsoft . Extensions . Configu rati on ;
usi ng Microsoft . EntityFrameworkCo re;
namespace SportsStore {
puЫic class Startup {
IConfigurationRoot Configura ti on;
puЫic St artup(IHostingEnvironment env) {
Configuration = new ConfigurationBuilder()
Глава 9. SportsStore: навигация 269
. SetBasePath(env . ContentRootPath)
. AddJsonFile(appsettings .j son) . Build() ;

puЫic void ConfigureServices(IServiceCollection services)


services . AddDbCon t ext<ApplicationDbContext>(options =>
options . UseS q lServer(
Configurat i on[Data : SportStore Pr odu cts : Connection String ] )) ;
services . AddTransient<IProductRe pos i tory , EFProductRepos it ory>() ;
services . AddMvc() ;
services.AddМemoryCache();
services.AddSession();

puЫic void Configure( I ApplicationBu i lder арр ,


IHostingEnvironment e nv , I LoggerFacto r y loggerFacto r y) {
app . UseDeveloperExcept i o n Page() ;
app . UseSta t us CodePages() ;
app . UseStati c Fil es() ;
app.UseSession();
app . UseMvc(routes => {
11 .. . для краткости конфигурация маршрутизации не показана .. .
}) ;
SeedData. En surePopu l ated(app) ;

Вызов м етод а AddMemoryCache () настраивает хранилище данных в памяти.


М етод AddSession () регистр и рует служ бы, используемые для доступа к данным се ­
анс а , а м етод UseS e ssion () позволяет систем е сеансов автоматичесии а ссоцииро­
вать з апросы с сеансами, когда они поступают от клиента .

Реализация контроллера для корзины


Для обработки щелчков на кнопках Add То Сагt понадобится создать контроллер .
Доб авьте в папку Controllers файл класса по имени CartContro ll er . cs с опр еде­
ле нием , пр едставленным в листинге 9.19.

Листинг 9.19. Содержимое файла CartController. cs из папки Controllers


using System .Linq ;
using Microsof t. AspNetCore . Ht t p ;
using Microsoft . AspNetCore . Mvc ;
using SportsSto r e . Infrastructure ;
using SportsStore . Models ;
namespace SportsS t ore . Cont r ol l ers
puЬlic class Cart Cont r ol l e r : Contro ll e r
pr i vate IPro duct Re pos i tory repos itor y ;
puЫic Car t Contro ll er( IP roductRepo sit ory repo) {
repository = repo ;
270 Часть 1. Введение в инфраструктуру ASP.N ET Саге MVC

puЫic RedirectToActionResult AddToCart(int productid, string returnUrl) {


Product product = repository.Products
.Fir stOrDefault(p => p . ProductID == productid) ;
if (product != null) {
Cart cart = GetCart();
cart.Additem(product , 1) ;
Savecart(cart) ;

return RedirectToAction(Index , new { returnUrl )) ;

puЬlic RedirectToActionResult RemoveFromCart(int productid ,


string returnUrl) {
Product product = r epository . Products
.F irstOrDefault(p => p . ProductI D == productid);
if (product ! = nu ll) {
Cart cart = GetCart() ;
cart.RemoveLine(product);
SaveCart(cart);

return RedirectToAction(Index , new { returnUrl )) ;

private Cart GetCart() {


Cart cart = HttpContext.Session . GetJson<Cart >(Cart) ?? new Cart() ;
retu rn cart ;

private void SaveCart(Cart cart) {


HttpContext.Session.SetJson(Cart, cart);

Относительно этого контроллера необходимо сделать несколько замечаний. Для


сохранения и извлечения объ е ктов Cart применяется средство состояния сеанса
ASP.NET, с которым и взаимодействует метод GetCart (). Промежуточное програм­
мное обеспечение , зарегистрированное в предыдущем раздел е, использует сооkiе­
наборы или переписывание URL, чтобы ассоциировать вместе множество запросов
от определенного пользователя с целью формирования отдельного сеанса просмотра .
Связанным средством является состояние сеанса, которое ассоциирует данные с сеан­
сом. Это идеально подходит для класса Cart: мы хотим, чтобы каждый пользователь
имел собственную корзину, и эта корзина сохранялась между запросами. Данные.
связанные с сеансом, удаляются по истечении времени существования сеанса (обыч­
но из-за того , что пользователь не отправляет запрос какое-то время) , т.е . упр авлять
хранилищем или жизненным циклом объектов Cart не придется.
В методах действий и
RemoveFromCart () применялись имена пара ­
AddToCart ()
метров, которые соответствуют именам элементов input в НТМL-формах , созданных
в представлении ProductSummary. cshtml . Это позволяет инфраструктуре МVС ассо­
циировать входящие переменные НТТР-запроса POST формы с параметрами и означа ­
ет, что делать что-то самостоятельно для обработки формы не нужно. Такой процесс
называется привязкой модели и с его помощью можно значительно упрощать классы
контроллеров, как будет объясняться в главе 26.
Глава 9. SportsStore: навигация 271

Определение расширяющих методов состояния сеанса


Средство состояния сеанса в ASP.NET Core хранит только значения int, string
и byte []. Поскольку мы хотим сохранять объект Cart, необходимо определить рас­
ширяющие методы для интерфейса ISession, которые предоставят доступ к дан­
ным состояния сеанса с целью сериализации объектов Cart в формат JSON и их об­
ратного преобразования. Добавьте в папку Infrastructure файл класса по имени
SessionExtensions . cs с определениями расширяющих методов, показанными в
листинге 9.20.

Листинг 9.20. Содержимое файла SessionExtensions. cs


из папки Infrastructure

using Microsoft . AspNetCore.Http;


using Microsoft .Asp NetCore . Http . Features;
using Newtonsoft . Json;
namespace SportsStore . Infrastructure {
puЬlic static class SessionExtensions
puЫic static void SetJson(this ISession session,
string key , object value)
session . SetString(key , JsonConvert . SerializeObject(value) ) ;

puЬlic static Т GetJson<T>(this ISession session , string key) {


var sessionData = session.GetString(key);
return sessionData == null
? default(T) : JsonConvert.DeserializeObject<T>(sessionData);

При сериализации объектов в формат JSON (JavaScript Object Notation - систе­


ма обозначений для объектов JavaScript) эти методы полагаются на пакет Json.NET
(глава 20). Пакет Json.NET не потребуется добавлять в файл package. j son, т.к. он
уже используется "за кулисами" инфраструктурой MVC для поддержки средства заго­
ловков JSON, которое описано в главе 21. (И нформация о работе с пакетом Json.NET
напрямую доступна по адресу www . newtonsoft . сот/ j son.)
Расширяющие методы делают сохранение и извлечение объектов Cart очень лег­

ким . Для добавления объекта Cart к состоянию сеанса в контроллере применяется


следующий вызов:

HttpContext . Session .SetJson (Cart , cart) ;

Свойство HttpContext определено в базовом классе Controller, от которого


обычно унаследованы контроллеры, и возвращает объект HttpContext . Этот объект
предоставляет данные контекста о запросе, который был получен, и ответе, находя­
щемся в процессе подготовки. Свойство HttpContext. Session возвращает объект,
реализующий интерфейс ISession. Данный интерфейс является именно тем типом,
где мы определили метод SetJson (), принимающий аргументы, в которых указы-
272 Часть 1. Введение в инфраструктуру ASP.NEТ Core MVC

ваются ключ и объект, подлежащий добавлению в состояние сеанса . Расширяющий


метод сериализирует объект и добавляет его в состояние сеанса, используя функцио­
нальность, которая лежит в основе интерфейса ISession.
Чтобы извлечь объект Cart, применяется другой расширяющий метод, которому
указывается тот же самый ключ:

Cart cart = HttpContext.Session.GetJson<Cart>(Cart);

Параметр типа позволяет задать тип объекта, который ожидается извлечь; этот
тип используется в процессе десериализации.

Отображение содержимого корзины


Финальное замечание о контроллере Cart касается того, что методы AddToCart ()
и RemoveFromCart () вызывают метод RedirectToAction (). Результатом будет от­
правка клиентскому браузеру НТГР-инструкции перенаправления, которая заставит
браузер запросить новый URL. В данном случае браузер запросит URL, который вы­
зывает метод действияIndex () контроллера Cart.
Мы планируем реализовать метод Index () и применять его для отображения со­
держимого объекта Cart. Если вы еще раз взглянете на рис. 9.7, то увидите, что это
та часть рабочего потока, которая инициируется щелчком пользователя на кнопке
добавления в корзину.
Представлению, которое будет отображать содержимое корзины, необходимо пе­
редать две порции информации: объект Cart и URL для отображения в случае, если
пользователь щелкнет на кнопке Continue shopping (Продолжить покупку). Для этой
цели мы создадим простой класс модели представления. Добавьте в папку Models/
ViewModels проекта SportsStore файл класса по имени CartindexViewModel. cs
с содержимым, приведенным в листинге 9.21.

Листинг 9.21. Содержимое файла CartindexViewModel. cs


из папки Models/ViewModels
using SportsStore.Models;
namespace SportsStore.Models.ViewModels
puЫic class CartindexViewModel
puЫic Cart Cart { get ; set; }
puЬlic string ReturnUrl { get ; set;

Имея модель представления, можно реализовать метод действия Index () в нлассе


CartController, нан поназано в листинге 9.22.

Листинг 9.22. Реализация метода действия Index () в файле CartController. cs


using System.Linq;
using Microsoft .AspNe tCore.Http ;
using Microsoft .AspNetCore.Mvc ;
using SportsStore.Infrastructure;
using SportsStore.Models;
using SportsStore.Models.ViewModels;
Глава 9. SportsStore: навигация 273
namespace SportsStore . Co nt ro l lers {
puЫic class CartControl l er : Controller
private I ProductReposito r y reposito r y ;
puЫic CartControl l er( I Pr od u ctRepo s itory repo) {
repository = r e p o ;

puЫic ViewResul t Index (string returnUrl)


return View(new CartindexViewModel {
Cart = GetCart(),
ReturnUrl = returnUrl
}) ;

// .. . для краткости другие методы не показаны ...

Действи е Index извлекает объ ект Cart из состояния с е анса и использует его для
со здания объ е1па Car t indexViewModel, которы й зате м пер едается методу View ()
для пр име н е ния в качеств е модели пр едставл е ния.

Посл едний шаг при отображении содержимого кор зины предусматрива ет созда ­
ние пр едставл ения , которое будет визуализировать действи е I ndex. Со здайте папку
Vi ews/Cart и поместите в н ее файл представления Razor по им е ни Index . csh t rnl с
размет кой, приведенной в листинг е 9.23.

Листинг 9.23. Содержимое файла Index. cshtml из папки Views/Cart


@model CartindexViewMo d el
<h2>Your cart</h2>
<tаЫе c l ass= t a Ы e taЬle - bordered taЬle - striped>
<thead>
<tr>
<th>Qua n tity</th>
<th>Item</th>
<th class=text - r i g h t>Price</ t h>
<th class=text - r i g ht >S u btotal</ t h>
</tr>
</thead>

<tbody>
@foreach (va r li n e in Model . Ca r t .L i n es) {
<tr>
<td class = t e xt - center>@line . Quant i ty</td>
<td class=t e xt - left>@line . Prod u ct . Name</td>
<td class = text - r i g h t>@ l i n e . Pr o du c t. Price . ToString (c)</td>
<td c l ass= t ext - ri gh t>
@( (l ine . Qua n t i t y * l i n e . Pr o duct. Pri ce ) . ToSt r ing(c))
</td>
</tr>
)
</tbody>
274 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

<tf oo t >
<tr >
<t d c ol s p a n=З c l a s s =t ext-right>Total: </ t d>
<t d class= t ex t- right>
@Model . Cart .ComputeTo t a l Va l ue () .ToSt ring(c)
</td>
</tr>
</ tfoot >
< / t аЫ е>

<div class=text - cen ter >


<а c l a s s =b t n btn -p r ima r y hr e f=@Mod e l .Re turnU rl >Con t inue shopping</a>
</div>

Представление проходит по элементам в корзине и добавляет в НТМL-таблицу

строку для каждого элемента вместе со стоимостью и итоговой суммой по корзине .


Классы, назначенные элементам, соответствуют стилям Bootstrap для таблиц и вы­
равнивания текста .

В результате доступна базовая функциональность корзины для покупок. Во­


первых, товары выводятся вместе с кнопками Add То Cart (Добавить в корзину), как
показано на рис. 9.8.
Во-вторых, щелчок пользователя на кнопке Add То Cart приводит к добавлению со ­
ответствующего товара в его корзину и отображению сводной информации по корзи­
не (рис. 9.9). Щелчок на кнопке Continue shopping (Продолжить покупку) возвратит на
страницу товара , из которой произошел переход .

=-==-------==----=
D SportsStor. х

С [2 l~calh~t:бO_ooo..:;_=;c=~.=,=,fP=age1 fJ
SPO! !TS STOHE

Н от в

Thinking Сар
lmprove brai11 efficiency Ьу 75%
Soccer

Watersports

Unsteady Chair lf.VИJ


Secretly glve your opponent а disadvantage

Рис. 9.8. Кнопки Add То Cart


Глава 9. SportsStore: навигация 275

С] Spo r tsSto r~ Х

~ с @)-io~~~·~0ooo/c;~"1nd~~'::"~~~~2~~~~--~~=~=---~·==-~=--=~
Sf'ofПS STOllE.

1
Home
1
Your cart
Chess
1 Quantity ltem Price SuЫota l
1 Soccer
.! Tl1 inking Сар 51 6.00 $16.00
Wat erspoГis
1 Unsteady Chair 529.95 S29.95
1

! Human Chess Вoard 575.00 S75.00

1 Knyak $275.00 $275.00 •


:1 Tolal: $395.95
1.

l--=--J Ри с. 9.9. Отображение содержимого корзины для покупок

Резюме
В настоящей главе мы начали расширять пользовательские части приложения
SportsStore. Мы предоставили средства. с по мощью которых пользователь может пе­
реходить по категориям, и создали базовые строительные блоки, позволяющие добав­
лять элементы в корзину для покупок. В следующей главе разработка приложения
будет продолжена.
ГЛАВА 10
SportsStore:
завершение постр оения

корзины для покуп ок

в настоящей главе мы продолжим строить пример приложения SportsStore. В пре­


дыдущей главе мы добавили базовую поддержку I{Qрзины для покупок, а теперь
собираемся усовершенствовать и завершить создание этой функциональности.

Усовершенствование модели
корзины с помощью службы
В предыдущей главе был определен класс модели Ca r t и продемонстрирована воз­
можность его сохранения с использованием средства состояния сеанса, что давало

возможность пользователю формировать набор товаров для покупки . Обязанность по


управлению постоянством класса Cart возлагалась на контроллер Cart , в котором

явно определялись методы для получения и сохранения объектов Car t.


Проблема такого подхода в том , что нам пришлось дублировать код для получения
и сохранения объектов Car t во всех компонентах, которые его применяли . В этом
разделе мы собираемся использовать средство служб, находящееся в самой основе
инфраструктуры ASP.NET Core, чтобы упростить способ, которым управляются объ­
екты Ca rt, освобождая индивидуальные компоненты, такие как контроллер Cart , от
необходимости напрямую иметь дело с деталями.
Службы чаще всего применяются для сокрытия деталей реализации интерф е йсов
от компонентов , .которые от них зависят. Вы видели пример, когда создавалась служ ­
ба для интерфейса IP r o duct Repo si t o ry, которая позволила гладко заменить фик ­
тивный класс хранилища реальным хранилищем Entity Framework Core. Но службы
могут использоваться для решения множества других задач, а также применяться для

придания и изменения формы приложения, даже когда работа производится с конк­


ретными классами наподобие Cart.

Создание класса корзины, осведомленного о хранилище


Первым шагом по приведению в порядок способа использования класса Cart
будет создание подкласса, который осведомлен о том, как сохранять самого себя
с применением состояния сеанса . Добавьте в папку Mode l s файл класса по имени
SessionCa r t . cs и поместите в него определение, показанное в листинге 10.1.
Глава 1О. SportsStore : завершение построения корзины для по ку пок 277
Листинг 10.1. Содержимое файла SessionCart. cs из папки Models
using System ;
using Microsoft . AspNetCore .H ttp ;
using Microsoft . Extensions . Dependency inj ection ;
using Newtonsoft . Json;
using SportsStore . Infrastructure ;
namespace SportsStore . Models {
puЫic class SessionCart : Ca r t
puЫic static Cart GetCart(IService Provider services) {
ISession session = services . GetRequiredService<IHttpContextAccessor>()?
. HttpContext.Session ;
SessionCart cart = sess i on? . GetJson<SessionCart> ( " Cart " )
?? new SessionCart() ;
cart . Session = session ;
return cart ;

[Jsonignore]
puЫic ISession Session { get ; set; }
puЫic override void Additem(Product product , int quantity) {
base.Additem(product , quantity) ;
Session . SetJson( " Cart ", this) ;

puЬlic override void RemoveLine(Product product) {


base . RemoveLine(product) ;
Session . SetJson( " Cart ", this) ;

puЬlic override void Clear()


base . Clear() ;
Session . Remove( " Cart " ) ;
}
}

Класс SessionCart является производным от класса Cart и переопределяет мето­


ды Addrtem () , RemoveLine () и Clear () ,так что они вызывают базовые реализации
и затем сохраняют обновленное состояние в сеансе, используя расширяющие мето­
ды интерфейса ISession, которые были определ ены в главе 9. Статический метод
GetCart () - это ф абрика для создания объектов SessionCart и их пр едоставления
с помощью объекта реализации ISession, так что они могут себя сохранять.
Получение объекта реализации ISession несколько затруднено. Мы должны по­
лучить экземпляр службы IHttpContextAccessor , который предоставит доступ к
объекту HttpCont ext , а тот, в свою очередь, к объекту реализации ISession. Такой
окольный подход т ребуе тся из -за того, что сеанс не пр едоставляется как обычная
служба .

Регистрация службы
Сл едующий шаг заключается в создании службы для rшасса Cart . Цель в том,
чтобы удовлетворять запросы для объектов Cart выдачей объектов Session Cart,
которые будут сохранять себя самостоятельно. Создание службы иллюстрируется в
листинге 10.2.
278 Часть 1. Введение в инфраструктуру ASP.NET Со ге MVC

Листинг 10.2. Создание службы корзины в файле Startup. cs

puЬlic void ConfigureServices(IServiceCollection services)


services.AddDbContext<ApplicationDb Context>(options =>
opt i ons . UseSqlServer(
Configuration["Data : SportStoreProducts:ConnectionString " J)) ;
services . AddTransient<IProductRepository , EFProductRepository>() ;
services.AddScoped<Cart>(sp => SessionCart.GetCart(sp));
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services . AddMvc();
services . AddMemoryCache() ;
services . AddSession() ;

Метод AddScoped () уиазывает, что для удовлетворения связанных запросов к эк­


земплярам Cart должен применяться один и тот же объект. Способ связывания за­
просов может быть сконфигурирован, но по умолчанию это значит, что в ответ на
любой запрос энземпляра Cart со стороны компонентов , ноторые обрабатывают тот
же самый НТГР-запрос, будет выдаваться один и тот же объект.
Вместо предоставления методу AddScope d () отображения между типами, как
делалось для хранилища, уназывается лямбда-выражение, которое будет вьmолнять­
ся для удовлетворения запросов к Cart . Лямбда-выражение получает коллекцию
служб, которые были зарегистрированы, и передает ее методу GetCart () класса
SessionCart . В результате запросы для службы Ca r t будут обрабатываться путем
создания объектов Sess i onCart , которые сериали зируют сами себя как данные се­
анса, когда они модифицируются .
Мы также добавили службу с использованием метода AddSingleton () ,которы й
указывает, что всегда должен применяться один и тот же объект. Со зданная служба
сообщает инфраструктуре MVC о том, что когда требуются реализации интерфейса
IHttpConte xtAccessor , необходимо использовать класс HttpContextAccessor.
Данная служба обязательна, поэтому в классе SessionCart можно получать доступ
к текущему сеансу , как делалось в листинге 1О . 1.

Упрощение контроллера Cart


Преимущество создания службы такого вида свя зано с тем, что она позволит уп ­
ростить контроллеры , в которых применяются объекты Cart. В листинге 10.3 приве ­
ден переделанный класс CartControl l er , где задействована нов ая служба.

Листинг 10.3. Использование службы Cart в файле CartCon troller. cs


using System .L inq ;
using Microsoft.AspNetCore . Mvc ;
using SportsStore.Models ;
using SportsStore.Models . ViewMode ls;
namespace SportsStore . Controllers {
puЫic class CartController : Co ntrol ler
private IProductRepository repository ;
private Cart cart ;
Гл ава 1О. SportsStore: завер ш ение построения к орзины для по купок 279
puЫic CartControl ler (I ProductRepo sitory repo , Cart cartService)
repository = repo ;
cart = cartService ;

puЫic ViewResu l t Index(string returnUrl)


return View( ne w CartindexViewMode l {
Cart = cart ,
ReturnUrl = returnUrl
}) ;

puЬlic RedirectToActionResult AddToCart( int product i d , string returnUr l) {


Product product = repository . Products
. FirstOrDefault(p => p . ProductID == produ ctid) ;
i f (product ! = null) {
cart.Additem(product , 1) ;

return RedirectToAction( "Index", new { r eturnUrl }) ;

puЬlic RedirectToActionResult RemoveFromCart(int productid ,


string returnUr l) {
Prod uct product = repository . Products
.FirstOrDefault(p => p.ProductID == productid) ;
if {product != null) {
cart . RemoveLine(product) ;

return RedirectToAction( "Index " , new { returnUrl }) ;

Класс CartController указывает на то, что он нуждается в объекте Cart, за счет


объявления аргумента конструктора. Это позволяет удалить методы, которые читают
и записывают данные в сеанс, а также код, требующийся для записи обновлений.
Результатом явля ется контроллер , который не только проще , но и более сосредоточен
на своей роли в приложении, не беспокоясь о том , как объекты Cart создаются или
хранятся. И поскольку службы доступны по всему приложению, любой компонент мо­
жет получать корзину пользователя с применением одного и того же приема.

Завершение функциональности корзины


После ввода службы Cart наступило врем.я завершить построение функциональ­
ности корзины, добавив два новых средства . Первое средство позволит поль з ователю
удалять элемент из корзины. Второе средство даст возможность отображать итоговую
информацию по корзине в верхней части страницы.

Удаление элементов из корзины


Мы уже определили и протестировали метод действия Remove FromCart () в кон ­
троллере, поэтому для предоставления пользователям возможности удаления эле­

ментов достаточно лишь открыть доступ к данному методу в представлении, доба­


вив 1шопки Remove (Удалить) ко вс ем строкам в итоговой информации по корзине .
Изменения , которые понадобится внести в файл Views/Cart/ I ndex . cshtml, пока­
заны в листинге 10.4.
280 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

Листинг 10.4. Добавление кнопок Remove в файле Index.cshtml из папки Views/Cart

@model Ca r tindexViewModel
<h2>Your cart</h2>
<tаЫе class= " taЫe taЬle - bordered taЬle - striped " >
<thead>
<tr>
<th>Quantity</th>
<th> Item</th>
<th class= " text - right " >Price</th>
<th class= " text - right " >SuЬtotal</th>
</tr>
</thea d>
<tbody>
@foreach (var line in Model . Cart .L ines) {
<tr>
<td class= " text - center " >@line . Qu antity</td>
<td class= " text -l eft " >@ line.Product.Name</td>
<td class= " text-right " >@ l ine . Product.Price .T oString( " c " )</td>
<td class= " text - right " >
@( (line . Quantity * line. Product . Price) . ToString ( " с " ))
</td>
<td>
<form asp-action="RemoveFromCart" method="post">
<input type="hidden" name="ProductID"
value="@line.Product.ProductID" />
<input type="hidden" name="returnUrl"
value="@Model.ReturnUrl" />
<Ьutton type="suЬmit" class="Ьtn Ьtn-sm Ьtn-danger ">
Remove
</button>
</form>
</td>
</tr>

</tbody>
<tfoot>
<tr>
<td colspan= " З " c l ass= " text-right " >Total : </ td>
<td class= " text - right " >
@Mode l .Cart.ComputeTotalValue() . ToString( " c " )
</td>
</tr>
</tfoot>
</tаЫе>

<div class= " text - center " >


<а class= " Ьtn Ьtn-prirnary " href= " @Model . ReturnUrl " >Continue shopping</a>
</div>

Мы добавили J\ каждой строке таблицы новый столбец, содержащий элемент form


со скрытыми элементами input , которые указывают товар, подл ежащий удалению,
и URL возврата , а также кнопку для отправки формы .
Глава 1О. SportsStore: завершение nостроения корзины для nокуnок 281
Чтобы увидеть кнопки Remove в работе. запустите приложение и добавьте несколь­
ко элементов в корзину для покупок. Поскольку корзина уже обладает функциональ­
ностью удаления элементов, ее можно протестировать, щелкнув на одной из новых
кнопок (рис. 10.1).

IJ Sport$Stor~

~ с [~~ r;,~1~~~~~~-~;~~:~?~;~;~.n::~~~s~;-~~~~~~~~-~-=·.~~==--· ~l _

Homo
Your cart
Chess
Ouonlity ltem Price SuЬtota l
Soccer

""
Thlnking Сар S16.00 $16.00
Wate,sports

Unsleady Cl1В ir

Stadium
S29.95

$79,500.00

Total:
S29.95

$79,500.00

$79,545.95
1111 1

1
$79.500.00

Total:
$79.500.00

$79.5 16.00
"
11111

Fфjфii.ii, .!фi l ав"111


'""''....."''-"'''~'1<1~~~i!li!!!~lifll!ll~"~'-"'~~~~~~~~~-~ .i
l·.~~~~~~~~~~~~:1.~--~1
Рис. 10.1. Удаление элемента из корзины для покупок

Добавление виджета с итоговой информацией по корзине


Мы имеем функционирующую корзину , но еще должны определить способ ее встра­
ивания в пользовательский интерфейс . Пользователи могут выяснять, что находится
в их корзинах, только за счет просмотра экрана со сводкой по корзине. А попасть на
этот экран можно только путем добавления нового элемента в корзину.
Для решения упомянутой задачи мы создадим виджет (графический элемент) с
итоговой информацией по содержимому корзины, щелчок на котором будет приво­
дить к отображению товаров, находящихся в корзине, и сделаем его доступным в
любом месте приложения . Он будет реализован почти так же, как виджет для нави­
гации - в виде компонента представления, вывод которого может быть включен в
разделяемую компоновку Razor.

Добавление пакета Font Awesome


В качестве части итоговой информации по корзине мы будем отображать кнопку.
которая позволит пользователю перейти к оплате. Вместо отображения каких-либо слов
на кнопке мы хотим использовать символ корзины. При отсутствии художественных
навыков можно применить пакет с открытым кодом Font Awesome, предлагающий вели­
колепный набор значков, которые допускается интегрировать в приложения как шриф­
ты. где каждый символ шрифта представляет собой отдельное изображение. Получить
дополнительные сведения о пакете Font Awesome, а также просмотреть содержащиеся в
нем значки, можно по адресу http: / /fortawesome. github. i o/Fon t-Awesome .
282 Часть 1. Введение в инфраструктуру ASP.NET Саге MVC

Выберите проект SportsStore и щелкните на кнопке Show All ltems (Показать все
элементы) в верхней части окна Solution Explorer, чтобы отобразить ф айл bower . j s on.
Добавьте пакет Font Awesome в раздел dependencies этого файла (листинг 10.5).

Листинг 10.5. Добавление пакета Font Awesome в файле bower. j son

"narne": "as p . ne t ",


"pri va te ": t r ue ,
"dependencies ": {
"bootst r ap": " 3 . 3 . 6",
"fontawesome": "4.6.3"

После сохранения файла bower . j son среда Visual Studio с помощью инструмента
Bower загрузит и установит пакет Font Awesome в папку www/liЬ/fontawesorne.

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


Добавьте в папку Components файл класса по имени CartSummaryViewComponent . cs
и определите в нем компонент представления, показанный в листинг е 10.6.

Листинг 10.6. Содержимое файла CartSummaryViewComponen t. cs


из папки Componen ts
using Microso f t . AspN e tCore . Mvc ;
using SportsStore . Models ;
narnespace Spo r tsStore . Cornponents
puЬl i c c l ass Cart SurnrnaryViewCornponent : ViewComponen t
pr ivate Ca r t cart ;
puЬl i c Car t SurnrnaryViewCornponen t (Ca r t c a r tSe r vi ce) {
cart = cartService ;

IVi ewCornponentResult Invoke() {


puЬl i c
return Vi ew(ca r t) ;

Этот компонент представления способен задействовать в своих интересах службу ,


созданную ранее в главе для получения объекта Car t , принимая ее как аргумент конс ­
труктора. Результатом оказывается простой компонент представления, который пере ­
дает объект Cart методу View () ,чтобы сгенерировать фрагмент НТМL-разм етки для
включения в компоновку. Чтобы создать ко мпоновку, создайте папку Views/Sha r ed/
Components/CartSummary, добавьте в не е файл представления Razor по имени
Defaul t . c s html и поместите в этот файл разметку, приведенную в листинге 10.7.
Глава 1О. SportsStore: завершение построения корзины для покупок 283
Листинг 1О.7. Содержимое файлаDefaul t. csh tml из папки
Views/Shared/Components/CartSummary
@model Cart
<div class= "" >
@if (Model . Lines . Coun t(} > 0)
<small class= " navbar -tex t " >
<b>Your cart:</b>
@Model . Lines.Sum(x => x.Quanti ty) item(s)
@Model . ComputeTotalValue() . ToString( " c " )
</small>

<а class= "Ьtn Ьtn - sm Ьtn - defau lt navbar-Ьtn "


asp - controller="Cart" asp-ac ti on= " Index"
asp- route-returnurl="@ViewContext .Ht tpContext . Request . PathAndQuery() " >
<i class= " fa fa - shopping-cart " ></ i>
</а>
</div>

Представление отображает кнопку со значком корзины Font Awesome и, когда в кор­


зине присутствуют товары, пр едоставляет снимок, который сообщает количество эле­
ментов и общую сумму . Теперь, имея компонент представления и представление, мож­
но модифицировать разделяемую компоновку. чтобы итоговая информация по корзине
включалась в ответы, генерируемые контроллерами приложения (листинг 10.8).

Листинг 10.8. Добавление итоговой информации по корзине в файле_Layout. cshtml


< !DOCTYPE html>
<html>
<head>
<meta name =" viewport " content="width=device - width" />
<link rel="stylesheet " asp-href-include= " liЬ/bootstrap/dist/css/* . min.css" />
<link rel="stylesheet" asp-href-include="/lib/fontawesome/css/*.css" />
<title>SportsStore</title>
</head>
<body>
<div class= " navbar navbar-inverse " r ole= " navigation">
<а class= " navbar - b rand" href= " # " >SPORTS STORE</a>
<div class="pull-right">
@awai t Componen t. InvokeAsync ( "CartSuпnnary")
</div>
</div>
<div class= " row panel " >
<div id=" categories " class= " col-xs-3 " >
@await Component.InvokeAsync("NavigationMenu " )
</div>
<d i v class =" col - xs - 8 " >
@RenderBody ()
</div>
</div>
</body>
</html>
284 Часть 1. Введение в инфраструктуру ASP.NET Саге MVC

Запустив приложение, можно увидеть итоговую информацию по корзине. Когда


корзина пуста, отображается только кнопка перехода к оплате. По м е ре добавле­
ния элементов в корзину выводится их количество и общая стоимость (рис. 10.2).
Благодаря такому дополнению пользователь знает, какие товары находятся в корзине,
и получает очевидный путь для перехода к оплат е покупок.

yak
,oat for one person

~" .,,...__ .-Ь ,,..,,,.......,,... .,....U " • " • ф •• tr nь" ,


Рис. 10.2. Отображение итоговой информации no корзине

Отп ра в ка з а казо в
Итак, мы добрались до финального пользовательского средства приложе ния
SportsStore: возможности пер ехода к оплате и оформлению заказа. В последующих
разделах мы расширим модель предметной области , чтобы обеспечить поддержку по­
луч ения от пользователя под робной информации о достав1<е . и добавим в прилож е ни е
обработку этой информации.

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


Добавьте в папку Mode l s файл класса по имени Orde r. c s и приведите его содер­
жимое в соответствие с листингом 10.9. Этот класс будет использоваться для пр ед­
ставления информации о доставке пользователю.

Листинг 10.9. Содержимое файла Order. cs из папки Models


using System . Co l lect i ons . Generic ;
us ing System . ComponentModel . DataAnno t ations ;
u sing Microsof t. AspNetCore . Mvc . Model Binding ;
namespace SportsSto r e . Mode l s
p uЫic clas s Or de r {
[BindNever]
puЫic int OrderID { ge t; se t; }
[ BindNeve r ]
puЬlic I Col l ection<Ca r t Line> Lines { get ; set ; }

[Requi red (ErrorMessage = " Pl ea se e n te r а name" )]


11 В в е дит е имя
Глава 1О. SportsStore : завершение построения корзины для покупок 285
puЫic string Name { get ; set; )
[Required(ErrorMessage = "Pl ease enter the first add r ess line")]
//Введите первую строку адреса
puЫic string Linel get; set ; )
puЫic string Line2 get; set ; }
puЫic string LineЗ get; set; )
[Required(ErrorMessage = "Please enter а city name " )]
11 Вв едите название города
puЫic string Ci ty { get; set; )
[Required(ErrorMessage = " Please enter а state name " )]
11 Введите название штата
puЫic string State { get; set ; )
puЫic string Zip { get ; set; )
[Required (ErrorMessage = "P lease enter а country name")]
11 Введите название страны
puЫic string Country { get ; set; )
puЫic bool GiftWrap { get ; set ; )

Здесь применяются атрибуты проверки достоверности из пространства имен


Systern . CornponentModel . DataAnnotations, как мы делали в главе 2. Провер1<а до­
стоверности подробно рассматривается в главе 27.
Кроме того, используется атрибут BindNever , который предотвращает предо­
ставление пользователем значений для снабженных им свойств в НТТР-запросе. Это
ср едство системы привязки моделей, которая описана в главе 26.

Добавление реализации процесса оплаты


Наша цель заключается в том, чтобы обеспечить пользователям возможность
ввода информации о доставке и отправки заказа. Для начала потребуется добавить
к представлению итоговой информации по корзине кнопку Checkout (Перейти к оп­
лате). Изменения, которые необходимо внести в файл Views/Cart/Index . cshtrnl,
показаны в листинг е 10.10.

Листинг 10.10. Добавление кнопки Checkout в файле Index. cshtml из папки Views/Cart

<div class= " text - center " >


<а class= "btn Ьtn -p rima r y " href="@Model . ReturnUrl " >Continue shopping</a>
<а class="Ьtn Ьtn-primary" asp-action="Checkout" asp-
controller="Order">
Checkout
</а>
</div>

Изменения об е спечивают генерацию ссылки, стилизованной в виде кнопки, щел­


чок на которой приводит к вызову метода действия Checkout () контроллера Order,
создаваемого в следующем разделе. На рис. 10.З по1<азано, как выглядит эта кнопка .
286 Ч асть 1. Вв е д е ни е в инфраст рукту р у ASP.NET С оге MVC

Ноте

Your cart
Chess
Quantity ltem Price SuЫotaJ
Soccer

"
Ufejacket Sд8.95 $48.95
Watersports

Soccer Вall $19.50 $19.50


111

Ри с. 10 . З . К ноп к а Ch eckout

Теперь понадобится определить контроллер Order. Добавьте в папку Controllers


файл класса по имени OrderController . cs с определением, приведенным в листин­
ге 10 .11 .

Листинг 10. 11 . С одержимое файла Or d erContro ller. c s из папки Controllers

using Microsoft.AspNetCore . Mvc;


using SportsStore . Models ;
namespace SportsStore.Controllers
puЫic class OrderController : Controller
puЬlic ViewResult Checkout() => View(new Order()) ;

Метод
Checkout () возвращает стандартное представление и передает новый объ ­
ект ShippingDetails в качестве модели представления. Чтобы создать представле­
ние, создайте папку Views/Order и поместите в нее файл представления Razor по
имени Checkout. cshtml с разметкой. п оказанной в листинге 10.12.

Листинг 10. 12. Содержимое файла Check out. c shtml из папки Views /Or d er
@model Order
<h2>Check out now</h2>
<p>Please enter your details, and we ' ll ship your goods right away!</p>
<form asp - action= " Checkout" method="post" >
Глава 1О . SportsStore: завершение построения корзины для по купо к 287
<hЗ>Ship to</hЗ>
<div class= " form - group " >
<label>Name:</labe l ><input asp-for="Name" class="form-control " />
</d i v>
<hЗ>Address</hЗ>
<di v class= " form -gr oup">
<label>Line 1 :</label><input asp -for="Linel" class="form-control " />
</div>
<div class= "form-gr oup " >
<label>Line 2 :</labe l><inp ut asp-for="Lin e2 " class="form-control " />
</div>
<div class= "form-group" >
<label>Line 3 : </label><input asp -f or= " LineЗ" c lass ="form- control" />
</div>
<div class= " form -gr oup ">
<label>City : </label><input asp-for="City " class= " form - control" />
</div>
<div class= "form - group " >
<label>State:</label>< inp ut asp-for= "S tate " class= " form - control" />
</div>
<div class= " form -group" >
<label>Z ip:</label><input asp - for= " Zip " class= " form - cont rol" />
</div>
<div class= " form -gr oup " >
<label>Country : </label><input asp-for= "Country" class="form-control" />
</div>
<hЗ>Options</hЗ>
<div class="checkbox">
<label>
<input asp- f or ="Gi ftWrap " /> Gift wrap these items
</label>
</div>
<div class= " text - center " >
<input class= " Ьtn Ьtn-primary " type= "submit" value= "Complete Order " />
</div>
</form>

Для каждого свойства в модели мы создали элементы label и input для поль­
зовательского ввода, сформатированные с помощью Bootstrap. Атрибут asp -f or
в элементах input обрабатывается встроенным дескрипторным вспомогательным
классом, который генерирует атрибуты type , id, name и value на основе указанного
свойства модели, как объясняется в главе 24.
Чтобы увидеть результат добавления нового метода действия и представления
(рис. 10.4). запустите приложение, щелкните на кнопке со значком корзины в верх­
ней части страницы и затем щелкните на кнопке Checkout (Оплата) . Попасть на это
представление можно также, запросив URL вида /Order/Checkout.

Реализация обработки заказов


Мы будем обрабатывать заказы путем их записывания в базу данных. Разумеется,
большинство сайтов электронной коммерции на этом не останавливаются, но мы не
будем заниматься обработкой кредитных карт или других форм оплаты. Чтобы сосре ­
доточиться на MVC, вполне достаточно простого сохранения в базе данных .
288 Часть 1. Введение в инфраструктуру ASP. NEТ Core MVC

С) SportsStore
1 f- ·· С
Х
ГФ' 1ocalh;~;;QOO{o~-.~/Checka<1 1 ________________ __ " ______ */
1 ·-------------··-----·-·---·-·----·---··--·-"·--J
Sl'OfHS ~~ТОНЕ а

Horno

Chess
Check out now
Please enter your details, and \'1e'll ship your goods rk;ht 8\vay!
Soccer
Ship to
Waterspor1s
Name:

Address
Une 1:

Une2:

Un eз:

City:

State:

Zip:

Country;

Options
О Gift wrnp 11-.ese items

.•",.
Рис. 10.4. Форма для сбора деталей о доставке

Расширение базы данных


Когда на месте основной связующий код , созданный в главе 8 , добавить в базу дан ­
ных новый вид модели легко. Добавьте в класс контекста базы данных новое свойс­
тво , как показано в листинге 10.13.

Листинг 10.13. Добавление свойства в файле ApplicationDЬContext. cs


usi ng Mi c r oso f t .En tityFrarnewo r kC or e ;
narnes p ace Spo rt s Sto r e .Model s {
Глава 1О . SportsStore : завершение построения к орзины для покупок 289
puЫic class ApplicationDbContext : DbContext {
puЬlic ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base (options) { }
puЬlic DbSet<Product> Products { get ; set;
puЬlic DbSet<Order> Orders { get; set; }
}

Такое изменение является достаточным основанием для инфраструктуры Entity


Framework Core создать миграцию базы данных, которая по зволит объектам Order
сохраняться в базе данных. Для создания миграции откройте консоль диспетчера па­
кетов , выбрав пункт меню ToolsqNuGet Package Maпager (Сервис q Диспетчер пакетов
NuGet) , и запустите следующую команду:

Add - Migration Orders


Эта команда сообщает EF Core о необходимости получить новый снимок приложе­
ния, выяснить его отличия от предыдущей базы данных и сгенерировать новую миг­
рацию под названием Orders . Чтобы обновить схему базы данных, запустите такую
команду:

Update - Database

Переустановка базы данных

Если вы часто вносите изменения в модель, то столкнетесь с ситуацией, когда миграции и


схема базы данных потеряют синхронизацию . Самое простое, что можно сделать - удалить
базу данны х и начать заново . Однако, естественно, такой подход приемлем только на ста­
дии разработки, потому что все сохраненные данные будут утрачены .
Выберите пункт SQL Server Object Explorer (Проводник объектов SQL Server) из меню
View (Вид) среды Visual Studio и в от к рывшемся окне щелкните на кнопке Add SQL
Server (Добавить сервер SQL) . В поле Server Name (Имя сервера) введите ( localdb) \
mssqllocaldb и щелкните на кнопке Соппесt (Подключиться).

В окне проводника объектов SQL Server появится новый элемент, который можно раскрыть ,
чтобы увидеть созданные базы данных LocalDB. Щелкните правой кнопкой мыши на име­
ни базы данных , которую вы хотите удалить, и выберите в контекстном меню пункт Delete
(Удалить). В открывшемся диалоговом окне отметьте флажок для закрытия всех существу­
ющих подключений и затем щелкните на кнопке ОК, чтобы удалить базу данных .

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

Update - Database
Эта команда переустановит базу данных, так что она в точности отразит вашу модель и
позволит возвратиться к разработке приложения.

Создание хранилища заказов

Чтобы предоставить доступ 1< объектам Order, мы последуем тому же самому шаб­
лону, который использовался для хранилища товаров. Добавьте в папку Models файл
класса по имени IOrderReposi tory. cs и определите в нем интерфейс, приведенный
в листинге 10.14.
290 Часть 1. Введение в инфраструктуру ASP.NET Саге MVC

Листинг 10.14. Содержимое файла IOrderReposi tory. cs из папки Models


using System . Collections . Generic ;
namespace SportsStore.Models {
puЫic interface IOrderRepository
IEnumeraЫe <Order> Orders { get;
void SaveOrder(Order order) ;

Для реализации интерфейса хранилища заказов добавьте в папку Models файл


класса по имени EFOrderReposi tory . cs с определением , представленн ым в листин­
ге 10.15.

Листинг 10.15. Содержимое файла EFOrderReposi tory. cs из папки Models


using System . Collections . Generic ;
using Microsoft.EntityFrameworkCore ;
using System . Linq ;
namespace SportsStore . Models {
puЫic class EFOrderRepos i tory : I OrderRepository {
private ApplicationDbContext context ;
puЬlic EFOrderRepository(App li cationDbContext ctx)
context = ctx ;

puЫic IEnumeraЫe<Order> Orders => context . Orders


. Include(o => o.Lines)
. Theninclude(l => l.Product);
puЬlic void SaveOrder(Order order) {
context . AttachRange(order . Lines.Select(l => l.Product) ) ;
if (order.OrderID ==О) {
context.Orders.Add(order) ;

context.SaveChanges();

Класс EFOrderReposi tory реализует интерфейс IOrderReposi tory с примене­


нием Entity Framework Core. по з воляя извлекать набор сохраненных объектов Order
и создавать либо изменять заказы.

Особенности хранилища заказов

Реализация хранилища для заказов в листинге 10.15 требует небольшой дополнительной


работы . Инфраструктуру Eпtity Framework Core необходимо проинструктировать о загрузке
связанных данных, если они охватывают несколько таблиц. В листинге 10.15 с помощью ме­
тодов Incl ude () и Thenlnclude () указано, что когда объект Order читается из базы
данных, то также должна загружаться коллекция, ассоциированная со свойством Lines,
наряду с объектами Product, связанными с элементами коллекции:
Глава 1О . SportsStore : завершение nостроения корзины для nокуnок 291

puЫic IEnumeraЫe<Order> Ord e r s => co nt ext . Orders


. Include (o => o . Lines}
.Theninclude (l => l . Produ ct} ;

Это гарантирует получение всех объектов данных, которые нужны, не выполняя запросы и
не собирая данные напрямую.

Дополнительный шаг требуется та кже и при сохранении объекта Order в базе данны х .
Когда данные корзины пользователя десериализируются из состояния сеанса, пакет JSON
создает новые объе кты, не известные инфраструктуре Eпtity Framework Core , которая затем
пытается записать все объекты в базу данных. В случае объектов Produ ct это означает,
что инфраструктура EF Core попытается записать объекты, которые уже были сохранены,
что приведет к ошиб ке. Во избежание проблемы мы уведомляем Eпtity Framework Core о
том , что объекты существуют и не должны сохраняться в базе данных до тех пор, по ка они
не будут модифицированы:

context .AttachRange (order.Li n es . Select(l => l . Product}} ;

В результате инфраструктура EF Core не будет пытаться записывать десериализированные


объекты Product , которые ассоциированы с объектом Order .

Затем хранилище заказов регистрируется как служба в методе ConfigureServices (}


кла сса Startup (ли стинг 10. 16).

Листинг 10.16. Регистрация службы хранилища заказов в файле Startup. cs

puЬlic void ConfigureServi ces(IServiceCo l lection services}


services . AddDbContext<App l icat i onDbContext>(option s =>
options . UseSqlSe r ver(
Config u ratio n[" Data : Spo rt StorePro duct s : Conne ct ionStri ng"J) } ;
services . AddTrans i ent<IPro du ctRepos i to r y , EFProduc t Re p os it o r y>(} ;
services . AddScoped<Cart>(sp => SessionCart.GetCart(sp}) ;
services . AddSing l eton< I HttpContextAcce s sor , HttpContextAccessor>(} ;
services.AddTransient<IOrderRepository, EFOrderRepository>();
services . AddMvc(} ;
services . AddMemoryCache() ;
services . AddSession(} ;

Заверше н и е построени я контроллера Order


Для завершен ия класса Ord e r Con tro l ler понадобится модифицировать конс ­
т руктор так , чтобы он получ ал службы , требующи е ся е му для обработки з ака за , и
добавить новый м етод действия, который будет обрабатывать НТГР-запрос POST
формы , когда пользователь щелкает на кнопке Complete order (Завершить заказ). Оба
и змене ния показаны в листинге l О. l 7.
Метод дей ствияCheckout (} декорирован атрибутом Ht tpPost, т. е. он будет вы­
зываться для запроса POS T - в это м случае, когда пользователь отправля ет форму.
Мы снова полагае мся на систему привязки моделей, так что можно получить объект
Order , дополнить е го данными из объекта Ca rt и сохранить в хранилище .
292 Часть 1. Введение в инфраструктуру ASP.NEТ Core MVC

Листинг 10.17. За вершение контроллера в файле OrderCon troller. cs


using Microsoft . AspNetCore . Mvc ;
using Spo rtsStore . Models ;
using System. Linq;
namespace SportsStore . Control l ers
puЫic class OrderController : Contro lle r
private IOrderRepository repository;
private Cart cart;
puЫic OrderController(IOrderRepository repoService, Cart cartService) {
repository = repoService;
cart = cartService;

pu Ы i c ViewResult Checkout() => Vi ew( new Order()) ;


[HttpPost]
puЫic IActionResult Checkout(Order order) {
if (cart.Lines . Count() == 0) {
ModelState.AddМodelError("", "Sorry, your cart is empty!");

if (ModelState.IsValid) {
order . Lines = cart.Lines.ToArray();
repository.SaveOrder(order);
return RedirectToAction(nameof(Completed));
else {
return View(order);

puЫic ViewResul t Completed ()


cart.Cl.ear();
return View () ;

Инфраструктура МVС контролирует огран ич е ния пров ерки достов е рности, кото­
рые были прим е н е ны I< классу Order посредством атрибутов аннота ций данных , и
через свойство ModelState сообщает м етоду действия о любых пробл емах . Чтобы вы ­
яснить , есть ли пробл е мы, мы проверяем свойство ModelState . IsValid. Мы вызы­
ваем м етод Mode l State. AddModelError () для р е гистрации сообщения об ошибке,
есл и в корзине н ет элементов. Вскоре мы объясним , как отобр ажать таки е сообщения
об ошибках , а более подробное описание привязки модел ей и пров е рки достов е рности
будет представлено в гл авах 27 и 28.

Модульное тестирование: обработка заказа

Чтобы выполнить модульное тестирование класса Or derContro l ler, необходимо прове­


рить поведение версии POST метода Ch eckout () . Хотя этот метод выглядит коротким и
простым, использование привязки моделей MVC означает наличие многих вещей, происхо­
дящих "за кулисами", которые должны быть протестированы .
Глав а 1О. Spo rtsStore: завершение построения к орзины для по купо к 293
Мы хотим обрабатывать заказ , только если в корзине присутствуют элементы , и поль­
зователь предоставил достоверные детали о доставке . При любы х дру г и х обстоятельс ­
твах пользователю дол жн о быть сообщено об ошибке . Вот первый тестовый метод, кото ­
ры й определен в фа й ле класса по имени OrderControllerTests . cs внутри проекта
SportsStore . Tests:
using Microsoft . AspNetCore . Mvc ;
using Moq;
using SportsStore . Controllers;
using Spor tsSto re.Models;
using Xuni t ;
namespace SportsStore.Tests
puЬlic class OrderControllerTests
[Fact]
puЬlic void Cannot_ Checkou t_Empty_ Cart()
11 Организация - создание имитированн ого хранилища заказов
Mock<IOrderRepository> mock = new Mock<IOrderRepository>() ;
11 Организация - создание п устой корзины
Cart cart = new Cart () ;
11 Организация - создание заказа
Order order = new Order() ;
11 Организация - создание экземпля р а контроллера
OrderController target = new OrderController(mock.Object , cart) ;
11 Действие
ViewResult result = target . Checkout(order) as ViewResult ;
11 Утверждение - проверка , что заказ не был сохранен
mock . Verify(m => m. SaveOrder(It .I sAny<Order>()) , Times . Never);
11 Утверждение - проверка , что метод возвращает стандартное
11 представление
Asser t . True(string.IsNullOrEmpty(result . ViewName)) ;
11 Утверждение - проверка , что представлению передана
11 недопустимая модель
Assert . False(result . ViewData . ModelState . IsValid) ;

Тест проверяет отсутствие возмож ности перехода к оплате при пустой корзине. Мы удосто­
веряе м ся , что метод SaveOrder () и митированной реализации IOrderRepos i tory ни­
когда не вызывается, что метод возвращает стандартное представлен и е ( которое повторно
отобразит введенные пользователем данные, давая ему шанс откорре ктировать их) и что
состояние модели , передаваемое представлению, помечено ка к недопустимое. Это мо жет
выглядеть ка к и злишн е ограничивающий набор утверждений, но для провер к и правильности
поведения нуж ны все три утвер ждения. Следующий тестовый ме тод работает в основном
так же , но внедряет в модель представления ошибку, эмулирующую проблему, о которой
сообщает средство привяз ки модели (что долж но происходить в производственной среде,
когда пользователь вводит не корректные данные о достав ке):
294 Часть 1. Введение в и нфрастру ктуру ASP.NEТ Соге MVC

[Fact ]
puЫic void Cannot_ Checkout Invalid_ShippingDetails() {
//Организация - создание имитированного хранилища заказов
Mock<IOrderRepository> mock = new Mock<IOrderRepository>() ;
/ / Организация - создание корзины с одним элементом
Cart cart = new Cart() ;
cart . Additem(new Product() , 1) ;
//Организация - создание экземпляра контроллера
OrderControlle r target = new OrderController(mock . Object , cart) ;
//Организация - добавление ошибки в мо д ель
target.ModelState . AddModelError( " error ", " error " ) ;
// Действие - попытка перехода к оплате
ViewResult result = target.Checkout(new Order()) as ViewResult ;
11 Утверждение - проверка , что заказ не был сохранен
mock . Verify(m => m. SaveOrder(It . IsAny<Order>() ) , Times . Never) ;
//Утверждение - проверка , что метод возвращает стандартное
//представление
Assert . True(string . IsNullOrEmpty(result . ViewName)) ;
//Утверждение - проверка , что представлению передается
//не д опустимая модель
Assert . False(result.ViewData . ModelState . IsValid) ;

Удостоверившись в том, что пустая корзина или некорректные данные о доста в ке предо­
твращают сох ранение за каза , необходимо проверить , что нор м альные за казы со х раняются
дол жным образом . Ни же приведен тест.

[Fact]
puЫic void Can Checkout_And_S u bmit_Order() {
//Организация - создание имитированного хранилища заказов
Mock<IOrderRepository> mock = new Mock<IOrderRepository>() ;
//Организация - создание корзины с одним элементом
Cart cart = new Cart() ;
cart.Additem(new Product() , 1) ;
//Организация - соз д ание экземпляра контроллера
OrderController target = new OrderController(mock . Object , cart) ;
// Действие - попытка перехода к оплате
RedirectToActionResult result =
target.Checkout(new Order()) as RedirectToActionResult ;
//Утверждение - проверка , что заказ был сохранен
mock . Verify( m => m. SaveOrder(It . IsAny<Order>()) , Times . Once) ;

//Утверждение - проверка , что ме т од перенаправляется на действие Completed


Assert . Equal( " Completed ", result . ActionName) ;

Тестировать возможность идентификации допустимы х сведений о достав к е не нуж но . Это


автоматичес к и обрабатывается средством привязки моделей с использованием атрибутов ,
примененны х к свойствам класса Order .
Глава 1О . SportsStore: завершение построения корзины для покупок 295

Отображен ие сообщений об ошибках проверки достоверности


Для пров ерки пользовательских данных инфраструктура МVС будет использовать
атрибуты проверки достов е рности, примененные к классу Order . Тем не мен е е , чтобы
отобразить сообщения о проблемах, понадобится внести небольшое изменение. Здесь
задействован еще один встро енный дескрипторный вспомогательный класс, который
инспектирует со с тояние проверки достоверности данных, предоставленных пользова­

телем, и добавляет предупреждающие сообщения для каждой обнаруженной проблемы.


В листинге 10.18 демонстрируется добавление НТМL-элемента, который будет обраба­
тываться этим де скрипторным вспомогательным классом, в файл Chec kout . cs h tml .

Листинг 1О.1 В. Добавление области с итогами проверки достоверности


в файле Checkou t. csh tml
@model Or der
<h2>Check out now</h2>
<p>Please enter your detai l s , and we 'l l s h i p yo ur goods right away ! </p>
<div asp-validation-summary="All" class="text-danger"></div>
<form asp - action= " Checkout " method= "po s t " >
<hЗ>Sh ip to</hЗ>

Благодаря такому простому изменению пользователю отображаются сообщения


об ошибках проверки достоверности. Чтобы увидеть результат, посетите URL вида
/Order/Checkout и попробуйте перейти к оплате , не выбрав ни одного товара или
не указав сведения о доставке (рис. 10.5). Дескрипторный вспомогательный класс,
ген е рирующий такие сообщения, является частью системы проверки достоверности
моделей, которая подробно рассматривается в главе 27.

D SportsStore Х

~ • С l CD lo~alh~t:бOOOOjQ~d;;/o~-::~-~~'----=-~====-==--~
SPOH ГS STOf~Г:: . . а

Нот е

Check out now


Chess
Please ente r your details, and we'll ship your goods right aYJay!
Soccer
Please enter а name
Please enter the first add ress line
Watersports
Pleaso enter а с~у name
Plaase eoter а stale паm е
Please er1ter а country name
Sorry, your cart is empty 1

Ship to
Name:

Рис. 10.5. Отображение сообщений об ошибках проверки достоверности


296 Часть 1. Введение в инфраструктуру ASP.NET Соге MVC

Совет. Данные, отправленные пользователем, перед проверкой посылаются серверу, что


известно как проверка достоверности на стороне сервера, для которой инфраструктура
MVC предлагает великолепную поддержку. Проблема с проверкой достоверности на сто­
роне сервера заключается в том, что пользователю сообщается об ошибках лишь после
того, как данные отправлены серверу и обработаны, а также сгенерирована результирую­
щая страница - на занятом сервере все это может занять несколько секунд . По указан­
ной причине проверка достоверности на стороне сервера обычно дополняется провер­
кой достоверности на стороне клиента, при которой введенные пользователем значения
проверяются с помощью кода JavaScript до отправки серверу данных формы . Проверка
достоверности на стороне клиента будет описана в главе 27.

Отображение итоговой страницы


Чтобы завершить реализацию процесса оплаты, необходимо создать представ­
ление, которое будет отображаться, когда браузер перенаправляется на действие
Compl eted контроллера Order. Добавьте в папку Vi e ws /Order файл представления
Razor по имени Comp l eted . c s h tml и поместите в него разметку . приведенную в лис­
тинге 10.19.

Листинг 10.19. Содержимое файла Completed. cshtml из папки Views/Order


<h2> Th a nk s ! < / h 2 >
<p >Than ks for pla cin g yo u r o rder . </p>
<p >We'll s h i p yo u r goods as soon as poss iЫ e .</ p>

Для интеграции этого представления в приложение никаких изменений в код вно­


сить не придется, пос1юльку требуемые операторы уже были добавлены при опреде­
лении метода действия Comp leted () в листинге 10.17. Теперь пользователь может
проходить через весь процесс, начиная с выбора товаров и заканчивая переходом к
оплате. При условии, что пользователь предоставил корректные сведения о доставке
(и в корзине есть какие-то товары), после щелчка на кнопке Complete order он увидит
итоговую страницу (рис . 10.6).

Spor1sStore' Ord•r Subm Х

~ -> С ·15 i;;~~lh~~tб0000/ord;,:/Coinpt;t;d·---·-------·--·-------·----·- ~ Е

SPOATS STORE . . [~] .

Home Thanks!
1
Chess Thanks fог placing your огdе г.

We'll ship уош goods as soon as poss iЫe.


Soccer
1
1 Watersports
l _______________________________________ J
Рис. 10.6. Итоговая страница завершенного заказа
Глава 1О. SportsStore: завершение построения корзины для поку пок 297

Резюме
Мы завершили все основные части приложения SportsStore, отвечающие за вза­
имодействие с пользователями. Конечно, до сайта Aшazon приложению далеко. но в
нем имеется каталог товаров с возможностью просмотра по категориям и страницам,

аккуратная корзина для покупок и простой процесс оплаты.


Архитектура с хорошим разделением означает, что мы можем легко изменять

поведение любой порции приложения, не беспокоясь о возникновении проблем или


несовместимости где-либо в приложении. Например, мы могли бы изменить способ
сохранения заказов, и это никак бы не повлияло на 1<0рзину для покупок, каталог то ­
варов или любую другую область приложения. В следующей главе мы добавим средс­
тва, необходимые для администрирования приложения SportsStore.
ГЛАВА 11
SportsStore:
администрирование

в настоящей главе мы продолжим построение приложения SportsStore, чтобы


предоставить администратору сайта способ управления заказами и товарами.

Управление заказами
В предыдущей главе была добавлена поддержка для получения заказов от пользо­
вателей и сохранения их в базе данных. В этой главе мы собираемся создать простой
инструмент администрирования, который позволит просматривать получ е нные зака­
зы и помечать их как отгруженные.

Расширение модели
Первым изменением, которое необходимо внести, является расширение модели ,
чтобы можно было фиксировать, какие заказы были отгружены. В листинге 11.1
показано добавление нового свойства в класс Order , который определен в файл е
Order. cs внутри папки Models.

Листинг 11.1. Добавление свойства в файле Order. cs


using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Microsoft .AspNetCore . Mvc . ModelBinding ;
namespace SportsStore . Models
puЫic class Order {
[BindNever]
puЫic int OrderID { get ; set ; }
[BindNever]
puЫic I Col lection <CartLine> Lines { get ; set; }

[BindNever]
puЬlicbool Shipped { get; set; }
[Required(ErrorMessage =" Please enter а name " )]
/ / Введите имя
puЫic string Name { get; set; )
[Required(ErrorMessage = " Please enter the first address line")]
11 Введите пер вую строку адреса
Глава 11 . SportsStore: администрирование 299
p u Ьlic str ing Line l get; set;
puЫ i c str ing Line2 get; s et ;
puЫic stri ng LineЗ get; set;
[Required(ErrorMessage = "Pl ease enter а city name " ) ]
11 Вв едите название г орода
puЫic string City { get ; set; }
[Required (ErrorMessage = "Plea se e n ter а state name")]
11 Вве дите наз вание штата
puЫic string Sta te { get ; s et; }
puЫic st ri ng Zip { get; set ; }
[Required(ErrorMessage = "Ple ase ente r а country name")]
11 Вв едит е название страны
puЫic string Country { get ; s et ; }
рuЫ ic boo l Gi ftW r ap { get ; set; }

Такой итеративный подход к расширению и приспосабливанию модели для под­


держки ра зличных средств типичен при разработке приложений МVС. В идеальном
мире у нас была бы возможность полностью определить классы моделей в начале
прое1па и просто строить приложение на их основе, но так случается только в про­

стейших про е ктах, а на практике следует ожидать итеративную разработку по мере


понимания того , что требуется разрабатывать и развивать .
Миграции Entity Framework Core облегчают этот процесс , поскольку нам не при­
ходится вручную удерживать схему базы данных в синхронизированном состоянии с
кл ассами моделей, создавая и запуская команды SQL. Чтобы обновить базу данных
для отражения свойства Shipped , добавленного в класс Order, откройте консоль дис­
петчера пакетов и запустите следующие команды, которые создадут новую миграцию

и применят ее к базе данных:

Add- Migration Shippe d Orders


Opdate - Database

Добавление действий и представления


Функциональность, требуемая для отображения и обновления набора заказов в
базе данных, относительно проста, потому что она строится на основе средств и ин­
фраструктуры. которые бьши созданы в предшествующих главах. В листинге 11 .2 к
контроллеру Order добавляются два метода действий.

Листинг 11.2. Добавление двух методов действий в файле OrderController. cs


using Microsoft . AspNetCo re. Mv c ;
using SportsStore . Model s;
using System . Linq;
namespace SportsStore . Controllers
puЫic class OrderContro ll er : Controller
private IOrderRepository repository ;
private Cart cart ;
300 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

puЫic OrderController(IOrderRepository repoService , Cart cartService) {


repository = repoService ;
cart = cartService ;

puЬlicViewResul t List () =>


View(repository.Orders.Where(o => !o.Shipped));
[HttpPost]
puЬlic IActionResul t MarkShipped (int orderID) {
Order order = repository.Orders
.FirstOrDefault(o => o.OrderID orderID) ;
if (order != null) {
order.Shipped = true;
repository.SaveOrder(order);

return RedirectToAction(narneof(List));

puЫic ViewResult Checkout() => View(new Order()) ;


[HttpPost]
puЫic IActionResult Checkout(Order order) {
i f (cart.Lines . Count() ==О) {
ModelState . AddModelError( "", " Sorry , your cart is empty!");

if (ModelState . IsValid) {
order . Lines = cart . Lines . ToArray() ;
repository . SaveOrder(order) ;
return RedirectToAction(nameof(Completed)) ;
else {
return View(order) ;

puЫic ViewResult Completed() {


cart . Clear() ;
return View () ;

Метод List () выбирает все объекты Order в хранилище, свойство Shipped кото­
рых имеет значение false , и передает их стандартному представлению . Этот метод
действия будет использоваться для отображения администратору списиа неотгружен­
ных заказов.

М етод MarkShipped () будет получать запрос POST, указывающий идентифика­


тор заказа, который применяется для извлечения соответствующего объекта Order
из хранилища, чтобы установить его свойство Shipped в true и сохранить.
Для отображения списка неотгруженных зюtазов добавьте в папку Views/Order
файл представл ения Razor по имени List . cshtml и поместите в него разм ет1<у из
листинга 11.3. Элемент tаЫе используется для отображения ряда деталей, включая
сведения о приобретенных товарах.
Глава 11 . SportsStore : администрирование 301
Листинг 11.З. Содержимое файла List. cshtml из папки Views/Order

@mode l IEnumeraЫe<Order>

@{
ViewBag.Title = "Orders ";
Layout = "_AdminLayout ";
}
@if (Model . Count() > 0) {
<tаЫе class= " taЫe taЫe - bordered taЫe-striped" >
<tr><t h>Name</th><th>Zip</th>< t h colspan=" 2" >Detail s</th><t h></th></ tr>
@foreach (Order о in Model) {
<tr>
<td>@o . Name</td><td>@o . Zip</td><th>Product</th><th>Quantity</th>
<td> .
<form asp - ac ti on="Ma rkS hipped " method="post " >
<i nput type= "hidden " name ="orderid" value= " @o.OrderID " />
<butt on type= " s ubmit" class= "btn btn - sm btn -danger " >
Ship
</button>
</form>
</td>
</tr>
@foreach (CartLine line in o . Li nes) {
<tr>
<td colspan= " 2 " ></ t d>
<td>@line . Product . Name</td><td>@l ine. Quantity</td>
<td></td>
</tr>

</tаЫе>
else {
<div class= " text - center " >No Unshipped Orders</d i v>

Каждый заказ отображается с кнопкой Ship (Отгрузить) , которая отправляет фор­


му методу действия MarkShipped ().С помощью свойства Layout для пр едставления
List указана другая компоновка, которая переопределяет компоновку, заданную в
файле_ ViewStart . cshtml .
Для добавле ния компоновки создайт е в папке Vie ws/Share d файл по имени
_AdminLayout . cshtml с при м ен ением шабл она элемента MVC View Layout Page
(Страница компоновки представления MVC) и поместите в него разметку, показан­
ную в листинге 11.4.
Листинг 11.4. Содержимое файла _ AdminLayou t. csh tml из папки Views / Shared

< !DOCTYPE html >


<html>
<head>
<meta name= "viewport " content= " width=de vice - width" />
<link rel= "stylesheet " asp -hr ef -in clude= "li Ь/bootstrap/dist/css/* . min.css " />
<title>@ViewBag . Title</title>
</head>
302 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

<body class="panel panel-default " >


<div class= " panel-heading"><h4>@ViewBag.Title</h4></div>
<div class= " panel-body " >
@RenderBody ()
</div>
</body>
</html>

Чтобы просматривать и управлять заказами в приложении, запустите приложение,


выберите некоторые товары и перейдите к оплате. Затем посетите URL вида /Order /
List. Вы увидите сводку по созданным заказам (рис. 11.1). Щелкните на кнопке Ship;
база данных обновится, а список ожидающих заказов будет пуст.

~ Ord<rs Х

f- · С 1ф localhost.60000/ 0rder/list - -;-1 : 1


1--~~~..:=====~=-==:::.:::..:::..:::...=-~-==-7--=========-7-.=::::.:::.:::::::::::.:::...:=:..:::о....=:::_:::::_1...:._,
Orders 1

1
Name Zip Details

Alice 10036 Product

Lifejacket
Quantity

• 1
1

l ______ -
1
Soccer Вall

1
- - - - - "" ______ " ____ - · - - - - ",_" ____ ·-·--- -- - · - - - J

Рис. 11.1. Управление заказами

На заметку! В настоящий момент ничего не может воспрепятствовать запросу пользовате­


лем URL вида /Order/List и администрированию своего заказа. В главе 12 объясня­
ется, как ограничивать доступ к методам действий.

Добавление средств управления каталогом


Соглашение для управления более сложными коллекциями элементов предусмат­
ривает предоставление пользователю страниц двух типов: страницы списка и страни­

цы редактирования (рис. 11.2).


Вместе эти страницы позволяют пользователю создавать, читать, обновлять и уда­
лять (create, read, update, delete - CRUD) элементы в коллекции. Такие действия на­
зываются операциями CRUD. Разработчики нуждаются в реализации операций -СRUD
настолько часто, что средство формирования шаблонов в Visual Studio предлагает
сценарии для создания контроллеров CRUD с заранее определенными методами дейс­
твий (вЮiючение средства формирования шаблонов рассматривалось в главе 8) . Но,
как и со всеми шаблонами Visual Studio, я считаю, что изучать возможности ASP.NET
Core MVC лучше напрямую.
Глава 11. SportsStore: администрирование 303

List Screen Edit ltem: Kayak

ltem Actions Name: 1 Kayak


:=::=====:::::::
Kayak Edit 1Delete Description: 1 А boat for one ре. "
Lifejacket Edit 1Delete Category: Watersports
Soccer ball Edit 1Delete Price ($): 275 .00

Add New ltem Save Cance\

Рис. 11.2. Эскиз пользовательского интерфейса CRUD для каталога товаров

Создание контроллера CRUD


Начнем с создания отдельного контролле ра для управления каталогом товаров.
Добавьте в папку Controllers файл класса по имени AdminController . cs с кодом,
прив еденны м в листинге 11 .5.

Листинг 11.5. Содержимое файла AdminController. cs из папки Controllers


using Microsoft . AspNetCore.Mvc ;
using SportsStore . Models ;
namespace SportsStore.Controllers
puЫic class AdminController : Controller
private IProductRepository repository ;
puЫic AdminController(IProductRepository repo) {
repository = repo;

puЬlic ViewResult Index() => View(repository . Products) ;

В конструкторе контроллера объявлена зависимость от интерфейса


IProductReposi tory, которая будет распознаваться при создании экземпляров.
В классе контроллера определен единственный метод действия Index {), который
вызывает метод View () , чтобы выбрать стандартное представление для действия, и
передает ему в качестве модели п редставления набор товаров из базы данных.

М одульное тестирование: метод действия Index ()


Нас интересует поведение метода действия Index () в к онтроллере Admin , которое за­
ключается в корректном возвращении объектов Product из хранилища. Протестировать
это можно за счет создания имитированной реализации хранилища и сравнения тестовых
данных с данными, которые возвращает метод действия. Ниже показан код модульного
теста , помещенный в новый файл по имени AdminControllerTests. cs внутри проекта
SportsStore . Tests.
304 Часть 1. Введен и е в инфрастру ктуру ASP.NET Со ге MVC

using System . Collections . Generic ;


using System.Linq ;
using Microsoft . AspNetCore . Mvc;
using Moq;
using SportsStore.Controllers ;
using SportsStore . Models;
using Xunit ;
namespace SportsStore.Tests
puЬlic class AdminControllerTests
[ Fact ]
puЫic void Index_Contains_All Products() {
//Организация - создание имитированного хранилища
Mock<IProductRepository> mock = new Mock<IProductRepository>();
mock.Setup(m => m. Products) .Returns(new Product[] {
new Product {ProductID 1 , Name "Pl " } ,
new Product {ProductID 2 , Name " Р2 " } ,
new Product {ProductID 3 , Name " РЗ " } ,
) ) ;

//Организация - создание контроллера


AdminController target = new AdminController(mock . Object) ;
11 Действие
Product [ J result
= GetViewModel<IEnumeraЫe<Product>>(target . Index())? . ToArray() ;

//Утверждение
Assert . Equal(З , result.Length) ;
Assert.Equal( " Pl " , result [ OJ . Name) ;
Assert . Equal( " P2 " , result[l] . Name) ;
Assert.Equal( " PЗ ", result[2 ] . Name) ;

private Т GetViewModel<T>(IActionResult result) where Т class {


return (result as ViewResu l t)?.ViewData.Model as Т;

В тест был доб авлен метод GetViewModel () для расп аков к и ре зультата, воз враща е м ого
методом де йствия, и получен и я данных модели представления . Далее в глав е будут реали­
зованы дополнительные тесты , к оторые используют этот метод .

Реализация представления списка

Следую щим ш аг о м будет добавление п р едставления для метода действия Index ( )


контроллер а Admin. С оздайте пап ку Views/Admin и доб авьте в нее файл п редставле ­
ния Razor по имени Index . cshtml с содержимы м, приведенным в листин ге 11 .6.
Глава 11 . SportsStore : администрирование 305
Листинг 11.6. Содержимое файла Index. cshtml из папки Views/Admin

@model IEnumeraЫe<Product>
@{
ViewBag . Title = "All Produ cts ";
Layout = " _Adm inLayout";

<tаЫе class= " taЫe taЫ e -strip ed taЬle-bordered taЬle-condensed " >
<tr>
<th class= "text - right">ID</th>
<th>Name</th>
<th c l ass= "text - right " >Price</t h >
<th class= " text - ce n ter " >Actions</t h >
</tr>
@foreach (var item in Model) {
<tr>
<td class="text-right " >@ item.ProductID</td >
<td>@item . Name</td>
<td class= " text - right " >@item .P rice .T oStr in g (" c ") </td>
<td c l ass= " text - ce n ter " >
<form asp - ac ti on= " De l ete " method="post " >
<а asp-action= " Edit" class= "btn btn-sm btn -warni n g "
asp - route - productid= " @item .Produ ct I D" >
Edit
</а>
<input type= " hidden " name= " ProductID " value= " @item .Produ ctID " />
<button type= "s ubmit " class = "b tn btn - danger btn - sm">
Delete
</button>
</form>
</td>
</tr>

</tаЫе>
<div class= " text - cen te r" >
<а asp - act i on= " Create " class= "Ьt n Ьtn - primary " >Add Product</a>
</div>

Представление содержит таблицу, в ноторой для наждого товара предусмотрена


строна с ячейками, содержащими наименование и цену товара. Кроме того, в каждой
строке присутствуют I<нопки , которые по зволят редактировать сведения о товаре и

удалять его, отправляя запросы к действиям Edi t и Delete . В дополнение к таблице


имеется кнопка Add Product (Добавить товар), нацеленная на действие Crea te. Мы
добавим действия Edit, Delete и Create в по следующих разделах, а пока можно
посмотреть, как отображаются то вары, запустив приложение и запросив URL вида
/ Admin/ Index (рис. 11.3).

Совет. Кнопка Edit (Редактировать) находится внутри элемента form в листинге 11.6, так что
две кнопки располагаются рядом благодаря примененному интервалу Bootstrap. Кнопка
Edit будет посылать серверу НТТР-запрос GET для получения текущих сведений о товаре;
это не требует элемента form. Однако поскольку кнопка Delete (Удалить) будет вносить
изменение в состояние приложения , необходимо использовать НТТР-запрос POST, кото­
рый требует элемента form.
306 Часть 1. Введение в инфраструктуру ASP.NET Саге MVC

D All Products Х

j_:_ ~--С [~ loca~os~OOOO/Adm:~/ln~~-


All Products

ID Name Price Aclions

1 Kayak $275.00
"111
2 l ilejackel

3 Soccer ВaJI

4 Corner Flags
$48.95

$19.50

$34.95
., . "
Е181
5 S\adium 579.500.00
B lll
6 Т11in king С а р $16.00
-· 1111
7 Unsleady Chair $29.95
131111
8 Human Cl1ess Board $75.00
"' 111
9 Bling..Bling Кing $1,200.00
111 1
1

1
ФФtil
1
[__· - · - - - - - - ___._______ j
Рис. 11.3. Отображение списка товаров

Редактирование сведений о товарах


Чтобы предоставить средства создания и обновления, мы добавим страницу ре­
дактирования сведений о товаре, подобную показанной на рис. 11.2. Задача состоит
из двух частей:

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


для свойств товара;

• добавление метода действия, который обработает внесенные и зменения, когда


они будут отправлены.

Соэдание метода действия Edi t (;


В листинге 11 .7 приведен код метода действия Edi t ( ) , добавленного в контроллер
Adrni n, который будет получать Н1ТР-запрос, отправляемый браузером, когда пользо­
ватель щелкает на кнопке Edit.

Листинг 11.7. Добавление метода действия Edi t () в файле AdminController. cs


using Mic r osoft . AspNe tCo re. Mvc ;
using Spo r t sStor e. Mode ls;
using System.Linq;
Глава 11 . SportsStore : администрирование 307
namespace SportsStore . Controllers {
puЬlic c l as s AdminCo n t r o l l e r : Cont ro l le r
private I ProductReposi tor y r eposito r y ;
puЫic AdminController(IP r od u ctRepository repo) {
repository = repo ;

puЫic ViewResult I n dex() = > View(repository . Produc t s) ;


puЫic ViewResult Edit(int productid) =>
View(repository.Products
.FirstOrDefault(p => p.ProductID == productid));

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


нию п а раметра productid, и передает его как объект модели представления методу
Vi e w () .

М одульное тестирование: метод действия Edi t ()


В методе действия Edi t () нам необходимо протестировать две линии поведения. Первая
заключается в том, что мы получаем запрашиваемый товар, когда предоставляем допусти­
мое значение идентификатора, чтобы удостовериться в редактировании ожидаемого това­
ра. Вторая проверяемая линия поведения связана с тем, что мы не должны получать товар
при запросе значения идентификатора, отсутствующего в хранилище. Ниже показаны тес­
товые методы, добавленные в файл класса AdminControl l erTests . cs.

[Fact]
puЬlic void Can_Ed i t Prod uct( ) {
11 Организация - создание имитирова нн о г о храни л ища
Mock<IProductRepository> mock = new Mock<IProduc t Repository>() ;
moc k . Setup(m => m.Products) .Returns(ne w Produ ct [ ] {
new Product {ProductID 1, Name " Pl " ) ,
n e w Product { Product I D 2 , Name " Р2 "),
new Product {Product I D З , Na me " РЗ "},
}) ;
11 Организация - созда н ие ко н т р ол лера
AdminC o ntrol l er target = n ew Admi n Contro ll er(mock . Obj ect );
11 Д ействие
Pro duct pl GetViewMode l <Product>(target . Edit(l)) ;
Product р2 = GetV i ewModel <Product>(target . Edit(2)) ;
Product рЗ = GetViewMode l <Product>(ta rg e t.E dit(З) );
11 Утверждение
Assert . Equal(l , pl.Produ ct I D) ;
Assert . Equal(2 , p2 . Prod u ct ID ) ;
A ssert . Eq u al(З , pЗ . Prod u ct ID ) ;

[Fa c t ]
puЬlic void Cann ot_Edit_Non ex is te nt _ Pr o du ct( ) {
11 Организация - созда ни е ими т ирован н о г о хранилища
Mock<IProductRepository> mock = new Mock<IProductRepository>() ;
308 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

mock . Setup(m => m.Products) . Returns(new Product[]


new Product (ProductID 1, Name " Pl " } ,
new Product {ProductID 2 , Name " Р2 " ) ,
new Product ( ProductID 3, Name "РЗ"},
}) ;

11Организация - создание контроллера


AdminController target = new AdminController(mock.Object) ;
11 Действие
Product result = GetViewModel<Product>(target.Edit(4)) ;
//Утверждение
Assert . Null(result);

Создание представления редактирования


Теперь, располагая методом действия, можно создать представление для отоб­
ражения . Добав ьте в папку Views/Admin файл представления Razor по имени
Edi t . csh trnl и поместите в него разметку, приведенную в листинге 11.8.

Листинг 11.8. С одержимое файла Edi t. cshtml из папки Views/Admin


@model Product
@{
ViewBag.Title = " Edit Product" ;
Layout = "_AdminLayout ";

<form asp - action="Edit " method= "p ost " >


<input type= " hidden " asp-for= " ProductID " />
<div class="form- group " >
<label asp-for= " Name " ></label>
<input asp - for= " Name " class= " form - control " />
</div>
<div class= " form - group " >
<label asp - for="Description " ></label>
<textarea asp - for="Description " class= " form-control"></textarea>
</div>
<div class="form- group " >
<label asp - for= " Category"></label>
<input asp - for= " Category" class= " forrn - control " />
</div>
<div class= " form - group">
<label asp - for="Price " ></label>
<input asp - for= " Price " class= " form - control" />
</div>
<div class="text - center " >
<button class="btn btn - primary" type="submit " >Save</button>
<а asp - action= "I ndex " class="btn btn - default">Cancel</a>
</div>
</form>
Глава 11. SportsStore: администрирование 309
В представлении имеется форма HTML, большая часть содержимого которой гене­
рируется посредством дескрипторньrх вспомогательных классов, включая установку

целей для элементов form и а, установку содержимого элементов label и выдачу ат­
рибутов name, id и value для элементов input и textarea .
Чтобы увидеть НТМL-разметку, генерируемую представлением, запустите прило­
жение, перейдите на URL типа /Adrnin/Inde x и щелкните на кнопке Edit для одного
из товаров (рис. 11.4).

1 f- С i ф lo;a lhostбO·JOO/P.dm111/Edit?productld=3
1 -
i1 Edit Product

j Name
1 Soc.cer Ball

1 Description

FIFA-approved size and weighl


/,

Category

Soccer
1
Price

19.50
'
i
i - Cancel 1
1
j_ ·- - ----- ---- ---- _1
Рис. 11.4. Отображение сведений о товаре для редактирования

Совет. Скрытый элемент input для свойства ProductID применяется ради простоты.
Значение ProductID генерируется базой данных как первичный ключ, когда новый объ­
ект сохраняется инфраструктурой Eпtity Framework Саге, и его безопасное изменение мо­
жет оказаться сложным процессом.

Обновление хранилища товаров


Пр ежде чем можно будет обрабатывать результаты редактирования, хранили­
ще товаров понадобится расширить, добавив возможность сохранения изменений.
Первым делом необходимо добавить к интерфейсу IProductRepository новый ме­
тод (листинг 11 .9).
310 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

Листинг 11.9. Добавление метода в файл IProductResposi tory. cs


using System . Collections . Gener i c ;
namespace SportsStore . Models {
puЫic interface IProductRepository
IEnumeraЫe < Product> Products { get ;
void SaveProduct(Product product);

Затем к реализации хранилища с помощью Entity Framework Core , которая


определена в файле EFProductReposi tory. cs, можно добавить новый метод
(листинг 11.10).
Листинг 11.1 О. Реализация метода SaveProduct () в файле EFProductReposi tory. cs

using System.Collections . Generic ;


using System.Linq;
namespace SportsStore . Models {
puЬlic class EFProductRepository : IProductRepository {
pr ivate App li ca tionDbContext context ;
puЫic EFProductRepository(ApplicationDbContext ctx) {
context = ctx ;

puЫic IEnumeraЫe<Product> Products => context.Pr oducts ;


puЫic void SaveProduct(Product product)
if (product.ProductID ==О) {
context.Products.Add(product);
else {
Product dЬEntry = context.Products
.FirstOrDefault(p => p.ProductID == product.ProductID);
if (dЬEntry != null) {
dЬEntry.Name = product.Name;
dЬEntry.Description = product.Description;
dЬEntry.Price = product.Price;
dЬEntry.Category = product.Category;

context.SaveChanges();

Реализация метода SaveChanges () добавляет товар в хранилище, если значение


Product I D равно О ; в противном случае применяются изменения к существующей
записи в базе данных.
Мы не хотим здесь вдаваться в детали инфраструктуры Entity Framework Core, пос­
кольку, как упоминалось ранее , это отдельная крупная тема, к тому же она не является

частью ASP.NET Core МVС. Тем не менее, в методе SaveProduct () есть кое-что, что
оказьmает влияние на проектное решение, положенное в основу приложения МVС.
Глава 11. SportsStore : администрирование 311
Нам известно, что обновление должно выполняться, когда получен параметр
Product, который имеет ненулевое значение ProductID. Это делается путем извле­
чения из хранилища объекта Product с тем же самым значением ProductID и об­
новления всех его свойств, чтобы они соответствовали значениям свойств объекта,
переданного в качестве параметра.

Причина таких действий в том, что инфраструктура Entity Framework Core отсле­
живает объекты, которые она создает из базы данных. Объект, переданный методу
SaveChanges () , создается системой привязки моделей MVC , т.е. инфраструктура
Entity Framework Core ничего не знает о новом объекте Product, и она не будет при­
менять обновление к базе данных, иогда объеит Product модифицирован. Существует
множество способов решения уиазанной пробл емы, но мы принимаем самый простой
из них. предполагающий поиск соответствующего объекта, о котором известно инф­
раструктуре Entity Framework Core, и его явное обновление.
Добавление нового метода в интерфейс IProductRepository нарушает работу
класса имитированного хранилища FakeProductReposi tory, который был создан
в главе 8. Имитированное хранилище использовалось для быстрого старта процес­
са разработки и демонстрации возможности применения служб для гладкой заме­
ны реализаций интерфейса, не изменяя компоненты, которые на них опираются.
Имитированное хранилище больше не понадобится. В листинге 11.11 видно, что ин­
терфейс IProductReposi tory удален из объявления класса, поэтому продолжать мо­
дификацию класса по мере добавления функций хранилища не придется.

Листинг 11.11. Отсоединение класса от интерфейса в файле


FakeProductRepository.cs

using System.Collections . Generic ;


namespace SportsStore . Models {
puЬlic class FakeProductRepository /* : IProductRepository */
puЫic IEnumeraЬle<Product> Products => new List<Product>
new Product { Name "Football" , Price = 25 },
new Product { Name "Surf board ", Price = 179 } ,
new Product { Name " Running shoes", Price = 95 }
};

Обработка запросов POST в методе действия Edi t ()


К настоящему моменту все готово для реализации в контроллере Admin перегру­
женной версии метода действия Edi t () , иоторая будет обрабатывать запросы POST ,
инициируемые по щелчку администратором на кнопке Save (Сохранить) . Код нового
метода приведен в листинге 11.12.

Листинг 11.12. Определение версии метода действия Edi t () , обрабатывающей


запросы POST, в файле AdminController. cs
using Microsoft.AspNetCore .Mvc;
using SportsStore . Models;
using System . Linq ;
312 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

namespace SportsStore.Controllers {
puЫic class AdminContr oll er : Controller
private IP roductRepository repository ;
puЬlicAdminContro l ler( I ProductRepository repo) {
repository = repo ;

puЬlic ViewResult Index() => View(repository .Products) ;


puЫic ViewResult Edit(int prod uctid) =>
View(repository . Products
. FirstOrDefault(p => p . ProductID == productid));
[HttpPost]
puЫic IActionResul t Edi t (Product product)
if (ModelState.IsValid) {
repository.SaveProduct(product);
TempData [ "message"] = $ {product. Name} has been saved";
11

return RedirectToAction("Index");
else {
11 Что-то не так со значениями данных
return View(product);

Мы выясняем, смог ли процесс привязки модели проверить достоверность отправлен­


ных пользователем данных, для чего читаем значение свойства ModelState . IsValid.
Если здесь все в порядке, тогда мы сохраняем изменения в хранилище и направляем
пользователя на действие I ndex , таJ< что он увидит модифицированный список това­
ров. В случае какой-нибудь проблемы с данными мы снова визуализируем стандарт­
ное представление, чтобы пользователь мог внести 1юрректировки.
После сохранения изменений в хранилище сообщение сохраня ется с использо­
ванием средства TempDa t а, которое является частью средства состояния сеанса
ASP.NET Core. Это словарь пар "1шюч /знач ение", похожий на применяемые ранее
средства данных сеанса и ViewBag . Основное отличие объекта TernpData от данных
сеанса в том, что он хранится до тех пор, пока не будет прочитан.
В такой ситуации использовать ViewBag невозможно, потому что объект ViewBag
передает данные между контроллером и представлением, и он не может удерживать

данные дольше, чем длится текущий НТТР-запрос. Когда редактирование успешно,


браузер перенаправляется на новый URL, поэтому данные ViewBag утрачиваются.
Мы могли бы прибегнуть к средству данных сеанса, но тогда сообщение хранилось
бы вплоть до его явного удаления, чего не хотелось бы делать.
Таким образом, объе1п TempData подходит ка~< нельзя лучше. Данные ограничи­
ваются сеансом одного пользователя (пользователи не видят объекты TempData друг
друга) и хранятся достаточно долго, чтобы быть прочитанными . Мы будем читать
данные в представлении , которое визуализируется методом действия, куда был пере­
направлен пользов атель, и определяется в следующем разделе.
Глава 11 . SportsStore: администрирование 313

М одульное тести ро вание: метод действия Edi t () ,


обр абаты вающий запросы POST

В м етоде де й ствия Edi t () , обрабатывающем запросы POST , мы дол ж ны удостоверить­


ся , что х ранилищу товаров для сохранен и я передаются допустимые обновления объекта
Product , полученного в качестве аргумента метода. Кроме того , необходимо проверить ,
что недопусти мые обновления (т.е. содержащие ошиб к и провер к и достоверности модели)
в хранилище не передаются. Ниже приведены тестовые методы, которые добавлены в файл
Ad.minContro l lerTests.cs .

[Fact]
puЫic void Can Save_ Valid_ Changes() {
11 Организация - создание имитированного хранилища
Mock<IProductRepository> mock = new Mock<IProductRepository>() ;
11 Организация - соз д ание имитированных временных данных
Mock<ITempDataDictionary> tempData = new Mock<I TempData Dictionary>() ;
11 Организация - создание контроллера
AdminController target = new AdminCont r o l ler(mock . Object) {
TempData = tempData . Object
};
11 Организация - создание товара
Product product = new Product { Name = "T est " };
11 Действие - попытка сохранить товар
IActionResult result = target . Edit(product);
11 Утверждение - проверка того , что к хранилищу был о произведено обращение
mock.Verify(m => m. SaveProduct(product)) ;
11 Утверждение - проверка , что типом р езультата является перена правление
Assert . Is Type<Redirect ToActionResu l t>(result) ;
Assert . Equal( " Index ", (result as Red i rectToActionResult) .ActionName);

[Fact]
puЫic void Cannot_Save_ Invalid_ Chang e s() {
11 Организация - создание имитированно г о хранилища
Mock<IProductRepos i tory> mock = n ew Mock<IProductRepository>() ;
11 Организа ция - созда н ие контроллера
AdminController target = new AdminController(mock . Object) ;
11 Организация - создание товара
Product product = new Product { Name = " Test " };
11 Организация - добавле н ие ошибки в состояние модели
target . ModelState . AddModelE r ro r ( " e r ror ", " error " ) ;
11 Действие - попытка сохранить товар
IActionResult resu l t = target . Edit(produ ct) ;
11 Утверждение - проверка того , что к хранилищу был о произведено обращение
mock.Verify(m => m. SaveProduct(It.IsAny<Produ ct>()) , Times.Never()) ;
11 Утверждение - проверка типа результата метода
Assert . IsType<ViewRes u l t >(result) ;
314 Часть 1. Введение в инфраструктуру ASP.NET Соге MVC

Отображение подтверждающего сообщения


Мы будем иметь дело с сообщением, сохраненным с помощью Temp Data , в файле
компоновки AdminLayout . cshtm l (листинг 11.13). За счет обработки сообщения
в компоновке мы можем создавать сообщения в любом представлении, которое при­
меняет эту компоновку, без необходимости в создании дополнительных выражений
Razor.
Листинг 11.1 З. Обработка сообщения ViewBag в файле _AdminLayou t. csh tml
< ! DOCTYPE html>
<html>
<head>
<meta name="viewport" conten t ="widt h=device-width " />
<l ink r el= "stylesheet" asp - href -in clude="liЬ/bootstrap/dist/css/*.min.css" />
<title>@ViewBag.Ti tle</t i tle>
</head>
<body class= "panel panel-default ">
<div c l ass= "panel-heading " ><h4>@ViewBag . Title</h4></div>
<d i v class= "panel-bod y">
@if (TernpData["rnessage"] != null) {
<div class="alert alert-success">@TernpData [ "rnessage"] </div>

@RenderBody ()
</div>
</body>
</html>

Совет. Преимущество такой работы с сообщением внутри компоновки заключается в том,


что пользователи будут видеть его на любой странице, визуализированной после сохра ­
нения изменений. В данный момент мы возвращаем его списку товаров , но рабочий поток
можно изменить с целью визуализации какого-то другого представления , и пользовател и

будут по-прежнему видеть это сообщение (при условии, что следующее представление
использует ту же самую компоновку).

Теперь мы располагаем всеми фрагментами для редактирования сведений о това­


рах. Чтобы увидеть, как они все работают, запустите приложение, п ерейдите на URL
вида /Admin/Index , щелкните на кнопке Edit и внесите изменение. Затем щел кните
на 1шопке Save. Произойдет перенаправление на /Admin/Index и отобразится со­
общение из TempData (рис . l l.5) . Если вы перезагрузите страницу со списком това­
ров, то сообщение исчезнет. поскольку после чтения объект TempData удаляется . Это
очень удобно, т.к. не приходится иметь дело со старыми сообщениями.

Добавление проверки достоверности модели


Мы добрались до точки, когда к классам модели необходимо добавить правила про­
верки достоверности. Пока что администратор может ввести отрицательнь1е значения
для цен или оставить описания пустыми - и приложение SportsStore благополучно
сохранит эти данные в базе. Смогут ли недопустимые данные успешно сохранить­
ся, зависит от того, удовлетворяют ли они ограничениям в таблицах SQL, созданных
инфраструктурой Entlty Framework Core, и для большинства приложений таких мер
безопасности будет недостаточно .
Глава 11. SportsStore : администрирование 315

CJ Ed~ Produc< Х !' .


гп- -· -·----· --.--:: D All P•odu«o Х ''

1--- -- .==:.""'=--'=--== _.!:__:_. С


1 ~ С 1 <D l0<:alhost·60000/Admin/Erl1t< --- ·------------~l
1<D_ lo~alhost600:~:Adrn:-'ln~~x _ ·- _ *.J
'. Edit Product /
t All Products
1 1
1 Namв

1 Greeп Kayak Green Kayak /1as Ь!!еn saved


Descri tion
1D Name Price Aclions
А t for one person
1 Green Kayak $275.00

"~
2 Lifejackel $48.95

3 Socce rВalf $19.50


111
1
4 Corner Flags

5 Stadium
$34.95

579,500.00 .""
".
1 Са

111
----·-------~
6 ThinkingCap $16.00

--
7 Unstaady Chair $29.95
l_ "_ ·-----

·-
в Human Chess Вoard $75.00

9 Bling-Bling Кing $1,200.00

!
IИIU 1
. _________.____________ ._ _1I
L
Рис. 11.5. Редактирование сведений о товаре и отобра жение сообщения из TernpData

Чтобы защититься от недопустимых значений данных, свойства класса Product


декорируются с помощью атрибутов, как это делалось для класса Order в главе 10
(листинг 11 . 14).
Листинг 11.14. Применение атрибутов проверки достоверности в файле Product. cs
using System.Componentмodel.DataAnnotations;
using Microsoft.AspNetCore.Mvc.ModelBinding;
name space SportsStore . Models {
puЬlic class Product {
puЫic int ProductID { get; set; }
[Required(ErrorMessage = "Please enter а product name")]
11 Введите наименование товара
puЫic string Name { get ; set; }
[Required(ErrorMessage = "Please enter а description")]
11 Введите о п исание
puЫic string Descr ipti on { get; set; }
[Required]
[Range(0.01, douЬle.MaxValue,
ErrorMessage = "Please enter а positive price")]
11 Введите положительное значение для цены
316 Часть 1. Введение в инфраструктуру ASP.NET Соге MVC

puЫic decima l Price { ge t ; s e t; )


[Required (ErrorMessage = "Pl.ease specify а category") ]
//Укажи т е кат егор и ю
puЫ i c str ing Category { get ; se t ; }

В главе 10 использовался дескрипторный вспомогательный класс для отображения


сводки по ошибкам проверки достоверности в верхней части формы. Здесь мы при­
меним похожий подход , но будем отображать сообще ния об ошибках рядом с эле ме н­
тами формы в представлении Edi t (листинг 11.15).
Листинг 11. 15. Добавление элементов для отображения ошибок проверки
достоверности в файле Edi t. csh tml
@model Produ ct
@{
ViewBag . Title = "Edi t Product ";
Layout = "_AdminLayout ";

<form asp - actio n=" Edit " method=" post " >
<input typ e= "h idde n " asp - for= " ProductID " />
<div c l ass= " form - group " >
<l abe l as p- f or=" Name " ></labe l >
<div><span asp-validation-for="Name" class="text-danger"></span>
</div>
<input asp -f or=" Name " class =" form - con t rol " />
</div>
<div class =" form - group " >
<label asp - for= " De scription " ></ l abel>
<div><span asp-val.idation-for="Description" cl.ass="text-danger">
</span></div>
<textarea a s p - for= " Desc ri ption " c lass= " form - cont r ol " ></textarea>
</div>
<d i v class =" fo r m- group " >
<label asp - for= " Category " ></la be l >
<div><span asp-validation-for="Category" class="text-danger"></span>
</div>
<input asp - for =" Category " c l ass =" form - con t rol " />
</div>
<div c l ass= " fo r m- g r oup " >
<label asp - for= " Price " ></label>
<div><span asp-validation-for="Price" class="text-danger"></span>
</div>
<input asp - for= "P r i ce " c l ass= " fo rm- co nt r o l" />
</div>
<div class ="t ex t- c e nt er " >
<button class =" Ьtn Ьtn - primary " t ype =" submit " >Save</button>
<а asp - action= " Index " c l ass =" Ьtn Ьtn - default " >Cancel</a>
</div>
</form>
Глава 11. SportsStore : администрирование 317
Когда атрибут asp - validation-for применяется к элементу span, он использу­
ет дескрипторный вспомогательный класс, который добавляет сообщение об ошибке
проверки достоверности для указанного свойства, если при проверке возникли кюше­
то проблемы.
Дескрипторные вспомогательные классы будут вставлять сообщение об ошиб­
ке в элемент span и добавлять элемент в класс inp ut-va lidation - error, кото­
рый позволит легко применять стили CSS к элементам с сообщениями об ошибках
(листинг 11.16).

Листинг 11.16. Добавление стиля CSS в файле _ AdminLayou t . csh tml


<!DOCTYPE html>
<html>
<head>
<meta name= " viewport" content= " width=device - width" />
<l ink rel="stylesheet " asp-href-include="liЬ/boots tra p/dist/css/ * .rnin. css " />
<ti t le>@ViewBag.Title</title>
<style>
.input-validation-error { border-color: red; background-color: #fee
</style>
</head>
<body class= " panel panel - default " >
<di v clas s="p anel - heading " ><h4>@ViewBag . Title</h4></div>
<div class= "panel-body " >
@if (TempData [ "message " ] ! = nul l ) {
<div class="ale rt al e r t- success " >@TernpData [" rnessage " ]</div>

@RenderBody ()
</div>
</body>
</htrnl>

Определенный здесь стиль CSS выбирает элементы, которые являются членами


класса input - va li dat i on - error, и устанавливает для них границу красного цвета
и фон .

Совет. Явная установка стилей при использовании библиотеки CSS, подобной Bootstrap,
может привести к несоответствиям, когда применяются темы содержимого . В главе 27
будет продемонстрирован альтернативный подход , при котором для применения классов
Bootstrap к элементам с сообщениями об ошибках проверки достоверности используется
код JavaScript, сохраняя все в согласованном состоянии.

Дескрипторные вспомогательные классы для сообщений проверки достоверности


можно применять где угодно в представлении , но по соглашению (и это вполне разум­
но) принято размещать их поближе к проблемному элементу, чтобы ввести пользова­
теля в 1typc дела. На рис. 11.6 показано, как выглядят сообщения об ошибках провер­
ки достоверности и отображаемые подсказки, для чего нужно зютустить приложение ,
отредактировать сведения о товаре и отправить недопустимые данные.
318 Часть 1. Введение в инфраструктуру ASP.NET Соге MVC

Включение проверки достоверности на стороне клиента


В текущий момент проверка достоверности данных применяется, только когда
пользователь-администратор отправляет результаты редактирования серверу, но

большинство пользователей ожидают немедленного отклика при наличии проблем с


введенными данными.

CJ Ed~ Product Х

f- - С 1ф l
localhost:60000/Adm1.;/~-----------·-----·----- - - - -'tl
1 ·-·----·--=====-----=--===--==--==---:::=:.:=--=..:----·-·-·--'
1 Edit Product
1
1 1
Namo
Please eriter а prodt1ct name 1
1 1
i
1
/ Description
1
1 Please enter а description

,с 1

1
! Category
1 Please spec~y а category
1

~~~::е а pos~ive
1
1 enter price i

1
[ -275.00·- - - - - - - - - - - - - - - - - -
--- ~ i
i
!L______ - Cancel

"_____________ J
Рис. 11.6. Проверка достоверности данных при редактировании сведений о товаре

Именно потому разработчики часто предпочитают выполнять проверку достовер­


ности на стороне клиента, при которой данные проверяются в браузере с исполь­
зованием JavaScript. Приложения MVC могут выполнять проверку достоверности на
стороне миента на основе аннотаций данных, применяемых к классу модели пред­
метной области.
Прежде всего, понадобится добавить библиотеки JavaScript, которые предоставят
приложению средство проверки достоверности на стороне клиента, что делается в

файле bower. j son (листинг 11.17). Чтобы увидеть файл bower . j son, может потре­
боваться выбрать проект SportsStore и щелкнуть на кнопке Show All ltems (Показать
все элементы) в верхней части окна Solution Explorer.

На заметку! Пакеты проверки достоверности на стороне клиента не установятся корректно,


если вы не заменили инструмент gi t среды Visual Studio , как было описано в главе 2.
Глава 11. SportsStore : администрирование 319
Листинг 11.17. Добавление пакетов JavaScript в файле bower. j son

11
name 11 : 11 asp . net 11 ,
11
private 11 : true ,
11
dependenci e s 11 : {
11
bootstrap 11 : 11 3 . 3 . 6 11 ,
11
fontawesome 11 : 11 4 . 6 . 3 11 ,
"jquery": "2. 2. 4",
11
jquery-validation 11 : 11 1.15.0 11 ,

"jquery-validation-unoьtrusive": 11
3. 2. 6 11

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


библиотеки jQuery, которая упрощает работу с АРI - инте рфейсом DOM браузера.
Следующий шаг связан с добавлением файлов JavaScript в компоновку, чтобы они за­
гружались , когда используются средства администрирования приложения SportsStore
(листинг 11.18).
Листинг 11. 18. Добавление библиотек проверки достоверности на стороне клиента
в файле _ AdminLayou t. csh tml
< ! DOCTYPE html >
<html>
<head>
<meta name= 11 viewport 11 conte nt =" wi dth=de v i c e -width 11 />
<link rel=11 stylesheet 11 as p-href - include= 11 li Ь/bootstrap/d i st/ css/* .min . css 11 />
<title>@Vi e wBag . Ti t le</title>
<s ty l e >
. input - val i dat i on- e rr o r { borde r- co l o r: red ; bac kgro und- color : #fee
</style>
<script asp-src-include= 11 liЬ/jquery/**/jquery.min.js 11 ></script>
<script asp-src-include= 11 liЬ/jquery-validation/ * */jquery.validate.min.js 11 >
</script>
<script asp-src-include="lib/jquery-validation-unoьtrusive/**/*.min.js">
</script>
</head>
<body class = 11 panel pan e l -default 11 >
<div class = 11 panel - headi ng 11 ><h4>@ViewBag . Tit l e</h4></div>
<div class= 11 pane l- body 11 >
@if (TempData [" message "] != null) {
<div c l a s s = 11 alert alert - success 11 >@T empData[ 11 message "] </d i v>

@RenderBody ()
</div>
</body>
</html>

В доб авл е нной разм е тке для выбор а файлов, которые включаются в элементы

s cript , применяется дескрипторный вспомогательный класс. Работа этого процес ­


са объясняется в глав е 25. Он позволяет использовать групповые символы для выбо­
ра файлов JavaScript, а это означает, что приложени е н е ра зрушится в случае , если
320 Часть 1. Введение в инфраструктуру ASP. NET Core MVC

имена файлов в пакете Bower изменятся, когда выйдет его новая версия. Однако тре­
буется определенная осторожность, потому что (как будет показано в главе 25) до­
вольно легко выбрать не те файлы, которые ожидались.
Включение проверки достоверности на стороне клиента не приводит к каким-либо
визуальным изменениям, но ограничения, которые указаны с помощью атрибутов,
примененных к классу модели С#, вступают в силу на уровне браузера, пр едотвращая
отправку пользователем формы с недопустимыми данными и обеспечивая немедлен­
ный отклик при наличии проблемы. За дополнительными сведениями обращайтесь
в главу 27.

Создание новых товаров


Далее мы реализуем метод действия Crea te () , который указан для кнопки Add
Product на главной странице со списком товаров. Он позволит администратору добав­
лять новые элементы в каталог товаров. Добавление возможности создания новых това­
ров требует одного небольшого дополнения в приложении. Это является великолепной
демонстрацией мощи и гибкости хорошо структурированного приложения МVС. Для на­
чала добавьте в контроллер Admin метод Create (),как показано в листинге 11.19.

Листинг 11.19. Добавление метода действия Create () в файле AdminController. cs


using Microsoft.AspNetCore . Mvc;
using SportsStore . Models;
using System . Linq;
namespace SportsStore . Controllers
puЫic class AdminController : Controller
private IProductRepository repository ;
puЬlic AdminController(IProductRepository repo) {
repository = repo;

puЬlic ViewResult Index() => View(repository.Products);


puЫic ViewResult Edit(int productid) =>
View(repository.Products
. FirstOrDefault(p => p.ProductID == productid)) ;
[HttpPost]
puЬlic IActionResult Edit(Product product)
i f (ModelState. IsValid) {
repository . SaveProduct(product) ;
TempDa ta [ " message " ] = $" {product . Name) has been saved ";
return RedirectToAction( "In dex");
else {
11 Что - то не так со значениями данных
return View(product) ;

puЬlic ViewResul t Create () => View ( "Edi t", new Product ()) ;

Метод Create () не визуализирует свое стандартное представление. Взамен он ука­


зывает, что должно использоваться представление Edi t. Применение в методе действия
Глава 11. SportsStore: администрирование 321
представления, которое обычно связано с другим методом действия, вполне допустимо.
В рассматриваемом случае мы предоставляем новый объект Product в качестве модели
представления , так что представление Edi t заполняется пустыми полями .

На заметку! М одульный те ст для метода дей ствия Create () не до ба вл яе тся . О н п озв олил
бы пр о верит ь только сп особн ость об р аб от ки и нф растр у ктур о й ASP. NEТ Core MVC р езуль ­
тата, в о звращаем о го методо м дей ствия - то, что мы считаем сам о с о бой р азумеющим­
ся . (Обычн о тесты для функ ци о нал ьных с редств инфраструктуры не пишутся, есл и тол ьк о
нет подозрения о нал и ч ии дефекта. )

Это единственное изменение, которое потребовалось, поскольку метод действия


Edi t ( ) уже настроен на получение объектов Pr oduct от системы привязки моделей
и на их сохранение в базе данных. Чтобы протестировать фующиональность, запус­
тите приложение, перейдите на URL вида /Admin/ In dex, щелкните на кнопке Add
Product заполните форму и отправьте ее. Информация, указанная вами в форме, будет
использоваться для создания нового товара в базе данных , который затем появится
в списке (рис. 11.7).

l:J AJi Prod uФ )(


t . - - - · - · - - - - · - · - , · - - - - - - - - - - - ·- - · · - ~--.

1 ~ .:... ~1~~~:~~0..'1:~°?'1~dn~n~;'' -.-~·--~~-.,_-о,с= ~~


Ci Edit ?tod..ict х
All Products
С ГQ) ~aiho~;.toOOO;Ad;;~i:~Cr~~~e ;
- --· =.-;-. ""'" --·==·-"''-'· -~1

Edit Product ! Snorkol tlOS i>eE!J saVl>d


1
Namo
t Ю Namo Price Acl!OOS

~-
Snorkel 1 Gr(!en Kayalti $275.00

·-
а 111
Description 2 Lilejacl<el $48.95 . 1 .

--
Breat/1.Q underwaler З Soccer Вoll $19.50

4 Corner Flags $34.95

--
Category

~-
Watorsports s Stndtum $79,500.00

6 Тltinking Сэр $ 16.00


Price

--."
аав
10.50 7 UnslearJy Cliair $29.95

111 Gancol l! 8 Human CllQSS Вo•rd $7500

Вllng-Вllng Кing
l ..
_._
1
[
9 51,200.00
IUllll
10 Sno1ko1 $10.50
1 ~~~~~~~~~--~~~~~~~ 1
!

-~1·~=· __ j

Рис. 11 .7. Добавление н о в о го товара в катал о г

Удалени е товаров
Обеспечить поддержку удаления элементов из каталога довольно просто. Для на­
чала добавьте в интерфейс IProductReposi t ory новый метод, как показано в лис­
тинге 11.20.
322 Часть 1. Введение в инфраструктуру ASP.NEТ Core MVC

Листинг 11.20. Добавление метода для удаления товаров в файле IProductReposi tory. cs
using System . Collections . Generic ;
namespace SportsStore.Models {
puЫic interface IProductRepository
IEnumeraЫe<Product> Products { get ;
void SaveProduct(Product product) ;
Product DeleteProduct (int productID);

Затем этот метод необходимо реализовать в классе хранилища Entity Framework


Core, т.е. EFProductRepository (листинг 11.21).

Листинг 11. 21. Реализация поддержки удаления в файле EFProductReposi tory. cs

using System . Collections . Generic;


using System . Linq ;
namespace SportsStore . Mode l s {
puЬlic class EFProductRepository : IProductRepository {
private ApplicationDbContext context;
puЫ i c EFProductRepository(ApplicationDbContext ctx) {
context = ctx;

puЬlic IEnumeraЫe<Product> Products => context.Products ;


puЫic void SaveProduct(Product product)
if (product.ProductID == 0) {
context . Products . Add(product) ;
else {
Product dbEntry = context . Products
. 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 ;

context . SaveChanges() ;

puЫic Product DeleteProduct (int productID)


Product dЬEntry = context.Products
.FirstOrDefault(p => p.ProductID == productID);
if (dЬEntry != null) {
context.Products.Remove(dЬEntry);
context.SaveChanges();

return dЬEntry;

Финальный шаг связан с реализацией метода действия Delete () в контроллере


Admin. Он должен поддерживать только запросы POST, потому что удаление объектов
Глава 11. SportsStore: администрирование 323
не является идемпотентной операцией. Как будет показано в главе 16, браузеры и
кеши вольны выдавать запросы GET без явного согласия пользователя, по этому мы
должны проявить осторожность , чтобы избежать внесения изменений как следствия
запросов GET . Код нового метода действия приведен в листинге 11 .22.

Листинг 11.22. Добавление метода действия Delete () в файле


Adm.inController.cs
using Microsoft . AspNetCore.Mvc ;
using SportsStore.Models ;
using System .Linq;
namespace SportsStore . Contro ll ers
puЫic class AdminController : Contro ller
private IProductRepository repository ;
puЫic AdminController(IProductRepos itory repo) {
repository = r epo ;

puЫic ViewResult Index() => View(repository . Product s) ;


puЫic ViewResu lt Edit( i nt productid) =>
View(repository .P roducts
. FirstOrDefault(p => p.ProductID == productid)) ;
[HttpPost]
puЫic IActionResult Edit(Product product)
i f (ModelState . IsValid) {
repository . SaveProduct(product) ;
TempData[ "message "] = $ "{produ c t.N a me } has been saved ";
return RedirectToAction( " Index " ) ;
else {
11 Что-то не так со значениями данных
ret urn View(product) ;

puЬlic IActionResult Create() => View( " Edit" , new Product()) ;


[HttpPost]
puЫic IActionResul t Delete (int productid) {
Product deletedProduct =
repository.DeleteProduct(productid);
if (deletedProduct != null) {
TempData["message"] = $ 11 {deletedProduct.Name} was deleted";

return RedirectToAction("Index");

Модульное тестирование: удаление товаров

Нам нуж но протестировать основное поведение метода действия Delete () , которое за­
ключается в том, что при передаче в качестве параметра допустимого идентификатора
ProductID метод действия должен вызвать метод DeleteProduct () хранилища и пе­
редать ему корректное значениеProductID удаляемого товара. Вот тест, добавленный в
файл AdminControlle rT ests . cs :
324 Часть 1. Введение в инфраструктуру ASP.NET Саге MVC

[Fac t]
puЫic v oid Ca n_De l ete_ Va l id_Pr odu cts( ) {
11 О р ганиза ц ия - созд а ние объек т а Pr oduc t
Product prod = new Product { ProductID = 2 , Name = " Test " } ;
//Ор г анизация - создание имитирова н но г о хранилища
Mock< IP roduct Repository> mock = new Moc k<I ProductRepository>() ;
mock . Setup (m => m. Products) . Retur ns(new Pr odu c t[ ] {
new Pr oduct {ProductID = 1 , Name = " Pl " }, prod ,
new Product {P r oduc t I D = 3 , Name = " РЗ " } , }) ;
//Ор ганизация - создание контроллера
Adm inCont ro ller ta r ge t = ne w Admin Con t r o ll er (mock . Ob j ec t ) ;
//Действие - . удаление т овара
target . Delete(prod.ProductID) ;
//Утв ержде н и е - проверка того , что был вызван метод удаления
/ / в х р а н ил и ще с корректным объе кт ом Pr odu c t
mock . Ve rify(m => m. Del e teProduct(prod .ProductID)) ;

Чтобы увидеть средство удаления в работе, запустите приложение. перейдите на


URL вида /Admi n/Index и щелкните на одной из кнопок Delete (Удалить) на стра­
нице со списком товаров (рис. 11.8). Можно заметить, что с помощью переменной
TernpData отображается сообщение об удалении товара из каталога.

Ю N Amв

1 Creen!Uyak

Price Acliona

3 &x::cErВalt

4 Cornit Fl;ig.s
5275.00

54595
m •
1'!:1111
S St&dium
$ t Q.50
аа

m
• Шlll
G Th1nkng Сар $.34.95

7 UMt1Ыdy C h.tr1
$79.500.00

.... , ша
8 Hu nюr1 Ch0s Oo.1rd ! 16.00

. 111
10 Snor!ФI
$7 500
lmia:I
~-
$ 1.200.00

-1

Рис. 11.8. Удаление товара из каталога

Рез юм е
В этой главе были введены средства администрирования и показано, как реализо­
вать операции CRUD, которые позволяют администратору создавать, читать , обно в ­
лять и удалять товары из хранилища и помечать заказы как отгруженные. В следую ­
щей главе мы продемонстрируем способ защиты административных функций, чтобы
они не были доступны всем пользователям , и разверн ем приложение SportsStoгe в
производственной среде .
ГЛАВА 12
SportsStore:
защита и развертывание

в предыдущей главе мы добавили в приложение SportsStore поддержку админис­


трирования, и от вашего внимания, вероятно, не ускользнул тот факт, что если
развернуть приложение в том виде как есть. то модифицировать каталог товаров смог
бы любой пользователь. Для этого ему лишь нужно знать, что средства администри­
рования доступны через URL вида /Adm in / Index и /Order /List . В настоящей главе
мы покажем, как предотвратить использование административных функций случай­
ными посетителями, защитив их паролем . Обеспечив возможность защиты, мы объ­
ясним, каким образом подготовить и развернуть приложение SportsStore в производс­
твенной среде.

Защита средств администрирования


Аутентификация и авторизация предоставляются системой ASP.NET Core Identity,
которая аккуратно интегрируется как в платформу ASP.NET Core, так и в приложе­
ния МVС. В последующих разделах мы создадим базовую настройку защиты, которая
позволит одному пользователю по имени Adrnin проходить аутентификацию и полу­
чать доступ к административным функциям в приложении. Система ASP.NET Core
ldentity предлагает множество других средств для аутентификации пользователей,
а также авторизации доступа к функциям и данным приложения. Более подробные
сведения вы найдете в главах 28-30, где будет показано, как создавать и управлять
пользовательскими учетными записями, каким образом применять роли и политики
и 1<ак поддерживать аутентификацию от третьих сторон, таких как Microsoft, Google,
Facebook и 1\vltter. Однако цель этой главы - создать лишь столько функциональнос­
ти , сколько достаточно для предотвращения доступа пользователей к чувствитель ­
ным частям приложения SportsStore, что будет содействовать пониманию того, как
аутентификация и авторизация вписываются в приложение МVС.

Добавление в проект пакета ldentity


Первый шаr заключается в добавлении к проекту SportsStore средства ASP.NET
Core Identity, которое требует ряда новых пакетов NuGet . В листинге 12.1 показаны
добавления в файле project . j son из проекта SportsStore.
326 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

Листинг 12.1. Добавление средства ASP.NET Core ldentity в файле project. j son
из проекта SportsStore

"dependencies ": {
"Microsoft.NETCore . App ":
"version ": " 1 . 0 . 0 ", " type ": "platform "
}'
"Microsof t. AspNetCore . Diagnostics ": " 1.0 . 0 ",
" Microsoft . AspNetCore . Server . IISintegration ": " 1 . 0 . 0 ",
"Microsoft . AspNetCore . Server . Kestrel ": " 1 . 0 . О ",
"Microsof t. Extensions . Logging.Console " : "1. 0 . 0 ",
"Microsoft. AspNetCore . Razor . Tools ": {
"version ": "l. 0 . 0- preview2 -fina l ", " type ": " build" },
"Microsoft . AspNetCore . StaticFiles ": " 1. О. О ",
"Microsoft . AspNetCore . Mvc ": "1.0. О ",
" Microsoft . EntityFrameworkCore . SqlServer ": " 1.0.0 ",
"Microsoft . EntityFrameworkCore . Tools ": " l . O. O- preview2 - final ",
"Microsoft . Extensions .Configuration . Json ": " 1 . 0 . 0 ",
"Mi crosof t. AspNetCore . Session ": "1 .0 . О ",
"Microso ft. Extens ions.C aching . Memory ": "1.0.0",
"Microsoft . AspNetCore . Http . Extensions ": " 1 . 0 . 0 ",
"Microsoft.AspNetCore.Identity.EntityFrameworkCore": 11 1 . 0.0 11
}'

После сохранения файла proj ect . j son среда Visual Studio с помощью NuGet за­
грузит и установит пакет Identity.

Создание базы данных ldentity


Система ASP.NET Core Identity чрезвычайно конфигурируема и ра с ширяема , под­
держивая многочисленные варианты хранения данных о пользователях. Мы собира­
емся ис пользовать наиболее распро страненный вариант, который пр едус матрива ет
хранение данных с применением Microsoft SQL Server и доступ к ним с помощью
Entity Framework Core.

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


Нам необходимо создать файл контекста базы данных, который будет дейс­
твовать в качестве шлюза между базой данных и объектам и моделей Identity, пре­
доставляющими к ней доступ . Добавьте в пап ку Models файл класса по имени
Appidenti tyDbContext. cs с определением, приведенным в листинге 12.2.
Листинг 12.2. Содержимое файла Appiden ti tyDЬCon text. cs из папки Models
using Microsoft . AspNetCore . Identity . EntityFrameworkCore ;
using Microsoft . EntityFrameworkCore;
namespace SportsStore . Mode l s {
puЬlic c las s AppidentityDbContext : IdentityDbContext<IdentityUser> {
puЫic App i dentityDbContext(DbContextOptions<Appide ntityDbContext> options)
: base ( options) { }
Глава 12. SportsStore: защита и развертывание 327
Класс Appidenti tyDbContext является производным от класса Identi tyDb
Context, который предлагает связанные с Identity средства для Entity Framework
Core. В параметре типа используется Identi tyUser, представляющий собой встроен­
ный класс, который применяется для представления пользователей. В главе 28 будет
продемонстрировано использование специального класса, который можно расширять
с целью добавления дополнительной информации о пользователях приложения.

Определение строки подключения


На следующем шаге определяется строка подключения, предназначенная для базы
12.3 показаны добавления, внесенные в файл appsettings . j son
данных . В листинге
про е кта
SportsStore . которые соответствуют тому же самому формату. что и строка
подключения, определенная для базы данных товаров в главе 8 .

Листинг 12.3. Определение строки подключения в файле appsettings. j son

" Data ": {


"SportStoreProducts ": {
"ConnectionString ": "Server=(loca l db)\\MSSQLLocalDB ;
Database=SportsStore ;T rusted_Connection=True ;
MultipleActi veResu ltSe ts=true "
} 1

"SportStoreidentity": {
"ConnectionString": "Server=(localdЬ)\\
MSSQLLoca1DB;DataЬase=Identity;
Trusted_Connection=True;MultipleActiveResultSets=true"

Вспомните, что строка подключения должна определяться как единственная не­


разрывная строка кода в файле appsettings . j son, а в листинге она выглядит так
из-за ограниченной ширины печатной страницы. Добавленная разметка определяет
строку подключения по имени Spo r tsStoreidentity , в которой указывается база
данных LocalDB под названием Identity.

Конфигурирование приложения
Подобно другим средств ам ASP.NET Core система Identity конфигурируется в
классе Start . В листинге 12.4 приведены добавления для настройки Identity в про­
ект е Spo rts Store с применением ранее определенного класса контекста и строки
подключения.

Листинг 12.4. Конфигурирование средства ldeпtity в файле Startup. cs

using Mic r osoft . AspNetCore .Builder ;


using Microsoft . AspNetCore.Ho s ting ;
using Microsoft . AspNetCore .Http ;
using Microsoft.Extensions. Dependencyinjection ;
using Microsoft . Extensions . Logging;
using SportsStore . Models ;
using Microsoft.Extensions . Configuration ;
328 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

us i ng Mi crosoft.EntityFrameworkCore ;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
namespace SportsStore {
puЫic class Startup {
IConfigurationRoot Conf i guration ;
puЫic Startup(IHostingEnvironment env) {
Configuration = new ConfigurationBuilder()
. SetBasePath(env . ContentRootPath)
. AddJsonFile ( " appsettings . j son " ) . Build () ;

puЬlic void Conf igu reServices(IServiceCollection services)


servi ces.AddDbContext<ApplicationDbCon te xt>(options = >
options . UseSqlSe r ver(
Configuration [" Data : SportStoreProducts : ConnectionStr ing"] ) );
services . AddDbContext<AppidentityDbContext>(options =>
options.UseSqlServer(
Configuration["Data:SportStoreidentity:ConnectionString"]));
services.Addidentity<IdentityUser, IdentityRole>()
.AddEntityFrameworkStores<AppidentityDЬContext>();

services . AddTransient<IProductRepository , EFProductRepository> () ;


services . AddScoped<Cart>(sp => SessionCart.GetCart(sp)) ;
services . AddSingleton<IHttpContextAccessor , HttpContextAccessor>();
services . AddTransient<IOrderRepository, EFOrderRepository>() ;
services . AddMvc() ;
services.AddMemoryCache() ;
services . AddSession() ;

puЫic void Configure(IApplicationBuilder арр ,


IHostingEnvironment env , ILoggerFactory loggerFactory)
app . UseDeveloperExceptionPage() ;
app.UseStatusCode Pages() ;
app.UseStaticFiles();
app . UseSession() ;
app . Useidentity();
app . UseMvc(routes =>
11 .. . для краткости маршруты не показаны ...

}) ;
SeedData.EnsurePopulated(app) ;
IdentitySeedData.EnsurePopulated(app) ;

В методе ConfigureServices () конфигурация Entity Framework Core расширена


дл я регистрации класса 1юнте кста и с помощью метода Addidenti ty (} устанавли­
вает службы Identity, используя встро енн ые нлассы для представления пользовател ей
и ролей . Внутри метода Configure () вызывается метод Useidenti ty () для уста-
Глава 12. SportsStore : защита и развертывание 329
новки компонентов, которые будут перехватывать запросы и ответы для внедрения
политики бе з опасности.
Кроме того , добавлен вызов метода Ident i tySeedData . Ens u rePopulated (), ко­
торый будет с о здан в следующем разделе для добавления данных о пользователях в
базу данных.

Определение начальных данных


Мы планируем явно создать пользователя Admin, наполняя базу данных началь­
ными данными при запуске приложения. Добавьте в папку Models файл класса по
имени IdentitySeedData . cs и определите в нем статический класс, как показано
в листинге 12.5.

Листинг 12.5. Содержимое файла Identi tySeedData. cs из папки Models


using Microsoft . AspNetCore.Builder ;
using Microsoft . Asp NetCore .Identity ;
using Microsoft . AspNetCore .I dentity .Ent i tyFrameworkCore ;
using Microsoft . Extensions . Dependencyin jection ;
namespace Spo rt sStore . Mode l s {
puЬlic static class IdentitySeedData
pri va te const string adminUse r = " Admin ";
private const string admin Passwo rd = "S ecret123$ ";
puЬlic static a syn c void EnsurePopu late d(IApp l icationB uil der арр)
UserManage r < I dent it yUs er> userManager = app .Applicat ionServ ic es
. GetRequ ir edServ i ce<Us erMa n age r < Ide nti tyUser>>() ;
IdentityUser user = await u serManager . FindByidAsync(adminUser);
i f (user == null) {
use r = new I denti tyUser ( " Admin " ) ;
await userManager . CreateAsync( user, adminPassword) ;

В коде применяется класс UserManager<T>, который предоставляется систе мой


ASP.NEТ Core Identity в виде службы для управления пользователями, как описано в
главе 28. В базе данных производится поиск учетной записи пользователя Admin, ко­
тор ая в случае ее отсутствия создается (с паролем Secret1 23$). Не изменяйте жестко
за кодированный пароль в этом примере, поскольку система Identity имеет политику
проверки достоверности, которая требует, чтобы пароли содержали цифры и диапа­
зо н символов. Способ изменения настроек , относящихся к проверке достоверности ,

описан в главе 28.

Внимание! Жесткое кодирование деталей учетной записи администратора часто требуется


для того, чтобы можно было войти в приложе ние после его развертывания и начать ад­
министрирование. Поступая так , вы должны помнить о необходимости изменения пароля
для учетной записи , которую создали . В главе 28 приведены детали того, как изменять
пароли , используя ldeпtity.
330 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

Соэдание и применение миграции базы данных


Все компоненты на месте, так что самое время воспользоваться средством мигра­
ций Entity Framework Core для определения схемы и применения ее к базе данных.
Откройте консоль диспетчера пакетов и запустите следующую команду для создания
миграции:

Add-Migra ti on Initial - Con text Ap pidentity DbCon text


Важное отличие от предшествующих команд базы данных касается использования
параметра - Co ntex t для указания имени класса контекста, ас социированного с ба­
зой данных, с которой нужно работать, т.е . Appidenti tyDbContext. При наличии в
приложении нескольких баз данных важно удостовериться, что работа производится
с правильной базой.
После того как инфраструктура Entity Framework Core сгенерировала начальную
миграцию, запустите приведенную ниже команду для создания базы данных и запус­
тите команды миграции:

Update-Da t abase - Context Appident i tyDbCo n text


Результатом будет новая база данных LocalDB по имени Identi ty. которую мож­
но просмотреть с помощью окна SQL Server Object Explorer (Проводник объектов SQL
Server) ср еды Visual Studio.

Применение базовой политики авторизации


Теперь. когда средство ASP.NET Core Identity установлено и сконфигурировано,
можно применить политику автори зации к тем частям приложения, которы е необхо­
димо защитить. Мы собираемся использовать наиболее баз овую политику авториза­
ции, которая предусматривает разрешение доступа любому пользователю , прошедш е ­
му аутентификацию. Хотя она может быть полезной политикой также и в реальном
приложении , существуют возможности для создания более детализированных эле­
ментов управления авторизацией (как описано в главах 28-30), но поскольку в при­
ложении SportsStore имеется только один пользователь, вполне достаточно провести
разл ичие между анонимными и аутентифицированными запросами.
Атрибут Autho r ize применяется для ограничения доступа к методам действий, и
в листинге 12.6 видно, что этот атрибут используется для защиты до ступа к админи с­
тративным действиям в контроллере Orde r .

Листинг 12.6. Ограничение доступа в файле OrderController. cs

using Microso ft. AspNetCore . Mvc ;


usin g Sp ortsS tore.Mo de l s;
us i ng Sy s tem. Linq;
using Microsoft.AspNetCore.Authorization;
namespace Sport s Store . Co n trollers {
p u Ьl ic class Or de r Co n trol l er : Co n trol l e r
p r ivate IOrd e r Re posit o ry r eposit or y ;
pr i vate Car t cart ;
p uЬli c Ord erContro l le r (IOrderRepos it ory r epo Se rvice , Car t cartServi ce ) {
r epos ito r y = repoSe r vice ;
ca r t = ca r t Se r v i ce ;
Глава 12. SportsStore : защита и развертывание 331
[Authorize]
puЬlic ViewResu lt List() =>
View(reposito r y . Orders.Where(o => !o . Shipped)) ;
[HttpPost]
[Authorize]
puЬlic IActionResul t MarkShipped(int orderID) {
Order order = repository . Orders
.FirstOrDefault(o => o . OrderID orderID) ;
if (order ! = null) {
order . Shipped = true ;
repository . SaveOrder(order);

return RedirectToAction(nameof(List) ) ;

puЫic ViewResul t Checkout () => View (new Order ()) ;


[HttpPost]
puЫic IActio nResult Checkout(Order o r der) {
if (cart . Lines.Co unt () == 0) {
ModelState . AddMode l Error( "", "S orry , your cart is empty! " ) ;

i f (Mode l State . IsValid) {


order . Lines = car t. Lines . ToArray() ;
rep os itory . SaveOrder(order);
return RedirectToAction(nameof(Cornp le ted)) ;
else {
return View(order) ;

puЫic ViewResult Completed() {


cart.Clear() ;
re tu rn Vi ew () ;

Мы не хотим препятствовать доступу пользователей , не прошедших аутенти­


фикацию, к остальным методам действий в контроллере Order. поэтому атрибут
Authorize применен только к методам List () и MarkSh ipped () . Нам нужно защи ­
тить все методы действий, определяемые контроллером Admin, и этого можно достичь
за счет применения атрибута Authorize к самому классу контроллера, что приведет
к применению политики авторизации ко всем содержащимся в нем методам действий
(листинг 12.7).
Листинг 12.7. Ограничение доступа в файле AdminController. cs
using Microsoft . AspNetCore . Mvc ;
using SportsStore . Models ;
using Systern . Linq ;
using Microsoft.AspNetCore.Authorization;
namespace SportsSto re.C on trollers {
332 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

[Authorize]
puЫic class AdminController : Controller {
private IProductRepository repos it ory;
puЫic AdminController(IProductRepository repo) {
repository = repo ;

puЫic ViewResult Index() => View(repository.Products) ;


puЫic ViewResult Edit(int productid) =>
View(repository . Products
. FirstOrDefaul t (р => р . ProductID == productid));
[HttpPost]
puЫic IActionResult Edit(Product product)
if (ModelState.IsValid) {
repository . SaveProduct(product);
TempData[ "message "] = $"{product . Name) has been saved ";
return RedirectToAction( " Index " );
else {
// Что - то не так со значениями данных
return View(product) ;

puЬlic ViewResul t Create () => View ( "E di t", new Product ()) ;
[HttpPost]
puЫic IActionResult Delete(int productld)
Product deletedProduct = repository . DeleteProduct(productid);
if (deletedProduct ! = null) {
TempData["message"J = $"{deletedProduct . Name} was deleted ";

return RedirectToAction( " Index " ) ;

Создание контроллера Accoun t и представлений


Когда пользователь. не прошедший аутентификацию, посыл ает запрос, который
требует авторизации, он перенаправляется на URL вида /Account/Login, где прило­
жение может пригласить пользователя ввести свои учетные данные. В качестве под­
готовки создайте модель представления для учетных данных пользователя, добавив в
папку Models/ViewModels файл класса по имени LoginModel. cs с определением,
показанным в листинге 12.8.

Листинг 12.8. Содержимое файла LoginModel. cs из папки Models/ViewModels

using System . ComponentModel.DataAnnotations;


namespace SportsStore . Models . ViewModels
puЫic class LoginModel {
[Required]
puЫic string Name { get ; set; }
Глава 12. SportsStore : защита и развертывание 333
[Required]
[UIHint("password")]
puЫic string Password { get; set; )

puЫic string ReturnUrl { get; set ; } " / ";

Свойства Name и Password декорированы атрибутом Required, который исполь­


зует проверку достоверности модели для обеспечения того, что значения были пре­
доставлены. Свойство Password декорировано атрибутом UIHint, поэтому в случае
применения атрибута asp-for внутри элемента input представления Razor, пред­
назначенного для входа, дескрипторный вспомогательный класс установит атрибут
type в password; таким образом, вводимый пользователем текст не будет виден на
экране. Использование атрибута UIHint описано в главе 24.
Далее добавьте в папку Controllers файл класса по имени AccountController. cs
с определением контроллера, приведенным в листинге 12.9. Этот контроллер будет
отвечать на запросы к URL вида /Account/Login.

Листинг 12.9. Содержимое файла AccountController.cs из папки Controllers

using System.Threading.Tasks ;
using Microsoft .As pNetCore . Authorization;
using Microsoft . AspNetCore . Identity ;
using Microsoft . AspNetCore .Identity.Ent ityFrameworkCore ;
using Microsoft . AspNetCo re.Mvc ;
using SportsStore . Models .Vi ewModels ;
namespace SportsStore . Controllers
[Authorize]
puЫic class Accountcontroller : Controller
private UserManager<IdentityUser> userManager;
private SigninManager<IdentityUser> signinManager;
puЫic AccountController(UserManager<IdentityUser> userMgr ,
SigninManager<IdentityUser> signinMgr) {
userManager = userMgr;
signinManager = signinMgr;

[AllowAnonymous ]
puЫic ViewResult Login(string returnUrl) {
return View(new LoginModel {
ReturnUrl = returnUrl
) );

[HttpPost)
[AllowAnonymous]
[Va li dateAntiForgeryToken ]
puЬlic async Task<IActionResult> Login(LoginModel loginModel) {
if (ModelState. IsValid) {
IdentityUser user =
await userManager.FindByNameAsync(loginModel.Name);
334 Часть 1. Введение в инфраструктуру ASP.NET Саге MVC

i f (user ! = null) {
await signinManager . SignOutAsync() ;
if ((await signinManager . PasswordSigninAsync(user ,
loginModel . Password , false, false)) . Succeeded)
return Redirect(loginModel? .ReturnUrl ?? 11 /Admin/Index 11 ) ;

11
ModelState .AddModelEr r or ( 1111 , Invalid name or password 11 ) ;

return View(loginModel) ;

puЫic async Task<RedirectResult> Logout(string returnUrl 11 / " ) {

await signinManager .Si gnOutAsync() ;


return Redirect(returnUrl);

Когда пользователь перенаправляется на URL вида /Account/Login, версия GET


метода действия Log in ( ) визуализирует стандартное представление для страницы и
создает объект модели представления, включающий URL, на который браузер должен
быть перенаправлен, если запрос на аутентификацию завершился успешно.
Учетны е данные аутентификации отправляются версииPOST метода дейс­
твия Login (), которая применяет службы Us e r Manager<Identi tyUser> и
SigninManager<Identi tyUser>, полученные через конструктор класса контрол­
лера, для аутентификации пользователя и его входа в систему. Работа упомянутых
классов объясняется в главах 28-30, а пока достаточно знать, что в случае отказа в
аутентификации создается ошибка проверки достоверности модели и визуализиру­
ется стандартное представление. Если же аутентификация прошла успешно, тогда
пользователь перенаправляется на URL, к которому он хотел получить доступ перед
тем, как ему было предложено ввести свои учетные данные.

Внимание! В целом использование проверки достоверности данных на стороне клиента яв­


ляется хорошей практикой . Она освобождает от определенной работы сервер и обеспе­
чивает пользователям немедленный отклик о предоставленных ими данных . Тем не менее,
не поддавайтесь искушению выполнять на стороне клиента аутентификацию, поскольку
это обычно предусматривает передачу клиенту допустимых учетных данных, которые бу­
дут применяться при проверке вводимых имени пользователя и пароля, или , по меньшей
мере, наличие доверия сообщению клиента о том , что аутентификация завершилась ус­
пешно . Аутентификация должна всегда выполняться на сервере .

Чтобы снабдить метод Login () представлением для визуализации, создайте папку


Views/Account и поместите в нее файл представления Razor по имени Login . cshtml
с содержимым, показанным в листинге 12.10.

Листинг 12.10. Содержимое файла Login.cshtml из папки Views/Account


@model LoginModel
@{
ViewBag . Title = "Log In ";
Layout = 11 _AdminLayout ";
Глава 12. SportsStore : защита и развертывание 335
<div class="text - danger " asp - validation -summary= "All " ></div>
<form asp-action= " Login" asp-controller="Account" method="post">
<input type= " hidden " asp -f or= "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>
<div class= " form - group " >
<label asp - for= " Passwo rd" ></label>
<div><span asp - validation - for= " Passwo r d " class= " text - danger " ></span>
</div>
<input asp - for="Password " class= " form - control " />
</div>
<button class= " Ьtn Ьtn - p ri mary " type= "s ubmit " >Log In</button>
</forrn>

Финальный шаг связан с изменением разделяемой компоноюш для админис­


трирования , чтобы добавить кнопку Log Out (Выход). которая позволит текущему
пользователю выходить из приложения за счет отправки запроса действию Logou t
(листинг 12 .11). Это удобное средство, облегчающее тестирование приложения, без
которого пришлось бы очищать сооkiе-наборы браузера , чтобы возвращаться в состо­
яние, 1югда аутентификация еще не прошла.

Листинг 12.11. Добавление кнопки Log Out в файле _AdminLayout.cshtml

<!DOCTYPE html>
<htrnl>
<head>
<meta name= "viewport " content= " width=de vi ce - width " />
<link rel= "stylesheet " asp- href-include=" liЬ/bootstrap/dist/css/* .min . css " />
<title>@ViewBag . Title</title>
<style>
.input-validation-error { border - color : red ; background- co l or : #fee
</style>
<script asp-src-include="liЬ/jquery/**/jquery . min.js " ></script>
<script asp -s rc - include= " liЬ/jquery - validation/**/jquery .v alidate . min .j s " >
</script>
<script asp - src-include= "l iЬ/jquery-validation-unoЬtrusive/**/* . rnin . js " >
</script>
</head>
<body class= "panel panel - default " >
<div class= "p anel - heading " >
<h4>
@ViewBag . Title
<а class="Ьtn Ьtn-sm Ьtn-primary pull-right"
asp-action="Logout" asp-controller="Account">Log Out</a>
</h4>
</div>
<div class= " panel - body " >
@if (TempData[ "rnessage "] != null) {
<div class= " alert alert - success">@TempData["message"J</div>
336 Часть 1. Введение в инфраструктуру ASP. NЕТ Core MVC

@RenderBody ()
</ d i v >
</ body>
</ h t ml >

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


Теперь можете протестировать политику безопасности, запустив приложение и
запросив URL вида /Adrnin / I ndex . Поскольку в настоящий момент вы еще не про­
шли аутентификацию и пытаетесь обратиться к действию, которое требует автори­
зации , браузер будет перенаправлен на URL вида /Ac count / Lo gi n . Введите Admin
и Secret123$ в качестве имени и пароля и отправьте форму. Контроллер Account
сравнит предоставленные учетные данные с начальными данными, добавленными в
базу данных Identity, и (при условии, что вы ввели правильные сведения) аутентифи­
цирует вас, после чего перенаправит обратно на /Account / Login, куда теперь у вас
имеется доступ . Процесс иллюстрируется на рис. 12.1.

D Log ln --------~----- D AJI Product~ х


~ с ГФ localhos . 0000/Лс г--·.---------------··-- ---·--·-·-, .,.
f·-·---"---.ic=:-.-::=== -~-'"-~ '-~~~-=~~~~~:~:;1;~~~="'--=-==---==11_ ~_
! Lo ln
1 g All Products 1111 J

1 --
Name

P::::ro Л· 1Kayak2 .
1
Ю Namo

2 Lilejacket

3 Soccвr 8311
Р гiсв

s21s.oo
$48.95

$1 9.50
". -
~-

-
ActIOns

-
1

1 ВМ lll!llfl!ll '1
4 ComerRags $34.95 ~
~ -
.. · - .

1
1_ _ _ _ _ _ _ _ _ _ _ _ _ _ J 5 S1ad111m $79,500.00 Ш lll

1 6 Thinking Cap $16.00 _,,. -


· ~·~--.. ......,..,....~ ..~..,,,.,...---
Рис. 12. 1. Процесс аутентификации/авторизации для административных функций

Р азвертывание приложени я
Все средства и функциональность приложения SportsStore на месте , так что са­
мое время подготовить приложение и развернуть его в производственной среде. Для
приложений ASP.NET Core МVС доступно множество вариантов размещения, и в это й
главе используется один из них - платформа Microsoft Azure. Она выбрана из-за того,
что поступает от Microsoft и предлагает бесплатные учетные записи , т.е. вы можете
полностью проработать пример SportsStore, даже если не хотите применять Azure для
собственных проектов.

На заметку! В данном разделе вам понадобится учетная запись Azure . Если у вас пока ее нет,
можете создать бесплатную учетную запись на h ttp : / / a zure .mi c r osoft . с от.
Глава 12. SportsStore: защита и развертывание 337

Создание баз данных


Начальной задачей является создание баз данных, которые приложение SportsStore
будет использовать в производственной среде. Такую задачу можно выполнять как
часть процесса разработки в Visual Studio, но трудность ситуации в том, что нужно
знать строки подключений для баз данных до развертывания, а это относится к про­
цессу , 1tоторый создает базы данных.

Внимание! Портал Azure часто меняется по мере того, как в Microsoft добавляют новые
средства и пересматривают существующие. Инструкции, приводимые в настоящем раз­
деле, были точными на время его написания, но к моменту выхода книги необходимые
шаги могут слегка измениться . Базовый подход должен остаться таким же, но имена по­
лей данных и точный порядок шагов может потребовать определенного экспериментиро­
вания, чтобы добиться правильных результатов.

Самый простой подход предусматривает вход на портал h t tp : / /portal . azure . сот


с применением своей учетной записи Azure и создание баз данных вручную. Посл е
входа выберите категорию ресурсов SQL Databases (Базы данных SQL) и щелкните на
кнопке Add (Добавить), чтобы создать новую базу данных.
Для первой базы данных укажите имя products. Щелкните на ссылке Configure
Required Settings (Конфигурировать обязательные настройки) и затем на ссылке Create
а New Server (Со здать новый сервер). Введите имя нового серв ера , 1юторое должно быть
уникальным в рамках Azure, и выберите имя пользователя и пароль для администра­
тора. В рассматриваемом примере было указано имя сервера sportsstorecoredЬ ,
имя пользователя sportsstoreadmin и пароль Secret123$. Вам понадобится ис­
пользовать друго е имя сервера и выбрать более надежный пароль . Выберите место­
положе ние для базы данных и щелкните на кнопке ОК, чтобы закрыть экран пара­
метров, и далее на кнопке Create (Создать), чтобы создать саму базу данных. Порталу
Azure потребуется несколько минут для выполнения процесса создания, после чего
база данных появится в категории ресурсов SQL Databases.
Создайт е еще один сервер баз данных SQL, указав на этот раз имя iden ti ty.
Вместо создания нового сервера баз данных можно применять тот, который был со­
здан ранее. Результатом окажутся дв е базы данных SQL Server, размещаемые Azure,
дет али которых приведены в табл. 12.1. У вас будут другие имена серверов баз дан­

ных и навернюш более надежные пароли.

Таблица 12.1. Базы данных Azure для приложения SportsStore

Имя пользователя­
Имя базы данных Имя сервера Пароль
администратора

products sp orts s torecoredЬ sportss t oreadmin Secret 123$


identity s p o r tsstoreco r edЬ spo rt sstoreadmin Secr et l 23$

Открытие доступа в брандмауэре для конфигурирования


Далее необходимо создать схемы баз данных, и проще всего это сделать, открыв
доступ в брандмауэре Azure, чтобы можно было запускать команды Entity Framework
Core из машины разработки.
338 Часть 1. Введение в инфрастру ктуру ASP.NET Core MVC

Выберите одну их двух баз данных в категории р е сурсов SQL Databases, щелкните
на кнопие Tools (С е рвис) и зат ем щел ините н а с сылие Ореп iп Visual Studio (Отирыт ь
в Visual Studio). Те перь щелините на ссылие Configure Your Firewall (Конфигуриров ать
брандмауэр) , щелкните на кнопке Add Client IP (Добавить IР-адр е с кли ента) и щелкни­
те на кнопке Save (Сохранить). Это позволит вашему те кущему !Р-адресу достигать
серв е ра баз данных и выполнять команды конфигурирования. (Проинсп ектиро в ать
схему базы данных можно , щелкнув на кнопке Open ln Visual Studio, что приведет к
открытию Visual Studio и использ ованию окна SQL Server Object Explorer дл я и с сл е­
дования базы данных.)

Получение строк подключений


Вскоре понадобятся строки подключений для новых баз данных . Портал Azure
предоставляет эту информацию по щелчку на базе данных в категории р е сурсов SQL
Databases чер ез ссылку Show Database Connection Strings (Показать строки подЮiюч е­

ний для баз данных). Строки подключений предлагаются для разных платформ раз­
рабо тки ; прилож е ниям .NET требуются строки ADO .NET. Вот строк а подключ е ния ,
которую портал Azure пр едоставляет для базы данных iden t i t у:

Serve r= tcp : sports s torecoredb . database . windows.net , 1433 ;


Data Source=sportsstorecoredb.database . windows . net ;
Initia l Catalog=produc t s ; Per si st Se curit y I nfo =False ;
User I D= {ваше_имя_пользователя}; Раsswоrd= {ваш_пароль}; Рооling=Fаlsе ;
Multip l eActiveResultSets= False ; Encrypt =T rue ; TrustServerCertifi cate=
False ; Connect i on Timeout=30 ;
В зависимости от того, как портал Azure подготовил базу данных , вы будете видеть
разные параметры конфигурации . Обратите внимание на отмеченные полужирным
заполнител и для и м ени пользователя и пароля, которые должны быть изм енены, ког­
да вы применяете ст року подключения при конфигурировании приложения .

Подготовка приложения
Перед те м , каи прилож е ние можно будет развернуть, пр едстоит выполнить опре­
деленные подготовительные шаги, чтобы привести его в готовность к прои з водств ен­
ной ср еде . В посл едующих р азделах будет и зменен способ отобр аже ния сообщений об
ошибках и настроены строки подключения для производств е нных баз данных.

Создание контроллера и представления


для отображения сообщений об ошибках
В настоящий момент приложение сконфшурировано на использование страниц оши­
бок , дружественных к разработчику , которые предоставляют полезную информацию, ког­
да случается пробл ема. Конечные пользователи не должны видеть таиую информацию,
поэтому добавьте в папку Con trollers файл класса по имени ErrorController . cs с
определением простого контроллера, показанного в листинге 12.12.
Листинг 12.12. Содержимое файла ErrorController. cs из папки Controllers
usi ng Mi cro s oft . As pNetCo r e . Mvc ;
namespace SportsStore . Controll e rs
puЬlic clas s ErrorContro ll er : Contro ll er
puЫ ic Vi ewResul t Erro r () => Vi ew ( ) ;
Глава 12. SportsStore: защита и развертывание 339
В контроллере определено действие Error, которое визуализирует стандартное
представление. Чтобы снабдить контроллер представлением, создайте папку Views/
Error, добавьте в нее файл представления Razor по имени Error . cshtml с размет­
кой, приведенной в листинге 12.13.
Листинг 12.13. Содержимое файла Error.cshtml из папки Views/Error
@{
Layout = null ;

<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device- width" />
<link rel= "stylesheet " asp-href-include="liЬ/bootstrap/dist/css/* .min. css " />
<title>Error</title>
</head>
<body>
<h2 class= "text-danger">Error. </h2>
<hЗ class= "text -danger" >An error occurred while processing your request. </hЗ>
</body>
</html>

Страница ошибки подобного рода является последним ресурсом , поэтому ее луч­


ше сохранить как можно более простой и не полагаться на разделяемые компоновки,
компоненты представлений или другие многофункциональные средства. В данном
случае мы отключаем разделяемые компоновки и определяем простой НТМL-документ
с сообщением о возникновении ошибки, не предоставляя нш<акой информации о том,
что произошло.

Определение настроек производственных баз данных


На следующем шаге создается файл, который будет снабжать приложение стро­
I<ами подключения к его базам данных в производственной среде. Добавьте в проект
SportsStore новый файл по имени appsettings . production. j son с применением
шаблона элемента ASP.NET Configurati on File (Файл конфигурации ASP.NET) и помести­
те в него содержимое, показанное в листинге 12.14.

Совет. В списке файлов окна Solution Explorer данный файл находится внутри узла
appsettings . j son, который понадобится раскрыть, если позже вы захотите отреда к­
тировать этот файл .

Листинг 12.14. Содержимое файла appsettings .production. json

" Data ": (


" SportStoreProducts":
"ConnectionString": "Server=tcp:sportsstorecoredЬ.database.windows.net,
1433;Data Source=sportsstorecoredb . database.windows .net;Initial Catalog=
products;Persist Security Info=False;User ID= {ваше_имя_пользователя};
Password={вaш_пap oль}; MultipleActiveResultSets=False;Encrypt=True ;
TrustServerCertificate=False;Connection Timeout=ЗO;"
},
340 Часть 1. Введение в инфраструктуру ASP. NEТ Соге MVC

" Sport Storei d e n tity ": {


" Con nect i o nStr i ng ": " Se r ver =tcp: s p or ts store co redb . database .
wi ndows . net , 1433 ; Data Sourc e =sport s storecoredb . da t abase . windows . net ;
Initial Catalog=identity ; Pers i st Secu r ity Info= False ; User ID= {ваwе_имя_
пoльзoвaтeля}; Passwo r d ={вaw_пapoль}; Mult ip leActiveResu l tSets=Fa l se ;
Encrypt =Tr ue ; TrustServerCertificate= False ; Connect i on Ti meout=ЗO ;"
}

Файл неудобен для ч тения, т.к. строки подключений разбивать нельзя. Соде ржимое
данного файла дублирует раздел строк подключений файла appsettings . j son , но
здесь используются строки поднлючений Azure. (Н е забудьте заменить заполнители
для имени пользователя и пароля . )

Конфигурирование приложения
Теперь можно и зменить код класса Sta r tup, чтобы в случае н ахождения в прои з ­
водств енной ср еде приложение вело с е бя по-другому, применяя контроллер Error и
строки подключений Azure. Изменения приведены в листинг е 12.15.

Листинг 12.15. Конфигурирование приложения в фа йле Startup. cs


us i ng Microsoft . AspNetCore . Bu i lder ;
us ing Microsoft . AspNetCore . Hosting ;
us i n g Mic r osoft . AspNe t Co r e . Htt p ;
u s ing Micro s oft . Extens i o n s . De p ende n c yin jection ;
using Microsoft . Extensions . Loggi n g ;
using SportsSto re. Models ;
using Mi crosoft . Exten s ions . Con figur a tion ;
using Microsoft . EntityFramewo r kCore ;
us i ng Microsoft . AspNetCore . Identity .E ntityFrameworkCore ;
namespace SportsStore {
p u Ыic c l ass Startup {
ICo n figura t ionRoot Conf i guration ;
puЫ i c Sta r tu p ( I Host i ngEnvi ro nme n t e nv) {
Configuration = new Conf i gurationBuilder()
. Se tBasePath(env . ContentRootPath)
.AddJso nFil e ( " appsett i ngs . j son" )
.AddJsonFile($"appsettings . {env . EnvironmentName}.json", true)
. Build() ;

puЫ i c vo i d ConfigureServi ces( I ServiceCo l lection services)


services . AddDbContext<Appl i ca t ionDbContext>(options =>
o p tions.UseSqlServer(
Conf i g u ration[ " Data : Sp ortSto r eProducts :Connect i o n Str ing" ])) ;
services . AddDbContext<Ap pidentityDbContext>(options =>
o p tions . UseSqlServer(
Config u ration [" Data : Spo r tS tor eidentity : Co nnectionStri ng" ])) ;
services . Addidentity<Ide ntityUse r , IdentityRole>()
. AddEntityFrameworkSto r es<Ap p i dentityDbContext>() ;
Глава 12. SportsStore : защита и развертывание 341
services . AddTransient<IProductRepository , EFProdu ctRepository>() ;
services .AddScoped<Cart>( sp => Sess i onCart . GetCart(sp)) ;
services . AddSingleton<IHttpContex tAcces sor , HttpContextAccessor>() ;
services.AddTransient<IOrderRepository , EFOrderRepository>() ;
services . AddMvc() ;
services . AddMemoryCache() ;
services.AddSession() ;

puЫic void Configure( IAppli cat i on Builder арр ,


IHostingEnvironment env , ILoggerFactory loggerFactory) (
if (env.IsDeveloprnent()) {
app .U seDeveloperExceptionPage() ;
app . UseSta t us Code Pages() ;
else {
app . UseExceptionHandler( 11 /Error 11 ) ;

app . UseStaticFiles() ;
app. UseSession( ) ;
app . Useide ntity( );
app.UseMvc(routes =>
routes .MapRoute (narne: 11 Error", ternplate: "Error",
defaul ts: new { controller = 11 Error 11 , action = 11 Error 11 } ) ;

routes . MapRoute(
name : null ,
template: " {cate gory)/Page{page : int) ",
defaults : new ( control l er = 11 Product 11 , actio n 11
List 11 )

) ;
routes . MapRoute(
name : null,
template: " Page ( page: int) 11 ,
defaults : new ( con t roller = " Product ", action '' List ",
page = 1 )
) ;
routes . MapRoute(
name : null ,
template : " { ca tegory} ",
11 11
defaults: new ( controller Product 11 , action Li st 11 ,
page = 1
) ;
routes . MapRoute(
name : null ,
template : 1111
defaul ts : new ( contro l ler = 11 Product 11 , action = 11 List ",
page = 1 } ) ;
routes . MapRoute(name : null, temp l ate : "( controller)/(action)/( id? ) " ) ;
}) ;
SeedData . EnsurePopulated(app) ;
IdentitySeedData . EnsurePopulated(app} ;
342 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

Интерфейс IHo s tingEnv ironmen t используется для предоставления информ ации


о среде , в которой функционирует приложение, такой как среда разработки или про­
изводств е нная среда .

Мы воспользовались преимуществом этого ср едства для загрузки разных конфи­


гурационных файлов с подходящими строками подключений, ориентированными на
ср еду разработки и производств е нную среду . а также изменения набор а ко м пон е нтов.
которы е применяются для обработки запросов. Таким обр азом, средства , сп ецифич­
ны е для р азработк и, вроде Browser Link не включаются, когда прилож ение р аз в е рты­
вает ся. До ступно множество параметров для настройки конфигурации прилож ения в

р азличных средах. которые будут рассматриваться в главе 14.


Обновление конфигурации проекта
Осталось вн е сти н е сколько фин ал ьных корректирово к в файл proj ect. j son про­
екта SportsSto r e , чтобы обеспечить развертыв ание правильных частей прилож е ­
ния (листинг 12. 16).

Листинг 12.16. Обновление файла project. j son проекта Spo rtsStore

"de p endencies " : {


"Microsoft . NETCore . App " :
" ve r s i o n" : " 1. 0 . О ",
" type ": "platform "
}'
"Microsoft.AspNetCore.Diag nostics ": "1 . 0 . 0 ",
"Microsoft.AspNetCore . Serve r . IISintegration " : " 1 . 0 . 0 " ,
"Microsoft . AspNetCore . Serve r. Kest r el ": " 1 . 0 . 0 " ,
"Microsoft . Extensions . Logg i ng . Console ": " 1 . 0.0 ",
"Microsoft . AspNetCore . Razor . Too l s ": {
" version": " l . 0 . 0-preview2 - fi na l",
" type " : "b uild "
}'
"Microsoft . As pNe t Core . Stati cFi les " : "1 . 0 . 0 ",
"Microsoft . AspNetCore . Mvc ": " 1 . О . О ",
"Microsoft . EntityFrameworkCore . Sq l Se r ver ": " 1 . 0 . О ",
"Microsoft . EntityFrame workCore .Too l s ": " l . 0 . 0- previ ew2 - final ",
"Microsoft . Exte n sions . Con f igu r ation . J son ": " 1 . 0 . 0 ",
"Mi cro s of t. AspNetCore . Sess i on ": " 1 . 0 . 0 " ,
"Mi crosoft . Extensions . Cach in g . Memory ": " 1 . 0 . 0 ",
"Microsoft . AspNetCore .H ttp . Extensions ": " 1 . 0 . 0 ",
"Microsoft . AspNe t Core . Ident ity . Ent i ty Frame workCore " : 01 1.0 . 0 "
}'
" too l s ": {
"Microsoft . AspNetCore . Razor . Tools ": " 1 . О . O- preview2-final ",
"Microsoft . As pNetCore . Server . IISintegration . Tools " :
" l . 0 . 0- preview2 - final ",
"Microsoft . Ent i ty Framewo rkCore . Too l s ": {
"ver si on" : "l . 0 . 0- previ ew 2- fina l",
" impo r ts ": [ " portaЫe - net4 5+win8+dnxcore50 " , " portaЬle -n et4 5+wi n8 " ]

}'
Глава 12. SportsStore: защита и развертывание 343
"f rarneworks ": {
" netcoreappl . 0 ":
" irnports ": [ " dotnetS . 6 ", " portaЫe - net45+win8 " ]

} 1

"buil dOptions ":


" ernitEntryPoint ": true ,
" preserveCompilationContext": true
} 1

" runtirneOptions ": {


" configProperties ":
" Systern . GC . Server ": true

} 1

"puЫishOptions":
11
include 11 : [ "wwwroot", "Views", "appsettings. json",
11
appsettings. production. j son 11 , "web. config 11 ]
} 1

" scripts ": {


"postpuЫ ish ": [ " dotne t puЫish-iis --p uЫ ish-fol der % puЬlish :
OutputPath % -- frarnework % puЫish:FullTargetFrarnework % "

Добавления к разделу puЫishOptions включают в процесс развертывания ос­


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

Применение миграций баз данных


Чтобы настроить базы данных с помощью схем, требующихся приложению, от-
кройте консоль диспетчера пакетов и запустите следующие команды:

Update - Database - Context ApplicationDbContext - Environment Production


Update - Database - Context AppidentityDbContext - En vironme n t Production
В параметре - Envirorunent у~<азывается среда размещения, которая используется
для получения строк подключения к базам данных. Если команды не выполнились,
тогда удостоверьтесь, что сконфигурировали брандмауэр Azure на разрешение досту­
па вашей машины разработки , как было описано ранее в главе.

Процесс развертывания приложения


Чтобы ра з вернуть приложени е, щелкните правой кнопкой мыши на элементе про­
екта SportsStore в окне Solution Explorer (проекта, но не решения) и выберите в кон­
текстном меню пункт PuЫish (Опубликовать). Среда VisuaJ Studio предложит выбрать
метод опубликования (рис. 12.2).
Выберите вариант Microsoft Azure Арр Service (Служба приложения Microsoft Azure).
Бам будет предложено ввести данные учетной записи Azure. Щелкните на кнопке New
(Новая). как показано на рис. 12.3.
344 Часть 1. Введение в инфраструктуру ASP.NET Саге MVC

Sefect а pu Ы ish target


l 41$ Microsoft Azure Арр Setv\c~

f@ lmpoit

1D fi/e System

Find oth•r hosting options at our ~~

Рис . 12.2. Выбор метода опубликования

~ Арр Service
l:j i:J Host your vJeb and moЬile applications. REST APls. and more in Azure

Subscriptio n

View
' R ~~ u~~ e G 10~ .-. - - - - - ------·- ·-· --

Se~ r ch

New".

ок [~

Рис. 12.З. Создание новой службы приложения Azure

В следующем диалоговом окне предлагается сконфигурировать настройки веб-при ­


ложения Azure (рис. 12.4). Выберите имя для приложения , которое должно быть уни­
кальным в Azure, поскольку по умолчанию все приложения совместно используют общее
доменно е имя . В качестве имени выбрано s p o r ts s toreco r e , т.е. развернутое прило­
жение будет доступно по адресу h ttp : / / spo r tsstorecore . az u rewebsi tes . сот.
Далее введите или выберите группу ресурсов из раскрывающегося списка . Группа
ресурсов применяется для категоризации облачных активов , созданных для прило­
жения, и полезна при управлении крупными развертываниями разных приложе ний.
В этом примере создан а группа р е сурсов под названием Sp o rt s Store .
Глава 12. SportsStore: защита и развертывание 345
х

f:4J Create Арр Service


UU Host your 1veb and mobile applicat.ions. REST APls. and more in Azure
, •• HostiП<J @ Yleb Арр Nome
Servic.es ;s~Ort~rtor~.'~~ . ___ ."- -·
Subscription

LWi--ndoм
1
Azur~ мSoN·- Visu; IStudio Ui~;at~ ~
------- --------- -----·---~ ---- -- -- ~J
R~ource Group

~~ort~Sto r; ._
Арр Servicc Р l м
New... 1(1

Clicking the Cre• te Ьutton win cre.ate the ro1&owing Azure resour ces

Арр Servicc - sportsstorecorc

lf you havc removed yo ur spcndi ng limit or you arc u~ing Р4у гs Vou Go, therc may Ь с monct ~нy impact if you provision "dditional r ~ou rcc.s .
t ~~111 ~- ! cr~

Ьoport" . Cre3te Canccl

Рис. 12.4. Конфигурирование службы приложения

Потребуется также создать план обслуживания. Щелкните на кнопке New (Новый)


и введите имя. выберите регион и укажите размер, который будет использоваться
для размещения приложения (рис. 12.5). В рассматриваемом примере указан план по
имени SportsStoreCorePlan, находящийся в регионе восточных США (East US) и
применяющий вариант размера Free (Свободный).
Щелкните на кнопке ОК , чтобы сохранить план обслуживания, и затем на кнопке
Create (Создать) для продолжения процесса развертывания. Когда среда Visual Studio
завершит настройку развертывания, вы увидите экран PuЫish (Опубликование), пред­
ставленный на рис. 12.6.

~r"] Configure Арр Service Plan


[ji:J An Арр Service plan is the container for your арр. The Арр ...
Арр Service Plan

~ё_~~s!~Core!~~-~:--~=---=~-=~-:_-=-_:_~~.::=-- =~~=- ·=~~==


Location
=]
rSize
~ ~rt.~~- .=~~--=--~-_ =~==~~==-~~==-=-~=-~~=--===--- ":J
. -·--- --- ---·-· ---·--· __ "_________
---]
1-Free ·-----~·----·----~·-·--- _____ ________ -
_____, - "

ОК 1\ Canc~I

Рис. 12.5. Выбор плана обслуживания


346 Часть 1. Введение в инфраструктуру ASP.NET Саге MVC

ф PuЬlish

Profile sportsstorecorc - Web D<>ploy

Conncctto~
PuЫish method: Гwd;~oy--====--==~-------·-3_
-~
Settings

Prtview
Servll!r:
..-------------·
1sportssto r ec~m.az u rewebsit~. n e:t;443 -- -=:J
Site name:

Username:

Password:
1··········~················~~-;;;;.~~~~~~
0 Save password
Dt•ti nation URl: \ http://sportsstorecoro.azurewebsites.ne_t _ _ _ _ _ _ r_._J
Validate Connection

1[__ __ < Prev j [§:J 1 PuЫish 11 Close

Рис. 12.6. Подготовка к опубликованию приложения

Последний шаг заключается в щелчке на кнопке PuЫish (Опубликовать). Среда


Visual Studio начнет процесс опубликования и развернет приложение в облаке Azure.
Процесс может занять некоторое время, потому что начальное развертывание проек­
та требует передачи большого количества файлов: последующие обновления проходят
быстрее, т.н:. загружаются только новые и измененные файлы.
Когда процесс опубликования завершается, платформа Azure запустит приложе­
ние, а среда Visual Studio откроет окно браузера с URL размещения (рис. 12.7).

Резюме
В этой и предшествующих главах демонстрировалось использование инфраструк­
туры ASP.NET Core MVC для создания реалистичного приложения электронной ком­
мерции. В приведенном расширенном примере были проиллюстрированы многие
основные средства MVC: контроллеры, методы действий, маршрутизация, представ­
ления, метаданные, проверка достоверности, компоновки, аутентификация и т.д . Вы
также видели, как пользоваться рядом ключевых технологий, имеющих отношение к
МVС, в том числе Eпtity Framework Core, внедрение зависимостей и модульное тести­
рование. Результатом стало приложение с ясной и ориентированной на компоненты
архитектурой, обеспечивающей разделение обязанностей, и кодовой базой , которую
будет легко расширять и сопровождать. На этом разработка приложения SportsStore
завершена. В следующей главе вы научитесь применять Visual Studio Code для созда­
ния приложений ASP.NET Core MVC.
Глава 12. SportsStore : защита и развертывание 347

CJ Spon<Store Х

С \ф sp~~s~orec~r~:z~~re\~:bsites.11et ----=~==-~--=====--~
SPOПTS STOHE а

Home

Chess Kayak IЩfj•l•I


Soccer А boat for one person 11\lli.P
Watersports

lifejacket
Protective and fashionaЫe
1
1

Soccer Ball IJ'!M••


FIFA-approved size and weight liih
1

1
Corner Flags litИJ
Give your playing field а professional touch 811 1

1 1
• 2 . 3
1
!
L--------·---------·-··---------· J
Рис . 12. 7. Развернутое приложение
ГЛАВА 13
Работа с
Visual Studio Code

в настоящей главеVisual
применением
кодом производства
будет показано, как создать приложение ASP.NET Саге MVC с
Studio Code -
Microsoft.
межплатформенного редактора с открытым
Несмотря на название, продукт
Visual Studio Code не
имеет отношения: кVisual Studio и основан на инфраструктуре Electгon, используе ­
мой редактором Atom, который популярен среди разработчиков, применяющих дру­
гие инфраструктуры для построения веб-приложений, такие как Angular.
Продукт Visual Studio Code поддерживает операционные системы Windows ,
OS X/macOS и наиболее популярные дистрибутивы Llnux. Продукт Visual Studio Code
находится на начальной стадии своего развития, поэтому не все средства работают
должным образом ; однако в Microsoft предлагают ежемесячные обновления: и про­
движение происходит довольно быстро. Некоторые текущие ограничения, такие как
отсутствие поддержки отладки для приложений ASP.NET Саге, могут быть устранены
ко времени выхода этой книги, но вьmолнение всех примеров в данной книге по-пре­
жнему требует Visual Studio и Windows.
Процесс разработки в Visual Studio Code менее автоматизирован, чем в полной
версии Visual Studio, но он вполне осуществим, предлагая: пристойную отправную
точку для построения приложений ASP.NET Core MVC в средах других операционных
систем или легковесную альтернативу Visual Studio 2015 в Windows.

На заметку! В Microsoft заявили, что инструментарий, используемый для создания прило­


жений ASP. NEТ Core MVC, в будущем изменится. Проверяйте веб-сайт издательства на
предмет обновлений, которые появятся после выпуска новых инструментов .

Настройка среды разработки


Настройка Visual Studio Code требует выполнения определенной работы, посколь­
ку некоторая функциональность, включенная в Visual Studio, обрабатывается здесь
внешними инструментами. Одни инструменты идентичны тем, которые среда Visual
Studio применяет "за кулисами", но другие являются: новыми в мире разработки для
.NET и могут быть незнакомыми. Хорошая новость в том. что эти инструменты ши­
роко используются разработчиками. которые ориентируются на другие инфраструк­
туры для построения: веб-приложений, причем качество и функциональные средства
находятся на высоком уровне. В последующих разделах мы исследуем процесс уста­
новки Visual Studio Code наряду с важными инструментами и дополнениями, требу­
ющимися: для: разработки приложений MVC.
Глава 13. Работа с Visual Studio Code 349
Установка Node.js
В мире разработки клиентской стороны Node.js (или просто Node) представляет
собой исполняющую среду, на которую полагаются многие популярные инструменты
разработки. Node была создана в 2009 году как простая и эффективная исполняющая
среда для серверных приложений, написанных на языке JavaScript. Она основана на
механизме JavaScript, применяемом в браузере Chrome, и предлагает АРI-интерфейс
для выполнения кода JavaScript за пределами среды браузера.
Исполняющая среда Node.js достигла определенных успехов в качестве сервера
приложений, но в этой главе она интересна тем, что предоставляет основу для нового
поколения межплатформенных инструментов построения и диспетчеров пакетов. Ряд
интелле1пуальных проектных решений, внедренных командой разработчиков Node, и
межплатформенная поддержка, обеспечиваемая исполняющей средой JavaScript бра­
узера Chrome, создали благоприятную возможность, которой воспользовались полные
энтузиазма проектировщики инструментов, особенно те из них, кто желал поддержи­
вать разработку веб-приложений.

На заметку! Доступны две версии Node.js. Версия LTS (Loпg Term Support - долгосрочная
поддер жка ) предоставляет стабильный фундамент для развертывания в производствен­
ных средах, где изменения должны быть сведены к минимуму. Обновления версии LTS
выпускаются каждые 6 месяцев и сопровождаются в течение 18 месяцев. Версия Curreпt
(текущая) является более часто изменяющимся выпуском, в котором преимущество от­
дается новым средствам взамен стабильности . В настоящей главе мы будем применять
выпуск Curreпt.

Установка Node.js в Windows


Загрузите и запустите установщик Node.js для Windows из веб-сайта http: / /
nodej s. org. При установке Node.js удостоверьтесь, что выбран вариант Add to РАТН
(Добавить в переменную РАТ Н). На рис. 13.1 показан установщик для Windows, ко­
торый предлагает модифицировать переменную среды РАТН в качестве варианта
установки.

Установка Node.js в 05 X/mac OS


Установщик для OS X/macOS может быть загружен из веб-сайта http : / /
nodej s. org. Запустите установщю< и примите стандартные настройки . Когда уста­
новка завер шится, удостоверьтесь в наличии пути /usr / local/Ьin в переменной
$РАТН.

Установка Node.js в Linux


Самый простой способ установки Node.js в Linux предполагает использование дис­
петчера пакетов; команда разработчиков Node предоставила инструкции для основ ­
ных дистрибутивов по адресу ht tp: / /nodej s . org / en/ download/package - manager .
В среде Ubuntu для загрузки и установки Node.js прим еняются следующие команды:

curl - sL https:// deb .nodesource.com/setup_б.x 1 sudo -Е bash


-sudo apt - get install - у node js
350 Часть 1. Введение в инфраструктуру ASP.NET Соге MVC

n d~
Custoni 5etup

г "",::;::-.~
1
. . ".._""'' """. .,
Select lhe way you want features to Ье instaИed.

"'"~
InstaП
@j ----

the core Node .js runtime


(node.exe).

Тhis feature requires 16МВ on your


hard drive. Ithas 2of 2
suЬfeaЬJres selected. Тhе
subleatures reQUire 2ОКВ on your
hard drive.

Рис. 13.1. Добавление в переменную среды РАТН

Проверка установки Node


После завершения установки откройте новое окно командной строки и запустите
показанную ниже команду:

node - v
Если установка прошла успешно, а путь к Node бьm добавлен в переменную среды
РАТН, тогда вы увидите номер версии. На момент написания главы текущей версией
Node являлась 6.3.0. Если во время проработки примеров, рассмотренных в главе, вы
получаете неожиданные результаты, то попробуйте воспользоваться указанной кон­
кретной версией.

Установка Git
Продукт Visual Studio Code вЮiючает интегрированную поддержку Git, но требует­
ся отдельная установка для поддержки инструмента Bower, который применяется при
управлении пакетами юшентской стороны.

Установка Git в Windows или OS X/macOS


Загрузите и запустите установщик из веб - сайта h t t р s : / / g i t - s ст . с от/
down loads.

Установка Git в Linux


В большинстве дистрибутивов Linux инструмент Git уже установлен. Если вы хо ­
тите установить его в любом случае, тогда ознакомьтесь с инструкциями по установке
для вашего дистрибутива по адресу https: //git -scт. com/download/linux.
Глава 13. Работа с Visual Studio Code 351
В среде Ubuntu используется следующая команда:

sudo apt -get install git

Проверка установки Git


После завершения установки запустите приведенную ниже команду в новом окне
командной строки/терминала, чтобы проверить , установлен и доступен ли инстру­
мент Git:
git -- version
Команда выведет версию установленного пакета Git. На момент написания главы
по следней версией Git для Windows была 2.9.0 , а для OS X/macOs/Linux - 2.8.1 .

Установка Yeoman, Bower и Gulp


Node .js поступает с диспетчером пакетов Node (Node Package Manager - NPM), ко­
торый применяется дл я загрузки и установки пакетов разрабоп<и. написанных на
JavaScript. Пакеты, полезные для разработки приложений ASP.NET Core MVC, описа­
ны в табл. 13.1.

Таблица 13.1. Пакеты NPM, полезные для разработки приложений ASP.NET Core
Имя Описание

уо ПакетУеоmаn (известный как уо ) - это инструмент, который об­


легчает начало новых разрабатываемы х проектов , устанавливая
исходное содержимое, как демонстрируется в разделе "Создание
проекта ASP.NET Core" далее в главе

bower Это тот же самый инструмент Bower, описанный в главе 6, кото­


рый используется для управления пакетами клиентской стороны

generator - aspnet Этот пакет снабжаетУеоmаn информацией, необходимой для со­


здания новых проектов ASP.NET Core MVC, как объясняется в раз­
деле "Создание проекта ASP.NET Core" далее в главе

В среде Windows указанные пакет ы устанавливаются с помощью такой команды:

npm install - g yo@l . 8 . 4 bower@l.7.9 generator - aspnet@0 . 2 .1


Ко времени выхода книги могут появиться более новые версии этих пакетов, но
при с о здании при ме ра применялись ве рсии, упомянутые в коман дах установки .

В средах Linux и OS X/macOS установка производится посредством команды sudo:


sudo npm install - g yo@l.8 . 4 bower@l . 7 . 9 generator - aspnet@0 . 2 . 1

Установка .NET Core


При раз работке приложений ASP.NET Core MVC требуется исполняющая среда
.NET Core. Для каждой поддерживаемой платформы предусмотрен собств енн ый про ­
цесс устан овки , оп и с ание которого доступно по адресу www . rnicrosoft . corn/net/
core. В Microsoft предлагают установщики для Windows и OS X/macOS, а также пре­
доставляют инструкции для установки в Linux с использованием архивов tar.
352 Часть 1. Введение в инфраструктуру ASP.NET Соге MVC

Установка .NET Core в Windows


Чтобы установить .NET Core в Windows, просто загрузите и запустите установщик
.NET Core SDK (который отделен от установщика .NET Core для Visual Studio, необхо­
димого в главе 2).

Установка . NET Core в OS X/macOS


Перед запуском установщика .NET Core в среде macOS должна быть установлена
последняя версия пакетаOpenSSL; в Microsoft рекомендуют применять для этого дис­
петчер пакетов Homebrew. Откройте новое окно терминала и выполните следующую
команду, чтобы установить Homebrew:

/usr/Ьin/ruby - е " $ (curl -fs SL


https : //raw.githubusercontent.com/Homebrew/install/master/install)"
Для обновления пакета OpenSSL запустите показанные ниже команды:

brew update
brew install openssl
brew link --force openssl
Загрузите установщик .NЕТ Core, находящийся по адресу h t tps : / / go. rnicrosoft .
corn/fwlink/?LinkID=809124, и запустите его, чтобы добавить .NET Core в свою
систему.

Установка .NET Core в Linux


По адресу www.microsoft . com/net/core пр едоставлены инструкции по уста­
новке .NET Core в большинстве популярных дистрибутивов Linux. В настоящей главе
используется Ubuntu, и процесс требует первоначальной настройки новой подачи для
apt-get с применением следующих команд:

sudo sh -с ' echo "deb [arch=amd64]


https://apt-mo.trafficmanager.net/repos/dotnet/ trusty
main" > /etc/apt/sources . l is t . d/dotnetdev .l ist'
sudo apt-key adv --keyserver apt-mo.trafficmanager.net
--recv-keys 417А0893
sudo apt - get update
Далее производится установка .NЕТ Core:
sudo apt-get install dotnet-dev-1.0.0-preview2-003121

Проверка установки .NET Core


Независимо от имеющейся платформы проверка того, что инфраструктура .NET
Core установлена и готова к использованию, осуществляется одинаково. Откройте но­
вое окно командной строни/терминала и выполните таную команду:

dotnet --version
Команда dotnet запускает исполняющую среду .NET, после чего отобразится но­
мер версии установленного пакета .NET. На момент написания главы текущим выпус­
ком был 1. О. 0-preview2 - 003121, но ко времени выхода книги он наверняка будет
заменен более новым выпуском.
Глава 13. Работа с Visual Studio Code 353

Установка Visual Studio Code


Самым в ажным шагом является загрузка и установка р едактора Visual Studio Code,
который доступен на веб-сайт е h ttp : //code . vis ualstudio . сот . Установочные па­
кеты предусмотрены для Windows, OS X/macOS и популярных дистрибутивов Linux.
Загрузит е и установите пакеты для предпочитаемой платформы .

На заметку! Компания Microsoft предлагает новые выпуски Visual Studio Code ежемесячно, а
это значит, что устанавливаемая вами версия будет отличаться о версии 1.3, которая при­
меняется в главе. Хотя основы должны остаться теми же самыми, выполнение некоторых
примеров может потребовать определенной доли экспериментирования.

Установка Visual Studio Code в Windows


Чтобы установить Visual Studio Code для Windows, просто запустите установщик.
Посл е з аве ршен ия процесса Visual Studio Code запустится, и вы увидите окно редак­
тора, представленное на рис. 13.2.

Установка Visual Studio Code в 05 X/macOS


Продукт Visual Studio Code предоставляется в виде архива zip для Мае , который
доступ е н для загрузки по адресу https : / / g o .microsof t. com/fwl i nk/ ? Lin kI D=
620882 . Раскройте архив и дважды щелкните на содержащемся в нем файле
Visual St udio Code. а р р , чтобы запустить Visual Studio Code и получить окно ре­
дактора. показ анное на рис. 13.2.

Установка Visual Studio Code в Linux


Компания Microsoft предлага ет файл . deb для DeЫan и Ubuntu и файл . rpm для
Red Hat, Fedora и CeпtOS . Загрузите и установите файл для подходящего дистрибути­
ва Linux. Поскольку в главе используется Ubuntu, понадобится загрузить файл . deb
и установить его с помощью следующей команды:

s u do dpkg - i code 1 . 3 . 0- 146 7 909982 a rnd 64 . deb


Посл е заверше ния установки выполните приведенную ниже команду , чтобы запус­
тить Visual Studio Code и получить окно редактора, п01шзанное на рис. 13.2:
/u s r /share / code / cod e

Проверка установки Visual Studio Code


Тестирование успешности установки Visual Studio Code сводится к проверке воз­
можности запуска приложения и открытию окна редактора (см. рис. 13.2). (Цветовая
схем а была изм ен ена , т.к . стандартные темные цвета не очень хорошо подходят для
получения экранных снимков .)

Установ ка расширения С# для Visual Studio Code


Редактор Visual Studio Code поддерживает функциональность , специфичную для
языка. через расширения, хотя это не те же самые расширения, которые поддержи­

ваются ср едой Visual Studio 2015. Самое важное расширени е , насающееся разработ­
ки приложе ний ASP.NET Core MVC, добавляет подде ржку для С# , отсутствие которой
может выглядеть как странное упущение в базовой установке.
354 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

----·-------------
Vntltled· 1 - v11u;:,1 Studlo Code

·• Unt•tltd·1.'V.sщ1S1~ю.-.Cod~ . .~ , . ,.. ~ ~ ~., ~· · ,· '- •


.
. ~ . :- '\ ~D ' Х'

Рис. 13.2. Выполнение Visual Studio Code в средах Windows, OS X/macOS и Ubuntu Linux

Тем не менее, такое положение дел отражает тот факт, что в Microsoft позициони ­
ровали Visual Studio Code как универсальный межплатформенный редактор, который
поддерживает широчайший спектр я зыков и инфраструктур.
Чтобы установить расширение С#, выберите пункт Command Palette (Палитра ко­
манд) в меню View (Вид) редактора Visual Studio Code. Палитра команд предоставляет
доступ ко всем командам, которые можно выполнять с помощью Visual Studio Code.
Введите ext и нажмите на клавишу <Return>. в результате чего Visual Studio Code от­
кроет окно Extensions (Расширения). Введите csharp и отыщите в списке расширение
С# for Visual Studio Code (С# для Visual Studio Code), как показано на рис. 13.3.
Щелкните на кнопке lnstall (Установить), после чего Visual Studio Code загрузит и
установит расширение. Щелкните на кнопке ЕnаЫе (Включить), чтобы активизиро­
вать расширение (рис. 13.4).

rn ... 1

С# Litense

Microsoft 1 'i' 331463 j ***У 36

С# for Visua l Studio Code (powered Ьу OmniSharp)


Mo.stor Dirv

Рис. 1 З.З. Нахождение расширения С#


Глава 13. Работа с Visual Studio Code 355

С# license

Microsoft 1 4-> 331463 1 ** *1. 36

О for Visual Studio Code (pov;ered Ьу OmniSJ1arp).

[!zma ~Nnstan'
Рис. 13.4. Включение расширения С#

Создание проекта ASP.NET Core


Редактор Visual Studio Code не имеет интегрированной поддержки для создания
проектов ASP.NET Core и в настройке начальной структуры папок и файлов пола­
гается на пакет Yeoman, который применяет шаблоны, предоставляемые пакетом
generator - aspnet. Откройте новое окно командной строки/терминала, перейдите в
каталог, где нужно создать про екты ASP.NET Core, и выполните следуЮIЦУЮ команду:

уо aspnet
Эта команда запускает пакет Yeoman и сообщает ему о необходимости создания
нового проекта ASP.NET Core. Весь процесс настройки проекта производится через
командную строку, переходя по параметрам с использованием клавиш со стрелками

и делая выбор с применением клавиши <Return>. В табл.13.2 описан набор шаблонов


проектов, доступных для разработки приложений ASP.NET Core. (Есть еще несколь­
ко других шаблонов, которые здесь не перечислены, но они не предназначены для
ASP.NEТ Core.)

Таблица 13.2. Шаблоны проектов ASP.NET Core из Yeoman для разработки


приложений ASP.NET Core
Имя Описание

Empty Web Application Этот шаблон создает проект ASP. NEТ Core с минималь­
(Пустое веб-приложение) ным начальным содержимым; он похож на шаблон Empty
(Пустой) в Visual Studio 2015
Web Application Этот шаблон создает проект ASP.NEТ Core с начальным
(Веб-прило жение) содержимым, которое включает контроллеры, пред­

ставления и аутентификацию. Он похож на шаблон Web


Application (Веб-приложение) в Visual Studio 2015 с вклю­
ченной аутентификацией

Web Application Basic Этот шаблон создает проект ASP.NET Core с начальным
(Базовое веб-приложение) содер жим ым, которое включает контроллеры и представ­

ления, но не аутентификацию

Web API Application Этот шаблон создает проект ASP.NET Core с контроллером
(Веб-приложение API) API (который будет описан в главе 20) . Он эквивалентен
шаблонуWеЬ API в Visual Studio 2015
356 Ч асть 1. Вв еде н ие в и н фрас тру кту ру AS P.NET Core MVC

Выберите шабл о н Empty Web Appli c a t i on и нажмите <Return>. В ответ на за­


прос имени для проекта введите Par t y inv i te s. Вот вывод, который вы будете видеть
во время создания пр о екта:

? What t ype of a pp li catio n do you want to c r eate? Emp t y Web Application


? Какой тип приложения вы хотит е со здать ?

? Wh a t ' s the name of you r ASP.NET a ppli cat i on? (Empt yWe bAppl i cat i on)
Partyi nvi t es
? Каков о имя вашего приложения ASP . NE T ? ( Empt yWe ЬApplication )

Нажмите <Retu rn>; пакет Yeoman создаст папку Partyinvi tes и наполнит е е ми­
нимальным набором файлов, которые требуются для проекта ASP. NEТ Core.

Подготов ка прое кта с помощью Visual Studio Code


Ч тобы открыть проект в Visual Studio Code, выберите пункт Open Folde r ( Открыть
папку) в меню Fi le ( Файл) , п е рейдит е в папку Party invi t e s и щелкните на кнопке
Se le ct Folde r (Выбрать папку) . Редактор Visual Studio Code откроет проект, и по про­
шествии нескольких секунд вы увидите сообщение, предлагающее добавить элементы
в проект (рис . 13.5).

'iii~ Required assets to build and debug are miss1n9 from your pro;ect. Add them? w~~
~~~т~
~~

Рис. 13.5. Пр игл а ш е н и е до бавить акт ивы в п р о ект

Щелкните на кнопке Yes (Да). Редактор Visual Studio Code создаст папку . vscode
и добавит в нее файлы, которые конфигурируют процесс построения . По умолчанию
Visual Studio Code использует компоновку из трех разделов. Боковая панель, выделен­
ная на рис . 13.6. предоставляет доступ к главным областям функцион ал ьности .
Самая верхняя кнопка позволяет от­
крыть панель проводника, которая отоб­
V 1~v1 Goto H~lp разит содержимое ранее открытой папки.
Остальные кнопки об еспечивают до ступ
-4 к средству поиска, к интегрированному

'
управлению исходным кодом, к отладчи ­

ку и к набору установленных расшире­


• .vscode
~ Properties
1 ний . Щелчок на имени файла в панели
проводника приводит к открытию файла
• \WMГOOI

.gitlgnore \ для редактирования . Допускается редак­


тировать несколько файлов одновремен-
Dockerfile
Program.cs
projectJso11
} но, и вы можете создавать новые панели

редактора, щелкая на кнопке S plit Edi tor


project.lock.Json
..; ( Разделить окно редактора) в правой в е р­
README.md ~ хней части окна. Редактор Visual Studio
Star1up.cs ~ Code вполне хорош , обладая неплохой
\veb.config ' поддержкой IntelliSense для файлов С# и
представлений Razor, а также содействи­
ем в ;завершении имен и версий пакетов

Рис. 13.6. Бо ков ая панель Visual Studio Code NuGet и Bower.


Глава 13. Работа с Visual Studio Code 357
Кроме содержимого папки проекта панель проводника показывает. канне файлы в
тенущий момент редактируются. Это позволяет легко сосредоточиться на подмножес­
тве ф айлов, с которыми производится работа, что является удобным дополнением,
когда приходится иметь дело с поднабором связанных файлов в крупном проекте.

Добавление в проект п акетов NuGet


Первый шаг связан с добавлением пакетовNuGet, которые содержат сборки .NET,
требующиеся для приложений MVC. В панели проводнина Visual Studio Code щелк­
ните на имени файл а pro j ect . j son и с помощью редактора кода внесите в разделы
dependencies и tools изменения, приведенные в листинге 13. 1. Редактор Visual
Studio Code выдаст предположения относительно имен и версий п акетов .
Ли стинг 1З . 1 . Добавление пакетов NuGet в файле proj ect . j s on

"dependencies ":
"Microsoft . NETCore . App " :
"version " : " 1 . 0 . 0",
"type " : "platforrn "
) ,
"Microsoft . As pNetCor e . Diagnos ti c s" : "1 . 0 . 0" ,
"Mi crosoft .AspNetCore . Se rver . IISintegrat i on ": " 1 . 0 .0" ,
"Microsoft .AspNetCore . Serve r. Kest r e l": " 1 . 0 . О ",
"Microsoft . Extensions . Logging . Conso le": " 1 . 0 . 0",
" Microsoft . Extensions . Conf i g u ra t ion.Enviro n rnentVa r iaЫes " : " 1.0 .0 ",
"Microsoft . Extensions . Configuration . Fi leExte ns i ons ": " 1 . 0 . 0",
"Microsoft . Extens i ons.Conf i gu rat i on . J son ": " 1 . 0 . 0",
"Microsof t . Extensions . Configura ti on . ComrnandLin e ": " 1 . 0 . 0",
"Mic r o s oft . AspNetCore.Mvc" : 11 1 .0.0 11 ,
"Micro s oft . AspNetCore.Stati c Fil es " : 11 1 .0.0 11
) '
"tools " : {
"Micro s oft .DotNet . Watc her. Tools " : 11
1. О . 0-preview2 - f i nal ",
"Microsoft. AspNetCore . Server . I IS integrat ion . Tool s" : "1 . 0 . 0- previ ew2 - f i nal "
},

Пакеты в раздел е dependenci es добавляют поддержку для МVС и доставки стати­


ческого содержимого , такого как файлы JavaScript и CSS. Пакет в разделе too ls де ­
л ает возможной итеративную разработку в Visual Studio Code, которая настраивается
в раздел е " Постро е ни е и запуск проекта" далее в глав е.

Сохраните изменения в файле p roj e ct . j son и в окне командной строки/ терми ­


нала выполните сл едующую команду. находясь внутри пашш Pa rt y i nv i t e s :
dotnet res t ore
Эта но манда обрабатывает файл pro j ect . j s on и загружает ука з анные в нем па ­
кеты NuGet. ( Иногда редактор Visual Studio Code будет определять, что есть панеты,
подлежащие загрузке . и предлагать выполнить такую команду, но на момент напи­

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


Явный запуск команды гарантирует, что приложение может быть скомпилировано .
Перед запуском команды res t o r e может понадобить ся закрыть файл pro j ect . j s on
в Visu al Studio Code .)
358 Часть 1. Введение в инфраструктуру ASP.NET Саге MVC

Добавление в проект пакетов клиентской стороны


Как и в Visual Studio 2015, при управлении пакетами клиентской стороны в про­
ектах Visual Studio Code применяется инструмент Bower, хотя для этого требуетсл до­
полнительнал работа.
Первым делом необходимо добавить файл по имени . bowerrc , который исполь­
зуетсл длл того, чтобы указать Bower, где устанавливать пакеты. Наведите курсор
мыши на элемент PARTYINVITES в панели проводника и щелкните на значке New
File (Новый файл), как показано на рис. 13.7.

pro;et"tJsoп х
(
"depe ndenc ies ·· :
" ~\ ic rosoft. NEТCore. Арр" :
4 "version'' : " 1 . 0 . 0 J
1
'

s "type": "platform"
6 }'
"M i crosoft . AspNetCore.Oiвgnostics":

Рис. 13. 7. Создание нового файла

Установите имл файла в . bowerrc (обратите внимание на наличие в имени двух


букв r) и поместите в него содержимое, приведенное в листинге 13.2.

Листинг 13.2. Содержимое файла . bowerrc

" directory ": " wwwroot/lib "

Далее создайте файл по имени bower. j son с содержимым, представленным в лис­


тинге 13.3.

Листинг 13.3. Содержимое файла bower. j son

"name": "Party!nvites",
"private": true,
"dependencies": {
"boo tstrap": " 3.3 .6",
"jquery": " 2 . 2. 3" ,
"j query-validation" : " 1. 15. О",
" jquery - validation-unoЬtrusive ": " 3.2.6 "

При добавлении пакетов в файл proj ect. j son или bower. j son редактор Visual
Studio Code обеспечивает поддержку IntelliSense, что облегчает выбор нужных паке­
тов и указание применяемых версий.
Глава 13. Работа с Visual Studio Code 359
Восполь зуйтесь окном ком андной строки/т ерминала , чтобы выполнить в папке
Partyinvi tes показанную ниже команду . которая применя ет инструмент Bower для
загрузки и установки пакетов клиентской стороны, указанных в файле bower . j son :
bower install

Кон ф игурирование приложения


Процесс инициализации проекта создал пустой проект без поддержки МVС . В лис­
ти нге 13.4 приведены и зменения , которые вносятся в файл Startup . cs для настрой­
ки МVС с использованием наиболее базовой конфигурации . Операторы в листинге
применяют пакеты , которые были добавлены к проекту в листинге 13.1 и описаны в
гл аве 14.

Листинг 13.4. Добавление поддержки MVC в файле Startup. cs


using System ;
using System . Collections . Generic ;
using System . Linq ;
using System . Threading . Tasks ;
using Microsof t .AspNetCore . Bui lder ;
using Microsoft . AspNetCore . Ho s ting ;
using Microsoft . AspNetCore . Ht t p ;
using Microsoft .Extensions . Depe n dency i n j ection ;
using Microsof t. Ex te ns i ons . Log g i n g ;
namespace Partyinvites {
puЬlic class Startup {
puЬlic void ConfigureServices( I ServiceCollection se r vices) {
services.AddМvc();

puЫic void Configure(IApplicationBuilder арр ,


IHos t ingEnvironmen t env, I Lo g ge rFacto r y logger Factory) {
app . UseStatusCodePages() ;
app.UseDeveloperExceptionPage() ;
app.UseStaticFiles();
app.UseMvcWithDefaultRoute();
}

Построение и зап уск проекта


Чтобы построить и запустить проект, в окне командной строки/терминала перей­
дите в каталог Pa r tyinvites и вьшолните следующую ком анду:

dotnet watch run


Редактор Visual Studio Code скомпилирует код в про е кте и с помощью с ервера при­
ложений Kestrel, рассм атриваемого в гл ав е 14, запустит приложение, ожидая НТГР­
запросы н а порте 5000. Любы е изменения в файлах С# будут инициировать авто­
м атич е скую п е рекомпиляцию. (Если вы хотите запустить про ект, игнорируя любые
изменени я. тогда и споль зуйте команду dotn e t run .)
360 Часть 1. Введение в инфраструктуру ASP.NET Соге MVC

Редактор Visual Studio Code не предоставляет аналога для средства Browser Link и
не открывает окно браузера автоматичес 1ш. Чтобы протестировать приложение, от­
кройте окно браузера и перейдите на URL вида http: / / localhost: 5000. Вы полу­
чите ответ, представленный на рис. Ошибка 404 связана с тем, что в текущий
13.8.
момент в проекте отсутствуют какие-либо контроллеры для обработки запросов.

D localhost:SOOO Х
1 · - - - - - - - · - - - - · - · -·- · - ,
[ ·f-- ~- с l~,;_~~~"(}"_ ~~~~ ~-- -1
l::~~~~ode: ~04~-~ot .~:~~~-------------·J
Рис. 13.8. Тестирование примера приложе ния

Воссоздание приложения Partylnvites


Все подготовительные шаги завершены, а это значит, что мы можем заняться созда­
нием приложения МVС. Мы воссоздадим простое приложение Partylnvites из главы 2,
но с рядом изменений и дополнений, которые подчеркнут особенности работы с Visual
Studio Code.

Создание модели и хранилища


Первым делом наведите курсор мыши на элемент PARTYINVITES в панели про­
водника и щелкните на значке New Folder (Новая папка), как показано на рис. 13.9. В
качестве имени папки укажите Models.

EXPLOREI< х

; OP.EN IOITORS
"de pendenc ies'":
f
pгoje•t,fien 2
3 "'r1i crosoft. NEТCore. Арр" : ~
" PARТYIN\llТfS
''version'' : '' 1.0.0'' ,
f
4
~ .vscode "'type'' : "platform"
i• Proper·ties 6 }, '
t· W\WlfOO! "Hic1·o~oft. AspNetCoi-e. Diag11ostics" : \
8 "Microsoft . A~pHe'tCore . Server. IISI1>t,.,
9 "l-lic1·osoft. д,pNetCo r·e. Ser>rer. Kest•·y
"!IF",.....111""' --ll!l!!!!lll[!Jl.,,,..,~,~.}J/J1iii1\~~on~o
Рис. 13.9. Создание новой папки

Щелкните правой кнопкой мыши на папке Models в панели проводника. вы­


берите в контекстном меню пункт New File (Новый файл), установите имя файла в
GuestResponse. cs и поместите в файл код С# из листинга 13.5.
Глава 13. Работа с Visual Studio Code 361

Работа с редактором Visual Studio Code


Продукт Visual Studio Code (и расширение С#, установленное ранее в главе) предоставляет
полнофункциональные средства для редактирования файлов С# и Razor, а также для файлов
распространенных веб-форматов, подобных JavaScript, CSS и HTML. В принципе написание
приложения MVC в Visual Studio Code имеет много общего с этим же процессом в редакторе
Visual Studio 2015: имеется поддержка lntelliSense, кодирование цветом и подсветка ошибок
(с советами по их исправлению) .
Основной недочетом Visual Studio Code является отсутствие возможности настройки, осо­
бенно когда речь идет о форматировании кода. На момент написания главы были доступны
варианты конфигурации для других языков, но расширение С# не допускает настройки, что
несколько затрудняет работу, если предпочитаемый вами стиль написания кода не совпада­
ет со стилем, предлагаемым по умолчанию. Однако в целом редактор отличается быстрой
реакцией и легкостью в применении, так что написание приложений MVC в среде OS Х/
macOS или Linux не вь1глядит второсортным процессом.

Листинг 13.5. Содержимое файла GuestResponse. cs из папки Models

using System.ComponentModel.DataAnnotations;
namespace Partyinvites . Models
puЫic class GuestResponse {
puЫic int id {get ; set; }
[Required(ErrorMessage = " Please enter your narne")]
11 Пожалуйста , введите свое имя
puЬlic string Narne { get; set; }

[Required(ErrorMessage = " Please enter your ernail address " )]


11 Пожалуйста, введите свой адрес электронной почты
[RegularExpression( ".+ \\@ .+ \\ .. +",
ErrorMessage = " Please enter а valid email address")]
11 Пожалуйста, введите допустимый адрес электронной п очты
puЫic string Emai l { get; set; }

[Required(ErrorMessage = " Please enter your phone nurnber " }]


11 Пожалуйста, введите свой номер телефона
puЫic string Phone { get ; set ; }

[Required(ErrorMessage " Please specify whether you 'll attend " ) ]


=
11 Пожалуйста, укажите, примете ли участие
puЫic bool? Wi llAttend { get; set; }

Добавьте в папку Models файл по име ни IReposi tory . cs с определением интер­


фейса. приведенным в листинге 13.6. Самое важное отличие приложения в настоящей
главе от приложения из главы 2 связано с тем. что мы собираемся хранить данные
модели в постоянной базе данных. Интерфейс IReposi tory описьшает, 1щким обра­
зом прилож ение будет получать доступ к данным модели, не указывая реализацию.
Добавьте в папку Models файл по имени ApplicationDbCon t ext . cs и определи­
те в нем класс контекста базы данных, как показано в листинге 13.7.
362 Часть 1. Введение в инфраструктуру ASP.NEТ Core MVC

Листинг 13.6. Содержимое файла IReposi tory. cs из папки Models

us in g System . Co l lecti ons . Gene ric ;


namespace Partyinvites . Mode l s {
puЫic interface I Repository {
IEnumeraЫe<Gues t Response> Responses {get ;
void AddResponse(GuestRe sponse re sponse) ;

Листинг 13.7. Содержимое файла ApplicationDbContext. cs из папки Models


usi ng Microsoft.EntityFrameworkCore ;
namespace Par t yinvi t es . Mode ls {
puЬlic c l ass ApplicationDbContext DbCon te xt {
puЬlic App licationDbCon t ext() {}
prot ected ove r ri de void OnConfi gur ing(DbCont extOpti onsBuilde r bui l der)
builder . UseSqlite( " Fil e name= . / Partyinvi tes . db " ) ;

p u Ыic DbSet<G uestRespo ns e> Invi t es {ge t; s et ; }

Средство SQLite хранит данные в файле, который указывается классом контекс­


та. В создаваемом примере приложения данны е будут храниться в файле по имени
Party i nv i tes . db , что определено в методе OnCon f iguring () .
Чтобы завершить набор кл ассов, не обходим ых для сохр анения и доступ а к данны м
модели, тр ебуется реализация интерф ейса I Repo si tory, которая использует клас с
контекста базы данных. Добавьте в папку Mode l s файл по им ени EFReposi tory . cs
и поместите в н е го код , представленны й в листинге 13.8.

Листинг 13.8. Содержимое файла EFReposi tory. cs из папки Models

us ing Sys t em. Col l ections . Ge neric ;


namespace Party i nv i tes.Mode l s {
p u Ыic c l as s EFRe positor y : IRepos i tor y
private ApplicationDbContext context = new ApplicationDbContext() ;
puЫic IEnumeraЬle<G u est R esponse> Responses => context . Invites ;

p uЫic void AddRes ponse(GuestRe s po ns e re s ponse) {


context .Inv i tes . Add(re s ponse) ;
context . SaveChanges() ;

Класс EFReposi tory следует шаблону, который похож на тот, что применялся в
главе 8 Для настройки базы данных SportsStore. В листинге 13.9 видно, что к методу
Глава 13. Работа с Visual Studio Code 363
ConfigureServ i ces () класса S t art u p добавлен оператор конфигурирования, кото­
рый сообщает инфраструктуре ASP.NET Core о необходимости созд а ния экземпляра
клас с а EFRepo si tory, когда требуются реализации интерфейса I Rep o si tory , с по­
м ощью ср едства внедр ения зависимостей (рассматриваемого в главе 18).

Листинг 13.9. Конфигурирование хранилища в файле Startup.cs

using System ;
using System . Co l lections . Gener i c ;
using System .Linq ;
us i ng System.T h reading . Tasks ;
us i ng Micro soft . AspNe t Core . Bui lder ;
using Mi crosoft . Asp NetCore . Host i ng ;
using Microsoft . AspNetCore . Http;
using Microsof t. Extens i ons . Depe n d ency i n j ectio n;
using Mic r osof t. Ex t ens i o n s . Logg i ng ;
using Partyinvites.Models;
namespace Partyinvite s {
p u Ы i c clas s Start up {

puЫic void ConfigureServic e s(IServiceColle c tion servi ces) {


services.AddTransient<IRepository, EFRepository>();
s e rvices.AddMvc() ;

puЫic void Configu r e(IApplicat i onBui lder арр ,


IHostingE n vironme n t env , ILo g gerFac t ory l ogge r Factory) {
app . UseStatusCode Page s () ;
app . UseDe v e l operEx cep ti on Pa ge ( ) ;
app . UseSta ti cFil e s () ;
app . UseMvcW i thDefa ul tRou t e() ;

Создание базы данных


В остальных главах книги всякий раз , когда нужно продемонстрировать функцио­
нальность, требующую постоянства данных , используется ср едство LocalDB. которое
представля ет с обой упрощенную версию Microsoft SQL Server. Но средство LocalDB
до ступно только в среде Windows. поэтому при создании приложений ASP.NET Core
MVC на других платформах понадобится какая-то альтернатива. Наилучшей альтер­
нативой LocalDB является SQLite - не нуждающаяся в конфигурировании межплат­
форменная система управления базами данных, которая может встраиваться в при­
л ожения и для которой компания Microsoft включила поддержку в Entity Framework
Core. В последующих разделах мы рассмотрим процесс добавления SQLite в проект и
ее примен е ни е в качестве х ранилища данных для ответов на приглашения поучаст­

вов ать в в е черинке.


364 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

Использование SQLite при разработке

Одна из причин того , что LocalDB является настолько полезным инструментом , связана с
возмо ж ностью разработ к и с применением механизма баз данных SQL Server, который де­
лает переход в производственную среду SQL Server простым и почти полностью лишенным
рисков. SQLite - великолепная база данны х , но она не очень х орошо подходит для крупно­
м асштабны х веб-приложений , а значит, что при развертывании пр и лож ения MVC требуется
пере ход на другую базу данны х . Из м енения в конфигурации м огут быть упрощены с ис­
пользование м средств конфигурирования , которые будут описаны в главе 14, но приложе­
ние должно быть тщательно протестировано в испытательной среде, чтобы выявить любые
отличия , вносимые производственной базой данных.

Почитайте статью по адресу https : / /www . sqli te . org /whentouse . html , если вы не
уверены в том, применять ли SQLite в производственной среде. Там приведен обзор ситуа­
ций, когда база данных SQLite может быть хорошим вариантом, а когда нет.

Важ но иметь в виду тот факт, что SQLite не поддер живает полный набор изменений схемы ,
которые Entity Framework Core может генерировать для других баз данны х. В целом это не
проблема , к огда SQLite используется при разработк е, посколь ку вы можете удалить файл
базы данных и сгенерировать новый файл с чис той сх емой. Тем не менее, ситуация усло ж­
няется, если вы обдумываете развертывание приложения, в котором применяется SQLite.
Если вы хотите использовать ту же самую базу данных в среде разработки и производс­
твенной среде , тогда ознакомьтесь со списком поддерживаемы х инфраструктурой Entity
Framework Core баз данных по адресу http : //ef . readthedocs . io/en/latest/
providers/ index . htrnl. На момент написания главы список был коротким, но в Microsoft
объявили о поддер жк е баз данных, которые больше, чем SQLite, подходят для процесса
разработки и могут функционировать также на платформах, отличных от Windows .

Добавление пакетов базы данных


Первый ш аг для любого нового средства в про екте ASP.NET Core пр едусматрив ает
до бавление обя зательн ых п акето в к файлу
proj ect . j son, и в Visual Studio Code оно
ничем не отличается. В листинге 13.10 пока заны добавления к файлу project. j son
для инфрастру~•туры Entity Framework Core и ее поддержки SQLite.

Листинг 13.10. Добавление пакетов базы данных в файле project.json

"dependencies ":
"Microsoft . NETCore . App ":
"version ": " 1.0.0 ",
" type ": "platform "
) 1

"Microsoft . AspNetCore .Diagnostics ": " 1 . 0 . 0 ",


"Microsoft . AspNetCore . Server .I ISintegration ": " 1.0 . 0 ",
"Microsoft . AspNetCore . Server.Kestrel ": " 1.0 . 0 ",
"Microsoft . Extensions.Logging.Console ": " 1 . 0 . 0 ",
" Microsoft . Extensions . Configuration . EnvironmentVariaЫes ": " 1 . 0 . 0 ",
"Microsoft . Extensions . Configuration . FileExtensions " : " 1.0.0 ",
"Microsoft . Extensions . Configuration . Json ": " 1 . 0 . 0 ",
"Microsoft . Extensions.Configuration . CommandLine ": " 1 . 0 . 0 ",
"Microsoft . AspNetCore. Mvc ": " 1 . О . О ",
"Microsoft . AspNetCore . StaticFiles ": " 1.0 . 0 ",
Глава 13. Работа с Visual Studio Code 365
"Мicrosoft.EntityFrameworkCore.Sqlite": "1.0.0",
"Мicrosoft.EntityFrameworkCore.Tools": "l.0.0-preview2-final"
) ,
"tools ": {
"Microsoft . DotNet . Watch er. Tools ": " l . 0 . 0- preview2 -fi na l",
"Microsoft .AspNetCore . Server .I ISintegration .Tools ": "1 . 0 . 0- preview2 - fina l",
"Мicrosoft.EntityFrameworkCore.Tools": {
"version": "1.0.0-preview2-final",
"irnports": [ "portaЫe-net45+win8+dnxcore50", "portaЫe-net45+win8" ]

) '

Сохраните изменения в файле proj ect . j s on, откройте новое окно командной
строки/терминала и выполните следующую команду в папке Partyinvi tes :
dotnet rest ore

Создание и применение миграции базы данных


Создание базы данных следует похожему процессу в смысле команд. используемых
средой Visual Studio 2015, хотя выполняется с применением инструмента dotnet.
Чтобы создать начальную миграцию базы данных, выполните показ анную ниже ко­
манду, находясь в папке Partyinvi t es:
dotnet ef migrations add In itial
Инфраструктура Entity Framework Core создаст папку по имени Mig r at i ons ,
содержащую файлы классов С#, которые будут использоваться для настройки схе­
мы базы данных. Для применения этой миграции базы данных запустите в папке
Partyinvi tes приведенную далее команду, которая создаст базу данных в папке
Ьin/Debug/netcoreappl . O:

dotnet ef database update


Редактор Visual Studio Code не включает поддержку для инспектирования баз дан­
ных SQLite, но на веб-сайте
http: / / sqli t ebrowser . org можно найти великолеп­
ный инструмент с открытым кодом для Windows, OS X/macOS и Linux.

Создание контроллеров и представлений


В этом разделе мы добавим в приложение контроллер и представления.
Начнит е с создания папки Controllers и добавьте в нее файл класса по имени
HomeControll er. cs с определением из листинга 13.11.

Совет. Создание пап ки в редакторе Visual Studio Code может вызвать затруднения, т.к. щел­
чок на элементе PARTYINVITES в панели проводника скрывает содер жим ое папки, а
не выбирает корневую папку. Щелкните на одном из файлов в корневой папке, таком как
proj e ct. j son, наведите курсор мыши поверх элемента PARTYINVITES и щелкните
на значке New Folder.
366 Часть 1. Введение в инфраструктуру ASP.NET Соге MVC

Листинг 13.11. Содержимое файла HomeController. cs из папки Controllers


using System;
using Microsoft . AspNetCore . Mvc ;
using Partyinvites.Models ;
using System . Linq;
namespace Pa r tyinvites . Controllers
puЫic c l ass HomeC o ntroller : Controller
p ri vate IRe pository repository ;
puЫic HomeController(IRepository repo) {
this . repository = repo ;

puЬlic ViewResu l t Index()


in t hour = DateTime . Now . Hour ;
ViewBag. Greeting = hour < 12 ? "Good Morning " " Good Afternoon";
return View ( " MyView " ) ;

[HttpGet]
puЫic ViewResult RsvpForm() {
return View() ;

[H ttpPost ]
puЫic ViewResult RsvpForm(GuestResponse guestResponse)
if (ModelState . IsValid) {
repository.AddResponse(guestResponse);
return View( " Thanks ", guestResponse) ;
e l se {
//Имеется ошибка проверки достоверности
return View();

puЬlic ViewResult ListResponses() {


return View(repository . Responses . Where(r => r . WillAttend true)) ;

Чтобы установить встроенные дес1<рипторные вспомогательные классы, создайте


папку Vi ews и добавьте в нее файл по имени_Viewimports . cshtml, который содер ­
жит выражение, показанное в листинге 13.12.

Листинг 13.12. Содержимое файла_Viewimports. cshtml из папки Views


@addTagHelper * , Microsoft . AspNetCore.Mvc . TagHelpers

Далее создайте папку Views/Home и добавьте в нее файл по имени MyView . cshtml,
в котором определяется представление , выбираемое методом действия Index () из
листинга 13.11. Пом естите в него разметку, приведенную в листинге 13. 13.
Глава 13. Работа с Visual Studio Code 367
Л и стинг 13.13. Содерж и м ое файла МyView. cshtml из папки Views/Home
@{
Layout = null ;

<!DOCTYPE html>
<html>
<head>
<meta name= "v i ewport " content= " width=device-width " />
<title>Index</title>
<link rel= " stylesheet " href= " / liЬ/bootstrap/dist/ css/bootstrap . css " />
</head>
<body>
<div class="text - center " >
<hЗ>We're going to have an exciting party!</hЗ>
<h4>And you are invited</h4>
<а class= " Ьtn Ьtn - primary " asp - act i on= " RsvpForm " >RSVP Now</a>
</div>
</body>
</html>

До бав ьте в папку Views/Home файл по и мени RsvpForm . cshtml с с оде ржимы м,
показ анным в листинге 13. 14. Это пр едставление пр едлаг ает НТМL- форму , которая
будет з аполняться по л ь з ователе м , чтобы принять или о тклонить пригл аш е ни е на
в е ч е рин ку .

Листинг 13.14. Содержимое файла RsvpForm. cshtml из папки Views/Home


@model Partyinvites . Models . GuestResponse
@{
Layout = null ;

<!DOCTYPE htm l >


<html>
<head>
<meta name =" viewport " content=" width=device - width " />
<title>RsvpForm</title>
<link rel= " stylesheet " h ref= " /liЬ/bootstrap/dist/css/bootstrap . css " />
</head>
<body>
<div class =" panel panel - success " >
<div class= " panel - heading text - center " ><h4>RSVP</h4></div>
<div class= " panel - body " >
<form class= "p - a - l " a s p - action= " RsvpForm " method=" post " >
<div asp - validation - summary= "Al l" ></div>
<div class= " form - group " >
<label asp-for= "Name " >Your name : </label>
<input class= " form - control " asp - for= " Name " />
</div>
<div class= " form- group " >
<l abe l asp - for= "Email " >Your emai l: </label>
<i nput class= " form - control " asp - for= " Email " />
</div>
368 Часть 1. Введение в инфраструктуру ASP.NET Саге MVC

<div class="form-group">
<label asp - for="Phone " >Your phone : </label>
<input class= " form - control" asp - for= " Phone" />
</div>
<div class= " form- group">
<label>Will you attend?</label>
<select class= " form-control" asp - for= "WillAttend">
<option value="">Choose an option</option>
<option value= " true " >Yes, I'll Ье there</option>
<option value="false " >No, I can ' t come</option>
</select>
</div>
<div class="text- center " >
<button class= " Ьtn Ьtn-primary" type="submit">
Submi t RSVP
</button>
</div>
</form>
</div>
</div>
</body>
</html>

Следующий файл представления называется Thanks . cshtml, также создается в


папке Views/Home и имеет содержимое, приведенное в листинге 13.15, которое отоб­
ражается, когда гость отправляет свой ответ.

Листинг 13.15. Содержимое файла Thanks. cshtml из папки Views/Home

@model Partyinvites.Models . GuestResponse


@{
Layout = null;

<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device - width" />
<title>Thanks</title>
<link rel= " stylesheet" href=" / liЬ/bootstrap/dist/ css/bootstrap. css" />
</head>
<body class= " text-center">
<р>
<hl>Thank you , @Model.Name!</hl>
@if (Model . WillAttend == true) {
@:It's great that you 're coming . The drinks are already in the fridge!
else {
@:Sorry to hear that you can ' t make it, but thanks for letting us know.

</р>
Click <а class= " nav-link" asp - action="ListResponses">here</a>
to see who is coming.
</body>
</html>
Глава 13. Работа с Visual Studio Code 369
Финальный файл представления имеет имя ListResponses. cshtml и подобно
другим представлениям в примере добавляется в папку Views/Home. Он отображает
список ответов гостей, используя разметку из листинга 13.16.

Листинг 13.16. Содержимое файла ListResponses. cshtml из папки Views/Home

@model IEnumeraЬle<Partyinvites . Models.GuestResponse>

@{
Layout = null;

<!DOCTYPE html>
<html>
<head>
<meta name= " viewport " content="width=device - width " />
<link rel=" stylesheet" href=" /liЬ/bootstrap/dist/ css /bootstrap . css " />
<title>Responses</title>
</head>
<body>
<div class= "pane l-body " >
<h2> Here is the list of people attending the party</h2>
<tаЫе class= "t aЫe taЬle-sm taЫe-striped taЬle -b ordered " >
<thead>
<t r><th>Name</th><th>Email</th><th>Phone</th></tr>
</t head>
<tbody>
@foreach (Partyinvites .Models .Gues tResponse r in Model) {
<t r> <td>@r . Name</ td><td>@r . Email</td><td>@r.Phone</td></tr>
}
</tbody>
</tаЫе>
</d iv>
</body>
</html>

Запущенная ранее в главе :команда dotnet watch гарантирует :компиляцию прило­


жения всякий раз, когда редактируется какой -либо 1ишсс С#, и вы можете просмотреть
завершенное приложение, перейдя на URL вида http: / /localhost: 5000 (рис. 13.10).

Модульное тестирование в Visual Studio Code


Редактор Visual Studio Code не поддерживает отдельные проекты модульного
тестирования, а это значит, что модульные тесты должны смешиваться с класса­

ми приложения MVC и :конфигурироваться с применением того же самого файла


proj ect . j son, с помощью :которого настраиваются пакеты и инструменты ASP.NET.
В последующих разделах мы добавим к приложению пакет тестирования xUnit, созда­
дим простой модульный тест и прогоним его.

Конфигурирование приложения
Первый шаг предусматривает добавление пакетов в файл proj ect . j son и указа­
ние деталей об используемом пакете тестирования (листинг 13.17).
370 Часть 1. Введение в инфраструктуру ASP.NET Соге MVC

1. ~
f- _,с
-i RSVP

Thank you , ВоЬ!


i We're going to have an exciting' v,.,, ~"''" n'.s grмt l l'чll you'm COПllПfJ Th~ dМk.1 а1е а1w..гау ln the !r dgo~
And you ~re lnv1ted А 8""
CllCk tюн1 ~ sве who lз oom!l'W].
.__.,,,, 1 Уощоmа!1· L
1
!...... _ _ _ _ --~---- _ _ _ _ _ _ _ ) ЬoЬO-><x<i"np\ecom
~- -- -·
Уаирlюо•'
С !_Ф I0(.1t :t-:.O<X.1/H~1 "f::1r_R11~;..ti1•'~ __
1

\V:~:: :ood?
7

Неге is the list of people attending t11e рагtу


1,

Namo µr\Of}Q
Yll!i.1"000\h&ro
1 55.> 1234

''**'' 5S!:o--(;789

Рис. 13.1 О. Выполнение завершенного приложения

Листинг 13.17. Конфигурирование модульного тестирования в файле proj ect. j son

" dependencies ": {


" Microsoft.NETCore . App ":
" version ": " 1 . 0 . 0 ",
"type": " platform "
) 1

" Microsoft.AspNetCore . Diagnostics ": " 1 . 0 . О ",


" Microsoft.AspNetCore . Server .II Sintegration ": " 1 . 0.0 ",
"Microsoft.AspNetCore.Server.Kestrel": "1.0.О",
"Microsoft .E xtensions . Logging .C onsole ": "1. 0 . О ",
" Microsoft . Extensions . Configuration.EnvironmentVariaЬles ": "1.0.0",
" Microsoft . Extensions . Configuration.FileExtensions" : "1. 0.О ",
" Microsoft . Extensions . Configuration . Json ": " 1 . 0 . 0",
" Microsoft . Extensions . Configuration . CommandLine" : " 1 . 0 . О ",
" Microsoft . AspNetCore . Mvc ": " 1.0.0 ",
" Microsoft . AspNetCore . StaticFiles ": "1.0.О",
" Microsoft .En tityFrameworkCore . Sqlite ": " 1 . 0 . О ",
" Microsoft . EntityFrameworkCore.Tools ": " l . 0 . 0-preview2-final ",
"xunit": "2.1.0",
"dotnet-test-xunit": "2.2 . 0-preview2-build1029"
) 1

"t estRunner ": " xunit ",


" too l s ": {
" Microsoft.DotNet.Watcher . Tools ": " l . 0 . 0-preview2 - final ",
"Microsoft . AspNetCore . Server . IISintegration.Tools ": "l.0.0-preview2-final",
" Microsoft.EntityFrameworkCore .T ools ": {
" version ": "l. 0.0 - preview2-final ",
" imports ": [ " portaЫe -n et45+win8+dnxcore50" , " portaЫe -n et45+win8" ]

} 1

/ / .. . для краткости остальные разделы не показаны ...


Глава 13. Работа с Visual Studio Code 371
Выполните в папке Partyinvi tes следующую команду для установки пакетов
тестирования:

dotnet restore

Создание модульного теста


Модульные тесты создаются так, как было описано в главе 7, но тестовые н:лас­
сы должны быть частью проекта приложения. Создайте папку Tests и поместите в
нее файл класса по имени HorneControl lerTests. cs с содержимым, приведенным
в листинге 13.18.
Листинг 13.18. Содержимое файла HomeControllerTests. cs из папки Tests
using System;
using System . Co llecti ons . Generic ;
using Partyinvites . Controllers;
using Partyinvites.Models ;
using Xunit ;
using Microsoft.AspNetCore.Mvc;
using System.Linq ;
namespace Partyinvites . Tests
puЫic class HomeControllerTests
[Fact]
puЬlic void ListActionFi l tersNonAttendees()
11 Организация
HomeController controller = new HomeController(new FakeRepository()) ;
11 Действие
ViewResult result = controller . ListResponses() ;
11 Утверждение
Assert . Equal(2 , (result . Model as IEnumeraЬle<GuestResp onse>) . Count());

class FakeRepository : IRepository {


puЫic IEnumeraЬle<GuestResponse> Responses =>
new List<GuestResponse> {
new GuestResponse { Name " ВоЬ ", WillAttend = true } ,
new GuestResponse { Name "Alice", WillAttend = true },
new GuestResponse { Name " Joe ", WillAttend = false }
};
puЫic void AddResponse(Gues tRespons e re sponse )
t hrow new NotimplementedException() ;

Это стандартный тест


xUnit, который проверяет действие Li stResponses в конт­
роллере Home и фильтрует в хранилище объекты GuestResponse со значением свойс­
тва WillAttend, равным false.

Прогон тестов
Редактор Visual Studio Code обнаруживает тесты и добавляет встроенную ссылку
для их з апус1ш (рис . 13.11).
372 Часть 1. Введение в инфраструктуру ASP.NET Core MVC

[Fact)
О refere rкeJ 1 run test !Jiebug test
puЬlic v___ """ •..• tionFiltersNonAttendees() {
//Arrange
HomeController controller = new HomeController(ne1~ FakeReposi tory());
// Act
ViewResult result = controller.ListResponses();
11 Assert
Assert.Equal(2, (result.Model as IEnumeraЬle<GuestResponse>).Count());
}

Рис. 13.11. Прогон теста внутри редактора кода

Щелчок на ссылке run test (прогнать тест) приведет к открытию окна вывода и
отображению результата . (На момент написания: главы ссылка debug test (отладить
тест) не работала, а в ряде случаев вывод мог вообще не отображаться.)
Более надежный подход предусматривает прогон всех тестов в проекте. Выполните
следующую команду в папке прое1па:

dotnet test
Все тесты в проекте запустятся, и в результате отобразится вывод, подобный по ­
казанному ниже:

xUnit . net .NET CLI test runner (64 -b it . NET Core win10-x64)
Discovering : Partyinvites
Discovered : Partyinvites
Starting : Partyinvites
Finished: Partyinvi tes
=== TEST EXECUTION SUMMARY ===
Partyinvites Total : 1, Errors : О, Failed : О, Skipped: О , Time : 0.196s
SUMMARY: Total: 1 targets, Passed: 1, Failed: О .
Прогнать все модульные тесты в проекте после каждого изменения классов С #
можно также с помощью следующей команды:

dotnet watch test


Такая команда не может применяться одновременно с командой dotnet wa tch run,
поскольку обе команды будут инициировать компиляцию проекта при наличии изме­

нений и пытаться создавать те же самые выходные файлы.

Резюме
В этой главе был предложен краткий обзор работы с редактором Visual Studio
Code - легковесным инструментом разработки , который поддерживает создание при­
ложений ASP.NET Core МVС в средах Windows, OS X/macOS и Linux. Редактор Visual
Studio Code пока еще не является заменой полного продукта Visual Studio, но предо­
ставляет основные средства, которые необходимы при построении приложений MVC,
и расширяется компанией Microsoft в ежемесячных выпусках.
Итак, первая часть книги завершена. Во второй части мы начнем погружени е в
детали и посмотрим, как работают средства, которые использовались для создания
приложения.
ЧАСТЬ 11

Подробные сведения
об инфраструктуре
ASP.NET Core МVС

к
настоящему моменту вы уже знаете, для чего существует инфраструкту­
ра .NET Core MVC , а также понимаете ее архитектуру и лежащие в основе
проектные цели. Вы получили эти знания благодаря построению реалистичного
приложения электронной коммерции. Наступило время заняться исследованием
внутреннего устройства инфраструктуры.

Во второй части книги мы погружаемся в детали. Мы начнем с изучения струк­


туры приложения ASP.NET Core MVC и способа обработки запросов . Затем мы
сосредоточим внимание на индивидуальных средствах, таких как маршрутиза­

ция, контроллеры и действия, система представлений и дескрипторных вспомо­


гательных классов, а также особенности работы MVC с моделями предметной
области.
ГЛАВА 14
Конфигурирование
v
приложении

т ема конфигурации может показаться неинтересной, но она открывает много ас­


пектов , связанных с функционированием приложений МVС и обработкой НТГР­
запросов. Вы не должны поддаваться соблазну пропустить эту главу. Вы обязаны вы­
делить время на изучение способа, которым система конфигурации придает форму
веб - приложениям MVC. Данная тема заслуживает внимания и формирует прочный
фундамент для понимания материала последующих глав.
Если вы работали с предшествующими версиями ASP.NET, то увидите, что од­
ним из наиболее заметных изменений в ASP.NET Core является способ, с помощью
которого приложение конфигурируется. Исчез целый набор файлов - Global . asax.
Fil t e rConfig. cs и Rou teCo nfig. cs, - а на его место пришли классы St artup и
Prograrn плюс комплект файлов JSON. В настоящей главе объясня ется , как все это
применять для конфигурирования приложений МVС, и демонстрируется, каким об­
разом инфраструктура МVС опирается на средства. предоставляемые платформой
ASP.NET Core. В табл. 14. l приведена сводка, позволяющая поместить конфигуриро­
вание приложений в контекст.

На заметку! В Microsoft объявили, что в будущем выпуске изменят способ конфигурирова­


ния приложений ASP.NET Core, скорректировав роль файла pr o j ec t. j son и представив
ХМL-файл конфигурации. Проверяйте веб-сайт издательства на предмет обновлений, ко­
торые появятся после выпуска новых инструментов.

Таблица 14. 1. Помещение системы конфигурации в контекст

Вопрос Ответ

Что это такое? Классы Pr o gram и Startup и файлы JSON используются для
конфигурирования работы приложения, а также указания паке­
тов , от которых оно зависит

Чем она полезна? Система конфигурации позволяет подстраивать приложения под


их среды и управлять зависимостями от пакетов

Как она используется? Самым важным компонентом является класс Startup, который
применяется для создания служб (объектов, предоставляющих
общую функциональность повсюду в приложении) и компонентов
промежуточного программного обеспечения (ПО) , используемых
для обработки НТТР-запросов
Глава 14. Конфигурирование пр илож ен и й 375
Окончание табл . 14. 1

В опрос Ответ

Существуют ли к а ки е- то В сло жны х приложениях конфигурация может стать трудной в


с к рытые ловуш к и или управлении. В разделе " Работа со слож ными конфигурациями"
огр а ничения? далее в главе описаны средства ASP.NET, предназначенные для
решения та кой проблемы

Существуют ли Нет. Систе м а конфигурации - это неотъе м ле м ая часть ASP.NET


альтернативы? и средство настрой к и приложений MVC
Из м енилась ли она по Система конфигурации полностью изменилась по сравнению с
сравнению с версией той , которая существовала в версии MVC 5, и поддерж ивает со­
мvс 5? вершенно новый подход , предназначенный для того , чтобы сде­
лать возмо жным функционирование приложений за пределами
традиционно й платформы llS

В табл. 14 .2 п редставле на сводка по глав е .

Таблица 14 .2. С водка по главе

Задача Решен и е Листинг

Добавлени е фун к циональности Добавьте па кеты NuGet в разделы dependen 14. 1-


в прилож ение cies и tools фа й ла
proj ect . j son 14.6

Управление инициал и зацией Используйте класс Program 14.7


приложения ASP.NET
Конф и гурирование приложен и я Применяйте методы ConfigureServices () 14.8,
и Configure () класса Startup 14.9
Создание обще й фун к циональности Используйте м етод ConfigureServices () 14.10-
для создания служб 14. 12
Генерация от ветов с содерж имым Создайте проме жуточное ПО для генера­ 14.13-
ции содерж имого 14. 15
Предотвращен и е прох ождения за­ Создайте проме жуточное ПО для обхода 14. 16-
просов через конве й ер запросов 14. 17
Редактирование запроса перед его Создайте проме жуточное ПО для 14.18-
обработ кой другими компонента м и редакт и рования запросов 14.20
про м ежуточного ПО

Редактирование ответа, к оторый Создайте проме жуточное ПО для 14.21 ,


был обработан другим и компонен­ редактирования ответов 14.22
тами промежуточного ПО

Настрой ка фун кциональности MVC Применяйте метод UseMvc () или 14.23


UseMvcWithDefaultRoute()
Изменение конфигураци и прило же­ Используйте служ бу среды размещения 14.24
ния для разны х сред

Рег и страция данны х прилож ения в Применяйте промежуточное ПО для 14.25-


ж урнале регистрации в журнале 14.27
Обработ ка ошибо к в приложении Используйте промежуточное ПО для обра­ 14.28,
бот к и ошибок в среде разработки или про ­ 14.29
изводственной среде
376 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC

Окончание табл. 14.2


Задача Решение Листинг

Управление несколькими браузера- Применяйте средство Browser Liпk (Ссыл ка 14.30


ми во время разработки на браузер)

Включение файлов изображений , Включите проме жуточное ПО для статичес- 14.31


JavaScript и CSS к ого содержим ого

Отделение данны х конфигурации от Создайте внешние источники конфигура- 14.32-


кода С# ции, такие как файлы JSON 14.37
Конфигурирование служб MVC Используйте средства параметров 14.38
конфигурации

Конфигурирование сло жн ы х Применя йте несколько внешн их файлов или 14.39-


приложе ний классов 14.43

Подготовка проекта для примера


В этой главе мы создаем новый проект по имени ConfiguringApps с использова­
нием шаблона Empty (Пустой). Мы будем конфигурировать приложение позже в главе,
но есть несколько базовых средств, которые необходимо поместить на свои места в
плане подготовки к предстоящим изменениям.

Мы собираемся применять Bootstrap для стилизации НТМL-содержим ого, поэтому


создайте файлbower . j son, используя шаблон элемента Bower Configuration File (Файл
конфигурации Bower), и добавьте в него пакет, как пшшзано в листинге 14.1.

Листинг 14.1. Добавление Bootstrap в файле bower. j son

" name ": " asp . net ",


"p rivate ": true ,
"dependencies ": {
"bootstrap": "3. 3 . 6"

Создайте папку Controllers и поместите в нее файл класса по имени


HorneController. cs с определе нием контроллера из листинга 14.2.
Листинг 14.2. Содержимое файла HomeController. cs из папки Controllers
using Systern . Collections.Generic;
using Microsoft.AspNetCore . Mvc ;
namespace ConfiguringApps.Control l ers
puЬlic class HomeController : Controller
puЫic ViewResult Index()
=> View(new Dictionary<string , string>
[ "Message " ] = "This is t he Index action "
}) ;
Глава 14. Конфигурирование приложений 377
Создайте папку Vi ews/Horne и добавьте в нее файл представления по имени
Index . cshtrnl с содержимым, приведенным в листинге 14.3.

Листинг 14.З. Содержимое файла Index.cshtml из папки Views/Home


@model Dictionary<string, string>
@{ Layout = null; }
< !DOCTYPE htrnl>
<html>
<head>
<meta name= "viewport " con tent=" wi dth=device -wi dth " />
<link asp - href - include= " liЬ/boo tstrap / dis t/css/* . min. css "
re l ="sty l esheet " />
<title>Res ul t</ title >
</head>
<body class= "panel-body" >
<tаЫе c l ass= " ta Ыe taЫe-condensed taЬle-bord ered taЬle-striped" >
@foreach (var kvp in Model) {
<tr><th>@kvp .Key</th><td>@kvp .Value</td></tr>

</tаЫе>
</body>
</html>

Элемент link в представлении полагается на встроенный дескрипторный класс


для выбора СSS-файлов Bootstrap. Чтобы вилючить встроенные дескрипторные илас­
сы, создайте в папке Views файл_Viewirnports. cshtrnl с применением шаблона
элемента MVC View lmports Page (Страница импортирования представлений MVC) и
поместите в него выражение, показанное в листинге 14.4.

Листинг 14.4. Содержимое файла_Viewimports. cshtml из папки Views

@addTagHelper *, Microsoft .AspNe t Core .Mvc.TagHelpers

На заметку! В настоящий момент приложение не компилируется и не запускается, поскольку


контроллер и представление зависят от средств, которые в проекте пока еще отсутствуют.

Проблема будет решена в последующих разделах при рассмотрении способа конфигури­


рования приложений ASP.NET Core MVC.

Конфигурационные файлы JSON


При разработке приложений ASP.NET Core формат JSON (JavaScript Object
Notation - система обозначений для объектов JavaScript) играет две разных роли.
Первая из них заилючается в том, что это предпочтительный формат для обмена дан­
ными между приложением MVC и его илиентами. В главе 20 будет показано, как со­
здавать контроллеры. которые возвращают своим клиентам данные в формате JSON
вместо HTML, что делает возможными асинхронные НТГР-запросы для извлечения
только данных, необходимых илиентам. Обычно такие запросы называют запросами
Ajax, хотя JSON в значительной степени заменил формат ХМL, который символизи­
рует буква "х" в Ajax.
378 Часть 11 . Подробные сведения об инфраструктуре ASP.NEТ Core MVC

В текущей главе нас интересует вторая роль формата JSON, связанная с тем, что
он является форматом для конфигурационных файлов. В табл. 14.3 описаны разно­
образные конфигурационные файлы JSON, которые могут быть добавлены к прило­
жению ASP.NET Core MVC.

Совет. Формат JSON используется для описания сериализированных объектов и не подде­


рживает какую-либо программную логику. В противоположность этому файлы с расши­
рением . j s содержат код JavaScript, который может быть выполнен. Файлы JSON не в
состоянии содержать код JavaScript, но файлы JavaScript могут включать данные JSON
(т.к . формат JSON основан на способе определения литеральных объектов JavaScript).

Таблица 14.З. Конфигурационные файлы JSON в проекте ASP.NET Core MVC

Имя Описание

global .j son Этот файл, находящийся в папке элементов решения, от­


вечает за сообщение среде Visual Studio о том, где искать
проекты внутри решения и какая версия исполняющей
среды .NET должна применяться для запуска приложения .
За более подробными сведениями обращайтесь в раздел
"Конфигурирование решения" далее в главе

launchSettings.json Этот файл, который отображается раскрытием элемента


Proper ties в проекте приложения MVC, используется для
указания, каким образом запускается приложение

appsettings . json Этот файл применяется для определения настроек, специфич ­


ных для приложения , как описано в разделе " Использование
данных конфигурации" далее в главе

bower . json Этот файл применяется инструментом Bower для перечисле­


ния пакетов клиентской стороны, которые установлены в про­
екте, как объяснялось в главе 6
bundleconfig . json Этот файл используется для пакетирования и минификации
файлов JavaScript и CSS files, как было описано в главе 6
project . json Этот файл применяется для указания пакетов NuGet, установ ­
ленных в приложении, как объяснялось в главе 6. Он также
используется для других настроек проекта, как показано в

разделе " Конфигурирование проекта" далее в главе

project . lock . json Этот файл, который отображается раскрытием элемента


proj ect. j son в окне Solutioп Explorer, содержит детали­
зированные зависимости между пакетами, установленными в

проекте. Он генерируется автоматически и не должен редак­


тироваться вручную

Конфигурирование решения
Файл globa l. j son применяется для конфигурирования р ешения в целом. Вот со­
держимое, которое Visual Studio по умолчанию добавляет для проекта ASP.NET Core:
Глава 14. Конфигурирование приложений 379

"projects ": [ " src ", " test " ],


"s dk ": {
"version ": "1.0.0-preview2-003121"

Настройка proj ects указывает набор папок. которые содержат проекты л ибо ис­
ходный код. По соглашению развертываемая часть решения - например, приложе­
ние МVС - помещается в папку src, тогда как проекты тестирования сохраняются
в папке test . Это только соглашение, и вы можете использовать файл global . j son
для перечисления любого желаемого набора папок и применять его для любой цели,
которая вам подходит. Настройка sdk сообщает среде Vlsual Studio о том, какая вер­
сия .NET будет использоваться для запуска проекта. Версия. указанная в этой на­
стройке, прим еняется для всех проектов в решении.

Формат JSON: кавычки и запятые


Если вы ранее не работали с форматом JSON, тогда полезно посвятить некоторое время
чтению спецификации на веб-сайте www. j son. org. С форматом JSON легко оперировать,
к тому же большинство платформ предлагают хорошую подцержку для генерации и разбо­
ра данных JSON, в числе которых и приложения MVC (примеры ищите в главах 20 и 21 ),
а на уровне клиента используется простой АРl - интерфейс
JavaScript. В действительности
большая часть разработчиков приложений MVC вообще не будут взаимодействовать с JSON
напрямую, а написание кода JSON вручную требуется только в файлах конфигурации .

Существуют две ловушки, в которые попадают многие разработчики, ранее не имеющие


дела с JSON. Хотя вы по-прежнему должны найти время на чтение спецификации, знание
наиболее распространенных проблем предоставит вам определенную отправную точку в
ситуации, когда Visual Studio или ASP.NEТ Core не смогут провести разбор ваших файлов
JSON . Ни ж е показано добавление к стандартному содержимому файла global . j son , в
котором присутствуют две самых распространенных проблемы (оно приводится только в
демонстрационных целях, потому что в случае добавления таких записей в global . j son
среда Visual Studio сообщит об ошибке):

" projects ": [ " src ", " test " ] ,


" sdk ": {
"version ": " l . 0 . 0-preview2-003121 "

mysetting : [ fast, slow ]

Во-первых, едва ли не все данные в JSON помещаются в кавычки. Очень легко забыть , что
вы пишете вовсе не код С#, и посчитать, что имена свойств и значения должны быть вос­
приняты без кавычек. Тем не менее, в JSON все кроме булевских значений и чисел должно
помещаться в кавычки, например:

" projects ": [ " src ", "test" ] ,


" sdk ": {
"vers ion ": " l . 0.0-preview2 - 003121 "

"mysetting" : [ "fast", "slow"]


380 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

Во - вторых, при добавлении нового свойства к JSОN-описанию объекта вы должны помнить


о необходимости добавления запятой после предшествующего символа фигурной скобки:

"project s ": [ " src ", " tes t" ] ,


" sdk ": {
"version " : " l . 0.0 - preview2-003121 "
} '
"myse t ti ng " : [ " fa s t ", " slow " ]

Заметить разницу довольно трудно даже в случае ее выделения полуж ирным (именно пото­
му такая ошиб ка является распространенной), но здесь помещена запятая после символа },
закрывающего раздел sdk. Одна ко будьте внимательны , потому что замыкающая запятая ,
после которой отсутствует раздел, такж е приводит к ошибке. Если внесенные вами изме ­
нения в файл JSON вызвали проблемы, то в первую очередь нужно выполнить проверку на
предмет двух указанны х выше ошибо к.

Конфигуриро в ание п роекта


Файл proj ect . j son прим е няется для конфигурирования одиночного про е кт а
внутри р е шения. Вот стандартно е содержимое файла pro j ect . j son для прило жения
MVC, которое со здается ш аблоном Empty:

"dependencies ": {
"Microsoft . NETCo r e . App ":
"version ": " 1 . 0 . 0 ",
"type ": "platform "
} 1

"Microsoft . AspNe t Core . Diagnost i cs ": " 1 . 0 . 0 ",


"Microsoft . AspNetCore . Server .II Si ntegration ": "1. 0 . 0 ",
"Microsoft . AspNetCore . Server . Kestrel ": "1 .0 . 0 ",
"Microsoft . Extensions . Logging.Console ": " 1 . 0 . 0 "
) 1

" tools ": {


"Mi crosoft . AspNetCore . Server . IISintegration.Too l s ":
" l . 0 . 0- preview2 - final "
) '
" frameworks ": {
"netcoreappl . 0 ":
"imports ": [" dotnet5.6 ", " po r taЬle - net45+wi n 8 " ]

) 1

"bui l dOptions ": { " emitEntryPo i nt ": true ,


"preserveCompilationContext ": tr ue },
" runtimeOptions ": {
" configProperties ": {
" System . GC . Se r v e r ": true

) 1
Глава 14. Конфигурирование прило ж ений 381
11
puЫ i shOptions
11
: {

11
incl ude 11
: [
11
wwwroot 11
,
11
web. conf i g 11
]

) '
11
scripts ": {
11
p o s tpuЫ i sh
11
: [
11
do tnet p u Ьlish -iis -- p u Ы i sh - folder
% puЫ is h : Output P ath % -- fra mewo r k
% puЫish : FullTargetFramewo r k % 11
]

В т абл. 14.4 описаны все разделы конфигурации в файл е p roj ec t. j son . Двумя
наиболее важными частями файл а proj ect . j s on являются d e pendenc i es и too ls,
которые будут об суждаться в последующих разделах.

Таблица 14.4. Разделы конфигурации в файле project. json

Имя Описание

dependen cies В этом разделе указываются пакеты NuGet, от которых зависит проект,
как объясняется ниже в разделе "Добавление зависимостей в файл
proj e ct . j son"
too l s В этом разделе настраиваются пакеты, которые используются в качес­
тве инструментов разработки, как показано в разделе "Регистрация
инструментов разработки в файле p r oj ect . j son" далее в главе

framewo r ks В этом разделе указываются инфраструктуры .NET, на которые ориен­


тирован проект, и требуемые ими зависимости

buildOptions Этот раздел применяется для конфигурирования способа построения


проектов

runtirneOptions Этот раздел используется для конфигурирования способа запуска


приложения

puЫ i s hOp t i ons Этот раздел применяется для конфигурирования способа опубликова­
ния проекта

scr i pt s В этом разделе указываются команды, которые выполняются в ключе­


вые моменты жизненного цикла построения, такие как момент перед
развертыванием приложения

Добавление зависимостей в файл project. json


Раздел depende n c ies относится к тем разделам файла, которые будут редактиро­
ваться наибол е е часто по мере добавления пакетов , которые предоставляют функцио­
нальность, требуемую в проекте. В листинге 14.5 добавлен набор пакетов, предостав­
ляющих основные средства , которые полезны при разработке приложений MVC.
Листинг 14.5. Добавление пакетов, полезных при разработке приложений MVC,
в файле p roj e c t. j son

11
dependencies 11
: {
11
Microsoft . NETCore . App 11
:

vers i on
11
1.0.0 11
:
11 11
,
11 11
typ e 11
pla tf orm :
11

} ,
382 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

"Microsoft . AspNetCore . Diagnos tics ": "1. О . О ",


"Microsoft . AspNetCore . Server.I ISin tegration ": "1. 0 . О ",
"Microsoft . AspNetCore .S erver .Kest rel": "1.0. 0",
"Microsoft . Extensions . Logging . Console ": "1. 0 . 0 ",
"Microsoft.AspNetCore.StaticFiles": 11 1.0.0 11 1

"Microsoft.AspNetCore .Mvc": "1. О. 0",


"Microsoft.VisualStudio.WeЬ.BrowserLink.Loader": 11
14.0.0",
"Microsoft.AspNetCore.Razor.Tools":
"version": "1. О. O-preview2-final",
"type": "build"

}/

Для большинства пакетов можно использовать простой синтаксис, который пре­


дусматривает указание имени пакета и требуемой версии:

"Microsoft . AspNetCore . Mvc " : "1. О . О",

Расширенный синтаксис позволяет указывать тип зависимости, что будет оказы­


вать воздействие на способ ее применения . В файле proj ect. j son присутствуют два
случая использования расширенного синтаксиса, включая следующий:

"Microsoft.NETCore . App ":


"version ": "1. О . О ",
" type ": "pl atform "
},

В свойстве version указывается номер выпуска пакета точно так же, как в про­
стом синтаксисе. В свойстве type предоставляется дополнительная информация о
роли пакета. Здесь допускается указывать одно из трех значений, которые описаны
в табл. 14.5.

Таблица 14.5. Значения свойства type в расширенном синтаксисе описания


зависимостей в файле project. json
Имя Описание

defaul t Это значение указывает обычную зависимость разработки, так что прило­
жение при выполнении своей работы полагается на сборки в пакете.
Такое значение применяется в простом синтаксисе

platfo rm Это значение указывает, что пакет предоставляет средства уров-


ня платформы. Данное значение должно использоваться для пакета
Microsoft.NE TCore . App
build Это значение указывает, что сборки в пакете применяются в процессе пос­
троения и не предоставляют какие-то средства , требующиеся приложению
во время выполнения. Такое значение используется средством формиро­
вания шаблонов Visual Studio, конфигурирование которого рассматрива­
лось в главе 8
Глава 14. Конфигурирование приложений 383
Регистрация инструментов разработки в файле project. json
Определенные пакеты, добавляемые в раздел dependenc i es, будут предоставлять
инструм енты, которые прим е няются на стадии разработки и для своего функциони­
рования должны р е гистрироваться в разделе too l s ф айла project . j s on. В листин­
ге 14.6 показано добавление в раздел too ls новой записи, 1юторая регистрирует фун­
кциональность, пр едоставля емую пакетом Mic r osoft . As p NetCore . Razor . Tool s;
она добавляет поддержку IпtelliSense для встроенных дескрип торных вспомогатель­
ных классов к р едактору ф айл ов представле ний Razor среды Visual Studio.

Листинг 14.6. Регистрация инструментов в файле project. json

" tools ": {


"Microsoft.AspNetCore.Razor.Tools": "1.0.0-preview2-final",
"Microsoft .AspNetCore . Server . IISinte gr ati on . Tools ": "l. 0 . 0- preview2 - final "
}/

Когда вы добавляете инструменты в свой проект, пакеты обычно поступа­


ют с инструкциями о том, к ак должна выглядеть запись для ра здела tools файла

project . j son. В листинге 14.6 можно видеть самые распространенные инструмен­


ты дл я д е скрипторных вспо м огательных классов, присутствующие в проект ах, а так ­

же пакет, который добавляет команды Entity Framework Core для управления базами
данных, как было описано в главе 8.

Класс Program
Класс Program определ е н в файл е по им е ни Program . cs и об е спечивает точку
входа для запуска прилож е ния , предоставляя инфраструктуре .NET мето д Main ().
который может быть выполн е н для конфигурирования среды размещения и выбора
кл а сса , конфигурирующего приложение. В листинге 14.7 приведен стандартный код
кл ас са Program, добавляемый к проектам средой Visual Studio.
В бол ьш и нстве проектов из м енять класс Program н е придется. е сли только вы
не производите ра з вертывание в необычной или высокоспециализированной среде
разм е щения. Одно тако е измен е ние будет продемонстрировано в разделе "Работа со
сложными конфигурациями" далее в главе , но в проектах, которые развертывают­
ся на стандартных платформ ах , подобных IIS или Azure , м ожно использовать класс
Program, пр едлага емый по умолчанию .

Листинг 14. 7. Стандартное содержимое файла Program. cs

using System . IO ;
using Mic r osoft.AspNetCore.Hos t ing ;
namespace ConfiguringApps
puЬlic c l ass Prog ram {
puЫic static void Main (string [ ] args) {
var host = new WebHostBu i lder()
.UseKestrel()
. UseContentRoot(Direct or y. GetCu rr entDirectory())
. UseIISintegration()
384 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

.UseStartup<Startup>()
. Build ();
host.Run();

Первый оператор в методе Main () настраивает среду размещения, создавая объ­


ект WebHostBuilder и вызывая последовательность методов конфигурирования, ко­
торые описаны в табл. 14.6.

Таблица 14.6. Методы конфигурирования в классе Program

Имя Описание

UseKestrel () Этот метод конфигурирует веб-сервер Kestrel, как объясняет­


ся ниже во врезке "Использование Kestrel напрямую"
UseContentRoot() Этот метод конфигурирует корневой каталог для приложения,
который применяется для загрузки файлов конфигурации и
доставки статического содержимого, такого как файлы изоб­
ражений, JavaScript и CSS
UseIISintegration() Этот метод включает интеграцию с llS и llS Express
UseStartup () Этот метод указывает класс, который будет использоваться
для конфигурирования ASP.NET, как описано в разделе "Класс
Startup" далее в главе

Build () Этот метод объединяет настройки конфигурации, предостав­


ленные всеми остальными методами, и подготавливает их к

применению

После того как конфигурация подготовлена, второе выражение в методе Main ()


запускает приложение, вызывая метод Run ( ) . В этот момент платформа размещения
способна получать НТГР-запросы и направлять их приложению для обработки.

Использование Kestrel напрямую

При добавлении пакетов в файл proj ect. j son вы заметите, что одна из стандартных за­
писей в разделе dependencies (даже в проектах, созданных с использованием шаблона
Empty) предназначена для Kestrel.

"Microsoft.AspNetCore.Server.Kestrel": "1.0.О",

Kestrel - это новый межплатформенный веб-сервер, спроектированный для выполнения


приложений ASP.NET Core. Он задействуется автоматически, когда вы запускаете приложе­
ние ASP.NET Core с применением llS Express (сервер, предоставляемый средой Visual Studio
для использования на стадии разработки) или полной версии llS, которая была традицион­
ной веб-платформой для приложений .NЕТ.
При желании веб-сервер Kestrel также разрешено запускать напрямую, что означает воз­
можность выполнения приложений ASP.NET Core MVC на любой из поддерживаемых плат­
форм, обходя ограничение только операционной системой Windows, которое накладывает
llS. Существуют два способа запуска приложения с применением Kestrel.
Глава 14. Конфигурирование приложений 385
Первый из них - щелчок на стрелке рядом с правой гранью кнопки llS Express в панели инс­
трументов Visual Studio и выбор элемента, который соответствует имени проекта. Это приведет
к открытию нового окна командной строки и запуску приложения с использованием Kestrel.
Того же результата можно достичь, открыв собственное окно командной строки, перейдя в
папку с файлами конфигурации приложения (ту, которая содержит файл pr o j ect. j son ) и
запустив следующую команду:

dotnet run
По умолчанию веб-сервер Kestrel начинает прослушивать порт 5000 на предмет поступле­
ния НТТР-запросов.

Класс Startup
Для конфигурирования функциональности приложений в ASP. NEТ Core применя­
ется класс С# по имени St ar tup . Он определен в файл е Start up . cs , который Visual
Studio добавляет в корневую папку проектов веб-приложений. Изучение работы клас ­
са Startup позволяет понять суть способа обработки НТТР-запросов и интеграции
инфраструктуры MVC с остальными частями платформы ASP.NET.

Совет. Имя класса Start up предоставляется как параметр типа методу Us eSt ar t up () ,
вызываемому в классе Pr ogram, т. е . при желании можно указать другое имя класса .

В этом разделе мы начнем с простейшего из возможных классов Sta r tup и доба­


вим средства для демонстрации влияния различных конфигурационных параметров,
получ и в в итоге конфигурацию, которая будет подходящей в большинстве проектов
MVC. В кач е стве отправной точки в листинге 14.8 по1шзан класс Sta r tup, добавля­
емый ср едой Visual Studio 1< прое1<там Empty, который настраивает всего лишь функ­
циональность, необходимую ASP.NET для обработки НТГР-запросов .

Листинг 14.8. Начальное содержимое файла Startup.cs


using Microsoft . AspNetCore . Bui l der ;
using Microsoft.AspNetCore .Hosting ;
using Microsoft . As pNetCo r e .H tt p;
using Mic r osoft . Exte n sions . Depe ndenc yinj ection ;
us i ng Mi crosoft . Ex te ns i o ns .Log ging;
narnespace Con f ig uringApp s {
puЬlic cla s s Sta r t up {
puЫ i c vo i d Co nf i g ur eServi ces( I Se r v i ceCo l lection s ervi ce s ) {
}
p uЫ i c void Con fi gu re(IAppl icat i onBui lder арр , I Hosti ngEnvi r onrne nt e nv ,
ILog ge r Factory l o ggerFactory) {
loggerFacto r y . AddConso l e() ;
if (env . Is Deve loprnent()) {
app . UseDeve l operExcep t i onPage ();

ap p. Run( async (con text ) => {


await conte x t .Respon se .WriteAs ync ( "H e l lo World !" ) ;
)) ;
386 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

В классе Startup определены два метода. ConfigureServices () и Configure (),


которые настраивают совместно используемые средства, требующиеся приложению,
и указывают ASP.NET о том, как они должны применяться. Работа этих методов объ­
ясняется в последующих разделах. Стандартный класс Startup содержит ровно
столько функциональности, сколько нужно для ответа на НТГР-запросы с помощью
простого сообщения, которое можно увидеть, запустив приложение (рис. 14.1).

Рис. 14.1. Запуск прил ожения с исп ользованием стандартн о го класса Startup

Особенности использования класса Startup


Когда приложение запускается, инфраструктура ASP.NET создает новый экземп­
ляр класса Startup и вызьmает его метод ConfigureServices (), так что приложе­

ние может создать свои службы. Как объясняется в разделе "Службы ASP.NET" далее
в главе, службы - это объекты, 1юторые предоставляют функциональность другим
частям приложения. Приведенное описание не отличается особой строгостью по той
причине, что службы могут применяться для предоставления практически любой
функциональности.
После того как службы созданы, ASP.NET вызывает метод Configure ().Целью ме­
тода Configure () является настройка конвейера запросов, который представляет со­
бой набор компонентов (называемых промежуточным программным обеспечением),
используемых для обработки входящих НТГР-запросов и генерации ответов на них.
В разделе "Промежуточное программное обеспечение ASP.NET" далее в главе будут
даны объяснения, как работает конвейер запросов, и продемонстрировано создание
компонентов промежуточного ПО. На рис. 14.2 показано, как ASP.NET имеет дело с
классом Startup.

Создается Вызывается метод Вызывается Начинается


Приложение
экземпляр Configure метод обработка
за п ускается
кл асса Startup Services () Configure () запросов

Службы Конвейер
созданы подготовлен

Ри с. 14.2. Применение класса Startup инфраструктурой ASP.NET


для конфигурирования приложения

Класс Startup, который для всех запросов просто возвращает одно и то же сооб­
щение "Hello World ! ",не особенно полезен, поэтому перед подробным обсуждением
методов класса понадобится немного забежать вперед и включить МVС (листинг 14.9).
Глава 14. Конфигурирование приложений 387
Листинг 14.9. Включение MVC в файле Startup. cs

using Mic r osoft.AspNetCore . Builder ;


using Microsoft.AspNetCore .Host ing;
using Microsoft.AspNetCore.Http ;
using Microsoft.Extensions . Dependencyinjection ;
using Microsoft . Extensions . Logging ;
namespace ConfiguringApps (
puЬlic class Startup {
puЫic void ConfigureServices(IService Coll ection services) (
services.AddМvc();

puЫic void Configure(IApplicationBuilder арр , IHostingEnvironrnent env ,


ILoggerFactory logg erFactory) (
app.UseМvcWithDefaultRoute();

Благодаря таким добавлениям (которые объясняются в последующих разделах)


инфраструктура может обрабатывать НТГР-запросы и генерировать ответы с исполь­

зованием контроллеров и представлений. Запустив приложение, вы увидите вывод ,


показанный на рис. 14.3.

·--·------·------------------,
С 1 ф locatl1ost:SOOO '(:;
L ___ ". ---".--"·-·-
- .,.. -·· .. "_ _____" ____ --·--
~. ··~ - -- .J 1
1 i\Ic.>ssage Пus is tl1e lпdex actio11
l______ - !
--- ,_______" _____ -- --·· ---------------- ____________.J

Рис. 14.З. Результат включения MVC

Обратите внимание, что содержимое не стилизовано. Минимальная конфигурация


в листинг е 14. 9 не предоставляет какую-либо поддержку для обслуживания статичес­
кого содержимого, такого как таблицы стилей CSS и файлы JavaScript. Таким образом,
элемент li nk в НТМL-разметке, визуализируемой представлением Index . cshtml,
инициирует запрос для таблицы стилей CSS из Bootstrap, который приложение не в
состоянии обработать, что предотвращает получение требуемой стилевой информа­
ции . Пробл ема будет устранена в раздел е '"Добавление оставшихся компонентов про­
межуточного программного обеспечения " далее в главе.

Службы ASP.NET
Инфраструктура ASP.NEТ вызывает метод Startup . ConfigureServices (),так
что приложени е может настроить требуемые службы. Термин "служба " относится к
любому объекту, который предоставляет функциональность другим частям приложе­
ния. Как уже отмечалось, такое описание не является строгим , поскольку службы спо­
собны делать для приложения все что угодно. В качестве примера создайте в проекте
папку Infrastructure и добавьте в нее файл класса по имени UptimeService. cs с
опр еделением, приведенным в листинге 14.10.
388 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

Листинг 14.10. Содержимое файла UptimeService.cs из папки Infrastructure


u si ng Sys tem. Diagno s tic s;
namespa c e Configur i ngApps . Infra s tr uc t ure
puЫic class UptimeService
private Stopwatch timer ;
puЬlic Upt i meServ i ce() {
timer = Stopwatch. Star t New() ;

puЬlic l o ng Upt i me => t imer . ElapsedMi l l i seconds ;

Когда создается экземпляр класса Up timeServ i ce , его конструнтор запускает тай­


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

приложения .

Службы ASP.NET регистрируются с исполь зованием метода ConfigureService s ()


класса Startup; в листинг е 14.11 представлена регистрация класса UptimeService.

Листинг 14.11. Регистрация специальной службы в фа йле Startup. cs


using Mi crosoft . AspNetCore . Bui l der ;
using Mic r oso f t . AspNetCore . Host i ng ;
u sing Microsoft. AspNetCore .H ttp ;
u sin g Mic ros o ft. Ex t ens i ons . Depende ncyi n jection ;
us ing Mi c r osoft . Extensions . Logging ;
using ConfiguringApps.Infrastructure;
name s pace Co n fig ur i ngApps
puЬlic c l ass Startup {
p u Ьl i c void ConfigureServices(ISe rviceCol l e c t i on services) {
services.AddSingleton<UptimeService>();
se r v i ces .AddM v c() ;

puЫic void Configure( I Applicat i onBuilder арр , IHostingEnvironment env ,


ILoggerFactor y loggerFactory) {
app .UseMvcWithDe f au l tRoute() ;

МетодConf i gu r eSe r vices получает в качестве аргумента объе кт, который р е ­


ализу ет интерфейс IServiceCol l ect i on . Службы р е гистрируются с помощью
расширяющего метода, вызываемого на интерф ейсе IServiceCollection , кото­
рому указываются разнообразные конфигурационные параметры. Доступны е пара­
метры для создания служ б будут описаны в главе 18 , а пока мы прим е ня е м метод
AddSingleton () , что означает совместное использование единственного объекта
Up t i meServ i ce по всему приложению.
Глава 14. Конфигурирование прило же ний 389
Службы тесно связаны со средством под названием внедрение зависимостей, ко­
торое позволяет компонентам вроде контроллеров легко получать службы и подробно
рассматривается в главе 18. Получить доступ к службам, зарегистрированным в мето­
де Startup . ConfigureServices (), можно за счет создания конструктора, который
принимает аргумент с требуемым типом службы. В листинге 14.12 показан конструк­
тор, добавленный в контроллер Home для доступа к совместно используемому объекту
UptimeService , которы й был создан в листинге 14. l l. Кроме того, обновлен метод
действия Index () контроллера, так что он включает в генерируемые данные пред­
ставления значение свойства Update службы.

Листинг 14. 12. Доступ к службе в файле Hornecon troller. cs


using System . Col l ections . Generic;
using Microsoft.AspNetCore . Mvc ;
using ConfiguringApps.Infrastructure;
namespace ConfiguringApps . Controllers
puЫic class HomeControl l er : Control l er
private UptimeService uptime;
puЬlic HomeController(UptimeService up)
uptirne = up;

puЫic ViewResult Index()


=> View(new Dictionary<string , string> {
[ "Message " ] = " This is the Index action ",
[" Uptime " ] = $ " {uptime . Uptime)ms "
));

Когда инфраструктуре MVC необходим экземпляр класса контроллера


Ho me для
обработки НТТР-запроса, она инспектирует конструктор HomeController и обнару­
живает, что ему требуется объект UptimeService . Затем MVC инспектирует набор
служб , которые были сконфигурированы в классе Startup , выясняет, что служба
UptimeService сконфигурирована так. чтобы для всех запросов применялся единс­
тв енный объект UptimeService, и передает этот объект в качестве аргумента конс­
труктору при создании экземпляра HomeContro l ler .
Службы могут р егистрироваться и потребляться более сложными способами, но
рассмотренный пример продемонстрировал главную идею , лежащую в основе служб,
и показал, как определение службы в классе Sta r tup позволяет определять функци­
ональность или данные, к оторые используются по всему приложению.

Запустив приложение и запросив стандартный URL, вы увидите ответ, который


включает колич ество миллисекунд. прошедших с момента старта приложения. Это
значение получа ет ся из объекта UptimeService, созданного в классе Sta r tup
(рис. 14.4).
Каждый раз, I<огда получается запрос к стандартному URL, инфраструктура MVC
создает новый объе кт HomeControl le r и предоставляет ему разделяемый объект
UptimeService в виде аргумента констру~пора. Это позволяет контроллеру Home по­
лучать доступ ко времени выполнения приложения. не заботясь о том, каким образом
данная информация предоставлена или реализована.
390 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

·-
1 ~ · l
С ф localhostSOCO --~
·--··------==:=-:::::-:==-====---:-=-=::::-:::-:-_____,___ ,
: \

l Message
~~ ~~
П1is is the Iшlex

------------··-"------··-·-------
Рис. 14.4.
actio11

Использование простой службы


____ J
1
1

Службы MVC
Пакет с таким уровнем сложности, как МVС , имеет дело со многими службами;
одни из них предназначены для внутреннего употребления , а другие предлагают фун­
кциональность разработчикам. Пакеты определяют расширяющие методы, которы е
настраивают все требующиеся им службы в единственном вызове метода. Для MVC
такой метод имеет имя Ad dMvc ( ) и является одним из двух методов, добавленных в
класс Sta r t up, чтобы обеспечить работу МVС:

p uЫ i c voi d Con fi gure Se r v i ces (I Se rviceC oll ec ti o n se r vices) {


s e r v i ces . AddSi ngleton<UptimeSe r v i ce>() ;
services.Adc!Мvc();

Этот метод настраивает каждую службу, в которой нуждается MVC, не загромож­


дая метод ConfigureServi ces () гигантским списком индивидуальных служб .

На заметку! Средство lпtelliSeпse в Visual Studio будет отображать длинный список


других расширяющих методов, которые можно вызывать на объекте реализации
IServ ic eCo l le cti on в методе Co n f igureS e r v i ces ( ) . Некоторые из этих мето­
дов, такие какAddS i ng leton () и Add Scoped () ,применяются для регистрации служб
различными способами. Другие методы, подобные AddRouting () или AddCors () ,до­
бавляют отдельные службы, которые уже использовались методом AddМvc () . В резуль ­
тате для большинства приложений метод Confi gur e Serv i ces () содержит небольшое
число специальных служб, вызов метода AddMvc ( ) и необязательно операторы конфи­
гурирования встроенных служб, которые описаны в разделе " Конфигурирование служб
MVC" далее в главе.

Промежуточное программное обеспечение ASP.NET


В ASP.NET Core термин промежуточное ПО используется в отношении компонен­
тов, которые объединяются для формирования конвейера запросов. Конвейер запро­
сов организован аналогично цепочке; когда поступает новый запрос, он п ередается
первому компоненту промежуточного ПО в этой цепо ч ке. Компонент инспектирует
запрос и решает, обработать его и сгенерировать ответ или передать следующему ком­
поненту в цепочке. После того как запрос обработан, ответ, который будет возвращен
клиенту, передается по цепочке обратно, что позволяет всем предшествующим ком ­
понентам инспектировать или модифицировать его.
Глава 14. Конфигурирование приложений 391
Работа компонентов пром ежуточного ПО может выгля деть несколько странной,
но она допускает большую гибкость в том, как приложения собираются вместе.
Понимание того, каким образом применение пром ежуточного ПО придает форму при­
л ож е нию , может оказаться важным, особенно если вы получаете не те ответы, кото­
рые ожидали. Чтобы выяснить, как функционирует система промежуточного ПО, мы
создадим несколько специальных компонентов, которые продем онстрируют каждый
из имеющихся четыр ех типов промежуточного ПО.

Создание промежуточного по для генерации содержимого


Самый важный тип промежуточного ПО генерирует содержимое для клиентов, и

именно к этой категории принадлежит MVC. Чтобы создать компонент промежуточ­


ного ПО для генерации содержимого, не имея дела со сложностью MVC, добавьте в
папку Infrastructure файл класса по имени ContentMiddlewa r e . cs с определе­
ние м, прив еденным в листинге 14. 13.

Листинг 14.13. Содержимое файла Contentмiddleware.cs из папки Infrastructure


using System . Text ;
using System . Th r eading.Ta sks ;
using Microsoft . AspNetCore . Http;
namespace ConfiguringApps . Infra s tr uctur e
puЫic class ContentMiddlewa r e {
private Re questDe l eg at e nex tDelegate;
puЫic ContentMi dd l eware(Re questDe le gate next) {
nextDelegate = next ;

puЬlic async Task I nvo ke(Http Con t ex t httpContext)


if (httpContext . Re quest . Path . ToStri ng () . ToLower () " /middl eware " )
await httpContext . Respon s e . WriteAs ync(
"This i s from t he content mi ddle ware ", Encodi ng . UTFB) ;
else {
await ne xtDelega t e . Invoke (http Context) ;

Компоненты промежуточного ПО не р е ализуют какой-нибудь интерфейс и не яв­


л яются ун а сл едованными от ка~юго-то общего базового класс а. Взамен они определя­
ют конструктор, который принимает объект Re que s tDe l egate , и метод Invoke () .
Объ е кт
RequestDelegate представляет следующий компонент промежуточного ПО в
цепочке, а м етод Invoke ( ) вызывается, когда ASP.NET получ ает НТГР -з апрос.
Информация о запросе и ответе НТГР, которая будет возвращена клиенту, предо­
ставляется методу Invo ke () ч ерез аргумент HttpContext . Класс HttpContext и его
свойства расс матриваются в главе l 7, а пока достаточно знать, что м етодI n vo ke ( ) в
листинге 14.13 инспектирует НТГР-запрос и проверяет, был ли он послан на URL вида
/midd l eware . Есл и это так, тогда клиенту отправляется простой текстовый ответ;
есл и использовался другой URL, то запрос перенаправля ется следующему компоненту

в цепочке .
392 Часть 11. Подробные сведения об инфраструктуре ASP.NET Саге MVC

Конв ейер запросов настраивается внутри метода Confi gu r e () класса S tartup .


В листинге 14.14 из прим е ра приложения удалены методы MVC и прим енен класс
ContentMiddlewa r e в кач е стве единственного компонента в конвейере.

Листинг 14.14. Использование специального компонента промежуточного ПО


для генерации содержимого в файле Startup. cs

using Mi c r osoft. AspN e t Core . Bu i l der ;


u si ng Mi c r o s oft .As pNetCore .H ost in g ;
using Microso f t . AspNetCore . Htt p;
using Micro s of t . Extensions.Dependen cyinjec t ion ;
using Microsof t. Ex t ensions . Logging ;
using ConfiguringApps. I nfrastructure ;
namespace Confi gu r ingApps
p uЫic class St a r t up {
pu Ы ic void ConfigureSe r v i ces (I ServiceCollection services) {
se r vices . Add Sin gleton<Upt i meService>() ;

p uЫic void Con figu r e ( IApplicat i o nBui l de r а рр , I HostingEnvi r o nment env ,


I Logger Factory l og g e rFactory)
app.UseМiddleware<Contentмiddleware>( ) ;

Специальные компоненты промежуточного ПО регистриру ются с помощью


расширяющего метода UseM i ddlewa r e () внутри метода Configure () . Метод

UseMiddleware ( ) прим е ня ет параметр типа для указания класса промежуточного


ПО . Именно так инфраструктура ASP.NEТ Саге мож ет построить сп и сок всех компо­
нентов промежуточного ПО . которые собирается использовать, и затем создать их
экз емпляры для формирования цепочки. Запустив приложение и запросив URL вида
/middleware, вы увидите р езультат, п01tазанный на рис. 14.5.
На рис . 14.6 изображен конвейер промежуточного ПО, созданный с применени­
ем класса ContentMiddl eware . Когда инфраструктура ASP.NEТ Core получает НТГР­
запрос, она передает его единств енному компоненту промежуточно го ПО, зареги ст­
риров анному в класс е Star t up . Если URL является / mi dd l eware, тогда компонент
генерирует результат, который возвращается ASP.NET Core и отправляется клиенту.
Если URL отличается от /middleware , то класс Conten tMiddleware пе редает
запрос следующему компоненту в цепочке. Поскольку других компонентов нет. за­
прос достигает ограничительного обработчика, предоставленного инфраструнтурой
ASP.NET Core при создании конвейера, который посылает запрос обратно чер ез кон ­
вейер в противоположном направл ении (после того. как вы озншшмите сь с работой
других типов промежуточного ПО, данный процесс обретет больший смысл) .

Использование служб в промежуточном ПО

Применять службы , настроенны е в методе Con f igureServices (), могут н е толь­


ко контроллеры. Инфраструктура ASP.NET инспектиру ет конструкторы классов про­
межуточного ПО и использует службы для предоставления знач е ний любым аргумен­
там , которые были определены .
Глава 14. Конфигурирование прило жений 393

f:j l0<•1host5000/middil!'Ф' Х
r-·-··- -··-------- ------·-::;::1
~
·-
.
·--
_______
с ~.:!!~~~~:::1~~.::._
. --- - ·- .. _____ ___
3..1 ;
-----·-- ----------
_. ~ - -----··-"-··-

11 Thi s is from the content middle1•1are /

L__ -------------- ________J


Рис. 14.5. Генерация содержимого из специального
компонента промежуточного ПО

Промежуточное ПО
для генерации содержимог о

1-
ш 'r ------ - .D

"...
z 1
Ответ
1
1
1
ф

s:
;r
а.: 1 :s;
1 1 :z;
1

ел 1
---- -
1 "'
Q.

~ о
~
~

'
<(

Рис . 14.6. Пример конвейера промежуточного ПО

В листинге 14 . 15 к конструктору класса ContentMiddleware добавлен ар­


гумент, который сообщает ASP.NET о необходимости предоставления объекта
UptimeService .

Листинг 14.15. Применение службы в файле Contentмiddleware. cs

using Systern . Text;


using Systern . Threading .Tasks;
using Microsoft.AspNetCore.Http;
narnespace ConfiguringApps .I nfrastructure
puЫic class Conte ntMiddleware {
private RequestDelegate next Delegate;
private UptimeService uptime;
puЬlic Contentмiddleware(RequestDelegate next, UptimeService up) {
nextDelegate = next ;
uptime = up;
puЫic async Task Invoke(HttpContext httpContext)
if (httpContext . Request.Path.ToString() . ToLower() " /rniddleware " ) {
await httpContext.Response.WriteAsync(
"This is from the content middleware "+
$" (uptime: {uptime.Uptime}ms) ", Encoding .UTF8 );
else {
awa it nextDelegate .Invoke(httpContext);
394 Часть 11. Подр о бные сведения об инфраструктуре ASP.NET Саге MVC

Возможность использования служб означает, что компоненты промежуточного ПО


могут разделять общую функциональность и избегать дублирования кода .

Создание промежуточного по для обхода


Следующий тип промежуточного ПО перехватывает запросы до того, как они до ­
стигнут компонентов для генерации содержимого, чтобы обойти проце сс прохожде ­
ния конвейера , часто в целях , связанных с производительностью. В листинге 14 .16
приведено содержимое файла класса по имени ShortCircu i tMiddleware . c s, добав­
ленного в папку I nfrastruc tu re.

Листинг 14. 16. Содержимое файла Sho rtCircui tмiddleware. cs


из папки Infrastructure

usi n g System . Li nq ;
using S y stem.Thr ead i ng .T a sk s ;
usi ng Microso ft . Asp NetCo r e . Http ;
na me s pace ConfiguringApps . Inf r ast r uctu r e
puЬlic c l ass ShortCircuitMiddleware {
pr i vate Req ue s t Delega t e nextDeleg ate ;
pu Ы icSho r tC i rcuitMiddleware(Req uestDelegate next) {
nextDelega t e = next;

puЫic async Ta s k Invoke(HttpContext httpContext)


if (h t tpCo nt ex t. Requ e s t . Heade rs[" User - Ag ent"J
. Any( h => h . To Lower () . Con t a i n s ( " edge " ))) {
httpContex t . Response . Sta t usCode = 403 ;
e l se {
await nex t Delega te.I nvoke( h ttpContext) ;

Этот компонент проме жуточного ПО проверяет заголовок User - Agent з апроса .


который браузеры применяют для идентификации самих себя. Использование заго­
ловка Use r- Agent для опознания специфических браузеров не вполне надежно для
реализации в реальном приложении, но достат очно для рассм атрива е мого прим е ра .

Понятие " обход" применяется из-за того, что такой тип промежуточного ПО не
всегда направляет запросы следующему номпоненту в цепочке. В данном случае, если
заголовок User - Agent содержит элемент edge, тогда компонент устанавливает код

состояния в 403 - Forbidde n (403 - запрещено) и не направляет запрос следующе­


му компоненту. Так как запрос отклонен, нет никакого смысла давать возможность
запросу обрабатываться остальными компонентами , что привело бы к не нужному
потреблению системных ресурсов. Взамен обработка запроса прекращается раньш е,
а клиенту посылается ответ 403.
Компоненты промежуточного ПО получают запросы в поря дке. в котором они
были настроены в классе S tartup , что означает необходимость настройки промежу­
точного ПО для обхода перед пром ежуточным ПО для генерации содержимого (лис­
тинг 14.17).
Гл а в а 14. Ко н фигуриро в а ни е п ри л оже н ий 395
Листинг 14.17. Регистрация промежуточного ПО для о бхода в фай л е S tartu p. cs

using Microsoft . As p NetCore . Builder ;


using Microso f t . AspNetCore . Hos tin g ;
using Mi crosoft . AspNetCore . Http ;
using Microsoft . Extens i ons . Dep e ndencyinjection ;
using Microsoft . Extens i ons.Logg i ng ;
u s ing Configu rin gApps . Inf rast ru ct u re ;
namespace Co n figuringApps
puЬlic class Startup {
p u Ьlicvo i d Confi gure Servic e s ( IServi ceCo l lect i on services) {
services.AddSing l eton< Upt i meSe r vice>() ;

puЫic void Configu re( I App l i c at i o n Builder арр , I Ho s t i ngEnvironme nt env ,


ILoggerFactor y l oggerFacto r y ) {
app . Us eMi ddl ewa re< Sho rtC ir cui tмi ddl eware >() ;
app.UseMiddleware<Cont entMi dd l ewa r e>() ;

З апу с тив прило же ние и запросив любой URL с использованием брауз е ра


Microsoft Edge, вы увидите ошибку 403. Запр о сы из других браузеров компонент
ShortCircui tMiddleware игно рирует и п ередает следующему компоненту в цепоч­

к е , а это значит, что ответ будет с ген е рирован , когда запрошенны м URL является
/middleware . На рис . 14.7 иллюстрируется добавление в конвейер компонента про ­
межуточного ПО для обхода.

Про ме жуточ н о е ПО
П р о м е жуточное ПО для ген е р а ц ии
дл я о бхода содер жим ого

1-
ш
.D
,..
~

-- --- - ,.. ------- <::;

z
CJ)
1 1 >--
:s:
т

От ве т От в ет s
о.: 1 1 1 1 :r:

(/) ~ 1------ , 1------k 1


"'
Q_
о
' ~

<(

Рис. 14. 7. Добав лен и е в ко н ве й ер ко м поне нта п р о межуто чн ого ПО для о бхода

Создание промежуточного ПО для редактирования запросов


Следующий тип компонента промежуточного ПО, который мы опиш ем, н е ген е ­
рирует отв ет. Вместо этог о они изменяет запр о сы перед т ем, как они достигнут дру­
гих компон е нтов, н аходя щихся дальше в це п очке . Такой вид промежуточного ПО
при меня ется главным образом для интеграции с платформой, чтобы обогатить пр ед­
ставлени е ASP.NET Core запроса НТТР средствами, специфич е скими для платфор мы .
О н также может ис п ол ьзов ать ся для подготовки запросов, так что их легче обр аб а -
396 Часть 11. Подробные сведения об инфраструктуре ASP.NET Саге MVC

тывать последующими компонентами. В качестве демонстрации добавьте в папку


Infrastructure файл BrowserTypeMiddleware . cs с определением компонента
пром ежуточного ПО, приведенным в листинге 14.18.
Листинг 14.18. Содержимое файла BrowserТypeМiddleware. cs
из папки Infrastructure

us ing System . Linq ;


us ing System . Th r eading . Tasks ;
using Microsoft . AspNetCore.Http ;
namespace ConfiguringApps . Infrastructure
puЬlic class BrowserTypeMiddleware {
private RequestDelegate nextDel e gate ;
puЫic BrowserTypeMiddleware(RequestDelegate next) {
nextDelegate = next;

puЫic async Task Invoke(HttpContext httpContext)


httpContext . Items[ " EdgeBrowser " ]
= httpContext . Request . Headers[ " User - Agent " ]
. Any(v => v . ToLower() . Contains( " edge " )) ;
await nextDelegate.Invoke(httpContext) ;

Новый компонент инспектирует заголовок User-Agent запроса и ищет в нем


элемент edge , что предполагает возможность выдачи запроса браузером Edge.
Объект HttpContext предоставляет через свойство Items словарь, который при­
меняется для передачи данных между компонентами, и ре зул ьтат поиска в заг о ­

ловке сохраняется с ключом EdgeBrowser. Чтобы продемонстрировать, как ком­


пон енты промежуточного ПО могут взаимодействовать, в листинге 14.19 пок азан
класс ShortCircui tMiddleware, который отклоняет запросы, когда они поступа ­
ют от браузе ра Edge, принимая решение на основе сгенерированных компон ентом
BrowserTypeMiddleware данных.

Листинг 14.19. Взаимодействие с другим компонентом в файле


ShortCircuitМiddleware.cs

using System.Linq ;
using System .Th reading.Tasks ;
using Microsoft . AspNetCore .H ttp;
namespace ConfiguringApps . Infrastructure
puЬlic class ShortCircuitMiddleware {
private RequestDelegate nextDelegate ;
puЫic ShortCircuitMiddleware(RequestDelegate next) {
nextDelegate = next ;

puЬlic async Task Invoke(HttpContext httpContext) {


if (httpContext. Items [ "EdgeBrowser"] аз bool? == true)
httpContext . Response.StatusCode = 403 ;
Глава 14. Конф и гурирование прило ж ений 397
} else {
await nextDelegate .I nvoke(httpContext) ;

И з-за своей природы компоненты промежуточного ПО , редакти рующие запро­


сы, необходимо помещать перед компонентами, с которыми они в заи модейству­
ют или которые полаг а ются на вносимые ими измен е ния. В листинг е 14.20 класс
BrowserTypeMiddleware зар е гистрирован как пе рвый компонент в конв е йере.

Листинг 14.20. Регистрация компонента промежуточного ПО для редактирования


запросов в файле Startup. cs

using Microsoft . AspNetCor e. Builder ;


using Microsoft . AspNetCo r e . Hosting ;
using Microsoft . AspNetCore . Http ;
using Microsoft.Extens i ons . Dependencyln j ect i on ;
using Microsoft . Extensions . Logging ;
using ConfiguringApps.Infrastructure ;
namespace ConfiguringApps {
puЫic class Startup {
puЫic void Conf i gureServices(IServiceCollection services) {
services.AddSingleton<UptimeService>( ) ;

puЫic void Configure( I ApplicationBuilder арр , IHostingEnvironment env ,


ILoggerFactory loggerFactory)
app.UseМiddleware<BrowserTypeMiddleware>();
app . UseMidd l eware<ShortCircuitMi ddl eware>() ;
app . UseMiddleware<ContentMiddleware>() ;

Пом е щение компон ента в начале конв ейера гарантирует, что до попадания в дру­
гие компоненты запрос уже будет модифицирован (рис . 14.8).

Проме жуточное ПО Промежуточное ПО


для редактирования Проме жуточное ПО для генерации
запросов для обхода содер ж имого

~
ш ~ 1
Запрос
1
-- - -- - --- ---
.!J

z 1
r
1
"'~
ф

~
а.: Ответ Ответ 1
1 1 1 1 I

(f) 1 1 "'
а.

<(
~

- --- - ~
-----f< - _1 о

Рис . 14.8. Добавление в конвейер компонента проме жуточного ПО


для редактирования запросов
398 Часть 11. Подробные сведения об и нфраструктуре ASP.NET Core MVC

Создание промежуточноrо по для редактирования ответов


Последний тип промежуточного ПО оперирует на ответах , генерируем ых другими
ко мпонентам и в конвей ере . Это поле з но для регистрации в жур нале деталей запро с о в
и их отв етов ил и для обработки ош ибок. В листинге 14. 21 представл ено соде ржим о е
ф айла Er r orMiddleware . cs, который добавлен в пап ку Infrastructure дл я де ­
монстрации данного вида компон ента пром ежуточного ПО .

Листинг 14.21. Содержимое файла ErrorMiddleware.cs из папки Infrastructure

using Sys tern . Text ;


using Systern . Threading . Tasks ;
using Microsoft . AspNetCore . Http ;
narnespace ConfiguringApps . Infrastructur e
puЫic c l ass ErrorMidd l eware {
private RequestDe l egate nextDe l egate ;
puЫic ErrorMiddleware(Reques t De l egate next) {
nextDe l egate = next ;

puЫic async Task Invoke(HttpContext httpContext)


await nextDelegate .I nvoke(httpContext) ;
if (h t tpContext . Response . Statu s Code == 403) {
await httpContext.Response
.WriteAsync( " Edge not suppor t ed ", Encod i ng . UTFB) ;
else if (httpContext.Response.StatusCode == 404) {
await httpContext . Response
. WriteAsync( " No conte n t rniddleware response ", Encoding.UTF8) ;

Компонент н е заинте ресован в з апросе до т ех пор , пока он н е про йдет сво й пут ь
по конвей е ру проме жуточного ПО и н е сгенерируется ответ. Если кодо м со стоя ния
отв ет а явля ется 403 или 404, тогда компон ент добавляет к от вету о пи сательно е со ­
общение. Все други е ответы игнорируются . В листинге 14.22 показана р еги стр а ция
кл асс а компонент а в классе Startup.

Совет. Вас может интересовать, от куда поступил код состояния 4 04 - Not Found (404 -
не найдено) , пос кольку он не устанавливается ни одним из тре х созданны х компонентов
промежуточного ПО. Дело в том, что именно так инфраструктура ASP.NET конфигурирует
ответ, когда запрос входит в конвейер, и этот ответ будет тем результатом, который воз­
вращается клиенту, если компоненты промежуточного ПО не изменяли его .

Листинг 14.22. Регистрация компонента промежуточного ПО


для редактирования ответов в файле Startup. cs
using Mic r osoft . AspNetCore . Builder ;
using Microsoft . AspNetCore . Hosting ;
using Microsoft . AspNetCore . Http ;
Гла ва 14. Конфигур и р о ван и е прил ож ений 399
using Mi c r o soft . Extensions . Dependencyinj e ction ;
using Mi c ros o ft . Extensi o ns . Logging ;
using Co nfiguringApps . Infrastruc t ure ;
namespace Co nfiguringApps {
pu Ыi c class Startup {
puЫic v o id ConfigureServices(IServiceCollection services) {
services . AddSingleton<UptimeService>() ;

puЫic void Configure(IApplicationBuilder арр , IHostingEnvironme n t e nv ,


ILoggerFactory l oggerFactory)
app. UseMi ddleware<Erro rMiddleware>( ) ;
app. UseMiddleware<BrowserTypeMiddleware>() ;
app . UseMiddl eware<ShortCircuitMiddleware > () ;
app. UseMidd l eware<ContentM i ddle ware>( ) ;

Класс Erro rMiddlewa r e зар егистрирован в первой позиции конвейера. Эт о м о­


жет выглядеть странным для ко мпонента , который заинтересован только в ответах ,
но регистрация компонента в начале цепочки обеспечивает ему возможность инспек­
тировать ответы , сгенерированные любыми другими компонентами ( рис. 14.9). В слу ­
ч ае помещения ком понента дальше в конвейере он см ожет инсп ектировать отв еты ,
генерируем ые только ко м понентам и , находящимися после него .

Промежуточ н ое П О П ромежуточ ное П О П ромежуточное ПО


для редактиров ан ия дл я редактирован ия Промежуточн ое ПО дл я генерации
от вето в за п росов дл я о б хода
содержимо г о

1- ______ _____ ...._


ш 1 1
--- - - ----- -
~ Запрос
z 1 1
От ве т
а.: 1 Отв ет
1 1 1
ел 1 1
~ч Ответ ----- --- - --~
<( ' ' '

Ри с. 14.9 . До бавление в ко нвей ер ко м по нента п р о межуточн ого П О дл я редакти ров ани я отв ето в

Эфф е кт от нового пром ежуточного ПО можно увидеть, запустив приложение и


запрос и в любой URL кром е /middleware . Результато м будет сообщение об ошибке ,
показ анно е на рис . 14.10.

С) localhostSOOO/otherURL Х

No content middleware response

Ри с. 14.1 О. Ре дакти ров ание от в ето в от друг их ко м пон е нтов про межуточного П О
400 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

Особенности вызова метода Conf iqure ()


Инфраструктура ASP.NET Core исследует метод Configure () перед его вызовом и
получает список его аргументов, которые он предоставляет с использованием служб,
настроенных в методе ConfigureServices (), или специальных служб, описанных
в табл. 14.7.
Таблица 14. 7. Специальные службы, доступные как аргументы метода Configure ()
Тип Описание

I Appli cationBuilder Этот интерфейс определяет функциональность, требуемую


для настройки конвейера промежуточного ПО приложения

IHostingEnvi r onment Этот интерфейс определяет функциональность, требуемую


для различения типов сред, таких как среда разработки
и производственная среда

ILoggerFactory Этот интерфейс определяет функциональность, требуемую


для настройки регистрации в журнале запросов

Использование интерфейса IAppl.ica tionВuil.der


Хотя вы не обязаны определять любые аргументы для метода Conf i gure (), в
большинстве классов
Startup будет применяться , по крайней мере, интерфейс
IApplicationBuilder, поскольку он позволяет создать конвейер пром ежуточно­
го ПО , как демонстрировалось ранее в главе . В случае специальных компонентов
промежуточного ПО для регистрации классов используется расширяющий метод
UseMiddleware ().Сложные пакеты промежуточного ПО для генерации содержимого
предоставляют единственный метод, который настраивает все их компоненты пром е­
жуточного ПО за один шаг -точно так же, как они предоставляют единственный ме­
тод для определения применяемых ими служб. Для MVC доступны два расширяющих
метода, описанные в табл. 14.8.
Таблица 14.8. Расширяющие методы IApplicationBuilder для MVC

Имя Описание

UseMvcWi thDefaul tRoute () Этот метод настраивает компоненты промежуточного ПО


для MVC с использованием стандартного маршрута

UseMvc () Этот метод настраивает компоненты промежуточного ПО


для MVC с применением специальной конфигурации мар­
шрутизации , указанной посредством лямбда-выражения

Маршрутизация- это процесс, с помощью которого URL запросов сопоставля­


ются с контроллерами и действиями, определенными в приложении: маршрутиза­
ция подробно рассматривается в главах 15 и 16. Метод UseMvcWi thDefaul tRoute ()
полезен при изучении разработки приложений MVC, но в большинстве приложе­
ний вызывается метод UseMvc (), даже если результатом является явное опреде­
ление той же самой конфигурации маршрутизации, которая со здает ся методом
UseMvcWi thDefau l tRoute (), как показано в листинге 14.23. Такой подход делает
конфигурацию маршрутизации , используемую в приложении, очевидной для других
разработчиков и облегчает дальнейшее добавление новых маршрутов (что в какой-то
момент требуется практически во всех приложениях).
Глава 14. Конфигурирование прило ж ений 401
Листинг 14.23. Настройка компонентов промежуточного ПО для MVC в файле Startup. cs

using Microsoft . AspNetCore . Bui l der ;


using Microsoft . AspNetCore . Hosting ;
using Microsoft . AspNetCo r e . Http ;
using Microsoft . Extens i ons. Dependency inj ection ;
using Microsoft . Extensions. Logging ;
using ConfiguringApps . Inf r astructure ;
narnespace Conf i gur i ngApp s {
pu Ыi c c l a s s Start up {
puЫic void ConfigureS e rvices(IServi ceColl ection services) {
services . AddSingleton< Upt i rneService>() ;
services.AddМvc();

puЬlic void Configure(IApp l icationBuilder арр , IHosti ngEnvironrnent env ,


ILog ge r Factory l oggerFacto r y)
app. Us eMiddlewa r e<Er r orMi ddl eware> ( ) ;
app . UseMiddleware<Br ow serTypeMiddl eware>() ;
app . UseMiddleware<Shor t Ci rcu i tM iddl eware>() ;
app . UseMiddleware<Co ntentMidd l ewar e>() ;
app.UseМvc(routes => {
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
}) ;

Поскольку MVC настраивает компоненты промежуточного ПО для г е н е рации со­

держимого, метод UseMvc () вызывается после регистрации всех других компонен­


тов промежуточного ПО. Чтобы подго товить службы , от которых зависит MVC, метод
AddМvc () долж ен вызываться в м етоде Conf i gureServices () .

Использование интерфейса IHos tingEnvironmen t


Интерфейс IHost in gEnvirorune nt предоставляет базовую - но важную - инфор­
мацию о среде ра з м е щения. в которой функционирует приложе ние, с применени е м
свойств, описанных в табл. 14.9.
Сво йства ContentRootPat h и WebRootPath интересны, но в большинстве при­
ложений не нужны, потому что имеется встроенный компонент промежуточного ПО,
которы й мож но исполь зовать для достав1ш статич еского содержимого , как описано в
раздел е " Включени е статического содержимого " дале е в главе.
Важным свойством является Environme nt Name , которо е позволя ет модифици­
ровать конфигурацию приложения на основе ср еды. где оно выполня ется. По согла­
шению суще ствуют три распростране нных среды (среда разработки (Deve l opment),
подготовительная среда (Stagi ng) и производственная ср еда (Production)) .
Текущая среда размещения устанавливается с применением пер ем енной среды
по им е ни ASPNETCORE_ENVIRONMENT . Чтобы установить переменную с реды, выбе­
рите пункт ConfiguringApps Options (Параметры Con figur in gApps) из меню Project
(Проект) в Visual Studio и перейдите на вкладку Debug (Отладка) . Дважды щелкни­
те н а поле Value (Значение) для пере м енной среды, по умолчанию установленной в
Development, и и з м енит е ее на Staging (рис. 14. 11).
402 Часть 11 . Подробные сведен и я об инфрастру ктуре ASP.NET Core MVC

Таблица 14.9. Свойства IHostingEnvironment


Имя Описание

ApplicationName Это сво й ство возвращает имя приложения , установленное


размещающей платформой

EnvironmentName Это свойство возвращает стро ку, которая описывает теку­


щую среду, как объясняется ни же

ContentRootPath Это свойство возвращает путь , по которому на ходятся


файлы содер жимого и конфигурации пр и ложения

WebRootPath Это свойство возвращает строку, указывающую каталог,


в к отором находится статическое содер жимое для прило­

жения. Обычно это папка wwwroot


ContentRootFileProvider Это свойство возвращает объе кт, который реализует ин ­
тepфeйc Mi crosoft . AspNetCore . FileProviders .
IFileProvi der и может использоваться для чтения фай­
лов из папки, указанной свойством ContentRootPath

WebRootFileProvider Это свойство возвращает объект, который реализует ин­


тepфeйc Microsoft . AspNetCore . FileProviders .
IFileProvider и может применяться для чтения файлов
из пап ки , указанной свойством WebRoo tPath

А Profi!e

Launch: 115 Eцi reS-'

г:;. :.-:--:--:;:-;:--;:-::.;;------
L..... . -- - --· -·-· -

Envisonment Venab~ s \/alue


ASPN EТC OR E_E №ЛRONM E NT S:a~"9

\•/еЬ Str.-er Settings


" " , . . . . .....--".",_ ,,j
_ " _____",·-"-~,,,,..,...... ".
.....

Р ис. 14.11. Установка имени среды раз м ещения

Сохр аните измен е ния , чтобы новое имя ср еды вступил о в силу .

Совет. Имена сред не чувствительны к регистру символов , так что Staging и staging
трактуются ка к одна и та же среда . Хотя Development, Staging и Production явля­
ются традиционными именами сред , вы можете использовать любое желаемое имя. Это
может быть полезно, напри м ер , когда над проектом трудятся несколько разработчи ков ,
каждый из которы х требует разных конфигурационных настрое к. В разделе " Работа со
сло ж ными конфигурациями " далее в главе объясняется, ка к учитывать слож ные различия
ме жду конфигурациями сред.
Глава 14. Конфигурирование приложений 403
Внутри метода Configure () можно выяснить, какая среда размещения приме­
няется, прочитав свойство IHostingEnvironrnent. En viro nrnentName или исполь­
зуя один из расширяющих методов , которые оперируют над объектами реализации
IHostingEnvironrnent и описаны в табл. 14.10.
Таблица 14.10. Расширяющие методы IHostingEnvironment

Имя Описание

IsDeveloprnent () Этот метод возвращает true, если именем среды размещения


является Developrnent
IsStaging () Этот метод возвращает true, если именем среды размещения
является Staging
IsProduction() Этот метод возвращает true , если именем среды размещения
является Production
IsEnvironrnent(env) Этот метод возвращает true, если имя среды размещения сов ­
падает со значением аргумента env

Расширяющие методы применяются для изменения набора компонентов про­


межуточного ПО в конвейер е, чтобы приспособить поведение приложения к раз­
личным средам размещения. В листинге 14.24 с помощью одного из расширяю­
щих методов гарантируется, что специальные компоненты промежуточного ПО,
созданные ранее в главе, присутствуют только в среде размещения Developrnent.
Листинг 14.24. Использование среды размещения в файле Startup . cs

using Microsoft . AspNetCore .Builder ;


using Microsoft . AspNetCore .H osting ;
using Microsoft . AspNetCore . Http;
using Microsoft . Extensions . Dependencyi nje ction ;
using Microsoft . Extensions . Logging ;
using ConfiguringApps . Inf r astructure ;
namespace ConfiguringApps {
puЫic class Start u p {
puЫic void ConfigureServices(IServ ic eCo ll ection services) {
services . AddSingleton<UptimeService>();
services.AddMvc() ;

puЬlic void Configure( IAppli cat i onBuilder арр , IHostingEnvironment env ,


ILoggerFactory loggerFactory) {
if (env.IsDevelopment()) {
app.UseМiddleware<ErrorMiddleware>();
app.UseМiddleware<BrowserTypeМiddleware>();
app.UseМiddleware<ShortCircuitмiddleware>();
app.UseМiddleware<Contentмiddleware>();

app.UseMvc(routes =>
routes.MapRoute(
name : " default ",
template : " {cont rol ler=Home}/{action=Index}/ {id ?} ");
}) ;
404 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

Специальные компоненты промежуточного ПО не добавляются в конвейер при


текущей конфигурации, в которой переменная среда была установлена в Staging.
Запустив приложение и запросив URL вида /middleware . вы получите ошибку 404 -
Not Found, т.к. доступными компонентами промежуточного ПО являются лишь те,
которые настроены методом UseMvc (), а они не имеют контроллера для обработки
указанного URL.

На заметку! Протестировав влияние от изменения среды размещения, не забудьте изменить


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

Использование интерфейса ILoggerFactory


Интерфейс ILoggerFactory используется для конфигурирования регистрации
в журнале внутри приложения, так что отдельные компоненты могут предоставлять

диагностическую информацию. Метод Configure () класса Startup применяется


для указания, куда отправляется журнальная информация, и какова требуется сте­
пень детализации . Чтобы предоставить доступ к функциональности регистрации в
журнале, добавьте в проект новый пакет, как показано в листинге 14.25.

Листинг 14.25. Добавление пакета для регистрации в журнале в файле project. json

" dependencies ":


"Microsoft.NETCore.App ":
"version ": " 1.0 . 0 ",
" type ": "plat for m"
) '
"Microsoft.AspNetCore.Diagnostics" : " 1 . 0 . О ",
"Microsoft.AspNetCore . Server .IISintegration ": "1. 0.0 ",
"Microsoft.AspNetCore . Server . Kestrel ": " 1.0.0",
"Mi crosoft.Extensions . Logging . Console ": " 1 . 0 . 0" ,
"Microsoft.Extensions.Logging.Debug" : 11
1.0.0 11 ,

"Microsoft. AspNetCore. StaticFi les": "1. О. О ",


"Microsoft . AspNetCore. Mvc ": "1. О. О ",
"Mi cro s oft . VisualStudio . Web . Browse r Link . Loader": "1 4.0 . 0" ,
"Microsoft . AspNetCore . Razor .Tools ": (
"version ": " l . 0 . 0-preview2-final" ,
"type ": "bui ld "

) '

В листинге 14.26 приведена базовая конфигурация регистрации в журнале, кото­


рая подходит для проектов на стадии разработки. (В рассматриваемом примере не
задействована точная настройка, обеспечиваемая внешним файлом конфигурации,
которая демонстрируется позже в главе.)
Глава 14. Конфигурирование прило ж ений 405
Листинг 14.26. Конфигурирование регистрации в файле startup. cs
using Microsoft.AspNetCore . Builder ;
using Microsoft . AspNetCore .Hosting;
using Microsoft .AspNetCore.Http;
using Microsoft . Extensions . Dependencyinjection ;
using Microsoft.Extensions . Logging ;
using ConfiguringApps . Infrastructure ;
namespace ConfiguringApps
puЬlic class Startup {
puЬlic void ConfigureServices(IServi ceCollection services) {
services.AddSingleton<UptimeService>();
services . AddMvc();

puЬlic void Configure(IApplicationBuilder арр , IHostingEnvironment env,


ILoggerFactory loggerFactory)
loggerFactory.AddConsole(LogLevel.Debug);
loggerFactory.AddDebug(LogLevel.Debug);
if (env.IsDevelopment()) {
app .U seMiddleware<ErrorMiddleware>() ;
app.UseMiddleware<BrowserTypeMiddleware>() ;
app .U seMiddleware<ShortCircuitMiddleware>() ;
app .UseMiddleware<ContentMiddleware>();

app . UseMvc(routes =>


routes . MapRoute(
name : "default ",
template : "{ controller=Home}/{action=Index}/{id?} " ) ;
}) ;

Аргумент типа ILoggerFactory метода Conf igure () предоставляет объект, необ­


ходимый для конфигурирования системы регистрации в журнале. гл авная задача при
настройке регистрации заключается в указании, куда планируется отправлять жур­
нальные сообщения, для чего предназначены методы AddConsole () и AddDebug () .
Метод AddConsole () посылает журнальные сообщения на консоль, что удобно в слу­
чае запуска приложения из командной строки с использованием Kestrel, как было
описано ранее в главе. Метод AddDebug ( ) отправляет журнальные сообщения в окно
Output (Вывод) среды Visual Studio, когда приложение выполняется под управлением
отладчика. Эти два оператора полезны для получения регистрационной информации
во время разработки.

Совет. Посылать сообщения отладки можно не только на консоль и в окно Output. Доступны
также варианты с применением журнала событий и пакетов регистрации от независи­
мых разработчиков, таких как Nlog . Для этого понадобится добавить в приложение па­
кетMicrosoft . Extensions . Logging. EventLog или Microsoft. Extensions .
Logging . NLog и вызвать метод AddEventLog () или AddNLog () соответственно на
объекте реализации ILoggerFactory внутри метода Configure ().
406 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

Система р егистрации в журнале ASP.NET Core определяет ш есть уровней отладоч­


ной информации, которые описаны в табл. 14. 11 в порядке своей важности.

Таблица 14.11. Уровни отладочной информации ASP.NET Core


Уровень Описание

Trace Этот уровень используется для сообщений, которые полезны на стадии


разработки, но не требуются в производственной среде

Debug Этот уровень применяется для детализированны х сообщени й, необхо­


димых разработчикам при отладке в случае возни кновения проблем
Information Этот уровень ис пользуется для сообщений , которые описывают общее
функционирование приложения

Warning Этот уровень применяется для сообщений, описывающи х события, кото­


рые являются непредвиденными , но не прерывают работу приложения

Error Этот уровень используется для сообщений , которые описывают ошибки ,


прерывающие работу приложения

Critical Этот уровень применяется для сообщений, которые описывают катаст­


рофические отказы

None Этот уровень используется для от ключения регистрационны х сообщен ий

Чтобы включить все сообщения, методам AddConsole () и AddDebug () в качес ­


тве аргумента п ередается значение LogLevel . Debug . Оценить эффект от включения
регистрации в журнале можно, запустив приложение с применением отладчика Visual
Studio и заглянув в окно Output, где появятся р егистр ационные сообщения, которые
описывают, как обрабатывается каждый НТТР-з апрос, например:

Microsoft.AspNetCore.Hosting.Internal . WebHost : Inforrnation: Request


starting НТТР/1 . 1 GET http://l ocalhost :SOOO/
Microsoft . AspNetCore . Routing . RouteBase : Debug: Request successfully
matched the route with name 'default ' and template ' {controller=Home}/
{action=Index}/{id?} '.
Microsoft.AspNetCore.Mvc.Internal . ControllerActioninvoker:Debug :
Executing action
ConfiguringApps . Control l ers . HomeController . Index (ConfiguringApps)
Microsoft . AspNetCore . Mvc . Internal . ControllerActioninvoker : Inforrnation :
Executi ng actio n
rnethod ConfiguringApps . Controlle r s . HomeController . Index
(ConfiguringApps) with arg uments ()
- Mode lSt ate is Valid
Microsoft.AspNetCore . Mvc. In ternal .C ont r ollerActioninvoker : Debug :
Executed action method
ConfiguringApps . Controllers.HomeController . Index (ConfiguringApps) ,
returned result
Microsoft . AspNetCore . Mvc . ViewResult .
Microsoft.AspNetCore.Mvc.Razor.RazorViewEngine:Debug: View lookup
cache hit for view ' Index ' in contro ller ' Ноте' .
Microsoft.AspNetCore . Mvc . ViewFeatures.Internal.
ViewResultExecutor:Debu g : The view ' Index ' was found .
Microsoft.AspNetCore . Mvc . ViewFeatures.Internal .
Глава 14. Конфигурирование прилож ений 407
ViewResultExecutor : Information : Executing
ViewResult , running view at path /Views/Home/Index . cshtml.
Microsoft . AspNetCore . Mvc .I nte r nal . ControllerActioninvoker : Information : Е
xecuted action
ConfiguringApps.Controllers . HomeController . Index (ConfiguringApps)
in 8 . 9685ms
Microsoft . AspNetCore . Hosting . Inte r nal.WebHost : Information : Request
finished in 16 . BBбms 200 text/html ; c harset= u tf - 8
Microsoft . AspNetCore . Serve r. Kestrel : Debug : Connection id
" OHKTSDOEU8D4U " completed keep alive response .
Microsoft . AspNetCore . Hosting . Internal . WebHost : Information : Request
starting НТ Т Р/1 . 1 GET
http : //localhost : SOOO/liЬ/bootstrap/dist/css/bootstrap - theme . min . css
Microsoft . AspNetCore . Builder.Route r Mi ddleware : Debug : Request did not
match any routes .
Microsoft . Asp NetCore . Host ing.Interna l. WebHost : Information: Req u est
starting НТТР/1 . 1 GET
http : //localhost : SOOO/l i Ь/bootstrap/dist/css/bootstrap . min . css
Microsoft . AspNetCore . Hosting . Interna l. WebHost : Information : Request
fin i shed in 48 . 4602ms 404
Microsoft . AspNetCore . Builder . Route r Middleware : Debug : Request did not
match any routes .
Microsoft . AspNetCore . Server . Kestrel : Debug : Connection id
" 0HKT5DOEU8D4V " completed keep al i ve r esponse .
Microsoft . AspNetCore . Hosting . Inte r na l. WebHost : I n format i on : Request
finished in 85 . 0848ms 404
Microsoft . AspNetCore.Server . Kestrel : Debug : Connection id
" OHKT5DOEU8D50 " comp l eted ke ep alive r esponse .

Создание специальных журнальных записей


Хотя журнальные записи, создаваемые встроенны ми компонентами ASP.NET Core
и МVС , очень пол е зны, вы можете пр едоставить более точные сведения о том, к ак ра­
ботает прилож е ние , создав ая собственные журн ал ьные зап и си . Запись журнального
сообще ния осуществляется в два этапа: получение объекта р егистр ат ора в журнале и
записыван ие сообще ния. В листинг е 14.27 прив еден контролле р Home с подде ржкой
р егистрации в журнал е .

Листинг 14.27. Создание специальных журнальных записей в файле


HomeController.cs
using System . Collections . Gener i c ;
using Microsoft.AspNetCore . Mvc ;
using ConfiguringApps . Infrastructure ;
using Microsoft.Extensions.Logging;
namespace ConfiguringApps . Controllers
puЫic class HomeController : Contro l ler
private UptimeService uptime ;
private ILogger<HomeController> logger;
puЫic HomeController(UptimeService up, ILogger<HomeController> log) {
uptime up ;
logger = log;
408 Часть 11. Подробные сведения об инфраструктуре ASP.NEТ Core MVC

puЬlic ViewResul t Index () {


logger.LogDeЬug($"Handled {Request.Path} at uptime {uptime.Uptime}");
return View(new Dictionary<string, string>
["Message " ] = " This is the Index act ion",
[ " Uptirne "] = $ " {uptirne . Uptirne}rns "
}) ;

Интерфейс ILogger определяет функциональность, требуемую для создания жур­


нальных записей и получения объекта, который реализует этот интерфейс, а класс
HorneController имеет аргумент конструктора с типом ILogger<HorneController> .
Параметр типа позволяет системе регистрации в журнале использовать имя класса в
журнальных сообщениях, причем значение для аргумента конструктора предостав­
ляется автоматически через средство внедрения зависимостей, которое рассматри­
вается в главе 18 .
Имея объект реализации ILogger, можно создавать журнальные сообщения с при­
менением расширяющих методов, определенных в пространстве имен Microsoft.
Extensions . Logging. Методы предусмотрены для всех уровней регистрации, пере­
численных в табл. 14.11. Класс HomeController использует метод LogDebug () для
создания сообщения на уровне Debug. Чтобы увидеть результат, запустите приложе­
ние с применением отладчика Visual Studio и поищите в окне Output журнальное со­
общение вроде следующего:

ConfiguringApps.Controllers.HorneController : Debug: Handled /


at uptirne 19326
Когда приложение запус1<ается, в окне Output появляется много сообщений , что
может затруднить восприятие индивидуальных сообщений. Одиночные сообщения
легче различать, если щелкнуть на кнопке Clear All (Очистить все) в верхней части
окна Output и затем перезагрузить страницу в браузере - это гарантирует отображе­
ние только журнальных сообщений, которые относятся к отдельному запросу.

Добавление оставшихся компонентов


промежуточного программного обеспечения
Существует набор наиболее часто используемых компонентов промежуточного
ПО, которые полезны в большинстве проектов MVC и применяются в примерах, рас­
сматриваемых в настоящей книге. В последующих разделах по мере добавления этих
компонентов в конвейер запросов будет объясняться их работа.

Обеспечение возможности обработки исключений


Даже самое тщательно написанное приложение будет сталкиваться с исключения­
ми, по этому важно их должным образом обрабатывать. В листинге 14.28 демонстри­
руется добавление в конвейер запросов компонентов промежуточного ПО, которые
имеют дело с исключениями.
Глава 14. Конфигурирование приложений 409
Листинг 14.28. Добавление компонентов промежуточного ПО для обработки
исключений в файле s tartup. cs
using Microsoft . AspNetCore.Builder ;
using Microsoft . AspNetCore . Hosting ;
using Microsoft . AspNetCore . Http ;
using Microsof t.Extensions.Dependencyinjection;
using Microsoft .E xtens i ons. Logging ;
using Configuri ngApps . Infrastructure ;
namespace Conf ig uringApps
puЫic class Startup {
puЫic void Conf i gureServices(IServiceCo ll ect i on services) {
services.AddSingleton<UptimeServi ce>() ;
services . AddMvc() ;

puЬlic void Configure(IApplicationBui lder арр , IHostingEnvironment env ,


ILoggerFactory loggerFactory )
loggerFactory.AddConsole(LogLeve l . Debug);
logge r Factory . AddDebug(LogLevel.Debug);
if (env . IsDeve l opment()) {
app . UseDeveloperExceptionPage();
app.UseStatusCodePages();
else {
app . UseExceptionHandler("/Home/Error");

app . UseMvc(rou tes =>


routes . MapRoute(
name : "default ",
temp l ate: " {co ntroller=Home} /{act i on=Index }/ {id?}");
}) ;

Метод UseStatusCodePages () добавляет к ответам , не имеющим содержимого,


описательные сообщения, такие как 404 - Not Found (404 - не найдено) , которое мо­
жет быть удобным. по скольку не все браузеры отображают пользователю собствен ные
сообщения.
Метод UseDeveloperExceptionPage () настраивает компонент промежуточного
ПО для обработки ошибок, который отображает детали исключения в ответе, в том
числе связанную трассировочную информацию. Такая информация не должна быть
видимой польз ователям. поэтому вызов UseDeve loperExcep tionPag e () делается
только в среде разработки, которая опр еделяется с помощью объекта реализации
IHostingEn vi ronnunent.
В подготовительной или производств е нной среде взамен используется метод
UseExcept i onHandler () .Он настраивает обработку ошибок, позволяющую отобра­
жать специальное сообщение об ошибке , которое не раскрывает особенности внут­
р ен ней работы приложения. Аргументом метода UseExceptionHandler () является
URL. на который должен быть перенаправлен клиент, чтобы получить сообщение об
41 О Часть 11. Подробные сведения об инфраструктуре ASP.NET Саге MVC

ошибке . Это может быть любой URL. предоставляемый приложением, но по соглаше­


нию применяется / Home/Error .
В листинге 14.29 добавлена возможность генерации исключ ений по требованию
действия Index контроллера Home и определено действие Error, таr< что запросы,
генерируемые UseExceptionHandler () ,могут быть обработаны .

Листинг 14.29. Генерация и обработка исключений в файле HomeController. cs


using Sys t em.Collections . Generic ;
using Microsoft.AspNetCore . Mvc ;
using ConfiguringApps . Infrastructure ;
using Microsoft.Extensions.Logging;
namespace ConfiguringApps . Controllers
pub'lic class HomeController : Controller
private UptimeService uptime ;
private ILogger<HomeController> logger ;
puЫic HomeController(UptimeService up , ILogger<HomeContr oller> log)

uptime up ;
logger log ;

puЬlic ViewResult Index(bool throwException = false)


if (throwException) {
throw new System.NullReferenceException();

logger . LogDebug($ "Handled {Request.Path} at up t ime {uptime.Uptime} " ) ;


ret ur n View (new Dictionary<string , string>
["Message"] = "This is the Index action ",
[ " Uptime " ] = $ " {upt ime.Uptime}ms"
}) ;

puЬlic ViewResult Error()


return View("Index", new Dictionary<string, string> {
[ "Message"] = "This is the Error action"
}) ;

Внесенные в действие Index изменения с помощью средства привязки моделей,


которое обсуждается в главе 26, обеспечивают получение значенияthrowException
из запроса. Действие генерирует исключение NullReferenceException, если
значение throwException равно true, и выполня ется нормально, если значение
throwException равно fa l se .
Действие Error использует представл е ние Index для отображения про­
стого сообщения. Запустив приложение и запросив URL вида /Home/Index?
throwException =true , можно просмотреть результаты функционирования разных
компонентов промежуточного ПО для обработки исключений. Строка запроса пре­
доставляет значение для аргумента действия Index, а наблюдаемый ответ будет
Глава 14. Конфигурирование приложений 411
зависеть от имени среды размещения. На рис. 14.12 показан вывод, порождаемый
методом UseDeveloperExceptionPage () (для среды размещения Development) и
методом UseExceptionHandler () (для всех остальных сред размещения).

Q rr.tc~! S1t1W1frl'Q1'

~ (- с [р~~!~;!5~~~-;~;~~~~~~.;:~~--~:=-==-~-=~:~::-----------·--1 . ~
An unhandled exceptioп occшrecl while pюcessing t/1e r D """'' - - - - · - - - - ___________ -,
1 . •
~ f\:uliRerereгceExc ep:юn: O~ec t
. , . • J ~ ·, С 1(!) l~fllho$t;5000/Нoin•/1Mi'"?thrc.\\(11.Cf"P!>Ol'l- lrut
reference not Set to an instance 011 an сЬ;есч .. -- ~~--.:;---:=.::.--,:_-. .::-"::-~:---:-:;:-::--·- - - - - - ·
'(( 1
:.:::.=-:-.·:.;с;-:_,_···-::~.;:-.:' - rf
! Ccinfi9ur1r9.Gpps.U)ntro!lf!'S Homl!-ContФ\ler!nde:ir(8ooJtJn 1rtrowE~ci-pt1cn) 1n ~~Controlltr -'~· 11\ :-irм~~I" Thic is tlie Bn-oi· nctio" '.'

' ШJ О""'У Cook1e> Headois I___ ---"-·-··--]- ~~--~~~_____J


; Nt1HRefereoceExcep~ion: Object refer·erкe nGt set ю an юstance of an object.
Сс1' ~·_, 1:-~Лщ~: :;1;;r,·r.• :1c:rs HJ1·~eec"·шc:~cr:r.cг~ ВО:.~;.>.:'"1t'"''·)". ~ \ (€;)'.{"Jl'\J1n lk>ir>eControll~r . cs

; _,J..'·~~~~$·~~s,~~tpti7~t~)~#.,,..

Рис. 14. 12. Обработка исключений в среде разработки


и подготовительной/производственной среде

Страница исключения для разработчика предлагает детали исключения. а также


возможность исследовать его информацию трассировки стека и запрос, который при­
вел к исключению. В противоположность этому страница исключения для пользова­
теля должна применяться просто для указания, что что-то пошло не так.

Включение средства Browser Link


Средство Browser Link (Ссьmка на браузер) было описано в главе 6 и предназна­
чено для управления браузерами на стадии разработки. Серверная часть средства
Browser Link реализована как компонент промежуточного ПО, который должен быть
добавлен к классу Startup в качестве части 1юнфигурации приложения, потому что
без нее интеграция с Visual Studio работать не будет. Средство Browser Link полезно
только на стадии разработки и не должно использоваться в подготовительной или
производственной среде. поскольку оно модифицирует ответы, генерируемые дру­
гими компонентами промежуточного ПО, с целью вставки кода JavaScript. который
открывает Н1ТР-подключения с серверной стороной, чтобы она могла получать уве­
домления о перезагрузке страницы. В листинге 14.30 показано, как вызывать метод
UseBrowserLink (), который регистрирует компонент промежуточного ПО только
для среды размещения Development.

Листинг 14.30. Включение средства Browser Link в файле Startup. cs


using Microsoft . AspNetCore.Builder;
using Microsoft.AspNetCore .Hosting;
using Mic rosoft.AspNetCore.Http;
using Microsoft .Extensions . Dependencyinjection;
using Microsoft.Extensions.Logging;
using ConfiguringApps.Infrastructure;
namespace ConfiguringApps {
puЬlic cla ss Startup {
puЫic void ConfigureServices(IServiceCollection services) {
services.AddSingleton<UptimeService>();
services.AddMvc();
412 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Соге MVC

puЬlic void Configure(IApplicationBuilder арр , IHostingEnvironment env ,


ILoggerFactory loggerFactory) {
loggerFactory.AddConsole(Log Leve l.Debug) ;
loggerFactory . AddDebug(LogLevel . Debug) ;
if (env . Isoevelopment()) (
app.UseDeveloperExceptionPage() ;
app . UseStatusCodePages() ;
app.UseBrowserLink();
else {
app . UseExceptionHandler ( " /Home/Error " ) ;

app . OseMvc(routes =>


ro ut es.MapRoute(
пате : " default ",
template: " {cont r olle r=H ome}/{action = Index}/{id?} " ) ;
)) ;

Включение статического содержимого


Финальный компонент промежуточного ПО, полезный для большинства проектов,
предоставляет доступ к файлам в папке wwwroot , так что приложения могут включать
изображения, файлы JavaScript и таблицы стилей CSS. Метод UseSt ati cFiles ()
добавляет компонент, который обходит конвейер запросов для статических файлов
(листинг 14.31).

Листинг 14.31. Включение статического содержимого в файле Startup. cs


using Microsoft . AspNetCore . Bui l der ;
using Microsoft.AspNetCo re.Hosting;
using Microsoft . AspNetCore.Http ;
usin g Microsoft.Extensions . Dependencyin je ction ;
usi ng Microsoft . Exte n sions .Logging ;
usin g Configuri ngApps .In frastructure ;
namespace Conf iguringApps
puЫic class Startup {
puЫic void Configu r eServices(IServiceCol l ection services) {
services . AddSingleton<Opt imeService>() ;
services . AddMvc() ;

puЬlic void Configure(IApplicationBuilder арр , IHostingEnvironment епv ,


ILoggerFactory loggerFactory)
loggerFactory.AddConsole(LogLevel.Debug);
loggerFa ctory .AddDebug(LogLevel . Debug) ;
if (env .IsDeve l opment()) (
app .U seDeveloper Ex cept i onPage() ;
app . UseStatusCode Pages() ;
app . UseBrow serLink();
Глава 14. Конфигурирование прилож ений 41 З
e l se {
app . UseExceptionHa ndler( " / Home /E r ror " ) ;

app.UseStaticFiles();
app . UseMvc(routes => {
routes.MapRoute(
name : "defau l t ",
template : " {cont r olle r =Home }/{act i on=Index}/{ i d?} " ) ;
}) ;

Статическое содержимое обычно требуется вне зависимости от среды размеще­


ния, из-за чего метод UseStaticF i l es () вызывается для всех сред. Это означает,
что элемент link в представлении I n d ex будет надлежаще работать и позволит бра­
узеру загруз ить таблицу стилей CSS из Bootstrap. Запустив приложе ние, можно уви­
деть результат (рис . 14. 13).

С) Re~ vlt х

Message This is the lndex action


Uptime 28087ms

' - - - - - -----------·---------------'
Рис. 14.13. Включение статического содержимого

Использование данных конфигурации


Конфигурацию в классе Sta r tup можно дополнять внеrшшми конфигурационными
данными , которые позволяют конфигурации изменяться без необходимости в модифи­
кации кода внутри класса Star t up . Соглашение предусматривает применение конструк­
тора класса Startup для загрузки конфигурационных данных, по этому они могут быть
досrупны в методах ConfigureServices () и Con f igure () ,когда они вызываются.

Конфигурационные данные могут храниться в файлах JSON или ХМL, читаться из


командной строки или пр едоставляться через переменные среды. Формат JSON явля­
ется предпочтительным для новых проектов ASP.NET Core, а соглашени е заключается
в том, чтобы начать с файла по имени a pps et t ings . j son. В целях демонстрации
добавьте в проект файл appsett ing s. j s o n с использованием шаблона элемента
ASP.NET Configuration File (Файл конфигурации ASP.NET) и поместите в него настрой­
ки, приведенные в листинге 14.32.

Совет. Соглашение предполагает добавление конфигурационных данных в файл


appsetting s. j s o n , но вы можете также создавать и пользоватьсs~ новыми файлами
конфигурации, если располагаете достаточным объемом данных длs~ того, чтобы их хра­
нение в одном файле затрудняло управление ими.
414 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

Листинг 14.32. Содержимое файла appsettings. j son

" Logging ": {


" IncludeScopes ": false,
"LogLevel ": {
" Default ": "Deb ug ",
"S ystem ": " Information ",
"Microsoft ": " Information "

}'
" ShortCircuitMiddl eware" :
" EnaЬleBrowserShortCircuit ": true

Конфигурационные данные ASP.NET Core состоят из пар "ключ-значение" , кото­


рые могут группироваться в разделы . В файле appsettings. j son, показанном в
листинге 14.32, им еется раздел Logging, который содержит одну пару "ключ-зн аче ­
ние" (IncludeScopes - ключ. false - значение) и один раздел LogLevel . В свою
очередь раздел LogLeve l содержит три пары "ключ-значение" с ключами Defaul t.
System и Microsoft. Есть таюке раздел под названием ShortCircuitMiddleware,
который содержит единственный ключ EnaЫeBrowserShortCircui t.

Чтение конфигурационных данных


Инфраструктура ASP.NET Core предоставляет набор пакетов NuGet, которые при­
меняются для чтения конфитурационных данных из разнообразных источников и
описаны в табл. 14.12. Каждый пакет предлагает расширяющий метод, предн азна­
ченный для чтения конфигурационных данных .

Таблица 14.12. Пакеты NuGet для чтения конфигурационных данных

Имя Описание

Microsoft.Extensions. Этот пакет предоставляет основную поддержку конфигура­


Configuration ционных данных и может использоваться для определения

настроек программным образом с применением метода


AddinMemoryCollection()
Microsoft . Extensions . Этот пакет используется для чтения конфигурацион­
Configuration.Json ных данных из файлов JSON с применением метода
AddJsonFile ()
Micro soft . Extensions . Этот пакет используется для чтения конфигурационных
Configuration . CommandLine данных из командной строки с применением метода
AddCommandLine ( )
Microsoft.Extensions. Этот пакет используется для чтения конфигурационных
Configurat i on. данных из переменных среды с применением метода
EnvironmentVariaЬles AddEnvironmentVariaЫes()

Microsoft . Extensions . Этот пакет используется для чтения конфигурационных дан­


Configuration.Ini ных из файлов INI с применением метода AddiniFile ()
Microsoft . Extensions . Этот пакет используется для чтения конфигурационных дан­
Configuration.Xml ных из файлов ХМL с применением метода AddXmlFile ()
Глава 14. К онфигурирование прилож ений 415
Чтобы загрузить конфигурационны е данные из ф айл а appsetting . j son , не об­
ходимо добавить в ф айл proj ec t . j s on основной пакет для чтения конфигурацион ­
ных данных и пакет JSON , как показано в листинге 14.33.

Листинг 14.33. Добавление пакетов в файле package. j son

"dependencies " :
"Microsoft . NETCore . App 11
:

"version ": " 1 . 0 . О ",


"type ": 11
platform "
} 1

"Microsoft . AspNetCore . Diagnostics ": " 1 . 0.0 ",


"Microsoft . AspNetCore . Server . IISintegration " 1 . 0 . О ",
11
:

"Microsoft . AspNetCore . Server . Kestrel ": "1. О . О ",


"Microsoft . Extensions . Logg i ng . Console ": " 1 . О. О ",
"Micr6soft . Extensions . Logg i ng . Debug 11 : 11 1.0 . 0 11 ,
"Microsoft . AspNetCore . StaticFiles ": 1 . О . О ", 11

"Microsoft . AspNetCore . Mvc ": 11 1. О. О 11 ,


"Microsoft . VisualS t udio . Web . BrowserLink . Loader ": " 14 . 0 . 0 11 ,
"Microsoft . AspNetCore . Razor.Tools ":
11
version ": " l . 0 . 0- preview2 - final 11 1
" type ": "build "
}'
"Мicrosoft.Extensions.Configuration 11 :11
1.0.0 11 ,
"Microsoft.Extensions.Configuration . Json 11 : 11 1.0.0 11
}'

Чтобы прочитать содержимое файла appse tt ings . j son , в класс Start up добав­
л е н кон структор , в которо м с помощью м етода AddJsonFi l e () осуществля ется чте­
ние ф айла appset tings . j son (листи нг 14.34).

Листинг 14.34. Чтение конфигурационных данных в файле Startup. cs

using Microsoft . AspNetCore . Builder ;


using Microsoft . AspNetCore .Hosting ;
using Microsoft . AspNetCore . Http ;
using Microsoft . Extensions . Dependencyinjection ;
using Microsoft . Extensions . Logging ;
using ConfiguringApps . Infrastructure ;
using Microsoft.Extensions.Configuration;
namespace ConfiguringApps
puЫic cla ss Startup {
puЬlic Startup(IHostingEnvironment env)
Configuration = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile( 11 appsettings.json 11 )
.Build();

puЬlic IConfigurationRoot Configuration { get; set; }


416 Часть 11. Подробные сведения об инфрастру ктуре ASP.NET Core MVC

puЫic void ConfigureServices(IServiceCollection services)


services.AddSingleton<UptimeService>();
services.AddMvc();
}
puЬlic void Configure(IApplicationBui l der арр , IHostingEnvironment env ,
ILoggerFactory loggerFactory) {
11 .. . для краткости остальные операторы не показаны ...

Целью конструктора является установка значения свойства Configuration, воз­


вращающего объект, который реализует интерфейс IConfigurationRoot, обеспе­
чивающий доступ к конфигурационным данным. Интерфейс IConfigurationRoot
представляет точку входа в конфигурационные данные и является производны м от
интерфейса IConfiguration, который также используется для представления инди­
видуальных разделов конфигурации.
Конструктор Startup может принимать специальные службы, описанны е в
табл. 14.7. Служба
IHostingEnvironrnent требуется, когда конфигурационные дан­
ные загружаются, потому что свойство ContentRootPath пр едоставля ет доступ к ка­
талогу, который содержит файл appsettings . j son.
Процесс загрузки конфигурационных данных состоит из трех шагов. Первый шаг
заключается в создании нового объекта ConfigurationBuilder. Второй шаг предус­
матривает загрузку данных из индивидуальных источников с применением рас ши­

ряющих методов, таких как AddJsonFile (). Третий шаг предполагает вызов метода
Build () ConfigurationBuilder, который создает структуру пар "'клю ч­
на объекте
значение" и разделы , после чего присваивает результат свойству Configuration.
Доступно несколько в ерсий метода AddJsonFile (), описанных в табл. 14.13. Вьпuе
использовалась простейшая версия метода AddJsonFile (), которая сгенерирует ис­
ключение, если файл не существует, и будет игнорировать любые изменения в файл е .

Повторная загрузка конфигурационных данных

Система конфигурации ASP.NET Core подцерживает повторную загруз ку данных , когда фай­
лы конфигурации изменяются . Некоторые встроенные компоненты промежуточного ПО , та­
кие как система регистрации в журнале, подцерживают эту функциональность, что означает
возможность изменения уровней регистрации во время выполнения без необходимости в
перезапуске приложения. Аналогичные средства можно также предусмотреть при разработ­
ке специальных компонентов промежуточного ПО.
Однако просто тот факт, что средство делает что-то возможным, вовсе не означает, что оно
является целесообразным . Внесение изменений в файлы конфигурации производственных
систем - верный способ вызвать простой из-за отказа . Слишком легко допустить ошибку
при модификации и получить неправильно работающую конфигурацию . Даже при успешном
изменении могут возникать непредвиденные последствия, такие как переполнение дисков

регистрационными данными или резкое снижение производительности .

Рекомендуется избегать динамического редактирования и удостоверяться, что все измене ­


ния прошли через стандартные процедуры разработки и тестирования в подготовительной
среде, прежде чем развертывать их в производственной среде. Может возникнуть соблазн
заняться модификацией активной системы, чтобы установить причину проблемы, но это
редко заканчивается хорошо. Если вы обнаруживаете, что занимаетесь редактированием
файлов конфигурации производственной системы, то должны задаться вопросом, готовы
ли вы превратить небольшую проблему в гораздо более крупную.
Глава 14. Конфигурирование прилож ений 417
Таблица 14.1 З. Вер сии метода AddJsonFile ()

Метод Описание

AddJsonFile(name , Этот метод загружает данные из указанного файла. Если файл не


optiona l, reload) существует и значение аргумента optional равно false , тог­
да генерируется исключение . Если значение аргумента reload
равно true, то конфигурационные данные будут обновляться в
случае изменения файла JSON
AddJsonFile(name , Эквивалентно вызову AddJsonFile (name , optional , false)
optional)
AddJsonFile(name) Эквивалентно вызову AddJsonFile (name , false , false)

Работа с данными конфигурации

Данные из файла
appsettings . j son доступны через свой ств о Configuration,
добавленное в класс
Startup , которо е во зв раща ет объект, реализующий интерфейс
IConfigurationRoot. Доступ к значениям данных производится посредством ком­
бинации членов , определенных упомянутым интерфейсом, и расширяющих методов
из табл. 14.14.

Таблица 14.14. Члены и расширяющие методы для интерфейса IConfigura tionRoot

Имя Описание

[key ] Этот индексатор применяется для получения строкового


значения, соответствующего указанному ключу key
GetSection(name) Этот метод возвращает объект реализации IConfiguration ,
который представляет раздел конфигурационных данных

GetChildren () Этот метод возвращает перечисление объектов реализации


IConfiguration, которые представляют подразделы те-
кущего объекта конфигурации

GetReloadToken () Этот метод возвращает объект реализации IChangeToken,


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

Reload () Этот метод принудительно перезагруж ает конфигурацион­


ные данные

GetConnectionString (name) Этот метод эквивалентен вызову


GetSection ( " ConnectionStrings") [name]

Ч то бы получить значение, понадобится пройти по структур е данных до требу­


ем ого раздела конфигура ции, представленного с помощью объекта , который реа­
лизует интерфейс
IConfigura tion , предоставляющий подмножество доступных для
IConfigurationRoot членов (табл. 14.15).
В листинге 14.35 осущест вля ется перемещение по данным для нахождения
раздела конфигурации ShortCircuitMiddleнare и получ ения знач е ния настройки
EnaЫeBrowserShortCircui t, чтобы выяснить, добавлять ли специальные компо­
ненты пром ежуточно го ПО в конвейер запросов.
418 Часть 11. Подробные сведения об инфр а структуре ASP. NET Core MVC

Таблица 14.15. Члены, определенные интерфейсом IConfigura tion


Имя Описание

[key] Этот индексатор применяется для получения строкового значения ,


соответствующего указанному ключу key
GetSection(name) Этот метод возвращает объект реализации IConfiguration ,
который представляет раздел конфигурационных данных

GetChildren () Этот метод возвращает перечисление объектов реализации


IConfiguration, которые представляют подразделы текущего
объекта конфигурации

Листинг 14.35. Использование данных конфигурации в файле Startup. cs

puЫic void Configure(IApplicationBuilder арр , IHostingEnvironment env ,


ILoggerFactory loggerFactory) {
if (Configuration.GetSection("ShortCircuitмiddleware 11 )
? [ 11 EnaЫeBrowserShortCircuit 11 ] == 11
True") {
app.UseMiddleware<BrowserTypeМiddleware>();
app.UseMiddleware<ShortCircuitмiddleware>();

loggerFactory.AddConsole(LogLevel.Debug);
loggerFactory.AddDebug(LogLevel . Debug);
if (env . IsDevelopment()) {
app.OseDeveloperExceptionPage();
app . OseStatusCodePages() ;
app . OseBrowserLink() ;
else {
app . OseExceptionHandler( " /Home/Error " ) ;

app . OseStaticFiles() ;
app . OseMvc(routes => {
routes.MapRoute(
name : 11 default 11 ,
template : 11 {controller=Home}/{action=Index}/{id?} " );
) ) ;

Использование данных конфигурации для встроенных


компонентов промежуточного программного обеспечения
Некоторые компоненты промежуточного ПО могут настраиваться с прим ен ени е м
конфигурационных данных. Самым распространенным примером является пакет ре­
гистрации в журнале, который позволяет предоставлять уровни регистрации для раз ­
ных компонентов через раздел конфигурации. Раздел logging. включенный в файл
appsettings . j son в листинге 14.32, может использоваться с расширяющими мето­
дами, которые настраивают получат елей журнальных сообщений (листинг 14.36).
Глава 14. Конфигурирование приложе ний 419
Листинг 14.36. Применение конфигурационных данных для настройки регистрации в
журнале в файле Startup. cs

loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug(LogLevel.Debug);

Meтoд AddConsole () перегружендляприемаобъектареализации IConfiguration,


который конфигурирует. какой вывод должен быть послан на консоль. Раздел
Logging /LogLevel файла appsettings . j son используется для фильтрации жур ­
нальных сообщений , отправленных на консоль. Например, в листинге 14.37 показано .
как можно фильтровать сообщения, зарегистрированные классом HorneCont r o ller,
так что будут отображаться только сообщения уровня Cri tical .

На заметку! Такая фильтрация вступает в силу, только когда приложение запускается из ко­
мандной стро ки с применением Kestrel напрямую, как описано во врезке "И спользование
Kestrel напрямую" ранее в главе.

Листинг 14.37. Фильтрация журнальных сообщений, выводимых на консоль,


в файле appsettings. j son

"Logging " : {
"I ncludeScopes ": fa lse,
"LogLevel ": {
"Default ": " Debug",
"System": "Information",
"Microsoft": "Information",
"ConfiguringApps.Controllers.HomeController": "Critical"

}'
"ShortCircuitMiddleware":
" EnaЫeBrowserShortCircuit ": true

Конфигурирование служб MVC


Когда метод AddMvc () вызывается внутри метода ConfigureServices (). он на­
страивает все службы, которые требуются приложениям МVС. Преимущество такого
подхода заключается в удобстве. поскольку все службы МVС регистрируются за один
шаг, но если необходимо изменить стандартное поведение, то понадобится выполнить
дополнительную работу по реконфигурированию служб.
Метод AddMvc () возвращает объект, реализующий интерфейс IMvcBuilder, а
инфраструктура MVC предлагает набор расширяющих методов, предназначенных
для расширенного конфигурирования, наиболее полезные из которых описаны в
табл. 14.16. Многие методы конфигурирования связаны со средствами, рассматрива­
емыми в последующих главах.
420 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC

Таблица 14.16. Полезные расширяющие методы IMvcBuilder

Имя Описание

AddМvcOptions () Этот метод конфигурирует службы, используемые инф­


раструктурой MVC, как объясняется ниже

AddForrnatterMappings() Этот метод применяется для конфигурирования средс­


тва, которое позволяет клиентам указывать формат по­
лучаемых данных (глава 20)
AddJsonOptions( ) Этот метод используется для конфигурирования способа
создания данных JSON (глава 20)
AddRazorOptions() Этот метод применяется для конфигурирования меха­
низма визуализации Razor (глава 21)
AddViewOptions () Этот метод используется для конфигурирования способа
обработки представлений инфраструктурой MVC, в том
числе применяемые механизмы визуализации (глава 21)

Метод AddMvcOptions () конфигурирует самые важные службы MVC. Он прини ­


мает функцию , получающую объект MvcOptions, который предоставля ет набор кон­
фигурационных свойств; наиболее полезные свойства перечислены в табл. 14.17.

Таблица 14.17. Избранные свойства MvcOptions

Имя Описание

Conventions Это свойство возвращает список соглашений модели, ко­


торые используются для настройки способа создания конт­
роллеров и действий инфраструктурой MVC (глава 31)
Fil ters Это свойство возвращает список глобальных фильтров
(глава 19)
ForrnatterMappings Это свойство возвращает сопоставления, применяемые
для того, чтобы позволить клиентам указывать формат по­
лучаемых ими данных (глава 20)
InputForrnatters Это свойство возвращает список объектов, используемых
для разбора данных запросов (глава 20)
ModelBinders Это свойство возвращает список связывателей модели,
которые применяются для разбора запросов (глава 26)
ModelValidatorProviders Это свойство возвращает список объектов , используемых
для проверки достоверности данных (глава 27)
OutputForrnat ters Это свойство возвращает список классов, которые формати­
руют данные, отправляемые из контроллеров АР! (глава 20)
RespectBrowserAcceptHeader Это свойство указывает, должен ли учитываться заголовок
Accept , к огда принимается решение о том , как о й формат
данных задействовать для ответа (глава 20)

Описанные в табл. 14.17 методы конфигурирования используются для точ­


ной настройки способа функционирования инфраструктуры МVС, и в указанных
главах вы найдете подробны е описания связанных с ними средств. Тем не менее.
Глава 14. Конфигурирование приложений 421
в качестве небольшой демонстрации в листинге 14.38 показано, как можно приме­
нять метод AddМvcOptions () для изменения параметра конфигурации.

Листинг 14.38. Изменение параметра конфигурации в файле Startup. cs

puЫic void ConfigureServices(IServiceCollection services) {


services .AddSingle ton<UptimeSe r vice>() ;
services .AddМvc() .AddМvcOptions(options => {
options.RespectBrowserAcceptHeader = true;
}) ;

Лямбда-выражение, передаваемое методу AddMvcOptions () , принимает объект


MvcOptions , который используется для установки свойства RespectBrowserAccept
Header в true . Это изменение позволяет клиентам оказывать большее влияние на
формат данных. выбираемый процессом согласования содержимого, как объясняется
в главе 20.

Работа со сложными конфигурациями


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

Создание разных внешних конфигурационных файлов


Когда вы загружаете конфигурационные данные из внешнего источника, такого
как файл JSON, настройки и значения конфигурации переопределяют любые сущес­
твующие данные с теми же самыми именами. Это означает, что вы объединяете мно­
гочисленные файлы с целью переопределения частей конфигурационных данных для
разных ср ед размещения . Например, в листинге 14.39 приведено содержимое фай­
ла по имени appsettings . development. j son, который был создан с применением
шаблона элемента ASP.NET Configuration File. Конфигурационные данные в этом файле
устанавливают значение EnaЬleBrowserShortCircui t в false .

Совет. Может nоказаться, что файл appsettings . development . j son исчезает сразу же nос­
ле его создания. Щелкнув на стрелке слева от заnиси appsettings . j son в окне Solution
Explorer, вы увидите, что Visual Studio груnnирует вместе элементы с nохожими именами.

Листинг 14.39. Содержимое файла appsettings. development. j son

" ShortCircuitMiddleware ": {


" EnaЫ e BrowserShortCircuit ": false
422 Часть 11. Подробные сведения об инфраструктуре ASP.NET Соге MVC

Чтобы загрузить эти данные, необходимо добавить в конструктор Startup новый


вызов метода AddJsonFile (),включив в имя файла название среды размещения и
удостоверившись в установке аргумента optional в true , так что в случае недоступ­

ности конфигурационного файла, специфичного для среды. исключение возникать не


будет. Требуемые изменения приведены в листинге 14.40.

Листинг 14.40. Загрузка конфигурационного файла, специфичного для среды,


в файле Startup. cs

puЬlic Startup(IHostingEnvironment env) {


Configuration = new ConfigurationBuilder()
.SetBas ePath(env . ContentRootPath)
. AddJsonFile ( "a ppsettings .js on " )
.AddJsonFile($ 11 appsettings.{env.EnvironmentName}.json 11 , true)
. Build() ;

Конфигурационные файлы загружаются в порядке их указания, поэтому настройки


в более поздних файлах переопределяют настройки в файлах, указанных раньше. В ре­
зультате значением EnaЬleBrowserShortCircui t будет false, когда приложение
находится в среде Development, и true - когда в среде Staging и Production.

Внимание! При развертывании приложения вы обязаны обеспечить включение дополнитель­


ных конфигурационных файлов. В главе 12 рассматривался пример включения конфигура­
ционного файла во время развертывания в Azure.

Создание разных методов конфигурирования


Выбор разных файлов с конфигурационными данными может быть удобен, но не
предоставлять полного решения для сложных конфигураций, т.к. файлы данных не
содержат операторы С#. Если вы хотите варьировать операторы конфигурирования,
используемые для создания служб или регистрации компонентов промежуточного
ПО. тогда можете применять разные методы, причем имя метода должно включать
название среды размещения (листинг 14.41).

Листинг 14.41. Использование разных имен методов в файле Startup. cs


using Microsoft . AspNetCore.Builder;
using Microsoft.AspNetCore . Hosting;
using Microsoft . AspNetCore . Http;
using Microsoft . Extensions.Dependencyinjecti on ;
using Microsoft . Extensions .Loggi ng;
us ing ConfiguringApps . Infrastructure;
using Microsoft.Extensions . Configuration ;
namespace ConfiguringApps
puЫ ic c l ass Startup {
puЫic Startup(IHostingEnvironment env) {
Configuration = new ConfigurationBuilder()
Глава 14. Конфигурирование прило ж ений 423
. SetBasePath(env . ContentRootPath)
. AddJsonFile( " appsettings . json " )
. AddJsonFile($ " appsettings . {env .E nvironmentName} .j son" , true)
. Build () ;

puЫic IConfigurationRoot Configuration { get ; set ; }


puЫic void ConfigureServices(IServiceCollection services)
services . AddSingleton<UptimeService>() ;
services.AddMvc() . AddMvcOptions(options => {
options . RespectBrowserAcceptHeader = true ;
}) ;

puЫic void ConfigureDevelopmentServices(IServiceCollection services) {


services.AddSingleton<UptimeService>();
services.AddМvc();

puЬlic void Configure(IApplicationBuilder арр) {


app.UseExceptionHandler("/Home/Error");
app.UseStaticFiles();
app.UseMvc(routes => {
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
}) ;

puЫic void ConfigureDevelopment(IApplicationBuilder арр,


ILoggerFactory loggerFactory) {
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug(LogLevel . Debug);
app.UseDeveloperExceptionPage();
app.UseStatusCodePages();
app.UseBrowserLink();
app.UseStaticFiles();
арр. UseMvc (routes = > {
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
}) ;

Когда инфраструктура ASP.NET Core ищет методы ConfigureService () и


Configure () в классе Startup, она сначала выясняет, имеются ли методы, имена
которых включают название среды размещения. В листинге 14.41 был добавлен ме­
тодConfigureDevelopmentServices (), который в среде Development будет при­
меняться вместо метода ConfigureServices (),и метод ConfigureDevelopment (),

который в среде Development будет использоваться вместо метода Configure () .Вы


можете определить отдельные методы для каждой среды, которую необходимо подде­
рживать, и полагаться на стандартные методы. вызываемые в случае н едосту пно сти
424 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

методов, специфичных для той или иной среды. В рассматриваемом примере это оз­
начает, что методы Configur eServi ces () и Configure () будут применяться для
подготовительной и производственной сред.

Внимание! Стандартные методы не вызываются, если определены методы, специфич­


14.41 инфраструктура ASP.NET Саге не будет
ные для среды . Например, в листинге
вызывать метод
Configure () в среде Developrnent, потому что имеется метод
ConfigureDevelopment (). Другими словами, каждый метод несет ответственность
за полную конфигурацию, требуемую для среды, на которую он ориентирован.

Создание разных классов конфигурирования


Использование разных методов означает, что вы не обязаны применять операторы if
для проверки имени среды размещения, но в результате могут быть получены крупные
классы, которые сами по себе явлmотся проблемой. В случае особенно сложных конфи­
гураций последним движением вперед будет создание собственного конфигурационного
класса для каждой среды размещения . Когда инфраструктура ASP.NEТ Core ищет класс
Startup, она первым делом проверяет. есть ли класс с именем, включающим назва­
ние текущей среды размещения. С этой целью в про ект добавлен файл класса по имени
StartupDevelopment . cs , содержимое которого показано в листинге 14.42.

Листинг 14.42. Содержимое файла StartupDevelopment. cs


using ConfiguringApps .I nfrastructure ;
using Microsoft . AspNetCore . Builder ;
using Microsoft . AspNetCore .Hosting ;
using Microsoft .Extensions . Configurat i on ;
using Microsoft.Extensions.Dependencylnjection;
us in g Microsoft.Extensions.Logging;
namespace ConfiguringApps {
puЫic class StartupDevel opment
puЫ i c StartupDevelopment (IHost ingEnvironment env) {
Configuration = new ConfigurationBuilder()
.SetBasePath(env . ContentRootPath)
.AddJsonFile( "appsettings .js on ")
. AddJsonFile($ "appsettings . {env . EnvironmentName} . json ", true)
. Build() ;

puЫic IConfig urationRoot Configuration { get ; set ; }


puЬlicvoid ConfigureServices(IServiceCollection services)
services . AddSingleton<UptimeService>() ;
services . AddMvc() ;

puЫic void Configure(IApplicationB uilder арр ,


I LoggerFacto r y loggerFactory)
loggerFactory . AddConsole(Conf i gu r ation.GetSect i on( "Logging " ));
loggerFactory . AddDebug(LogLevel . Debug) ;
app . UseDeveloperExceptionPage() ;
app . UseStatusCode Pages() ;
app.UseBrowserLink();
app . UseStaticFiles() ;
Глава 14. Конфи гурирование при л о жений 425
app .UseMvc(rou t e s =>
route s. MapRou t e (
narne : "defau l t ",
ternp l ate : " {contr ol le r=H orne }/ {acti on=Inde x }/{ i d ?}" ) ;
}) ;

Класс Sta r tupDev el o p me n t содержит методы Co n fig ure Serv i c e s () и


Confi gu re () , которые специфичны для среды размещения De v el opment. Чтобы
предоставить инфраструктуре ASP.NEТ Core возможность найти класс St a rtup , спе­
цифичный для среды , в класс Prog r am потребуется внести изменение, показанное в
листинге 14 .43 .

Листинг 14.43. Включение класса Startup, специфичного для среды,


в файле Program . cs

using Systern . I O;
us ing Mi crosoft .AspNet Core.Hosting ;
namespac e Config u ringApps
puЫic c l ass Pr ograrn {
p uЫi c s t a ti c v oid Ma in (string[] a r gs) {
var host = new WebHo s tBui lde r ( )
. UseKe strel()
. UseConte n tRoot( Directo r y . Ge t CurrentDi re c t ory())
. Use IISin t e gra tion ( )
. UseStartup( " Co nfiguringApps")
. Build () ;
host . Ru n () ;

Вместо ук азания специфичного класса методу Us e St ar t up () передается имя


сборки, которая должна использоваться. Когда приложение запускается, инфраструк­
тура ASP. NEТ Core будет искать класс, имя которого включает названи е среды разме­
ще ния, такой как Star tupDevelopment или StartupP r oduction, и возвращаться к
применению класса Sta rtup, если специфичный для среды класс отсутствует.

Рез юме
В настояще й главе рассматривались способы конфигурирования приложений MVC.
Были описаны конфигурационные файлы J SON, объяснены особенности использо­
вания класса St artup и дано введение в службы и промежуточное ПО. Вы узнали,
как обрабатыв аются запросы с применением конвейера. и каким образом различные
типы промежуточного ПО используются для управления потоком запросов и ответов.
В следующей главе будет представлена система маршрутизации, посредством кото­
рой инфраструктура MVC имеет дело с отображением URL запросов на контроллеры
и действия .
ГЛАВА 15
Маршрутизация URL

в
ранних версиях ASP.NET предполагалось наличие прямой связи между запра­
шиваемыми URL и файлами на жестком диске сервера. Работа сервера заключа­
лась в получении запроса от браузера и доставке вывода из соответствующего файла.
Подобный подход успешно работал для инфраструктуры Web Forms, в которой каждая
страница ASPX представляла собой и файл, и самодостаточный ответ на запрос .
Это совершенно не имеет смысла в приложении MVC, где запросы обрабатывают­
ся методами действий из классов контроллеров, и однозначное соответствие между
запросами и файлами на диске отсутствует.
Для обработки URL в MVC платформа ASP.NET применяет систему маршрутиза­
ции, которая была модернизирована для инфраструктуры ASP.NEТ Core. В настоящей
главе вы узнаете, как использовать систему маршрутизации для обеспечения мощной
и гибкой обработки URL в своих проектах . Вы увидите, что система маршрутизации
позволяет создавать любой желаемый шаблон URL и выражать его в ясной и лаконич­
ной манере. Система маршрутизации выполняет две функции.

• Исследование входящих URL и выбор контроллера и действия для обработки


запроса.

• Генерация исходящих URL. Это URL, которые появляются в НТМL-разметке, ви­


зуализированной из представлений, так что специфическое действие может
быть инициировано, когда пользователь щелкает на ссылке (после чего ссылка
снова становится входящим URL) .
В главе внимание будет сосредоточено на определении маршрутов и их примене­
нии для обработки входящих URL, чтобы пользователь моr достичь контроллеров и
действий. Есть два способа создания маршрутов в приложении МVС: маршрутизация
на основе соглашений и маршрутизация с помощью атрибутов . Здесь мы рассмот­
рим оба подхода.
Затем в следующей главе будет показано, как использовать те же самые маршру­
ты для генерации исходящих URL, которые необходимо включать в представления, а
также объяснены способы настройки системы маршрутизации и продемонстрирова­
но применение связанного средства под названием области. В табл. 15.1 приведена
сводка, позволяющая поместить маршрутизацию в контекст.
Глава 15. Маршрутизация URL 427

Таблица 15.1. Помещение маршрутизации в контекст

Вопрос Ответ

Что это такое? Система маршрутизации несет ответственность за обработку вхо­


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

дящие URL
Чем она полезна? Система маршрутизации обеспечивает гибкую обработку запросов
без привязки URL к структуре классов в проекте Visual Studio

Как она Сопоставление URL с контроллерами и методами действий опре­


используется? деляется в файле Startup. cs или путем применения атрибута
Route к контроллерам

Существуют ли какие­ Конфигурация маршрутизации для сложного приложения может


то с к рытые ловушки стать трудной в управлении
или ограничения?

Существуют ли Нет. Система маршрутизации - это неотъемлемая часть ASP.NET


альтернативы? Core

Изменилась ли она по Система маршрутизации работает в значительной степени тем же


сравнению с версией самым образом, как в предшествующих версиях инфраструктуры, но
MVC 5? с изменениями, которые отражают более тесную интеграцию с плат­
формой ASP.NET Core .
Маршруты , основанные на соглашениях, определяются в файле
Startup . cs , а не в теперь уже устаревшем файле RouteConf ig . cs .
Классы маршрутизации теперь определены в пространстве имен
Mi c r osoft . AspNetCore. Rout ing.
Маршруты больше не соответствуют URL, если нет подходящего
контроллера и метода действия в приложении (в предыдущих вер­
сиях MVC маршруты могли соответствовать URL, но по-прежнему
возвращали ошибку 404 - Not Found (404 - не найдено)).

Основанные на соглашениях стандартные значения, необязательные


сегменты и ограничения маршрутов теперь могут быть выра жены
как часть шаблона URL, используя тот же синтаксис, что и при мар­
шрутизации с помощью атрибутов.

Запросы к статическим файлам (таким как изображения, CSS и


JavaScript) теперь обрабатываются выделенным промежуточным ПО
(как было описано в главе 14).
Наконец, изменение способа, которым реализована система марш­
рутизации, затрудняет модульное тестирование маршрутов. Такая
тенденция появилась с введением маршрутизации на основе атри­

бутов в MVC 5, но изолировать систему маршрутизации для модуль­


ного тестирования больше невозможно

В табл. 15.2 представлена сводка для настоящей главы.


428 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

Таблица 15.2. Сводка по главе


Задача Решение Листинг

Отображение между URL и методами Определите маршрут 15.1-15.10


действий

Возможность не указывать сегменты Определите стандартные значения 15.11-15. 13


URL для сегментов маршрута

Сопоставление сегментов URL, ко­ Определите статические сегменты 15.14-15.17


торые не имеют соответствующих

переменных маршрутизации

Передача сегментов URL методам Определите специальные перемен­ 15.18-15.20


действий ные сегментов

Возможность не указывать сегменты Определите необязательные 15.21-15.22


URL, для которых не предусмотрены сегменты
стандартные значения

Определение маршрутов, которые Используйте сегмент общего захвата 15.23-15.24


соответствуют любому количеству
сегментов URL
Ограничение URL, которым может Применяйте ограничения маршрута 15.25-15.34
соответствовать маршрут

Определение маршрута внутри Используйте маршрутизацию 15.35-15.39


контроллера с помощью атрибутов

Подготовка проекта для примера


Для целей этой главы создайте новый проект типа Empty (Пустой) по имени
Ur l sAn dRoutes с применением шаблона ASP.NET Core Web Applicatioп (.NET Core)
(Веб-приложение ASP.NET Core (.NET Core)). Добавьте обязательные пакеты NuGet в
раздел depende nc ie s файла pr o j ec t. j s on и настройте инструментарий Razor в
разделе t ool s , как показано в листинге 15.1. Разделы, которые не нужны для данной
главы, понадобится удалить.

Листинг 15. 1. Добавление пакетов в файле proj ect. j son

"dependencies ": {
"Mi cro s oft . NETCore . App ":
"ve r s i o n": " 1 . 0 . 0 ",
"type ": "pla t f o rm"
} 1

"Mic r osoft .AspNe tCo r e .Diag no s t i c s": " 1 . 0 . 0 ",


"Mi c rosoft . As pN e tCo re.Se rve r . II Si nte gra t ion": " 1 . 0 . 0 ",
"Microsof t. AspNetCo r e . Serv e r . Kest r el": "1.0. 0 ",
"Mi c r os oft .Exte ns i ons.Logging.Cons ole": "1.0. 0 ",
"Microsoft. AspNetCore. Mvc" : "l. О . О" ,
"Microsoft.AspNetCore.StaticFiles": 11 1.0.0 11 ,
"Microsoft.AspNetCore.Razor.Tools":
"version": "l.0.0-preview2-final",
"type" : "build"
}
} 1
Глава 15. Маршрутизация URL 429
" tools ": {
"Microsoft.AspNetCore.Razor.Tools": "1.0.0-preview2-final",
"Microsoft.AspNetCore . Server.IISintegration.Tools": "l. 0 . 0-preview2 -final "
},

"frameworks": {
" netcoreappl.O ":
"imports": [ " dotnet5 . 6", "portaЬle - net45+winB " ]

},

" buildOptions ":


" emitEntryPoint ": true , " preserveCompilationContext ": true

Б листинге 15.2 приведен масс Startup, который конфигурирует средства, пре­


доставляемые добавленными пакетами NuGet.
Листинг 15.2. Содержимое файла Startup. cs

using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Dependencyinjection ;
namespace UrlsAndRoutes
puЫic class Startup {
puЫic void ConfigureServices(IServiceCollection services) {
services.AddМvc();

puЫic void Configure(IApplicationBuilder арр) {


app.UseStatusCodePages();
app.UseDeveloperExceptionPage();
app.UseStaticFiles();
app.UseMvc();

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


Все усилия в настоящей главе снонцентрированы на сопоставление URL запросов
с действиями . Единств енному имеющемуся классу модели передаются детал и о конт­
роллере и методе действия, которые были выбраны для обработки запроса. Создайте
папку Models и добавьте в нее файл класса по имени Resul t. cs с определением,
пока зан ным в листинге 15.3.

Листинг 15.З. Содержимое файла Resul t. cs из папки Models

using System . Collections . Generic ;


namespace UrlsAndRoutes . Models {
puЬlic class Result {
puЫic string Controller { get; set ;
puЫic string Action { get; set ; }
430 Часть 11. Подробные сведения об инфраструктуре ASP. NEТ Core MVC

p u Ы icI Dict i onar y <stri n g , object > Dat a ( g et; }


= new Dict i o n ary<str i ng , object>() ;

Свойства Cont r o l ler и Ac t ion будут использоваться для у1<азания того, как за­
прос был обработан, а словарь Da ta - для хранения других деталей о запросе, выда­
ваемых систе мой маршрутизации.

Создание контроллеров
Для демонстрации работы маршрутизации нам нужны про ст ые контроллеры. Соз­
дайте папку Control lers и добавьте в нее файл класса по им ени HomeController . cs
с содержимым из листинга 15.4.

Листинг 15.4. Содержимое файла HomeController . cs из папки Controllers


u sing Microso ft. AspNetCore . Mvc ;
usi n g Ur l sAn dRo u tes . Mode l s ;
names p ace UrlsAndRo u tes . Co n trollers
p uЫi c class HomeCo n trol l e r : Con tr oll e r

p uЫ i c ViewRe s ul t Index () = > View ( " Res ul t ",


new Result (
Control l e r = nameof(HomeControl l e r ) ,
Ac t ion = n a meof( I ndex)
}) ;

Метод действия Index () , определенный в контроллере Home , вы з ыва ет метод


View ( ) для визуализации представления по имени Resul t (которое определяется в
следующем ра зделе) и передает Resu l t в качестве объекта модели. Свойств а объ е кта
модели устанавливаются с применением операции n ameof ; они будут исполь з оваться
для указания , какие контроллер и метод действия были задействованы для об служ и ­
вания запроса.

Аналогично тому, как было описано выше, добавьте в папку Contro ll ers
файл класса Cu st o merControl l er . cs с определением контроллера Customer
(листинг 15.5).

Листинг 15.5. Содержимое файла Customercontroller.cs из папки Controllers


usi n g Mi crosoft . AspNetCore . Mvc ;
using Ur l sAndRoutes . Models ;
name sp ace Ur l sAndRoutes . Con trol l ers
p u Ы i c cla s s Cu s tomerCont r oller : Control ler

p uЫi c Vi ewRe s u l t I n dex ( ) => Vi ew( " Res ul t ",


n e w Res u lt {
Cont r ol ler = n a meof(CustomerContro l ler} ,
Act i on = n ameo f ( I ndex)
}) ;
Глава 15. Маршрутизация URL 431
puЫic ViewResul t List () => View ( " Resul t" ,
new Result {
Controller = nameof(CustomerController) ,
Action = nameof(List)
) ) ;

Посл едний третий конт роллер определен в файле по имени AdminController . cs


внутри папки Controllers (листинг 15.6), который добавлен так, как демонстриро­
валось ранее.

Листинг 15.6. Содержимое файла AdminController.cs из папки Controllers


using Microsoft . AspNetCore . Mvc;
using UrlsAndRoutes . Models ;
namespace UrlsAndRoutes . Controllers
puЬlic class AdminController : Controller
puЫic ViewResult Index() => View( " Result ",
new Result {
Controller = nameof(AdminController) ,
Action = nameof(Index)
}) ;

Создание представления
Во всех методах действий, определенных в предыдущем разделе, указано представ­
ление Resul t, что позволяет создать одно представление, которое будет разделяться
всеми контроллерами. Создайте папку Views/Shared и добавьте в не е новый файл
представления по имени Resul t. cshtml с содержимым, показанным в листинг е 15.7.
Листинг 15. 7. Содержимое файла Resul t. csh tml
@model Result
@{ Layout = null ;
<IDOCTYPE html>
<html>
<head>
<meta name= " viewport " content="width=device - width " />
<title>Routing</title>
<link rel= "stylesheet " asp- href-include= "lib/bootstrap/dist/css/*.min.css" />
</head>
<body class= "panel-body " >
<tаЫе class= " taЫe taЬle -b ordered taЫe - striped taЬle-condensed">
<tr><th>Controller : </th><td>@Model . Controller</td></tr>
<tr><th>Action : </th><td>@Model . Action</td></tr>
@foreach (string key in Model . Data .Keys) {
<tr><th>@key : </th><td>@Model . Data[key]</td></tr>
}
</tаЫе>
</body>
</html>
432 Часть 11 . Подробные сведения об инфраструктуре ASP.N ET Core MVC

Представление содержит стилизованную с помощью Bootstrap таблицу, которая


отображает свойства из объекта модели. Чтобы добавить Bootstrap в проект, создайте
файл bower. j son, используя шаблон элемента Bower Configuration File (Файл конфи­
rурации Bower), и укажите пакет Bootstrap в разделе dependencies (листинг 15.8).

Листинг 15.8. Добавление пакета Bootstrap в файле bower. j son

"name": "asp.net ",


"priva te": true,
"dependencies ": {
"bootstrap": "3. 3. 6 11

Последний подготовительный шаг предусматривает создание в папке Views файла


_ Viewimports . cshtml, в котором настраиваются встроенные дескрипторные вспо­

могательные классы для применения в представлениях Razor и импортируется про­


странство имен модели (листинг 15.9).

Листинг 15.9. Содержимое файла_Viewimports. cshtml из папки Views

@using UrlsAndRoutes.Models
@addTagHelper * , Microsoft.AspNetCore . Mvc . TagHelpers

Конфигурация в классе Startup не содержит никаких инструкций относительно


того, как инфраструктура MVC должна сопоставлять Н1ТР-запросы с контроллерами
и действиями. После запуска приложения любой запрашиваемый URL будет давать в
результате ответ 404 - Not Found, как показано на рис. 15.1.

Рис. 15. 1. Запуск примера приложения

Введение в шаблоны URL


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

http://mysite.com/Admin/Index
Глава 15. Маршрутизация URL 433
URL могут быть разбиты на сегменты - части URL за исмючением имени хоста и
строки запроса, rюторые отделяются друг от друга символом /.В приведенном выше
примере URL есть два сегмента, как по1<азано на рис. 15.2.

http://mysite.com/Admin/Index
t
Первый
t
Второй
сегмент сегмент

Рис . 15.2. Сегменты в примере URL

Первый сегмент содержит слово Admin, а второй - слово Index . В глазах чело­

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


второй - к действию. Однако, естественно, такое отношение должно быть выражено
с использованием шаблона URL, который может быть воспринят системой маршру­
тизации. Вот шаблон URL, соответствующий примеру URL:
{controller}/{action}
При обработке входящего НТГР-запроса задача системы маршрутизации заключа­
ется в сопоставлении запрошенного URL с шаблоном и извлечении из URL значений
для переменных сегментов, определенных в шаблоне.
Переменные сегментов выражаются с применением фигурных скобок (символов
[ и }). В по1шзанном выше примере шаблона присутствуют две переменные сег ­
ментов с именами controller и action, так что значением переменной сегмента
controller будет Admin, а значением переменной сегмента act i on - Index.
Приложение МVС обычно будет иметь несколько маршрутов, и система маршру­
тизации будет сравнивать входящий URL с шаблоном URL каждого маршрута до тех
пор, пока не найдет совпадение. По умолчанию шаблон URL будет соответствовать
любому URL, который имеет подходящее количество сегментов. Например, шаблон
{controller } / {action} будет соответствовать любому URL с двумя сегментами,
как описано в табл. 15.3.

Таблица 15.3. Сопоставление URL


URL запроса Переменные сегментов

http://mysite.com/Admin/Index controller=Admin action= Index


http: / /mysi te. com/Admin Соответствия нет - сегментов слишком мало

http: / /mysi te . com/Admin/Index/Soccer Соответствия нет - сегментов слишком много

В табл . 15.3 отражены две мючевых линии поведения шаблонов URL.


• Шаблоны URL консервативны в отношении количества сопоставляемых сегмен­
тов. Совпадение будет происходить только для тех URL, которые имеют то же
самое количество сегментов, что и шаблон. Это можно наблюдать во втором и
третьем примерах в таблице.

• Шаблоны URL либеральны в отношении содержимого сопоставляемых сегмен­


тов. Если URL имеет правильное количество сегментов , то шаблон извлечет зна­
чение каждого сегмента для переменной сегмента, каким бы оно ни было.
434 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

Описанные две стандартных линии поведения являются ключом к пониманию


функционирования шаблонов URL. Позже в главе будет показано, как изменить эти
стандартные линии поведения.

Создание и ре г и стр а ци я простого м аршрута


Имеющийся шаблон URL можно использовать для определения маршрута.
Маршруты определяются в файле Sta rt up. cs и передаются в кач е стве аргументов
методу UseMvc (), который применяется для настройки MVC в методе Configure ().
В листинге 15.10 приведен базовый маршрут, отображающий запросы на контролле­
ры в примере приложения.

Листинг 15.1 О. Определение базового маршрута в файле s tartup. cs


us ing Mic r os oft . AspNetCore . Bui l de r;
usi ng Mic r os o f t . Ext en sio ns .Dependen cyi n j ect ion ;
names pace Url s AndRoutes {
puЫ ic c la s s Startup {
pu Ьlic void Con fig ureServi ce s ( I Se r viceCo ll ection services) {
s ervices.AddMvc();

puЬl ic vo i d Configure(IApplicat i onBu il der а р р) {


app . UseStatusCodePages() ;
app . Use Deve l op e rExce p ti on Page ();
app .Us e Stat i cFi les() ;
арр. UseМvc (rou tes => {

routes.МapRoute(name: "default", template: "{controller}/{action}");


}) ;

Маршруты создаются с использованием лямбда-выражения, передаваемого в виде


аргум ента конфигурационному методу UseMvc () . Это выражени е получает объект,
который реализует интерфейс IRout e Bu il der из
пространства имен Microsoft .
AspN etCo r e . Rout i ng , а маршруты определяются с применением расширяющего
метода Map Ro u te () . Чтобы облегчить понимание маршрутов, по соглашению при
вызове метода MapRoute () указываются имена аргументов. что объясня ет наличие
в листинге явно именованных аргументов name и temp late . В аргументе name указы­
вается имя маршрута, а в аргументе template определяется шаблон.

Совет. Назначать маршрутам имена не обязательно, и существует философский аргумент отно­


сительно того, что именование приносит в жертву четкое разделение обязанностей, которое
в противном случае обеспечила бы маршрутизация. В разделе " Генерация URL из специфи­
ческого маршрута" главы 16 объясняется, почему именование может стать проблемой .

Запустив пример приложения, можно увидеть эффект от изменений, внесенных в


маршрутизацию. При первом запуске приложения ничего не изменится-вы увидите
ошибку 404, но если перейти на URL, который соответствует шаблону {controller} /
{act ion} , то получится результат, подобный показанному на рис . 15.3, где иллюст­
рируется переход на /Admin/I nd e x .
Глава 15. Маршрутизация URL 435

Controller: AdmlnController

Action: lndex

Ри с. 15.З. Навигация с использованием простого маршрута

Корневой URL для приложения не работает из-за того, что маршрут, добавленный
в файле Startup. cs, не сообщает инфраструктуре МVС о том, как выбирать класс
контроллера и метод действия, когда URL запроса не содержит сегментов . Мы испра­
вим это в следующем разделе.

Определение стандартных значений


Пример приложения возвращает ошибку 404, когда запрашивается стандарт­
ный URL, поскольку он не соответствует шаблону маршрута, определенного в классе
Startup. Из-за отсутствия в стандартном URL сегментов, 1шторые могли бы соответс­
твовать переменным controller и action, определенным в шаблоне маршрута, сис­
тема маршрутизации не дает совпадения.

Ранее объяснялось, что шаблоны URL будут соответствовать только URL с указан­
ным количеством сегментов. Один из способов изменить такое поведение предусмат­
ривает применение стандартных значений. Стандартное значение используется,
когда URL не содержит сегмент, который можно было бы сопоставить с шаблоном
маршрута . В листинге 15. 11 определяется маршрут, использующий стандартное
значение.

Листинг 15.11. Предоставление стандартного значения в файле Startup. cs


using Microsoft . AspNetCore .Builder;
using Microsoft.Extensions.Dependencylnjecti on ;
namespace UrlsAndRou tes {
puЬlic class Startup {

puЫic void ConfigureServices(IServi ceCollection services) {


services . AddMvc() ;

puЬlic void Conf igure(IApplicationBuilder арр) {


app .Us eStatusCodePages() ;
app . UseDeveloper ExceptionPage();
app.UseStaticFiles() ;
арр . UseМvc (routes => {

routes .MapRoute (
name: "default",
template: "{controller}/{action}",
defaul ts: new { action = "Index" }) ;
}) ;
436 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

Стандартные значения задаются как свойства анонимного типа, передаваемого


методу MapRoute () в аргументе defaul ts. В листинге 15.11 для переменной action
предоставлено стандартное значение Index.
Как и ранее, этот маршрут будет соответствовать всем двухсегментным URL.
Например, в случае запроса URL вида http://mydomain.com/Home/Index маршрут
извлечет Home в качестве значения для controller и Index - для act i on.
Но теперь, когда имеется стандартное значение для сегмента action, маршрут
будет также соответствовать и односегментным URL. При обработке односегментно­
гоURL система маршрутизации извлечет значение для переменной controller из
URL и будет применять стандартное значение для переменной action. Таким обра­
зом, запрос пользователем /Home приведет к тому, что МVС вызовет метод действия
Index () контроллера Home (рис. 15.4).

Controller: AdminController

Action: lndex

Рис. 15.4. Использование стандартного действия

Определение встроенных стандартных значений


Стандартные значения могут также быть выражены как часть шаблона URL,
что дает более лаконичный способ определения маршрутов, как показано в листин­
ге 15.12. Встроенный синтаксис может применяться только для предоставления
стандартных значений переменным, которые являются частью шаблона URL, но, как
вскоре вы узнаете, зачастую удобно иметь возможность устанавливать стандартные
значения за пределами шаблона. По этой причине важно понимать оба способа вы­
ражения стандартных значений.

Листинг 15.12. Определение встроенных стандартных значений в файле Startup. cs


using Microsoft . AspNetCore .Builder ;
using Microsoft . Extensions.Dependency!njection ;
namespace UrlsAndRoutes {
puЫic class Startup {
puЫic void ConfigureServices(IServiceCollection services) {
services . AddMvc();

puЫic void Configure(IApplicationBuilder арр) {


app.UseStatusCodePages() ;
app . UseDeveloperExceptionPage() ;
app .U seStaticFiles() ;
app . UseMvc(routes => {
routes.MapRoute (name : " default ",
template: "{controller}/{action=Index}");
}) ;
Глава 15. Маршрутизация URL 437
Мы можем пойти еще дальше и сопоставлять с URL, которые вообще не содержат
переменных сегментов , полагаясь при идентификации действия и контроллера толь­
ко на стандартные значения. Для примера в листинге 15.13 демонстрируется отоб­
раже ние корневого URL прилож ения за счет предоставле ния стандартных знач е ний
для обоих сегм ентов.

Листинг 15.1 З. Предоставление стандартных значений для переменных controller


и action в файле Startup . cs
using Microsoft . AspNetCore . Bui ld er ;
using Microsoft.Ex t ensions . Depe nde ncyi nj ection ;
namespace Ur l sAndRoutes {
p u Ыic class Startup {
puЬlic void ConfigureServi ces(IS e r viceCo l lect i on s e rvi ces ) {
services . AddMvc() ;

puЫic vo i d Co nf igu re(IApplicati onBuil der арр) {


app . OseStat usCodePages() ;
app . UseDeveloperExcep tionPage() ;
app . UseStaticFiles() ;
app . OseMvc(routes => {
routes . MapRou t e(
name : "defa ult ",
template : "{controller=Home}/{action=Index}");
}) ;

За сч ет предоставл ения стандартных значений для переменных controller и


action м аршрут будет соотв етствовать URL с нулем , одним или двумя сегм ентами
(табл . 15.4).

Таблица 15.4. Соответствие URL


Количество
Пример На что отображается
сегментов

о / cont r oller=Home action=Index


/Customer controller=Customer ac ti on= Index
2 /Customer/List cont r ol l e r =Custome r act i on=Li st
з /Customer/List/All Соответствия нет - сегментов слишком много

Ч ем меньше сегментов получе но во входящем URL, тем больше маршрут полагает­


ся н а стандартные значения, вплоть до момента, когда URL без сегментов сопостав­
ляется с использованием только стандартных значений.
З апустив пример приложения, можно увидеть результат применения стандартных
значений . Когда бр аузер запрашивает корневой URL приложения, для переменных
с егментов control l er и act i on будут использоваться стандартные значения, ко­
торы е прив едут к тому , что инфраструктура MVC вызовет метод действия I ndex ()
ко нтролле ра · Ноте (рис . 15.5).
438 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

D Routing Х

f- - С ГФ lo~Jh~~\~s2994====~~=-=-!:J
. -· ---· - - · - -
:
" - - - - -- - - - - - - · - - - - · - - - - - - - - - · ---1
1

Controller: HomeController li

Action: lndex

__________"_________ --________J
l__________
Рис. 15.5. Применение стандартных значений для расширения области действия маршрута

Использование статических сегментов URL


Не все сегменты в шаблоне URL должны быть переменными. Допускается ТЮ{Же со­
здавать шаблоны , имеющие статические сегменты. Пр едположи м . что приложение
нуждается в сопоставлении с URL, которое снабжены пр ефиксом PuЫic, например:

http : //mydomain . com/PuЬlic/Home/ Ind ex

Это можно сделать с применением шаблона URL, подобного показанн ому в лис­
тинге 15.14.
Листинг 15.14. Шаблон URL со статическими сегментами в файле Startup. cs
using Microsoft.AspNetCore.Builder ;
using Microsoft . Extensions . Dependencyinjection ;
namespace UrlsAndRoutes {
puЫic c l ass Startup {
puЫic void ConfigureServices(IServiceCol l ection services) {
services . AddMvc() ;

puЫic void Configure(IApplicationBuilder арр) {


app . UseStatusCodePages() ;
app.UseDeveloperExceptionPage() ;
app .Us eStaticFiles() ;
app . UseMvc(routes => {
routes . MapRoute(
name : " defaul t",
template : "{ controller=Home}/{action=Index} " ) ;
routes. мapRoute (name: 11 11 ,
tempJ.ate: "PuЫic/{controJ.J.er=Home}/{action=Index}");
}) ;

Новый шаблон будет соответствовать только URL, содержащим три сегмента, пер­
вым из которых должен быть PuЬlic . Остальные два сегмента могут содержать лю­
бые значения, и они будут использоваться для переменных controller и a ction.
Если последние два сегмента не указаны. тогда будут применяться стандартные
значения.
Глава 15. Маршрутизация URL 439
Можно также создавать шаблоны URL, которые имеют сегменты, содержащие как
статические, так и переменные элементы, вроде шаблона в листинге 15. 15.

Листинг 15.15. Шаблон URL со смешанным сегментом в файле Startup. cs


using Microsoft . AspNetCore . Builder ;
using Microsoft .E xtensions.Dependencyinjection ;
namespace UrlsAndRoutes
puЫic class Startup {
puЬlic void ConfigureServices(IServiceCollection services) {
services . AddMvc() ;

puЫic void Configure(IApplicationBuilder арр) {


app . UseStatusCodePages();
app.UseDeveloperExceptionPage() ;
app . UseStaticFiles() ;
app . UseMvc(routes = > {
routes .MapRoute ( 1111 , 11 Х{ controller} / { action} 11 ) ;

routes . MapRoute(
name : 11 default 11 ,
template: 11 {controller=Home}/{action=Index} 11 ) ;
routes. MapRoute (narne : "",
11
template : P uЬlic/{controller=Home}/{action =I ndex} ") ;
}) ;

Шаблон в показанном маршруте соответствует любому двухсегментному URL, в


котором первый сегмент начинается с буквы Х . Значение для controller берется из
первого сегмента, исключая Х. Значение для action получается из второго сегмента.

Запустив приложение и перейдя на URL вида /XHome/Index, можно увидеть эффект


от добавления этого маршрута (рис. 15.6).

D Routing
f- - С ГФ localt;~st52994/Xнome/111dex --

Controller: HomeController
1

с
lndex

Ри с. 15.6. Смешивание в одном сегменте статических


и переменных элементов
440 Часть 11. Подробные сведения об инфраструктуре ASP.NET Саге MVC

Упорядочение маршрутов

В листинге 15.15 новый маршрут был определен и помещен перед другими маршрутами .
Так было сделано потому, что маршруты применяются в порядке, в котором они определе­
ны . Метод MapRoute () добавляет маршрут в конец конфигурации маршрутизации, а это
означает, что обычно маршруты применяются в порядке их определения. Мы сформулиро­
вали "обычно" , т.к . есть методы, которые позволяют вставлять маршруты в специфические
позиции. Я стараюсь не использовать такие методы, поскольку применение маршрутов в
порядке их определения упрощает понимание маршрутизации в приложении.

Система маршрутизации пытается сопоставить входящий URL с шаблоном URL маршрута,


который был определен первым , и продолжает сопоставление со следующим маршрутом
только в случае, если не произошло совпадение. Маршруты проверяются последователь­
но, пока не обнаружится соответствие или не исчерпается набор маршрутов. Вследствие
этого самые специфичные маршруты должны быть определены первыми. Маршрут, добав­
ленный в листинге 15.15, является более специфичным, чем следующие за ним маршруты .
Предположим, что порядок следования маршрутов изменен на обратный :

routes . MapRoute( "MyRoute ", " {contro ll er=Home}/{action=Index} " ) ;


routes . MapRoute( "", "X{controller}/ {action} " ) ;

Тогда первый маршрут, который соответствует любому URL с нулем, одним или двумя сег­
ментами, будет использоваться всегда. Более специфичный маршрут, который теперь вто­
рой в списке , никогда не будет достигнут. Новый маршрут исключает ведущую букву х из
URL, но это не будет сделано из-за совпадения со старым маршрутом. Таким образом, URL
такого вида :

http : //mydomain . com/XHome/Index


будет нацелен на контроллер по имени XHome, предполагая существование в приложении
класса XHomeController и наличие в нем метода действия Index ().

Статические сегменты URL и стандартные значения можно 1юмбинировать с це­


лью создания пс е вдонима для специфического URL. После развертывания приложе­
ния применяемая схема URL формирует контракт с пользователями , и при последую­
щем рефакторинге приложения необходимо предохранить предыдущий формат URL,
чтобы любые URL, добавленные в закладки, макросы или сценарии, написанные
пользователем, продолжали работать.
Представим. что у нас использовался контроллер по имени Shop, который те перь
заменен контроллером Horne . В листинге 15.16 показано, как с о здать маршрут для
предохранения старой схемы URL.

Листинг 15.16. Смешивание статических сегментов URL и стандартных значений


в файле Startup. cs
using Microsoft.AspNetCore . Builder ;
us i ng Microsoft . Extensions . Dependencyinjection ;
namespace UrlsAndRoutes
puЫic class Startup {
puЫic void ConfigureServices(IServiceCollection services) {
services.AddMvc() ;
Глава 15. Маршрутизация URL 441
puЫic void Configure(IApplicationBuilder арр) {
app .Us eStatusCodePages();
a pp .U seDeveloperExceptionPage();
a pp . UseSta ticFiles();
app.UseMvc (routes => {
routes. MapRoute (name: "ShopSchema",
templa te : "Shop / { action} " ,
defaul ts: new { controller "Ноте" } ) ;=
routes . MapRoute(" ", "X{controller}/{action}");
routes.MapRo ute(
name: " default" ,
templ a t e : "(controller= Home}/{action=Index}");
routes.MapRoute(name: "",
template: "PuЬlic/{controller=Home}/{action=Index}");
}) ;

Добавленный маршрут соответствует любому двухсегментному URL. в котором


первым сегментом является Shop. Значение action берется из второго сегмента URL.
Шаблон URL не содержит переменного сегмента для con troller, поэтому задейству­
ется стандартное значение. Аргумент defaul ts предоставляет значение c o ntroller,
потому что отсутствует какой-либо сегмент, к которому можно было бы применить
значение как часть шаблона URL.
В результате запрос действия из контроллера Shop транслируется в запрос для
контроллера Home. Запустив приложение и перейдя на URL вида /Shop/Index, мож­
но увидеть эффект от добавления такого маршрута . Как показано на рис. 15.7, но­
вый маршрут приводит к тому, что инфраструктура МVС вызывает метод действия
Index () из контроллера Ноте .

С) Rout ing х

r---- ------"'==
~ С ~ loca lhost:S2:i94/Shop/lndr.>: .
-----------··· =====:::::::
1 Controller: HomeController

1 Act/On' lnde)(

1___ - · - - - - · - - - - - - - - - - - - - - - - - - - - - - - - - - · - - - - - -

Рис. 15. 7. Создание псевдонима для предохранения схемы URL

Можно продвинуться на шаг дальше и создать псевдонимы также для методов


действий, которые в результате рефакторинга перестали существовать в контролле­
ре. Чтобы сделать это, понадобится просто создать статический URL и предоставить
стандартные значения для contro ller и action (листинг 15.17).
442 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC

Листинг 15.17. Создание псевдонима для контроллера и действия в файле Startup . cs


using Microsoft . AspNetCore . Builder ;
using Microsoft . Extensions . Dependencyinjection ;
namespace Ur lsAndRoutes
puЫic class Startup {
puЫic void ConfigureServices(IServiceCo l lection s ervices) {
services . AddMvc() ;

puЫic void Configure( I ApplicationBuilder арр) {


app . UseStatusCodePages() ;
app . UseDeveloperExceptionPage() ;
app.UseStat i cFiles() ;
app.UseMvc(routes => {
routes.MapRoute(name: "ShopSchema2",
template : "Shop/OldAction",
defaul.ts : new { control.1.er = "Home", action = "Index" }) ;
routes . MapRoute(name : " ShopSchema ",
template : " Shop/{action} ",
defaults : new { controller = "H ome" }) ;
routes . MapRoute( "", " X{controller}/{act i onl " ) ;
route s. Map Route(
name: " de f ault ",
template : " {control l er=Home)/{action =I ndexl " ) ;
routes . Map Route ( n ame : "",
template : " PuЬlic/ { controller=Home}/{action = Index} ") ;
}) ;

Обратите внимание, что новый маршрут определен первым, поскольку он являет­


ся более специфичным , чем маршруты, следующие после него. Если запрос для Shop/
OldAction обр аботается , к примеру, следУющим определенным маршруто м , тогда
может быть получен результат, отличающийся от того, который желательно получить
при наличии контролл е р а с м етодом действия OldAction () .

Определение специальных переменных сегментов


Перем енные с егментов contro ll er и action имеют специальное наз нач ени е в
прилож ениях MVC и соответствуют контроллеру и методу действия, которы е будут
использоваться для обслуживания запроса . Они являются всего л ишь встроенными
п е р е менны м и сег м ентов , и допускается также определять специальные п е рем е нны е

с е гм е нтов , как показано в листинге 15.18. (Маршруты, определенные в предыдущем


разделе, удалены, чтобы можно было н ачать сначал а .)
Глава 15. Маршрутизация URL 443
Листинг 15.18. Определение дополнительных переменных внутри шаблона URL
в файле Startup. cs
using Microsoft.AspNetCore.Builder ;
using Microsoft . Extensions . Dependencyinjection ;
namespace UrlsAndRoutes
puЫic class Startup (
puЫic void ConfigureServices(IServiceCollection services) (
services . AddMvc() ;

puЫic void Configure(IApplicationBuilder арр) {


app . UseStatusCodePages() ;
app.UseDeveloperExceptionPage() ;
app.UseStaticFiles();
app.UseMvc(routes => {
rou tes . MapRou te (name : "MyRou te" ,
template: "{controller=Home}/{action=Index}/{id=Defaultid}");
}) ;

Шаблон URL опр едел яет стандартные переменные controller и act i on, а так­
же специальную п е р еменную по имени id. Маршрут будет соответствовать URL с
количеством сегментов от нуля до трех . Содержимое третьего сегмента присваива­
етс я п е р емен ной id, а при отсутствии третьего сегмента применяется стандартное
з н а чени е.

Внимание! Не которые имена зарезервированы и не доступны для использования в качест­


ве имен специальных переменных сегментов. К ним относятся controller , action и
area. Назначение первы х двух очевидно, а области обсуждаются в следующей главе .

В классе
Controller, который является базовым для контроллеров, определе­
но свойство RouteData . Это свойство во звращает объект Microsoft . AspNetCore .
Routing . RouteData, предоставляющий детали о системе маршрутизации и способе,
которым был маршрутизирован текущий запрос. Внутри контроллера можно полу­
ч ать доступ к любой п е ременной сегмента в методе действия с применением с войс­
тваRouteData . Values, которое возвращает словарь, содержащий переменные сег­
мента. В цел ях демонстрации в контроллерHome добавлен метод дей ствия по имени
CustomVariaЫe (), пр и веденный в листинге 15.19.

Листинг 15.19. Доступ к специальной переменной сегмента внутри метода действия


в файле HomeController. cs
using Microsof t. AspNetCore . Mvc ;
using UrlsAndRoutes . Models;
namespace UrlsAndRoutes . Controllers
puЫic class HomeContro l ler : Contro l ler
444 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Саге MVC

puЫic ViewResult Index() => View( " Result ",


new Result {
Cont r olle r = nameof(HomeController),
Action = nameof(Index)
}) ;

puЫic ViewResult CustornVariaЬle()


Resul t r = new Resul t {
Controller = narneof(HorneController),
Action = narneof(CustornVariaЬle),
} ;
r. Data [ "id"] = RouteData. Values [ "id"] ;
return View("Result", r);

Этот метод действия получает значение специальной переменной id в шаблоне


URL маршрута, используя свойство RouteData . Values, которое возврашает словарь
переменных, порожденных системой маршрутизации. Специальная переменная до ­
бавляется к объекту модели представления и может быть просмотрена путем запуска
приложения и запроса следующего URL:
/Home/CustomVar i aЫe/Hello

Шаблон маршрута распознает третий сегмент в данном URL как значение для пе­
ременной id, давая приведенный на рис. 15.8 результат.

CJ Routing х

Controller: HomeController
Action: CustomVari aЫe

Рис. 15.8. Отобра же ние значения специальной переменной сегмента

Шаблон URL в листинге 15. 19 определяет стандартное значение для сегмента id,
т.е. маршрут может также соответствовать URL, которые имеют два сегмента. Увидеть
применение стандартного значения можно, запросив такой URL:

/Home/CustomVariaЫe

Система маршрутизации использует стандартное значение для специальной пере­


менной, как показано на рис. 15.9.
Глава 15. Маршрутизация URL 445

D Routin9 х

Controller: HomeController
Action: CustomVariaЫe

id: Defaultld
_J
Рис. 15.9. Стандартное значение для специальной переменной сегмента

Использование специальных переменных


в качестве параметров метода действия
Работа с коллекцией RouteData . Values - лишь один способ доступа к специаль­
ным переменным маршрута, а другой способ может быть намного более элегантным.
Если метод действия определяет параметры с именами, которые совпадают с имена­
ми переменных шаблона URL, то инфраструктура МVС будет автоматически переда­
вать методу действия значения, извлеченные из URL, в виде аргументов.
Специальная переменная, определенная в маршруте из листинга 15.18, имела имя
id. Модифицируйте метод действия CustomVariaЫe () в контроллере Home, так что­
бы он принимал параметр с тем же самым именем (листинг 15.20).

Листинг 15.20. Добавление параметра к методу действия в файле HomeCon troller. cs


using Microsoft . AspNetCore . Mvc;
using UrlsAndRoutes . Models ;
namespace UrlsAndRoutes . Controllers
puЫic class HomeController : Controller
puЬlic ViewResult Index() => View( "Resul t ",
new Result {
Controller = nameof(HomeController) ,
Action = nameof(Index)
}) ;

puЬlic ViewResul t CustomVariaЫe ( string id)


Result r = new Result {
Controller = nameof(HomeController) ,
Action = nameof(CustomVariaЫe),
};
r .Data [ "id"] = id;
return View( " Result ", r) ;
446 Часть 11. Подробные сведения об инфраструктуре ASP.NET Соге MVC

Когда система маршрутизации обнаруживает соответствие какого-то URL марш­


руту, который был определен в листинге 15.18, значение третьего сегмента URL при­
сваивается специальной переменной id. Инфраструктура MVC сравнивает список пе­
ременных сегментов со списком параметров метода действия, и в случае совпадения
имен передает значения из URL этому методу.
Типом параметра id является string, но MVC будет пытаться преобразовать зна­
чение из URL в любой тип, указанный для параметра. Если параметр id метода дейс­
твия определен как int или DateTime, тогда значение из URL будет получено в виде
экземпляра заданного типа . Это элегантное и удобное средство, которое избавляет от
необходимости обрабатывать преобразования самостоятельно. Запустив приложение
и запросив URL вида /Home/CustomVariaЫe/Hello, можно увидеть эффект от па­
раметра метода (рис. 15.10). Если опустить третий сегмент, то методу действия будет
предоставлено стандартное значение для переменной сегмента, которое также видно
на рис. 15.10.

D Rcuting Х !) Routing Х .\'

-~ ----- ~ _@-~~~~~ome~~~~-~~~~~~~~-~--°. '-~-~calh~s~~~:~~-":_~~~~-m~~Ыe _ •.:~ '-1


:::'"
~--------
::=~:: 1 :::'"'

___J___________________________J
::::,::: 1

Рис. 15.1 О. Доступ к переменным сегментов с применением параметров метода действия

На заметку! Для преобразования значений, содержащихся в URL, в типы .NET инфраструк­


тура MVC использует средство привязки моделей, которое может обрабатывать намно­
го более сло ж ные ситуации, чем продемонстрированная в приведенном выше примере.
Привязка моделей будет описана в главе 26.

Определение необязательных сегментов URL


Необязателы;_ым является такой сегмент URL, который пользователь может не
указывать, и для которого не предусмотрено стандартного значения. Необязательный
сегмент обозначается знаком вопроса (символом ?) после имени сегмента, как пока­
зано в листинге 15.21.

Листинг 15.21. Определение необязательного сегмента URL в файле Startup. cs

using Microsoft . AspNetCore . Builder ;


using Microsoft . Extensions . Dependency!njection ;
namespace UrlsAndRoutes
puЫic class Startup {
puЫic void ConfigureServices(IServiceCollection services) {
services.AddMvc () ;
Глава 15. Маршрутизация URL 447
puЫic void Configure(IApplicationBuilder арр) {
app.OseStatusCodePages() ;
app.OseDeveloperExceptionPage();
app . OseStaticFiles() ;
app . OseMvc(routes => {
routes. MapRoute ( name: "MyRoute ",
template: "{controller=Home}/{action=Index}/{id?}");
}) ;

Данный маршрут будет соответствовать URL, которые имеют или не имеют сег­
мента id. В табл. 15.5 демонстрируется его работа с различными URL.

Таблица 15.5. Соответствие 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 Соответствия нет - сегментов слишком много

В табл. 15.5 видно, что переменная id добавляется к набору пе ременных, только


когда во входящем URL присутствует соответствующий сегмент. Такая возможность
удобна, когда необходимо узнать, предоставил ли пользователь значение для пере­
менной сегмента. Если для необязательной переменной сегмента значение не было
задано, тогда значением соответствующего параметра будет null. В листинге 15.22
приведено обновл ение контроллера Home в целях уч ета ситуации , когда для перемен­
ной сегмента id з начение не указано.

Листинг 15.22. Проверка необязательной переменной сегмента в файле HomeController. cs


using Microsoft . AspNetCore . Mvc ;
using Or l sAndRoutes . Models ;
namespace OrlsAndRoutes . Controllers
puЫic class HomeController : Controller
puЫic ViewResult Index() => View( " Result ",
new Result {
Controller = nameof(HomeControlle r ) ,
Act i on = nameof( Index)
}) ;

puЫic ViewResult CustomVar i aЬle(string id) {


Result r = new Result {
Controller = nameof(HomeController),
Action = nameof(CustomVariaЬle) ,
};
448 Часть 11. Подробные сведения об инфраструктуре ASP. NEТ Core MVC

r.Data["id"] = id ?? "<no value>";


return View( " Resu l t", r);

На рис . 15.11 показан результат запуска приложения и перехода по URL вида


/Home/CustomVariaЬle, в котором не включено значение для переменной сегмента
i d.

Controller: HomeCoпtrolle r

Action: CustomVariaЫe

id: <ПО value>


1
L------·--·--· -··--····-·-·--·---------·--·- ·-·--- _____ J
Рис. 15.11. Обнаружение ситуации, когда URL не содержит
значения для необязательной переменной сегмента

Стандартная конфигурация маршрутизации

Добавление инфраструктуры MVC в классе Startup осуществляется с применением ме­


тода UseMvcWithDefaultRoute (). Он представляет собой просто удобный метод для
настрой ки наиболее распространенной конфигурации маршрутизации и эквивалентен сле­
дующему коду:

app . UseMvc(routes =>


routes . MapRoute(
name: "default ",
template : "{ co ntroller =Home}/{ac ti on=Index} /{ id?}" ) ;
}) ;

Такая стандартная конфигурация обеспечивает сопоставление с URL, которые нацелены на


классы контроллеров и методы действий по их именам, а также могут содер жать необяза­
тельный сегмент id. Если сегмент controller или action опущен, тогда используют­
ся стандартные значения, чтобы направить запрос контроллеру Ноте и методу действия
Index () .

Определение маршрутов переменной длины


Еще один способ изменения консервативного стандартного поведения шаблонов
URL предусматривает прием переменного количества сегментов URL. Это позволяет
маршрутизировать URL произвольно й длины в единственном маршруте. Поддержка
Глава 15. Маршрутизация URL 449
переменного числа сегментов определяется путем назначения одной из переменных
сегментов в качестве переменной общего захвата, для чего она предваряется симво­
лом звездочки (листинг 15.23).

Листинг 15.23. Назначение переменной общего захвата в файле startup. cs


using Microsoft . AspNetCore .Builder ;
using Microsoft . Extensions .Dependencyinjection ;
namespace UrlsAndRoutes
puЫic class Startup {
puЫic void ConfigureServices(IServiceCol l ection servi ces) {
servic es. AddMvc ();

puЫic void Co nfigure( IAppli cat ionBuilder арр) {


app .UseStatusCodePag es();
app .Use DeveloperExceptionPage() ;
app . UseStaticFiles() ;
app.UseMvc(routes => {
routes. MapRoute ( name: "M yRoute ",
template: "{controller=Home}/{action=Index}/{id?}/{*catchall}");
}) ;

Здесь был расширен маршрут из предыдущего примера за счет добавления пере­


менной общего захвата по имени catchall . Теперь этот маршрут будет соответство­
вать любому URL вне зависимости от количества содержащихся в нем сегментов либо
их значений . Первые три сегмента применяются для установки значений перемен­
ньrх controller, action и id. Если URL содержит дополнительные сегменты, то все
они присваиваются переменной catchall, как показано в табл. 15.6.

Таблица 15.6. Сопоставление URL с переменной общего захвата

Количество
Пример URL На что отображается
сегментов

о / controller=Home action=Index
/Customer control l er=Customeraction=Index
2 /Customer/List controller=Customeraction=List
3 /Customer/List/All controller=Customer action=List id=All
4 /Customer/List/All/Delete control l er=Customeraction=Listid=All
catchall=Delete
5 /Customer/List/All / controller=Customer act i on=
Delete/Perm List id=All catchall=Delete/ Perm

В листинге 15.24 приведен обновленный контроллер Customer, в котором дейс­


твие List передает представлению значение переменной catchall через объект
модели.
450 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

Листинг 15.24. Модификация метода действия в файле CustomerController.cs


us in g Microsoft . AspNetCore . Mvc ;
us i ng UrlsAndRoutes . Models ;
narnespace UrlsAndRo ut es.Cont r o l lers
puЬlic class Custorne rControl l er : Contro l ler
p u ЫicViewResul t Index () => View ( " Resu l t ",
new Result {
Control l er = narneof(Custorne r Control l er) ,
Action = narneof(Index)
}) ;
puЫic ViewResult List(string id) {
Resul t r =
new Resul t {
Controller = nameof(HomeController),
Action = narneof(List),
} ;
r. Data [ "id"] = id ?? "<no value>";
r.Data["catchall"] =
RouteData.Values["catchall"];
return View("Result", r);

Чтобы протестировать с егмент общего захвата , понадобится запустить прилож е ­


ние и запросить сл едующий URL:

/ Cu stome r / Li st/ Hel l o/1/2/3

Верхнего предел а количества се гментов , для которого шаблон URL в этом марш ­
руте будет давать совпадение, не существует. На рис . 15.12 пока з ан э ффект от опр е­
деления сегмента общего з ахвата. Обратите внимание, что сегменты , попадающие в
переменную общего захвата, представлены в форме с е гмент/ сегмент/ сегмент , и
мы сами отвечаем з а обработку строки для ее р а збиения н а отдельны е се гменты.

D Rou tiog х

f-
- -
С [Фlocalhost:52994/C~tomer/list/Hl?Ho/1f2/3
--
·----·-*l
---~-..:--:":':".:=:=--:-:-:-=--:-:--=-:-·=--=-==-=-~-=-=-.--:::::--:-:=..~~

Controller: HomeController

Action: List
id : Hello
catchall : 1/2/3

_______________________________] 1

Рис. 15. 12. Использование сегмента общего захвата


Глава 15. Маршрутизация URL 451

Ограничение маршрутов
В начале главы было указано , что шаблоны URL консервативны в отношении коли­
чества сопоставляемых сегментов и либеральны в отношении содержимого сопостав­
ляемых сегментов. В нескольких предшествующих разделах объяснялись различные
приемы управления степенью консервативности: увеличение или уменьшение числа

сегментов с применением стандартных значений, необязательных переменных и т.д.


Теперь наступило время взглянуть на то , каким образом управлять либеральнос­
тью при сопоставлении с содержимым сегментов URL, а именно - как ограничить
набор URL, с которыми будет сопоставляться маршрут. В листинге 15.25 демонстри­
руется использование простого ограничения URL, с которыми будет сопоставляться
маршрут.

Листинг 15.25. Ограничение маршрута в файле Startup. cs


using Microsoft.AspNetCore.Builder;
using Microsoft . Extensions.Dependency inje ct i on ;
namespace UrlsAndRoutes
puЬlic class Startup {
puЫic void Con figureSer v ices(IS erviceCol l ect i on services) {
services . AddMvc() ;

puЬlic void Conf i gure(IApp li cationBuilder арр) {


app . UseStatusCodePages() ;
app . UseDevelope rE xcept ionP age() ;
app . UseStaticFiles() ;
app . UseMvc(routes => {
routes . MapRoute(name : " MyRoute ",
template: "{controller=Home}/{action=Index}/{id:int?}");
}) ;

Ограничения отделяются от имени переменной сегмента дво еточием (сим вол : ).


Ограничением в листинге является int, и оно применяется к сегменту id. Это при­
мер встроенного ограничения, которое определено как часть шаблона URL и приме­
нено к одиночному сегменту:

template: "{ controller}/{act ion}/{id:int?}",

Ограничени е int разрешает сопоставление шаблона URL только с сегментами,


значение которых может быть преобразовано в тип int. Сегмент id необязателен,
так что маршрут будет сопоставляться с URL без сегмента id, но если он присутству­
ет, то должен быть целочисленным значением (в табл . 15.7 приведены примеры).
452 Часть 11. Подробные сведения об и нфрастру ктуре ASP.NET Core MVC

Таблица 15. 7. Сопоставление URL с ограничением

Пример URL На что отображается

/ controller=Home action= I ndex id=null


/Home/CustomVariaЫe/ H ello Соответствия нет - сегмент id не может быть
преобразован в значение int
/Home/CustomVariaЫe/l cont roller=Home action=CustomVariaЫe id=l
/Home/CustomVariaЬle/1/2 Соответствия нет - сег м ентов слишком много

Ограничения также м огут быть указаны за пр еделам и шабл она URL с использо­
ванием аргум е нта constra in ts метода MapRoute () при определ е нии м а ршрута.

Такой прием удобен , если вы предпочитаете удержи вать шаблон URL отдел ьно от ог­
раничений или следовать стилю маршрутизации, принятому в бол ее ранних в е р с иях
MVC , где встро е нны е огр аничения н е подде рживались. В листинге 15.26 показ ано то
же самое огран ич ение int на перем енно й сегмента id, выраженн о е с прим ен е ни ем
отдельного ограничения. При использовании такого формата стандартны е з нач е ния
также выражаются вн е шне.

Листинг 15.26. Выражение ограничения за пределами шаблона URL в файле Startup. cs

using Microsof t. AspNetCore . Bui l der ;


using Microsoft . Extensions . Dependencyinjection ;
using Microsoft.AspNetCore.Routing.Constraints;
narne space UrlsAndRoutes
puЫic class Startup {
void ConfigureServices(IServi ceCollection services) {
puЬl i c
services . AddMvc() ;

pu Ы i c void Configure(IApp l ica t ionBuilder арр) {


app .Us eStatusCodePa ges() ;
app . UseDeveloperExceptionPage() ;
app . UseStaticFi les() ;
app .U seMvc(routes => {
routes . MapRo ute(name : "MyRoute ",
template: "{controller}/{action}/{id?}",
defaults: new { controller = "Ноте", action = "Index" } ,
constraints: new { id =
new IntRouteConstraint() }) ;
));

Аргумент constrai n ts метода Ma pRout e ( ) определен с прим енени ем анонимного


типа, им е на свойств которого соотв етствуют ограничив аемым п е р еменны м сегм ен­
тов. Прост ранство имен Mic r osoft . AspNetCo r e . Rout i ng . Constraints соде ржит
набор классов , которые можно использовать для определ ения индивидуальных огра­
ничений. В листинге 15.26 аргумент con s trai n ts наст роен на при м енени е объ е кта
I ntRoute Const r aint для сегмента id, что дает т акой же результат. как встро е нное
ограничение и з листинга 15.25.
Глава 15. Маршрутизация URL 453
В табл. 15.8 оп и с ан полный н абор кл а ссов ограничений в пространст ве им е н
Microsoft .AspNetCore . Routing . Constraints вместе с их встро енными э квива­
л е нтами для огранич ений, которые могут прим е няться к одиночным элементам в
ша блон е URL; ч асть и з них будет р ассматрив аться в последующих разделах .

Совет. Можно ограничить доступ к методам действий со стороны запросов, которые сдела­
ны с помощью специфических НТТР-команд, таких как GE T или POST , используя набор
атрибутов MVC наподобие HttpGet и HttpPost. В главе 17 описано применение этих
атрибутов для обработ ки форм в контроллерах , а в главе 20 приведен полный список до­
ступных атрибутов.

Таблица 15.8. Ограничени я маршрутов на уровне сегментов

Встроенное
Описание Имя класса
ограничение

alpha Соответствует алфавитным Al phaRouteConstraint()


символам независимо от ре-

гистра (A-Z, a-z)


bool Соответствует значению, BoolRouteConstraint ()
которое может быть преобра-
зовано в тип boo l
datet irne Соответствует значению, DateT i rneRouteConstrain t ()
которое может быть преобра-
зовано в тип DateTirne
decirna l Соответствует значению, DecirnalRouteConstraint ()
которое может быть преобра-
зовано в тип decirnal
douЫe Соответствует значению, DouЬleRouteConstraint ()
которое может быть преобра-
зовано в тип douЫe

float Соответствует значению, Fl oatRouteCons traint ()


которое может быть преобра -
зов а но в тип float
guid Соответствует значению GuidRouteConst r aint ()
глобально уникального
идентификатора

int Соответствует значению, IntRouteConst r aint ()


которое может быть преобра-
зовано в тип int
length (l en) Соответствует строке , кото- Le ngthRouteConstraint ( len)
рая содержит у казанное ко -
length (min , max) личество символов или число LengthRouteConstraint (rnin , max)
символов между rnin и max
(включительно)

long Соответствует значению, LongRouteConstraint ()


которое может быть преобра-
зовано в тип long
454 Часть 11 . Подробные сведения об инфрастру ктуре ASP.NET Core MVC

Окончание табл . 15.8

Встроенное
Описание Имя класса
ограничение

maxlength(len) Соответствует строке с количе- MaxLengthRouteConstraint ( len)


ством символов не более len
max(val) Соответствует значению int, MaxRouteConstraint (val)
если оно меньше val
minlength(len) Соответствует строке, кото- MinLengthRouteConstraint (len)
рая имеет, по крайней мере,
len символов

min(val) Соответствует значению int, MinRouteConstraint (val)


если оно больше val
range (min , max) Соответствует значению int, RangeRouteConstraint (min , max)
если оно находится между
min и max (включительно)

regex(expr) Соответствует регулярному RegexRouteConstraint (expr)


выра ж ению

Ограничение маршрута с использованием регулярного выражения


Наибольшую гибкость предлагает огранич ение regex, которое сопоставляет сег­
мент с применением регулярного выражения. В листинге 15.27 сегмент controller
ограничивается диапазоном URL. с которыми он будет сопоставляться.

Листинг 15.27. Использование регулярного выражения для ограничения маршрута


в файле Startup. cs
using Microsoft.AspNetCore . Builder ;
using Mi crosoft . Extensions . Dependencyinjection ;
using Microsoft . AspNetCo r e . Routing . Constraints ;
namespace UrlsAndRoutes
puЫic class Startup {
puЫic void ConfigureServices(IServiceCollection services) {
services . AddMvc();

puЫic void Configure(IApplicationBuilder арр) {


app . UseStatusCodePages() ;
app . UseDeve loperExceptionPage() ;
app.UseStaticFiles();
app .UseMvc(routes => {
routes . MapRoute ( name : "MyRoute",
template: "{controller:regex("H .* )=Home}/{action=Index}/{id?}");
}) ;
Глава 15. Маршрутизация URL 455
Примененное к маршруту ограничение обеспечивает его соответствие только та­
ким URL, в которых сегмент controller начинается с буквы Н.

На заметку! Стандартные значения используются перед проверкой ограничений. Таким об­


разом, например, при запросе URL вида / для переменной сегмента controller при­
меняется стандартное значение Ноте. Затем проверяются ограничения , и поскольку зна­
чение controller начинается с Н, стандартный URL будет соответствовать маршруту.

С помощью регулярных выражений можно ограничивать маршрут так, что к


совпадению будут приводить только специфические значения для какого-то сегмен­
та URL. Это делается с использованием символа 1, как показано в листинге 15.28.
(Шаблон URL разбит на две части, чтобы уместиться на печатной странице, что в
реальном проекте совершенно необязательно . )

Листинг 15.28. Ограничение маршрута специфическим набором значений


переменной сегмента в файле Startup. cs
using Microsoft .AspNetCore.Builder ;
using Microsoft.Extensions.Dependencyinjection;
using Microsoft . AspNetCore.Routing . Constraints ;
namespace UrlsAndRoutes {
puЫic class Startup (
puЫic void ConfigureServices(IServiceCollection services) {
services . AddMvc() ;

puЫic void Configure(IApplicationBuilder арр) (


app . UseStatusCodePages() ;
app.UseDeveloperExceptionPage() ;
app.UseStaticFiles() ;
app . UseMvc(routes => (
routes.MapRoute(name: "MyRoute ",
template: "{controller:regex(лH.*)=Home}/"
+ "{action:regex(лindex$1"AЬout$)=Index}/{id?}");
}) ;

Такое ограничение разрешает маршруту соответствовать только URL, имеющим


сегмент action со значением Index или About . Ограничения применяются совмес­

тно, так что ограничения, наложенные на значение переменной action, комбиниру­


ются с ограничениями, наложенными на значение переменной controller. Другими
словами, маршрут в листинге 15.28 будет соответствовать только таким URL, в кото­
рых значение переменной controller начинается с буквы Н, а значением перемен­
ной action является Index или AЬout .

Использование ограничений на основе типов и значений


Большинство ограничений применяются для ограничения маршрутов, так что они
соответствуют только URL с сегментами, значения которых могут быть преобразова­
ны в указанный тип либо имеют специфический формат. Хорошим примером может
456 Часть 11. Подробные сведения об инфраструктуре ASP.NET Соге MVC

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


соответствовать, только 1шгда значение ограниченного сегмента может быть преоб­
разовано в значение .NЕТ-типа int. В листинге 15.29 демонстрируется применен ие
ограничения range, 1шторое ограничивает маршрут так. что он соответствует URL,
только когда значение сегмента может быть преобразовано в int и находится между
указанными значениями.

Листинг 15.29. Ограничение сегмента на основе типа и значения в файле Startup. cs

using Microsoft . AspNetCore.B u ilder ;


using Mic r osoft .Extensions . Dependencyinjection ;
using Microsoft.AspNetCore.Routing.Constraints;
namespace UrlsAndRoutes {
puЬlic class Startup {
puЫic void Configu re Services( I Se rvi ceCollect i on services) {
servic es . AddMvc() ;

puЫic void Configure(IApp li cationBuilder арр) {


app .UseS tatusCodePages() ;
app . UseDeveloperExceptionPage() ;
app . UseStaticFiles() ;
app .U seMvc(routes => {
routes . MapRoute(name : "MyRoute ",
template: "{controller=Home}/{action=Index}/{id:range(l0,20)?}");
}) ;

Ограничение в рассмотренном примере применялось к необязательному сегменту


id . Ограничение будет проигнорировано, если URL запроса не содержит, по крайней
мере, три сегмента. Если сегмент id присутствует, то маршрут будет соответствовать
URL, только когда значение сегмента может быть преобразовано в int и попадает в
диапазон между 10 и 20. Ограничение range является включающим, т.е. значения 10
и 20 считаются находящимися внутри диапазона.

Объединение ограничений
Если необходимо применить 1t одиночному элементу сразу несколыш ограничений,
тогда их можно соединить в цепочку, отделяя каждое ограничение от других двоето­

чием (листинг 15.30).

Листинг 15.30. Объединение встроенных ограничений в файле Startup. cs


using Mi crosoft . AspNetCore . Bui lder ;
using Microsoft .Ex tensions .Depe n dency inj ect i on ;
using Microsoft .AspNetCore .Routing . Constraints ;
namespace Ur l sAndRoutes {
puЬlic class Startup {
puЫic void ConfigureServices(ISe r viceCollection services) {
servi ces.AddMvc() ;
Глава 15. Маршрутизация URL 457
puЬlic void Configure(IApplicationBuilder арр) {
app .U seStatusCodePages() ;
app.UseDeveloperExceptionPage() ;
app.UseStaticFiles() ;
app . UseMvc(routes => {
routes . MapRoute(name: " MyRoute ",
template: "{controller=Home}/{action=Index}"
+ "/{id:alpha:minlength(б)?}");
}) ;

В этом листинге к сегменту id бьши применены ограничения alpha и minlength.


Знак вопроса, обозначающий необязательный сегмент, применяется после всех ог­
раничений. В результате объединения ограничений маршрут будет соответствовать
URL. только когда сегмент id опущен (поскольку он необязателен) или когда он при­
сутствует и содержит, по крайней мере, шесть алфавитных символов.
Если вы не используете встроенные ограничения, то должны применять класс

Microsoft . AspNetCore . Routing . Composi teRouteConstraint, который позволя­


ет ассоциировать несколько ограничений с одиночным свойством в анонимно типи­
зированном объекте. В листинге 15.31 показана комбинация ограничений, которые
использовались в листинге 15.30.
Листинг 15.31. Объединение отдельных ограничений в файле Startup. cs

using Microsoft . AspNetCore . Builder ;


using Microsoft .Extensions .Dependencyinjection ;
using Microsoft.AspNetCore.Routing.Constraints;
using Microsoft.AspNetCore.Routing;
namespace UrlsAndRoutes {
puЫic class Startup {
puЫic void ConfigureServices(IServiceCollection services) {
services . AddMvc() ;

puЫic void Configure(IApplicationBuilder арр) {


app . UseStatusCodePages() ;
app.UseDeveloperExceptionPage() ;
app.UseStaticFiles();
app . UseMvc(routes => {
routes . MapRoute(name : " MyRoute ",
template: "{controller}/{action}/{id?}",
defaults: new { controller = "Ноте", action = "Index" } ,
constraints: new {
id = new CompositeRouteConstraint(
new IRouteConstraint [] {
new AlphaRouteConstraint(),
new MinLengthRouteConstraint(б)
})
}) ;
) ) ;
458 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

Конструктор класса Composit e Rou te Con st r a i nt принимает перечисление объ­


ектов, реализующих интерфейс IRouteCon st ra int, которое определяет ограничения
маршрутов. Система маршрутизации разрешит соответствие маршрута URL, только
если все ограничения удовлетворены .

Определение специального ограничения


Если для обеспечения текущих потребностей стандартных ограничений оказыва­
ется н едостаточно, тогда можно определить собственные ограничения, реализовав
инте рфейс IRout e Con s tra i nt , который находится в пространстве и мен Mi crosoft .
As pNetCo r e . Rout i ng . Чтобы проверить такую возможность , добавьте в пример
проекта папку Inf ra s tr u ct ure и поместите в нее новый файл класса по имени
Week DayConst r a i nt . c s с содержимым из листинга 15.32.
Листинг 15.32. Содержимое файла WeekDaycon strain t . c s
из папки Infrastruc ture
using Mic rosoft . As pNetCore . Ht t p ;
u sing Mi c r osoft . As pNet Core . Rout in g;
u s i ng Sy s tem . Linq ;
n amespace UrlsAndRo u tes . Infra s tructure
puЫic clas s We ekDayConstra i nt : I RouteConstra in t
p ri vate stat i c str i ng[] Da y s = new [] { "mon " , "t ue ", " wed ", " thu ",
"fri ", "s at ", "sun " } ;
pu Ы ic bool Match(HttpCon t ex t http Cont ex t , IRo u ter ro u te ,
string rou t eKey , RouteVal u eDic tion ary v a lues ,
Route Direction routeDirect i on) {
retu r n Days . Contains{va l ues[route Key]? . ToString() . ToLowerinvariant()) ;

В интерфей се IRo u teCon s tr ain t определен метод Ma tch () , который вызывает­


ся , чтобы позволить ограничению решить, должен ли запрос соответствовать марш­
руту . Парамет ры метода Match () пр едоставляют доступ к запросу , по ступивш е му от
клиента , маршруту, имени сегм е нта, на который воздействует ограничение, перемен­
ным сегментов, извлеченным из URL, и признаку того, для какого URL проверяется
запрос - входящего или исходящего (исходящие URL рассматриваются в главе 16).
В этом прим е ре с помощью параметра routeKey из парам етра values и з вл е к ает­

ся значение переменной сегмента, к которому было применено ограничение, преобра ­


зуется в строку нижнего регистра и проверяется на предмет соответствия одному из

дней недели, определенных в статическом пол е Days . В листинге 15.33 новое ограни­
чение применяется к маршруту с использованием синтаксиса отдельно определяемых

ограничений.

Листинг 15.33. Применение специального ограничения в файле Startup . cs


using Microsoft . AspNetCore . Bu ilder ;
u sing Microsoft . Ext e nsions . Dependency i njection ;
usi ng Mi crosof t. AspN et Core . Routing . Con st r aints ;
us i ng Mi crosoft . As pN e t Core . Routing ;
using UrlsAndRoutes.Infrastructure;
namespac e Ur lsAndRoutes {
Глава 15. Маршрутизация URL 459
puЫic class Startup {
puЫic void Config ureS e r vices( I ServiceCollect i on services) {
services . AddMvc() ;

puЫic void Configure(IApplicationBuilder арр) {


app . UseStatusCodePages() ;
app . UseDeveloperExceptionPage() ;
app .U seStaticFi les();
app . UseMvc(routes => {
routes . MapRoute(name : " MyRoute ",
template: " {controller}/{action} /{id?} ",
defaults : new { co ntroller = "Н оте ", actio n = "I ndex " } ,
constraints: new { id = new WeekDayConstraint() }) ;
));

Этот маршрут будет соответствовать URL, только если сегмент id отсутствует


(например, /Customer/List) или совпадает с одним из дней недели, определенных
в ш~ассе ограничения (скажем, /Customer/List/Fri).

Определение встроенного специального ограничения


Настройка специального ограничения так, чтобы оно могло использовать­
ся встроенным образом, требует дополнительного шага по конфигурированию
(листинг 15.34).

Листинг 15.34. Применение специального ограничения встроенным образом


в файле Startup. cs

using Microsoft . AspNetCore . Builder ;


using Microsoft . Extensions.Dependency!njection;
using Microsoft . AspNetCore.Routing . Co ns traints ;
using Microsoft.AspNe tC ore . Routing;
using Ur l sAndRoutes . Infrastructure;
namespace UrlsAndRoutes
puЫic class Startup {
puЬlic void ConfigureServi ces(IServiceCol l ection services) {
services.Configure<RouteOptions>(options =>
options.Constraintмap.Add("weekday", typeof(WeekDayConstraint)));
services.AddMvc();

puЬlic void Conf ig ure( I ApplicationBuilder арр) {


app . UseStatusCo de Pages() ;
app . Use Deve l op e rExc ept ionPag e() ;
app.UseStaticFiles() ;
app.UseMvc(routes => {
routes . MapRoute(name : " MyRoute ",
template: "{controller=Home}/{action=Index}/{id:weekday?}");
) ) ;
460 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Саге MVC

В методе ConfigureService () конфигурируется объект RouteOptions, который


управляет рядом линий поведения системы маршрутизации . Свойство ConstraintMap
возвращает словарь, используемый для трансляции имен встроенных ограничений в
классы реализации IRouteConstraint, которые предоставляют логику ограничений .
В словарь было добавлено новое отображение, поэтому на класс WeekDayConstraint
можно ссылаться встроенным образом как на weekday:

template : " {controller= Home}/{act ion=Inde x }/{ id:weekday ?} ",

Результат будет тем же самым, но настройка отображения позволяет прим енять


специальный класс ограничения встроенным образом.

Использование маршрутизации
с помощью атрибутов
Во всех примерах, приведенных до сих пор в главе , маршруты определялись пос­
редством приема, который называется маршрутизач,ией на основе соглаше ний.
Инфраструктура MVC также поддерживает прием, известный как маршрутизач,ия
с помощью атрибутов, когда маршруты определяются через атрибуты С# , которые
применяются напрямую к классам контроллеров. В последующих разделах будет по­
казано, как создавать и конфигурировать маршруты с использованием атрибутов.
которые могут свободно смешиваться с маршрутами на основ е соглаш е ний, проде­
монстрированными в пр едшествующих примерах .

Подготовка для маршрутизации с помощью атрибутов


Маршрути зация с помощью атрибутов включается, когда в файле Startup . cs
вызывается м етод UseMvc () . Инфраструктура MVC исследует классы контроллеров
в приложении, находит любые из них , имеющие атрибуты маршрутизации, и создает
нужные маршруты.

В этом раздел е мы возвратим пример приложения к стандартной конфигурации


маршрутизации, описанной во врезке " Стандартная конфигурация маршрутизации"
ранее в главе, как показано в листинге 15.35.
Листинг 15.35. Применение стандартной конфигурации в файле Startup. cs
using Microsoft . AspNetCore.Builder;
using Microsoft . Extensions.Dependencylnjection;
u si ng Microsoft . AspNetCore.Routing.Constraints ;
using Microsoft . AspNetCore . Routing ;
using UrlsAndRoutes . Infrastructure ;
namespace Ur l sAndRoutes {
puЫic class Startup {
puЫic void ConfigureServices(IServiceCollect ion services) {
services . Configure<RouteOptions>(options =>
options.ConstraintMap .Add( " weekday ", typeof(WeekDayConstraint) )) ;
services . AddMvc() ;

puЬlic void Configure(IApplicationBuilder арр) {


app . UseStatusCodePages() ;
app . UseDeveloperExceptionPage() ;
app .U seStat i cFi l es() ;
Глава 15. Маршрутизация URL 461
app.UseMvcWithDefaultRoute();

Стандартный маршрут будет сопоставляться с URL, используя следующий


шаблон:

{cont r oller}/{ac t ion }/ {id ?}

Применение маршрутизации с помощью атрибутов


Атрибут Rou te используется при указании маршрутов для индивидуальных
контроллеров и действий. В листинге 15.36 атрибут Route применяется к классу
Custome r Con t roller .

Листинг 15.36. Применение атрибута Route в файле CustomerController.cs


using Mi crosoft . As pNetCore . Mvc ;
us i ng UrlsAndRoutes. Mode ls;
namespace OrlsAndRoute s. Contr ol l ers
puЫ i c c lass Cust ome r Co ntrolle r : Co ntro l l e r
[Route("myroute")]
puЫic ViewResul t Index() => Vi e w( " Res ult ",
new Result {
Controller = nameof(CustomerCo nt r oller) ,
Actio n = name of (Index )
}) ;
puЫic ViewRe su lt Li s t( s t r ing id) {
Res ult r = new Resu l t {
Controller = nameof(Home Control le r) ,
Action = nameof(Li st ) ,
};
r . Data [" id "] = i d ?? " <no value> ";
r . Data [ " ca t cha ll"] = RouteDa t a . Va l ues [ " catcha ll" ] ;
return View( " Re su l t ", r) ;

Атрибут Route определяет маршрут к методу действия или контроллеру , к которо­


му он применен. В листинге этот атрибут применен к методу действия I ndex () сука­
занием myr oute в качестве маршрута, который должен использоваться. Результатом
является изменение набора маршрутов, которые применяются для достижения мето­
дов действий, определенных контроллером Cu stomer (табл. 15.9).

Таблица 15.9. Маршруты для контроллера Customer

Маршрут Описание

/Customer/List Этот URL нацелен на метод действия Lis t () , полагаясь


на стандартный маршрут в файле Star tup . c s
/myro ute Этот URL нацелен на метод действия Index ( )
462 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

Следует отметить два важных момента. Во-первых, когда используется атрибут


Route, предоставленное для его конфигурирования значение применяется при оп­
ределении полного маршрута, так что myrou te становится завершенным URL для
достижения метода действия Index (). Во-вторых, присутствие атрибута Route
предотвращает использование стандартной конфигурации маршрутизации, поэто­
му метод действия Index () больше не будет достижимым с применением URL вида
/Customer/Index.

Изменение имени метода действия


Определение уникального маршрута для одиночного метода действия в большинс­
тве приложений не особенно полезно, но атрибут Route можно использовать более
гибким образом. В листинге 15.37 внутри маршрута применяется специальный мар­
кер [controller] для ссылки на контроллер и настройки базовой части маршрута.

Совет. Изменить имя действия можно также с помощью атрибута ActionName, который
рассматривается в главе 31 .

Листинг 15.37. Использование атрибута Route для переименования действия


в файле CustomerController. cs

using Microsoft . AspNetCore . Mvc;


using UrlsAndRoutes . Models ;
narnespace UrlsAndRoutes . Controllers
puЫic class CustornerController : Controller
[Route("[controller]/MyAction")]
puЫic ViewResult Index() => View( " Resu lt", new Result {
Controller = narneof(CustornerController) ,
Action = narneof(Index)
}) ;
puЫic ViewResult List(string id) {
Result r = new Result {
Controller = narneof(HorneController), Action narneof(List),
) ;
r.Data["id"] = id ?? "<no value> ";
r.Data["catchall"J = RouteData .Values[ " catchall "J;
return View( " Result ", r);

Применение маркера[c ontro ller] в аргументе для атрибута Route довольно


похоже на использование операции nameof и позволяет указывать маршрут к кон­
троллеру без жесткого кодирования имени класса. В табл. 15.10 описан эффект от
наличия атрибута в листинге 15.37.

Таблица 15. 1 О. Маршруты для контроллера Cus tomer

Маршрут Описание

/Customer/List Этот URL нацелен на метод действия List ()


/Customer/MyAction Этот URL нацелен на метод действия Index ()
Глава 15. Маршрутизация URL 463
Создание более сложного маршрута
Атрибут Rout e можно также применять к классу контроллера, позволяя опреде­
лять структуру маршрута, как показано в листинге 15.38.

Листинг 15.38. Применение атрибута Route к контроллеру в файле


CustomerController.cs
using Microsoft .AspNetCo r e . Mvc ;
using OrlsAndRoutes . Model s;
namespace Url s AndRoutes . Con tr ol l ers
[Route("app/[controller]/actions/[action]/{id?}")]
puЬlic class CustomerControl l er : Controller {
puЫic ViewResult Index() => View( " Re s ul t ",
new Resu lt {
Controller = nameof( Cus t omerCo ntro ll er) , Action nameo f (In de x )
}) ;
puЫic ViewResul t List (stri ng i d) {
Result r = new Resu l t {
Controller = nameof(HomeControlle r ) , Action nameof(L is t) ,
};
r . Data[ " id " J = id ?? " <по value> ";
r. Data[ " catchall "J = Ro uteData . Values [" catchal l" J ;
r etu rn View ( "Res u l t", r ) ;

В маршруте определ е на смесь статических и переменных сегментов, а также ис­


пользуются маркеры [cont r o ller] и [act i on] для ссылки на имена класса контрол­
лера и методов действий. В табл. 15.11 описан эффект от создания этого маршрута.

Применение ограничений к маршрутам


Маршруты, определенные посредством атрибутов , могут ограничиваться подобно
м аршрутам, которые определены в файле Startup . cs , с помощью того же самого
встроенного подхода, используемого для маршрутов на основе соглашений . В листин­
ге 15.39 созданное ран е е в главе специальное ограничение применяется к необяза­
тельному сегменту id, определенному в атрибуте Route .
Таблица 15.11. Маршруты для контроллера Cus torner
Маршрут Описание

app/customer/act i on s/index Этот URL нацелен на метод действия I nd e x ( )


app/custome r/act i ons/ i ndex /myid Этот URL нацелен на метод действия I ndex ( )
с необязательным сегментом i d, установлен­
ным в myi d

app/customer/actions/l i st Этот URL нацелен на метод действия List ()


app/customer/actions/l i s t /myid Этот URL нацелен на метод действия Li st ()
с необязательным сегментом id, установлен­
ным вmyi d
464 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC

Листинг 15.39. Ограничение маршрута в файле CustomerController. cs


using Microsoft.AspNetCore . Mvc;
using UrlsAndRoutes.Models;
namespace UrlsAndRout es .Controllers
[Route("app/[controller]/actions/[action]/{id:weekday?}")]
puЬlic class CustomerController : Controller {
puЫic ViewResult Index() => View("Result" ,
new Result {
Controller = nameof(CustomerController),
Act ion = nameof ( Index)
) ) ;

puЬlicViewResult List(string id) {


Result r = new Result {
Controller = name of( HomeController),
Action = nameof(List ) ,
};
r . Data["id"] = id ?? "<no value>" ;
r.Data["catchall"J = RouteData.Values["catchall"J;
return View( "Result ", r);

Можно использовать все ограничения, описанные в табл. 15.8, или, как проде­
монстрировано в листинге 15.39, применять специальные ограничения, которые
были зарегистрированы с помощью службы RouteOptions. Чтобы применить сразу
несколько ограничений, их понадобится соединить в цепочку, отделяя друг от друга
двоеточиями.

Резюме
В этой главе подробно обсуждалась система маршрутизации. Вы увидели, как оп­
ределяются маршруты по соглашениям и с помощью атрибутов, каким образом сопос­
тавляются и обрабатьmаются входящие URL, и как настраиваются маршруты за счет
изменения способа, которым они сопоставляются с сегментами URL, а также за счет
использования стандартных значений и необязательных сегментов. Кроме того, бьmо
показано, каким образом ограничивать маршруты, чтобы сузить диапазон запросов,
для которых они будут давать совпадение, с применением встроенных ограничений и
специальных классов ограничений.
В следующей главе мы рассмотрим генерацию исходящих URL на основе маршру­
тов внутри представлений и выясним, как использовать средство областей, которое
полагается на систему маршрутизации и может применяться для управления круп­

ньrми и сложными приложениями МVС.


ГЛАВА 16
Дополнительные
возможности

маршрутизации

в предыдущей главе было показано,


для обработки входящих
URL,
как использовать систему маршрутизации
должны быть в состоянии применять схему
URL
но это только часть общей картины. Мы также
для генерации исходящих URL, ко­
торые могут встраиваться в представления, чтобы пользователи имели возможность
щелкать на ссылках и отправлять формы обратно приложению, нацеливаясь на кор­
ректный контроллер и действие.
В этой главе мы рассмотрим различные приемы для генерации исходящих URL,
продемонстрируем способы настройки системы маршрутизации за счет замены стан­
дартных классов, реализующих маршрутизацию МVС, и объясним, как использовать
средство областей MVC, которое позволяет разбивать крупное и сложное приложение
MVC на управляемые фрагменты. IЛава завершится рядом советов по применению
схем URL в приложениях MVC. В табл. 16.1 приведена сводка, позволяющая помес­
тить дополнительные возможности маршрутизации в контекст.

Таблица 16. 1. Помещение дополнительных возможностей маршрутизации в контекст

Вопрос Ответ

Что это такое? Система маршрутизации предоставляет возможности, которые вы­


ходят за рамки сопоставления с URL для НТТР-запросов. Имеется
также поддержка для генерации URL в представлениях, замеще­
ния встроенной функциональности маршрутизации специальными
классами и структуризации приложения в виде изолированных

разделов

Чем они полезны? Каждое средство полезно по своим соображ ениям. Наличие воз­
можности генерации URL облегчает изменение схемы URL без
необходимости в обновлении всех представлений . Существование
возможности использования специальных классов позволяет под­

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


Наличие возможности структуризации приложения упрощает пост­
роение сложных проектов

Как они используются? Ищите детальные сведения в разделах настоящей главы


466 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

Окончание табл . 16. 1


Вопрос Ответ

Существуют ли какие­ Конфигурация маршрутизации для сложного приложения может


то скрытые ловушки стать трудной в управлении
или ограничения?

Существуют ли Нет. Система маршрутизации - это неотъемлемая часть ASP.NET


альтернативы? Core
Изменились ли они по Помимо базовых изменений, описанны х в главе 15, дополнитель­
сравнению с версией ные возможности изменились следующим образом.
MVC 5? Специальные классы маршрутизации реализуют интерфейс
IRouter, а не являются производными от класса RouteBase,
как было в ранни х версия х MVC .
Классы, которые реализуют маршрутизацию, теперь ответственны
за предоставление делегатов для обработки запросов, чтобы вы­
пускать ответ, а не только выполнять сопоставление с URL.
Когда применяются области, предполагается нахождение контрол­
леров в главной части приложения, если только они не были деко­
рированы атрибутом Area, даже когда файл класса создан в папке
Controllers области.

В табл . 16.2 прив еден а сводка для н астоящей главы.

Таблица 16.2. Сводка по главе

Задача Решение Листинг

Генераци я элемента а с URL Используйте атрибуты asp- action 16.1-16.5


и asp - controller
Предоставление значений для сег­ Применяйте атрибуты с префи ксом 16.6- 16.7
ментов маршрутизации asp- route -
Генерация полностью заданны х URL Используйте атрибуты asp- procotol , 16.8
asp- host и asp-f ragment
Выбор маршрута для генерации URL Применяйте атрибут asp - route 16.9-16.10
Генерация URL без НТМL-элемента Используйте вспомогательный метод 16.11 - 16.12
Ur 1 . Action () в представлении или
в методе действия

Настройка системы маршрутизации Применяйте метод Configure () 16.13


в классе Startup
Создание специального класса Реализуйте интерфейс IRouter 16.1 4-16.21
маршрутизации

Разбиение приложения на функцио- Создайте области и используйте 16.22-16.28


нальные разделы атрибут Area
Глава 16. Дополнительные возмо ж ности маршрутизации 467

Подготовка проекта для примера


Мы продолжим работать с проектом UrlsAndRoutes из предыдущей главы.
Единственное изменение потребуется внести в клас с Startup, где вызов метода
UseMvcWi thDefaul tRoute () заменяется явным маршрутом с тем же самым резуль­
татом, как показано в листинге 16.1.

Совет. Если вы не хотите воссоздавать примеры вручную , тогда загрузите готовые проекты
Visual Studio из веб-сайта издательства.

Листинг 16.1. Изменение конфигурации маршрутизации в файле Startup. cs

using Microsoft . AspNetCore .Builder ;


using Microsoft . Extensions . Dependencyinjection ;
using Microsoft . AspNetCore.Routing.Constraints ;
using Microsoft . AspNetCore . Routing ;
using UrlsAndRoutes.Infrastructure ;
namespa c e UrlsAndRoutes {
puЬlic class Startup {
puЫi c void ConfigureServices(IServi ceCol l ection services) {
services . Configure<RouteOptions>(options =>
options . ConstraintMap.Add( " weekday ", typeof(WeekDayConstraint)) ) ;
services . AddMvc() ;

puЫic void Configure(IApplicationBui lder арр) {


app . UseStatusCodePages() ;
app . UseDeveloperExceptionPage() ;
app . UseStaticFiles();
app.UseMvc(routes =>
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
}) ;

После запуска приложения браузер запросит стандартный URL, который будет на­
правлен на действие Index контроллера Home (рис. 16. 1).

D Routing )(

--~---°--[~;.~~-~~~s-~~~o;~a~:.==~-=-===~·-===- ~]---~!
Controller: HomeController 1

Action: lndex 1
________________________ )

Рис. 16. 1. Запуск примера приложения


468 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

Генерация исходящих URL в представлениях


Почти в каждом приложении МVС пользователи должны располагать возможностью
перехода от одного представления к другому, что обычно предполагает включение в пер­
вое представление ссылки , нацеленной на метод действия, который генерирует второе
представление. Бьmо бы заманчиво просто добавить статический элемент а (известный
как якорный элемент), указав в его атрибуте href нужный метод действия, например:
<а href="/Home/CustomVariaЬle">This is an outgoing URL</a>
Предполагая, что в приложении применяется стандартная конфигурация марш­
рутизации, такой НТМL-элемент создает ссылку, которая будет нацелена на метод
действия CustomVariaЬle () в контроллере Home. Вручную определяемые URL вроде
этого создавать быстро и просто. Но вместе с тем они чрезвычайно опасны: изменив
схему URL для приложения, вы нарушите работу всех жестко закодированных URL.
После этого придется пройтись по всем представлениям в приложении и обновить
все ссылки на контроллеры и методы действий -утомительный. чреватый ошибка­
ми и сложный в тестировании процесс. Более удачная альтернатива предусматривает
использование для генерации исходящих URL системы маршрутизации, что обеспе­
чивает применение схемы URL для динамического формирования URL способом, ко­
торый гарантирует учет схемы URL приложения.

Генерирование исходящих ссылок


Сгенерировать исходящий URL в представлении проще всего с использованием
дескрипторного вспомогательного класса для якоря, который создаст атрибут href в
НТМL-элементе а , как иллюстрируется в листинге 16.2, в котором приведено добав­
ление к представлению /Views/Shared/Result . cshtml.

Совет. Работа дескрипторных вспомогательных классов объясняется в главе 23.

Листинг 16.2. Использование дескрипторного вспомогательного класса


для якоря в файле Resul t. csh tml
@model Result
@( Layout = null;
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>Routing</title>
<link rel="stylesheet" asp-href-include="liЬ/bootstrap/dist/css/*.min.css" />
</head>
<body class="panel-body">
<tаЫе class="taЫe taЫe-bordered taЫe-striped taЫe-condensed">
<tr><th>Controller :< /th><td>@Model . Controller</td></tr>
<tr><th>Act ion: </th><td>@Mode l.Action</td></tr>
@foreach (string key in Model.Data.Keys) (
<tr><th>@key : </th><td>@Mode l.Da ta [key] </td></tr>
</tаЫе>
<а asp-action="CustomVariaЬle">This is an outgoing URL</a>
</body>
</html>
Глава 16. Доnолнительные возмо жности маршрутизации 469
Атрибут asp-action применяется для указания имени метода действия, на кото­
рый должен быть нацелен URL в атрибуте href. Запустив приложение , можно уви­
деть результат (рис. 16.2).

[j Rou tin9 Х

1 - ~ с [? -~;;;ih-;!:60588
1 ··-------·------- -- -· " ·--- "
1
Controller: HomeController
Action: lndex

This is an outgoing URL

~----------------------------

Рис. 16.2. Использование дескрипторного вспомогательного класса


для генерации ссылки

Дескрипторный вспомогательный :класс устанавливает атрибут href элемента а с


применением текущей конфигурации маршрутизации. Просмотрев НТМL-разметку,
отправленную браузеру , вы заметите , что она содержит следующий элемент:

<а href=" /Home/CustomVariaЫe" >T hi s is an outgoing URL</a>


Может показаться , что для воссоздания вручную определенного URL, который был
приведен ранее , пришлось затратить слишком много дополнительных усилий, но пре­
имущество данного подхода в том, что он автоматически реагирует на изменения в

конфигурации маршрутизации . Чтобы продемонстрировать сказанное , добавим в


файл Star tup. cs новый маршрут (листинг 16.3).
Листинг 16.3. Добавление нового маршрута в файле Startup. cs

using Microsoft . AspNetCore.Bu ilde r;


using Microsoft . Extens i ons . Dependencyinjection;
using Microsoft.AspNetCore . Routing . Constra i n ts;
using Mi crosoft . AspNetCore . Routing ;
using UrlsAndRoutes . Infrastructure ;
namespace UrlsAndRoutes
puЫic class Startup {
puЬlic void ConfigureServices(IServiceCol le ction services) {
services . Configure<RouteOptions>(op tions =>
options . ConstraintMap . Add( " wee kday ", typeo f(WeekDayCon straint) ) );
services . AddMvc() ;

puЫic void Configure(IApplicationBuilder арр) {


app . UseStatusCodePages() ;
app . UseDeveloperExceptionPage() ;
app . UseStaticFiles() ;
app . UseMvc(routes => {
470 Часть 11. Подробные сведения об инфраструктуре ASP.NET Соге MVC

routes.MapRoute(
name: "NewRoute",
template: "App/Do{action}",
defaults: new { controller ="Ноте" }) ;
routes.MapRoute (
name: "default",
template: "{controller=Home)/{action= Index)/{id?)");
}) ;

Новый маршрут изменяет схему URL для запросов, которые нацелены на контрол­
лер Home. Запустив приложение, вы увидите, что это изменение отразилось в НТМL­
разметке, сгенерированной дескрипторным вспомогательным классом:

<а href="/App/DoCustomVariaЫe" > This is an o utgoing URL </a>


Генерирование ссылок с использованием дескрипторного вспомогательного клас­

са решает важную проблему сопровождения. Мы имеем возможность изменять схему


маршрутизации, а исходящие ссьmки в представлениях автоматически отразят изме­

нения без необходимости вручную редактировать представления в приложении.


Когда производится щелчок на ссьшке, исходящий URL применяется для создания
входящего НТГР-запроса, и тот же самый маршрут затем используется для нацелива­
ния на метод действия и контроллер, которые будут обрабатывать запрос (рис. 16.3).

[J Routing Х

~ С 1· Q ~~lh~st:WS Sffl.p;/DoC~s;o~Vи11aЫ~- . -
Controller: HomeCoпtroller 1
1
Action: CLJstomVarlaЫe
1

~.p~--...· 1J ...,,. ..... ~ J


Рис. 16.З. Результатом щелчка на ссылке является применение
исходящего URL во входящем запросе

Сопоставление исходящих URL с маршрутами

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

• Для любой переменной сегмента, определенной в шаблоне URL, должно быть доступно
значение. Чтобы найти значения для каждой переменной сегмента, система маршрути­
зации просматривает сначала предоставленные (посредством свойств анонимного типа)
значения, затем значения переменных для текущего запроса и, наконец, стандартные

значения, определенные в маршруте. (Позже в главе мы еще возвратимся ко второму ис­


точнику значений из числа упомянутых.)
Глава 16. Дополнительные возмо ж ности маршрутизации 471
• Ни одно из значений, предоставленных для переменных сегментов, не должно конфлик­
товать с определенными в маршруте переменными , которые имеют только стандартные

значения . Это переменные, для которых были предоставлены стандартные значения, но


которые не встречаются в шаблоне URL. Например, в показанном ни же определении мар­
шрута myVar является переменной, имеющей только стандартное значение:
route s. MapRoute( "MyRo ut e ", " {control ler}/{action} ",
new { myVar = "true " }) ;
• Для соответствия такому маршруту значение для переменной myVar либо не дол ж но пре­
доставляться , либо должно совпадать со стандартным значением .

• Значения для всех переменных сегментов должны удовлетворять ограничениям маршру­


та. Примеры разных видов ограничений приводились в разделе "Ограничение маршрутов"
предыдущей главы .

Чтобы было совершенно ясно : система маршрутизации не пытается найти маршрут, который
дает наилучшее совпадение . Она находит только первое совпадение и использует данный
маршрут для генерации URL; любые последующие маршруты игнорируются. По этой при­
чине наиболее специфичные маршруты должны определяться первыми. Важно проверить
генерацию исходящих URL. Попытка генерации URL, для которого не удается найти соот­
ветствующий маршрут, приведет к созданию ссылки , содержащей пустой атрибут href:
<а href='"' >This i s an outgo in g URL< / a>
Та кая ссылка корректно визуализируется в представлении, но не будет функционировать
ож идаемым образом, когда пользователь выполняет на ней щелчо к. Если вы генерируете
только URL (как будет показано поз же в главе), тогда результирующим значением будет
null , которое визуализируется в виде пустой строки в представлениях. С помощью име­
нованных маршрутов можно получить определенный контроль над сопоставлением маршру­
тов . За более подробными сведениями обратитесь в раздел " Генерирование URL из специ­
фического м аршрута" далее в главе.

Направление на другие контроллеры

В случае указ а ния атрибута asp - action внутри эл ем ента а де скрипторны й вспо­
м огательный класс пр едполагает, что вы хотите установить в качестве цели действие
в том ж е контроллере, который вызвал визуализацию представления. Чтобы создать
исходящий URL, который направляет на другой контроллер, можно применить атри­
бут asp - controller (листинг 16.4).

Листинг 16.4. Направление на другой контроллер в файле Result.cshtml


@model Result
@( Layou t = null ;
< ! DOCTYPE html>
<html>
<head>
<meta name= " viewport " conte nt=" width=device - width " />
<title>Routing</t i tle>
<link rel= "stylesheet " asp - href -i nclude= "l iЬ/bootstrap/dist/css/* . min . css " />
</head>
<body class= "panel - body " >
<tаЫе class= " taЫe taЬle - bordere d taЬle - striped taЬle - condensed " >
<t r ><th>Cont r ol l er : </th>< td>@Mode l. Contro l le r </td></t r >
<t r><th>Action : </th><td>@ Model . Action</td></tr>
472 Часть 11 . Подробные сведен ия об инфраструктуре ASP.NET Соге MVC

@foreach (string key in Model .Data.Keys) {


<tr><th>@key : </th><td>@Model . Data[key]</td></tr>

</tаЫе>
<а asp-controller="Aclmin" asp-action="Index">This is an outgoing URL</a>
</body>
</html>

После визуализации представления вы увидите, что сгенерирована следующая


НТМL-разметка:

<а href= "/Admin" >This targets ano the r controller</a>


Запрос к URL, 1юторый нацелен на метод действия Index () контроллера Admin,
был выражен дескрипторным вспомогательным классом как /Admin. Системе марш­
рутизации известно, что маршрут, определенный в приложении. по умолчанию будет
использовать метод действия Index (), позволяя опускать ненужные сегменты.
При выяснении, каким образом нацеливаться на заданный метод действия. сис­
тема маршрутизации включает маршруты, которые были определены с применени­
ем атрибута Route. В листинге 16.5 атрибут asp - controller нацелен на действие
I n dex в контроллере Customer, к которому был применен атрибут Route в главе 15.

Листинг 16.5. Направление на действие, декорированное атрибутом Route,


в файле Resul t. csh tml

@model Result
@{ Layout = null;
< ! DOCTYPE html>
<html>
<head>
<meta name= " viewport " content= " widt h=device - width " />
<title>Routing</title>
<link rel=" stylesheet " asp-href-i nclude= " liЬ/bootstrap/dist/ css/* . min . css " />
</head>
<body class= " panel - body " >
<tаЫе class= " taЫe taЫe - bordered taЫe-striped taЫe - condensed " >
<tr><th>Controller : </th><td>@Model . Controller</td></tr>
<tr><th>Action : </th><td>@Model . Action</td></t r >
@foreach (string key in Model . Data . Keys) {
<tr><th>@key : </th><td>@Model.Data[key ] </td></tr>

</tаЫе>
<аasp-controller="Customer" asp-action="Index">This is an outgoing URL</a>
</body>
</html>

Вот иакая ссылка сгенерируется:

<а href= "/app/Customer/actions/Index" >This is an outgoing URL</a>


Результат соответствует атрибуту Route, который применялся к контроллеру
Customer в главе 15:

[Route("app/[controller]/actions/[action]/{id:weekday? }")]
puЫic class CustomerControl l er : Contro l ler {
Глава 16. Дополнительные возмо ж ности маршрутизации 473

Передача дополнительных значений

Системе маршрутизации можно передавать значения для переменных сегментов


за счет определения атрибутов с именами, начинающимися со строки asp - ro ut e -,
за которой следует имя сегмента. Таким образом, атрибут asp - route -id использует­
ся для установки значения сегмента id (листинг 16.6).

Листинг 16.6. Предоставление значений для переменных сегментов


в файле Resul t. csh tml
@model Result
@{ Layout = null ;
<!DOCTYPE html>
<html>
<head>
<meta name="viewport " content=" width=device - width " />
<tit le >Ro uting</title>
<link rel=" stylesheet" asp-href-include=" liЬ/bootstrap/dist/ css/* .rnin. css " />
</head>
<body class= " panel - body " >
<tаЫе class= " taЫe taЫe-bordered taЫe -st riped ta Ьle-condensed" >
<tr><th>Control ler: </ th ><td>@Model .Controller</td></tr>
<tr><th>Act ion:< / th><td>@ Model.Action</td ></ tr >
@foreach (string key in Model.Data.Keys) {
<tr><t h>@key : </th><td>@Model .Data[key]</td></tr>
}
</tаЫе>
<а asp-controller="Horne" asp-action="Index" asp-route-id="Hello">
This is a n outgoing URL
</а>
</body>
</h t rnl>

Здесь предоставляется значение для переменной сегмента по имени id. Если при­
ложение применяет маршрут, показанный в листинге 16.3, тогда при визуализации
представления будет получена следующая НТМL-разметка :

<а href=" /App/Doindex? id=Hello">This is an outgoing URL</ а>


Обратите внимание, что значение сегмента было добавлено как часть стро1ш за­
проса, чтобы вписываться в шаблон URL. определенный маршрутом. Причина свя­
зана с отсутствием переменно й сегмента, которая бы соответствовала id в этом
маршруте. Чтобы решить проблему, необходимо отредактировать файл Startup. cs,
оставив только маршрут, который имеет сегмент id (листинг 16.7).

Листинг 16. 7. Редактирование маршрутов в файле Startup. cs


using Microsoft .AspNetCore .Builder ;
using Microsoft . Extensions.Dependencyi nje ction ;
using Microsoft . AspNetCore.Routing.Constraints ;
using Microsoft.AspNetCore.Routing ;
using UrlsAndRoutes . Infrastructure;
namespace UrlsAndRoutes {
474 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC

puЫic class Startup {


puЫic void ConfigureServices(IServiceCollection services) {
services . Configure<RouteOptions>(options =>
options.ConstraintMap . Add( " weekday ", typeof(WeekDayConstraint))) ;
services.AddMvc();

puЫic void Configure(IApplicationBuilder арр) {


app.OseStatusCodePages() ;
app . UseDeveloperExceptionPage() ;
app.OseStaticFiles();
app . OseMvc( rout es => {
// routes.MapRoute(
11 name: "NewRoute " ,
11 template: "App/Do{action}",
11 defaul ts : new { con troller = "Home" } ) ;
r oute s.MapRoute(
name : " default ",
template: " {controller=Home}/{actio n =Index}/{ id? }" ) ;
}) ;

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


класс выдал следующий НТМL-элемент, в котором значение свойства id включено как
с егмент URL:
<а href= " /Home/Index/Hello" >This is an outgoing ORL</a>

Повторное использование переменных сегментов

При описании способа сопоставления маршрутов для исходящих URL объяснялось, что ког­
да система маршрутизации пытается найти значения для каждой переменной сегмента в
шаблоне URL маршрута, она просматривает текущий запрос. Такое поведение сбивает с
толку многих программистов и может вылиться в длительный сеанс отладки.

Предположим, что прило ж ение имеет единственный маршрут:

app .U seMvc(routes => {


r outes .MapRoute (name : " MyRoute ",
template : " { controller} / { action} / { color} / { page} ") ;
}) ;

Пусть в текущий момент пользователь запросил URL вида /Home/Index/Red/100 и ви­


зуализируется показанная ниже ссылка:

<а asp - controller= " Home" asp -a ction= "Index" asp -r oute - page="789 " >
This is an outgoing ORL
</а>
Глава 16. Дополнительные возмож ности маршрутизации 475
Мож но было бы ожидать , что система маршрутизации не сумеет выполнить сопоставление с
маршрутом, т. к . не было предоставлено значение для переменной сегмента colo r , к тому
же для нее не определено стандартное значение . Однако это не так. Система маршрути ­
зации будет сопоставлять с определенным ранее маршрутом. В результате сгенерируется
следующая НТМL-разметка:

<а hre f = " / Home/ Index/Red/789 " >This i s a n outgo i ng URL </a>

Система маршрутизации достаточно интеллектуальна, чтобы провести сопоставление с


маршрутом, поскольку при генерировании исходящего URL она будет повторно использо­
вать значения переменных сегментов из входящего URL. В этом случае переменная color
получит значение Red, т. к. его включал URL, с которого стартовал пользователь.

Это не поведение, предназначенное для крайних случаев . Система маршрутизации будет


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

Я настоятельно рекомендую не полагаться на описанное поведение и предоставлять значе­


ния для всех переменных сегментов в шаблоне URL. Если опираться на данное поведение,
то код станет гораздо труднее для восприятия, к тому же в коде будут делаться допущения
относительно порядка, в котором пользователи выдают запросы, что обернется проблема­
ми при сопровождении приложения .

Генерирование полностью заданных URL


Все ссылки, которые генерировались до сих пор, содержали относительные URL,
но дескрипторный вспомогательный иласс для якорного эл емента способен также ге­
нерировать полностью заданные URL, как показано в листинге 16.8.

Листинг 16.8. Генерирование полностью заданного URL в файле Resul t. csh tml

@model Resu l t
@{ Layout = null ;
< ! DOCTYPE html>
<htm l >
<h ead>
<meta name= " vie wpo rt" con t en t = " wi dt h=dev ice-wid t h " />
<title>Rout in g< /titl e>
<link rel= " styl e sheet" asp - href - inc lude ="liЬ/b ootstra p /d i st/c s s /*.min. css " />
</head>
<body class= " pane l- body " >
<tаЫе class= " taЫe ta Ы e - bordered t aЫe - striped taЬle - condensed " >
<tr><th>Con tro l ler : </th><td>@Model . Controller</ t d></t r >
<tr><th>Action : </th><td>@Model . Ac ti on</ t d></tr>
@foreach (string key in Model . Da t a .Keys) {
<tr><th>@key : </th><td>@Mode l. Dat a[ key ] </td></tr>
}
</tаЫе>
<а asp-controller="Home" asp-action="Index" asp-route-id="Hello"
asp-protocol="https" asp-host="myserver.mydomain.com"
asp-fragment="myFragment">This is an outgoing URL
</а>
</body>
</html>
476 Часть 11. Подробные сведения об инфраструктуре ASP.NET Соге MVC

Атрибуты asp -p ro t ocol , asp - ho s t и asp-fra gme nt применяются для указания


протокола (ht t p s в листинге) , имени сервера (my s erver . my doma in. com) и фрагмен­
та URL (myFragme n t ). Эти значения объединяются с выводом из систе мы маршрути­
зации для создания полностью заданного URL, который вы можете увидеть , запустив
приложение и просмотрев отправленную браузеру НТМL-разм етку:

<а href= " https : //myse r ver . mydomain . com/Home/Index/Hello#myFragment " >
This is an outgo i ng URL
</а>

Будьте осторожны при использовании полностью заданных URL, поскольку они со­
здают зависимости от инфраструктуры прилож ения и. когда инфраструктура изменя­
ется, вы должны не забыть о внесении соотв етствующих измен е ний в представления
мvс.

Генерирование URL из специфического маршрута


В предшествующих прим е рах систе ма маршрутизации выбирала маршрут, кото­
рый будет применяться для генерации URL. Если важно генерировать URL в сп е ци­
фическом формате, тогда можно указать маршрут, подлежащий использованию для
генерации исходящего URL. Чтобы продемонстрировать это в работе, добавьте в файл
Startup . c s новый маршрут, обеспечив наличие в примере приложения двух м арш­
рутов (листинг 16.9).

Листинг 16.9. Добавление маршрута в файле Startup. cs


u s in g Mi c r osoft .AspNetCo r e . Bu i lde r;
u s ing Microsoft . Extens i ons . Dependenc y i n ject i on ;
using Microsoft. AspNetCore .Routing.Constraint s ;
us ing Microsoft . AspNet Core . Ro ut ing ;
u s ing Url sAndRo u tes .I nf r astr u cture ;
namespace UrlsAndRoutes
p u Ыi c c l ass Sta r tu p {
puЫic void ConfigureServices(IS e rviceCol l ection services) {
services . Conf i gure<RouteOptions>(options =>
opt i ons . Co n strai n tMap . Ad d( " weekday ", typ eof( WeekDayConstraint))) ;
serv ices . AddMv c() ;

puЫ i c void Configu r e( I Appl i cat i o nB uilder арр) {


app .U seStat u sCode Pages() ;
app . UseDeveloperExceptionPage() ;
app . UseStaticFile s () ;
app . UseMvc(routes => {
routes . MapRoute(
name : " default ",
t e mplate : " {controller=Home)/{action=Index}/{id?} " ) ;
routes.MapRoute(
name: "out",
template: "outbound/{controller=Home}/{action=Index}");
)) ;
Глава 16. Дополнительные возможности маршрутизации 477
Представление , приведенное в листинге 16.10, содержит два якорных элемента,
каждый из которых указывает те же самые контроллер и действие . Разница в том,
что второй элемент применяет атрибут дескрипторного вспомогательного класса
asp - rou t e для указания на необходимость использования маршрута ou t при гене­
рации URL, относящегося к атрибуту h re f.

Листинг 16.10. Генерация URL в файле Result.cshtml

@model Re s ul t
@( La yout = null;
< !DOCTYPE html>
<html>
<head>
<meta name ="v i ewport " content= "wi dt h=devic e - wi dth " />
<t itle>Routing</title>
<l i nk re l="stylesheet " asp-h r ef- incl ude="liЬ/b o otstrap /dist/cs s / * .m i n.css" />
</head>
<body cla ss=" pane l- body ">
<tаЫе class= " ta Ы e ta Ьl e - bo r de r ed ta Ы e -s t rip ed t a Ы e - conde n s e d " >
<tr><th>Controller : </th>< t d>@Mode l . Co nt roll e r</ t d></tr>
<tr><th>Action : </th><td>@Model . Acti on</td></ t r>
@foreac h (s tr in g key in Mode l. Data .Keys ) (
<tr> <th >@ ke y : </th>< t d>@Model.Data[key] </td>< / t r>

</tаЫе>
<а asp-controller="Home" asp-action="CustomVariaЬle">
This is an outgoing URL</a>
<а asp-route="out">This is an outgoing URL</a>
</body>
</html>

Атрибут a s p - rou te может применяться только в ситуации, когда отсутствуют ат­


рибуты asp - control l e r и as p - act i on, т.е. выбирать можно лишь специфический
маршрут для контроллера и действия , которые вызывали визуализацию представле­
ния. Запустив приложение и запросив URL вида / H ome / C ustomVa r iaЬle , вы полу­
чит е дв а разных URL, которые сгенерировали маршруты:

<а href =" /Home/Cus t om V ariaЬl e" > T his is a n outgo i ng URL</a>
<а hr ef= " /outbound " >This is an out go i ng URL</a>

Аргумент против именованных маршрутов

Проблема с зависимостью от имен маршрутов при генерации исходящих URL связана с тем,
что это нарушает принцип разделения обязанностей , который занимает центральное место
в паттерне проектирования MVC. При генерации ссылки либо URL в представлении или ме­
тоде действия необходимо сосредоточиться на действии и контроллере , куда пользователь
будет направлен, а не на формате URL, который будет использоваться. Привнося знание о
различных маршрутах в представления или контроллеры, мы создаем зависимости, которых

можно было бы избежать. В своих проектах я стараюсь не именовать маршруты (указывая


null для аргумента name ) и предпочитаю применять комментарии в коде, чтобы не забыть
о том, для чего предназначен каждый маршрут.
478 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

Генерация URL (без ссылок)


Ограничение дескрипторных вспомогательных классов заключается в том, что они
трансформируют НТМL-элементы и не могут быть легко переориентированы, если
нужно генерировать URL для приложения без окружающей НТМL-разметки.
Инфраструктура MVC предлагает вспомогательный класс, который можно исполь­
зовать для создания URL напрямую, доступный через метод Url . Action (),как де­
монстрируется в листинге 16. 11.

Листинг 16. 11 . Генерация URL в файле Resul t. csh tml

@model Result
@{ Layout = null ;
< ' DOCTYPE html>
<html>
<head>
<meta name= " viewpo rt" content= " width=device-width " />
<title>Routing</title>
<link rel= "st ylesheet" asp-href-include= "l iЬ/bootstrap/dist/css/* . min.css" />
</head>
<body class= " panel-body">
<tаЫе class= " taЫe taЬle-bordered taЫe-striped taЬle-condensed " >
<tr><th>Controller : </th><td>@Model.Controller </td></tr>
<tr><th>Action:</th><td>@Model.Action</td></tr>
@foreach (string key in Model . Data . Keys) {
<tr><th>@key :< /th><t d>@Model.Data[key]< /td></t r>

</tаЫе>
<p>URL: @Url.Action("CustomVariaЬle", "Home", new { id = 100 })</р>
</body>
</htrnl>

В аргументах метода Url .Action () указываются метод действия, контроллер и


значения для любых переменных сегментов. В результате добавления разметки, вы­
деленной полужирным в листинге 16.11, генерируется следующий вывод:

<p>URL: /Home/CustomVariaЬle/100</p>

Генерирование URL в методах действий


Метод Url .Ac t ion () может применяться также и в методах действий для созда­
ния URL внутри кода С#. В листинге 16.12 модифицирован один из методов действий
контроллера Horne, чтобы генерировать URL с использованием Url. Act ion ().

Листинг 16.12. Генерация URL внутри метода действия в файле HomeController. cs

using Microsoft . AspNetCore . Mvc;


using UrlsAndRoutes.Models;
namespace UrlsAndRoutes.Controllers
puЫic class HomeCo ntroller : Controller
Глава 16. Дополнительные возможности маршрутизации 479
puЫic ViewResul t Index () => View ( "Resu l t ",
new Result {
Controller = nameof(HomeController) ,
Action = nameof(Index)
}) ;
puЫic ViewRes ult CustomV ariaЬle ( str ing i d) {
Result r = new Result {
Controller = nameof(HomeController) ,
Action = nameof(CustomVariaЬle) ,
};
r.Data[ " id " J = id ?? " <no value> ";
r.Data["url"] = Url.Action("CustomVariaЬle", "Ноте", new { id = 100 }) ;
return View ( " Resu l t", r) ;

Запустив пр иложение и запросив URL вида /Home/CustomVariaЬle, вы заметите,


что в таблице появилась строка, которая отображает этот URL (рис . 16.4).

[j Routil\g х

~ ' С ~a lhost:60588/Horne/CustornVarraЫe

Controller: HomeController
Action: Cu stomVariaЫe

1 id : <ПО value>
1 ~~l• /Hom&C"'tom,.V-/-:ro-i-~be/-le-~-~-:-o_m_V_a-ri_a_Ы_e_/1_0_0-.
L _ __
Рис. 16.4. Генерация URL в методе действия

Настройка системы маршрутизации


Вы уже видели, насколько гибкой и конфигурируемой является система маршру­
тизации, но если она не удовлетворяет существующим требованиям, тогда ее поведе­
ние можно настроить. В этом разделе будут показаны два способа такой настройки.

Изменение конфигурации системы маршрутизации


В главе 15 вы узнали, как конфигурировать объект RouteOptions в файле
Startup . cs для настройки специального ограничения маршрута.
Объект RouteOpt i ons также применяется для конфигурирования ряда средств
маршрутизации с помощью свойств, описанных в табл. 16.3.
480 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC

Таблица 16.З. Конфигурационные свойства RouteOptions


Имя Описание

AppendTrailingSlash Когда это свойство типа bool равно true, к генерируемым


системой маршрутизации URL добавляется завершающий
символ косой черты. Стандартным значением является false

LowercaseUrls Когда это свойство типа bool равно true , результирующие


URL преобразуются в нижний регистр, если контроллер, дейс­
твие или значения сегментов содержат символы нижнего ре­

гистра . Стандартным значением является false

В листинге 16.13 приведен файл Startup . cs с добавленными операторами для


настройки обоих конфигурационных свойств из табл. 16.3.

Листинг 16.1 З . Конфигурирование системы маршрутизации в файле Startup. cs


using Microsoft . AspNetCore . Builder;
using Microsoft.Extensions.Dependencyinjection;
using Microsoft . AspNetCore . Routing . Constraints ;
using Microsoft.AspNetCore . Routing;
using UrlsAndRoutes.Infrastructure;
namespace UrlsAndRoutes {
puЫic class Startup {
puЫic void ConfigureServices(IServiceCollection services) {
services.Configure<RouteOptions>(options => {
options.ConstraintMap . Add("weekday ", typeof(WeekDayConstraint)) ;
options.LowercaseUrls = true;
options.AppendTrailingSlash = true;
}) ;
services.AddMvc();

puЫic void Configure(IApplicationBu ilder арр) {


app.UseStatusCodePages() ;
app . UseDeveloperExceptionPage();
app .UseStaticfiles() ;
app . UseMvc(routes => {
routes.MapRoute(
name: "default ",
template : " {control ler =Home}/{action=Index)/{id?} " ) ;
routes . MapRoute(
name: "out" ,
ternplate: " outbound/{contr oll er=Horne}/{action=I ndex)");
}) ;

Запустив приложение и просмотрев URL, которые сгенерировала система марш­


рутизации, вы увидите , что изменение конфигурационных свойств об ес печило пре­
образование URL в нижний регистр и добавление к ним завершающей косой черты
(рис . 16.5).
Глава 16. Дополнительные возмо жн ости маршрутизации 481

D Routing х

С [.--··--·-·--···--------·-·-----····"------·· ----·-·---··l
Ф locall1ost:б0588/Home/CunomVaпaЬle -Q-
'---------------·
- -· - .... - .
~ ~ ~ ' -·-~· ~- --- - ....... --
- · - - -......
' --
Controller: HomeController
Action: CustomVariaЫe

id: <По value>


url: /home/customvariaЫe/1001

1 URL: /home/cus1omvaпaЫe/1001
!_____________________. - ---·--- ---·-·----- ------·-- - - - " - ·--
J
Рис . 16.5. Конфигурирование системы маршрутизации

Создание специального класса маршрута


Если вам не нравится способ. которым система маршрутизации производит сопос­
тавление с URL, или для вашего приложения необходимо реализовать что-то специ­
фическое, то можете создать собственные шшссы маршрутизации и использовать их
для обработки URL. Инфраструктура ASP.NET предоставляет интерфейс Microsoft .
AspNetCore . Routing . IRouter, который можно реализовать для создания специаль­
ного маршрута. Вот как выглядит определение интерфейса IRouter:

using System . Threading . Tasks ;


namespace Microsoft.AspNetCore.Routing
puЬlic interf ace IRouter {
Task RouteAsync(RouteContext context) ;
VirtualPathData GetVirtualPath(VirtualPathContext context);

Чтобы создать специальный маршрут, понадобится реализовать метод


RouteAsync () для обработки входящих запросов и метод GetVirtualPath ().если
нужно генерировать исходящие URL.
В целях демонстрации мы создадим специальный класс маршрутизации, который
будет обрабатывать запросы к унаследованным URL. Предположим, что мы перенесли
КЮ<ое-то существующее приложение в MVC, но есть пользователи, которые поместили
старые URL в закладки или жестко закодировали их в сценариях. Мы хотим по-пре­
жнему поддерживать такие старые URL. Мы могли бы обработ ать это с применением
обычной системы маршрутизации, но решение данной задачи будет хорошим приме­
ров для целей настоящего раздела.

Маршрутизация входящих URL


Чтобы понять, Iiaк работают специальные маршруты, мы начнем с создания одного
такого маршрута, который будет обрабатывать каждый аспект запроса самостоятель­
но, не используя контроллер и представление. Для этого в папке Infrastructure со­
здается файл класса по имени LegacyRoute . cs с реализацией интерфейса IRouter,
приведенной в листинге 16. 14.
482 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Саге MVC

Листинг 16.14. Содержимое файла LegacyRou te. cs из папки Infras tructure


us in g Mic r osoft. AspNe tCo re.H tt p;
us i ng Mic r os oft . As pNetCo r e . Ro ut i ng ;
using System ;
using System . Linq ;
usi ng System.Text ;
using System. Threadi ng . Tasks ;
namespace Ur l sAndRoutes .In f r a s truct ure
p uЫ i c class Lega c yRou t e : IRoute r {
private s tring[ ] urls ;
p u Ьlic Le gac yRo ute(p a rams s tr i ng [] tar ge tU rls ) {
this . urls = t a rget Urls ;

puЫic Ta s k Ro uteAs ync (RouteCo ntext context) {


string requested Url = cont e xt .H ttpCont e xt . Request . Path
. Val ue . TrimEnd( ' / ' ) ;
if (urls . Contains( r equestedUrl , StringComparer . Ordina li gnoreCase) )

co ntext .Ha nd le r = as yn c c tx =>


HttpResponse response = ctx . Respon s e ;
byte [] byte s = Encoding . ASCII . GetByt e s ( $ "URL : { reque stedU rl } " ) ;
awa i t r espo ns e.B ody . WriteAsyn c( byte s , О, bytes . Len gth) ;
);

r et urn Task.Compl e te dTask ;

puЫic Vi rtua lP athData GetVirt ua lPat h (Virtua lP athContext context) (


return null;

Класс LegacyRoute реализует интерфейс IRoute r, но определяет код только для


метода Rout e Async () , который применяется для обработки входящих запросов;
вскоре мы добавим поддержку и для исходящих URL.
В методе RouteAsync () находится совсем немного операторов, но в своей р аботе
они полагаются на несколько важных типов ASP.NET. Лучше всего начать исследова­
ние с сигнатуры метод а :

pu Ы ic as ync Tas k RouteAsync( RouteContext context ) {

Метод Rou t eAsync ( ) отвеча ет за оценку, может ли запрос быть обработан, и е сли
может, то за управление процессом, посредством которого генерируется отв ет, отправ­

ля емый обратно клиенту. Такой проц е сс выполняется асинхронно , по этому метод


RouteAsync () возвращает объе1п Ta s k.
М етод RouteAsync () вызывается с аргументом Ro u teContext , который предо­
ставляет доступ ко всему, что известно о запросе, и предлагает средства , требу емые
для отправки ответа клиенту . Класс Rou te Cont e xt находится в пространстве имен
Mi crosoft . AspNetCore . Routing и определяет три свойства, описанные в табл. 16.4.
Глава 16. Дополнительные возмож ности маршрутизации 483
Таблица 16.4. Свойства, определенные в классе RouteContext

Имя Описание

RouteData Это свойство возвращает объект Microsoft . AspNetCore .


Routing . RouteData. При написании специального маршрута,
который полагается на средства MVC ( как объясняется в следующем
разделе) , данный объект используется для определения контроллера,
метода действия и аргументов, применяемых для обработки запроса

HttpContext Это свойство возвращает объект Microsoft . AspNetCore . Http .


HttpContext, который предоставляет доступ к деталям НТТР ­
запроса и предназначен для выпуска НТТР-ответа

Handler Это свойство используется для предоставления системе маршру­


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

Система маршрутизации вызывает метод RouteAsync () каждог о маршрута в


приложении и после каждого вызова проверяет значение свойства Handler. Если
это свойство установлено в RequestDelegate, тогда маршрут пр едоставл яет систе­
ме маршрутизации делегат, который может об работать запрос, и такой деле гат вызы­
вается для генерации ответа. Ниже показана сигнатур а делегата RequestDelegate,
определенного в пространстве имен Microsoft . AspNetCore. Http:
using System . Threading .Tasks ;
namespace Microsoft .AspNetCore . Http
puЫ ic delegate Task RequestDelegate(HttpContext context) ;

Делегат RequestDelega te принимает объект HttpContext и возвращает объект


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

обр аботать запрос, и он а сгенерирует ответ


404 - Not Found (404 - не найдено).
С этой целью реализация метода RouteAsync () должна установить, может ли она
обработать запрос, для чего обычно требуется объект HttpContext. В рассматрива­
емом примере применяется свойство HttpContext . Request, которое воз вращает
объект Microsoft . AspNetCore . Http . HttpRequest, описывающий запрос. Объ ект
HttpRequest предоставляет доступ ко всей информации о запрос е, включая заголо в­
ки , тело и детали того, откуда запрос произ ошел , но мы заинтересованы в свойстве
Path, пос кольку оно дает подробные сведения относительно URL, зап рошенного кли­
енто м. Свойство Path возвращает объект PathString, предлагающий удобные ме­
тоды для объединения и сравнения путей URL. Мы ис пол ьзуем свойство Value , т.к.
оно дает полный раздел пути URL в виде строки, которую можн о сравнить с набором
поддерживаемых URL, полученным конструктором LegacyRoute.

string requeste.d Url = context.HttpContext.Request. Path.Value . TrimEnd ( ' / ' ) ;


if (urls . Contains( requestedUrl, StringComparer . Ordina li gnoreCase)) {

Здесь с помощью метода TrimEnd () из URL удаляется завершающая косая черта


( если она есть) . которая могла быть до бавлена либо пользователем, либо в результате
484 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

установки конфигурационного свойства AppendTrailingSlash, описанного в разде­


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

contex t.H andler = async ctx => {


HttpResponse response = ctx . Response;
byte(] bytes = Encoding.ASCII.GetBytes($"URL: {requestedUrl}");
await response.Body.WriteAsync(bytes , О , bytes.Length);
};

Свойство HttpContext. Response возвращает объект HttpResponse , который


может использоваться для создания ответа клиенту и предоставляет доступ к заголов­

кам и содержимому, подлежащему отправке клиенту. Метод HttpResponse . Body .


Wr i teAsync () применяется для асинхронной записи в качестве ответа простой стро­
ки ASCII. Такой прием не будет предприниматься в реальном проекте. но он позволяет
выпустить ответ, не выбирая и не визуализируя представления (хотя в следующем
разделе будет ПОI{азано, как это делать).
Когда свойство Handler установлено, системе маршрутизации известно, что ее
поиск маршрута закончен и можно вызывать делегат для генерации ответа клиенту.

Применение специального класса маршрута


Расширяющий метод MapRoute (), который до сих пор использовался для со­
здания маршрутов, не поддерживает применение специальных классов маршрутов.

Чтобы воспользоваться классом LegacyRoute , придется задействовать другой подход


(листинг 16.15).
Листинг 16.15. Применение специального класса маршрутизации в файле startup. cs

using Microsoft . AspNetCore . Builder ;


using Microsoft.Extensions.Dependencyinjection ;
using Microsoft . AspNetCore.Routing . Constraints ;
using Microsoft . AspNetCore . Routing ;
using UrlsAndRoutes . Infrastructure ;
namespace UrlsAndRoutes
puЫic class Start up {
puЫic void Config ureServices(I ServiceCollection services) {
services . Configure<RouteOptions>(options => {
options . ConstraintMap . Add( "wee kday ", typeof(WeekDayConst r aint)) ;
opt i ons .Lowe rcaseUrls = true;
options . AppendT r ail i ngSlash = true;
}) ;
services . AddMvc() ;

puЫic void Configure(IApplicationBuilder арр) {


app .UseStatusCodePages() ;
app.UseDeveloperExceptionPage();
app.UseStaticFi l es() ;
Глава 16. Дополнительные возмо ж ности маршрутизации 485
app . UseMvc(routes =>
routes.Routes.Add(new LegacyRoute(
11
/articles/Windows 3.1 Overview.html",
11
/otd/.NET_l.O_Cla;s_Library"));
routes . MapRo ute(
name : "de f a ult ",
template : " {controller=Home }/ {a c ti on= Index }/{ i d ?}" ) ;
routes.MapRoute(
name : "out ",
template : "outbo und/{cont r oll e r=Home)/{act i on=I ndex) ");
}) ;

При использовании специ альных классов потребу ется вызвать метод Ad d () на


коллекции маршрутов, чтобы зар егистрировать класс реализации IRou te r. В этом
пример е аргументами конструктора LegacyRoute являются унаследованные URL, ко­
торы е должен поддерживать специальный маршрут. Запустив приложение и запросив
/articles/W i ndow s_ З . l _ Ove r view . h tml, можно увидеть результат. Специальный
м а ршрут отобраз ит запрошенный URL (рис. 16.6).

[J l0<alhost:60S88/articles: Х

l~f- ·. С Lso_~~c~~~t:бOSB~:r~~cles.№~doщs;~.1_0verv1e111.html -~ :
--·-·--------~---··--· ~---- -----~~---- ·

UR L: /articles/Windows_З . l_Overview .html


-· ... •· - ----
J
------"------------------------·
Рис. 16.6. Использование специального маршрута

Маршрутизаци я на контроллеры MVC


Существует крупная брешь между сопоставлением с простыми строками URL и
применением системы I{QНТроллеров , действий и представлений Razor инфраструк­

туры MVC. К счастью, при создании специальных маршрутов реализовывать такую


функциональность самостоятельно не понадобится , потому что для выполнения всей
тяжелой р аботы может быть задействован класс, который MVC использует " за кули­
сами". Чтобы подготовиться к применению инфраструктуры МVС, добавьте в папку
Control l ers файл класса по имени Le gacyCo n t r ol l er . cs с содержимым, приве­
де нным в листинге 16.16.

Листинг 16.16. Содержимое файла LegacyController.cs из папки Controllers

using Mic r osoft . AspNetCore . Mvc ;


namespace UrlsAndRo utes . Contro ll e r s
puЬlic class Leg a c yCo ntro l ler : Con tr ol l e r
puЫic ViewRes ult GetLegacyUrl(s t ri ng legacyUrl)
=> Vi ew((object)legacyUrl) ;
486 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC

Метод действия GetLegacyURL () в этом простом контроллере принимает п ара ­


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

Совет. Обрат и те внимание , что в листинге 16.16 аргумент метода View() приводится к
типу obj ect. Одна из перегруженных версий метода View () прини мает параметр типа
s tr ing, указывающий имя представления для визуализации , и без приведения компи­
лятор С# выберет именно ее. Во избежание этого мы выполняем приведение к obj ect,
чтобы однозначно вызывалась перегруж енная версия, которая принимает модель пред­
ставления и применяет стандартное представление . Проблему можно было бы также
решить за счет использования перегруже нной версии, принимающей как и м я представ­
ления, так и модель представления, но предпочтительнее не делать явны х ассоциаций
между методами действий и представлениями , если это возмож но. За дополнительными
сведениями обращайтесь в главу 17.

Создайте папку Views/Legacy и поме стите в не е файл пр едс тавл ения по имени
GetLegacyUrl. cshtml с содержимым из листинга 16.17. Новое представление отоб­
ражает значение модели, которое покажет запрошенный клиентом URL.

Листинг 16.17. Содержимое файла GetLegacyUrl.cshtml из папки Views/Legacy

@model string
@{ Layout = null;
< !DOCTYPE html>
<htm l >
<head>
<meta name="vi ewport " content= " width=device-width " />
<title>Routing</tit l e>
<link rel="stylesheet" asp-href-include="liЬ/bootstrap/dist/css/* . min . css " />
</head>
<body class= "panel - body " >
<h2>GetLegacyURL</h2>
Th e URL requested was : @Model
</body>
</html>

В листинге16.18 класс LegacyRoute модифицирован тю<, чтобы обрабатываемые


им URL маршрутизировались на действие GetLegac yUr 1 конт ролл ера Legacy.

Листинг 16.18. Маршрутизация на контроллер в файле LegacyRou te. cs

using Microsoft . AspNetCore . Http ;


using Microsoft . AspNetCore.Routing ;
using System;
using System .Li nq ;
using System.Text ;
using System.Threading.Tasks ;
using Microsoft . AspNetCore.Mvc . Internal;
using Microsoft.Extensions.Dependencyinjection ;
namespace UrlsAndRoutes.Infr ast ru cture {
Глава 16. Дополнительные возможности маршрутизации 487
puЫic class LegacyRoute : IRouter
private string[] urls;
private IRouter mvcRoute;
puЫic LegacyRoute(IServiceProvider services,
params string[] targetUrls)
this.urls = targetUrls ;
mvcRoute = services.GetRequiredService<MvcRouteHandler>();

puЫic async Task RouteAsync(RouteContext context) {


string requestedUrl = context.HttpContext . Request . Path
. Value.TrimEnd('/');
if (urls.Contains(requestedUrl, StringComparer . OrdinalignoreCase))

context.RouteData.Values["controller"] = "Legacy";
context.RouteData.Values["action"] = "GetLegacyUrl";
context.RouteData.Val.ues["l.egacyUrl"] = requestedUrl;
await mvcRoute.RouteAsync(context);

puЫic VirtualPathData GetVirtualPath(VirtualPathContext context) {


return null;

Класс Microsoft . AspNetCore. Mvc. In ternal. MvcRouteHandler предоставляет


механизм, посредством которого переменные сегментов controller и action при­
меняются для нахождения класса контроллера, выполнения метода действия и воз­
вращения результата клиенту. Этот класс был написан таким образом. что к нему
можно обращаться из специальной реализации IRouter, которая предоставляет зна­
чения con t roller и action, а также любые другие требующиеся значения, подоб­
ные аргументам метода действия.
В листинге 16.18 создается новый экземпляр класса MvcRouteHandler, которому
делегируется задача нахождения :класса контроллера. Для этого необходимо предо­
ставить данные маршрутизации:

context.RouteData . Values["controller" ] = " Legacy";


context . RouteData.Values[ "action"] = "GetLegacyUrl";
context.RouteData.Values["legacyUrl"] = requestedUrl;

Свойство RouteContext. RouteData. Vales возвращает словарь. который исполь­


зуется для предоставления значений данных классу MvcRouteHandler. В стандарт­
ной системе маршрутизации значения данных создаются путем применения шаблона
URL к запросу, но в специальном :классе маршрута значения жестко закодированы,
таR что всегда производится направление на действие GetLegacyUrl контроллера
Legacy. Между запросами изменяется только значение данных legacyUrl, которое
устанавливается в URL запроса; оно будет применяться в качестве аргумента с таким
же именем, получаемого методом действия.
488 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

Последнее изменение в листинге 16.18 делегирует ответственность за поиск и ис­


пользование класс а контроллера для обработки запроса.

await mvcRoute.RouteAsync( context) ;

Объект RouteContext , теперь содержащий значения control l er , action и


legacyUrl, передается методу RouteAsync () объекта MvcRouteHandler, который
берет на себя обязанность за любую дальнейшую обработку запроса, включая уста­
новку свойства Handler. В результате класс LegacyRoute может сосредоточиться на
решении, какие URL он будет обрабатьmать, не имея дела с деталями работы с конт­
роллерами напрямую.

Объект MvcRou teHandler, выполняющий работу в расс мат р иваем ом пример е ,


должен запрашиваться как служба, что объясняется в главе 18. Чтобы предоставить
конструктор LegacyRoute с объектом реализации IServiceProvider , который не­
обходим ему для создания объекта MvcRouteHandler, оператор, определяющий мар­
шрут в классе Startup , обновл ен с целью снабжения его возможностью доступа к
службам приложения (листинг 16.19).
Листинг 16.19. Обеспечение доступа к службам приложения в файле Startup. cs

puЫic void Configure(IApplicat ionBuilde r арр) {


app .U seStatusCodePages();
app . UseDeveloperExceptionPage() ;
app . UseStaticFiles() ;
app . UseMvc(routes => {
routes.Routes . Add(new LegacyRou te (
app.ApplicationServices,
" /articles/Wi n dows_З . l _Ove r view .html",
"/old/.NET 1.0 Class Library"));
routes . MapRoute(
name: "default ",
template : " {control l er=Home}/{action=Index }/{id?} " ) ;
routes . MapRoute(
name : "out ",
template : " outbound/{controller=Home}/{act i on=Index} " ) ;
}) ;

Запустив приложение и снова запросив / articl es/Windows _ 3 .1_0verview. htrnl,


вы увидите, что простой текстовый ответ был заменен выводом из представления
(рис. 16.7).

GetlegacyURL
The URI_ requested was: raгtic les/Windows_з. 1 _overview.html

1
---~----------·-----~---- ... ·-·------·------·------------------.....1"

Рис. 16. 7. Делегирование работы с контроллерами и представлениями


Глава 16. Доnолнительные возможности маршрутизации 489

Генерация ИСХОДЯЩИХ URL


Для поддержки генерации исходящих URL мы должны реализовать в классе
LegacyRoute метод GetVirt ualPath () ,как показано в листинге 16.20.
Листинг 16.20. Генерация исходящих URL в файле LegacyRoute. cs

using Microsoft . AspNetCore . Http ;


using Microsoft .As pNetCore .Rout in g ;
using System ;
using System .Lin q ;
using System .Text ;
using System .T hreading.Tasks ;
using Microsoft.AspNetCore . Mvc .Inte rn a l;
using Microsoft . Extensions .Dependencyinjection ;
namespace UrlsAndRoutes.Infrastructure
puЬlic class LegacyRoute : I Router {
privat e s tr ing [] urls;
private IRou t er mvcRoute ;
puЫic LegacyRoute(IServiceProvider servi ces , params string [ ] targetUrls )
this . urls = targetUrls ;
mvcRoute = services . GetRequiredService<MvcRouteHandler>();

puЫic async Task Rou teAsync(RouteCo nte xt context) {


string requestedUrl = context.HttpContext.Request . Path
.Value.TrimEnd('/');
if (urls . Contains(requestedUrl, Str i ngComparer . Or di na li gnoreCase))

context . RouteData . Values (" co ntroller"J = "Legacy ";


context . RouteData .Va l ues[ "action " J = "GetLegacyUrl ";
context . RouteData . Va lues ["l egacyUrl "J = requestedUrl ;
await mvcRoute.RouteAsync(context);

puЬlic VirtualPathData GetVirtualPath(VirtualPathContext context) {


if (context.Values.ContainsKey("legacyUrl")) {
string url = context.Values["legacyUrl"] as string;
if (urls.Contains(url)) {
return new VirtualPathData(this, url);

return null;

Систем а маршрутизации вызывает метод GetVirtualPath () каждого маршрута ,


который был определен в классе Startup, давая каждому шанс сгенерировать исхо­
дящий URL, требующийся приложению. Аргумент метода GetVirtualPath () - это
объект Virtua l PathContext , который предоставляет необходимую информацию от­
носительно URL. В табл . 16.5 описаны свойства класса VirtualPathContext.
490 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

Таблица 16.5. Свойства, определенные в классе VirtualPathContext

Имя Описание

RouteName Это свойство возвращает имя маршрута

Va l ues Это свойство возвращает индексированный по имени словарь значений ,


которые могут применяться для переменных сегментов

AmЬientValues Это свойство возвращает словарь значений, которые полезны для гене­
рации URL, но не будут встраиваться в результат. При реализации собс­
твенного класса маршрутизации такой словарь обычно пуст

HttpContext Это свойство возвращает объект HttpContext, который предоставляет


информацию о запросе и ответе, находящемся в процессе подготовки

В рассматриваемом примере свойство Vа 1 u е s используется для получения


значения по имени legacyUrl, и если оно соответствует одному из URL, кото­
рые были сконфигурированы для поддержки маршрутом, тогда возвращается объ­
ект VirtualPathData, снабжающий систему маршрутизации деталями URL.
Аргументами конструктора класса VirtualPathData являются объект реализации
IRouter, который генерирует URL, и сам URL.

return new VirtualPathData( this, url ) ;

В листинге 16.21 показано измененное представление Resul t . cshtml для запра­


шивания исходящих URL, которые нацелены на специальное представление.

Листинг 16.21. Генерация исходящих URL из специального класса маршрута


в файле Resul t. cshtml

@model Resu l t
@{ Layout = null ;
<!DOCTYPE html>
<html>
<head>
<meta name= " viewport " content= " width=device - width " />
<title>Routing</title>
<link rel= " stylesheet " asp - href - include= " liЬ/bootstrap/dist/css/* . min . css " />
</head>
<body class= " panel - body " >
<tаЫе class= " taЫe taЫe - bordered ta Ыe -strip ed taЬle - condensed " >
<tr><th>Contro l ler : </th><td>@Model . Cont roll er</td></tr>
<tr><th>Action:</th><td>@Model . Action</td></tr>
@foreach (string key in Model . Data . Keys) {
<tr><th>@key : </th><td>@Model . Data[key]</td></tr>

</tаЫе>
<а asp-route-legacyurl="/articles/Windows_З.l_Overview.html"
class="Ьtn Ьtn-primary">This is an outgoing URL
</а>
<p>URL: @Url.Action(null, null,
new { legacyurl = 11 /articles/Windows_З.l Overview.html"})</p>
</body>
</html>
Глава 16. Дополнительные воз м ожности маршрутизации 491
В э том пример е нет необходимости указывать дескрипторному вспомогательному
классу контроллер и действие для исходящего маршрута, потому что при генерации
URL они не применяются. Учитьmая это, атрибуты дескрипторного вспомогател ьного
класса asp - control ler и asp - action в элементе а опушены. Когда генерируется
только URL, первые два аргумента для вспомогательного метода Url . Action () уста­
н авливаются в null по той же причине.
Если вы запустите приложение и просмотрите НТМL-размет1{у, полученную в от­

вет на запрос стандартного URL, то увидите, что для создания URL использовался
специальный класс маршрута:

<а class= " Ьtn Ьtn - primary " hr ef= "/articles/windows_З .l_overview.html/" >
This is an outgoing URL
</а>
<p>URL : /articles/windows_З.l_overview.html/ </p>

Завершающие косые черты, добавленные к URL, являются результ ат ом уста­


новки вtrue конфигурационного свойства AppendTrailingSlash внутри файла
Startup . cs, и важно, чтобы с опоставление входящего маршрута было спо с обно со­
ответствовать URL с добавленным символо м косой черты.

Совет. Если URL, который вы видите в НТМL-ответе, имеет отличающийся формат, такой
как/?legacyurl = %2Farticles %2FW indows 3 . l_Overview .html , тогда для ге­
нерации URL специальный маршрут не применялся , а вместо него был вызван какой-то
другой маршрут в прило жении. Поскольку никакого контроллера или действия не было
у казано, произошло нацеливание на действие Index контроллера Home, и к строке за­
проса URL добавилось значение legacyU rl. В таком случае удостоверьтесь в установ­
ке свойства IsBound в true внутри метода GetVirtualPath () , и проверьте , что в
конфигурации внутри файла Startup. cs указаны корректные URL для конструктора
класса LegacyRoute, а специальный маршрут определен перед всеми остальными
маршрутами .

Работа с областями
Инфраструктура ASP.NET Core MVC поддержив ает организацию веб-прилож е ния в
виде областей, где каждая область представляет функциональный сегмент приложе­
ния, такой как администри рование , выписка счетов- фактур. поддержка пользо вате­
лей и т. д . Это удобно в крупных проектах, в которых наличие единственного набора
папок для всех контроллеров, представлений и моделей может привести к сложнос­
тям в управлении.

Каждая область МVС имеет собственную структуру папок, позволяя все хранить
отдельно. Такой подход делает более очевидным то, 1<акие элементы проекта относят­
ся к каждой функциональной области приложения, и помогает множеству разраб от­
чиков т рудиться над проектом, не конфликтуя друг с другом. Области в значительной
степени поддерживаются системой маршрутизации и потому они рассматриваются
вместе с URL и маршрутами. В настоящем разделе будет показано, как наст раивать
и пользоваться областями в проектах MVC.
492 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

Создание области
Создание области требует добавления папок в проект. Папка верхнего уровня на­
зывается Areas . Внутри н ее для каждой необходимой области пр едусмотр ен а своя
пап к а, содержащая собственные подпапки Con t rollers , Vi ews и Models. Мы пла ­
нируем создать обл асть по имени Adrnin , что означает создание набора папок, опи­
санных в табл. 16.6. Для подготовки примера проекта со здайте вс е папки, п е речис­
л енные в табл. 16.6.

Таблица 16.6. Папки, требуемые для подготовки областей

Имя Описание

Areas Эта папка будет содержать все области в прилож ении MVC
Ar eas/Admin Эта папка будет содержать классы и представления для
области Admin
Areas/Adrnin/Controllers Эта папка будет содержать контроллеры
для области Admin
Ar eas/Admin / Views Эта папка будет содержать представления
для области Admin
Are as/Adrnin/Views/ Horne Эта папка будет содержать представления для контролле­
ра Horne в области Admin
Are as/Admi n/Mode l s Эта папка будет содержать модели для области Admin

Хотя каждая область применяется по отдельности, многие средства МVС полагают­


ся на стандартны е функциональные возможности С# или .NET. таки е как пространс­
тва им е н . Чтобы обл егчить использование области. первым дело м нужно добавить
файл импортирования представлений, который позволит работать с м оделями из об ­
ласти внутри пр едставлений, не включая пространства имен и получая пр е имущест­
во от применения дескрипторных вспомогательных классо в . Создайте в папке Areas/
Adrnin/Views файл импортирования представлений по имени _ Viewirnports . cshtml
и поместит е в н е го оп е раторы, приведе нные в листинге 16.22.

Листинг 16.22. Содержимое файла_Viewimports. cshtrnl


из папки Areas/Admin/Views
@us i ng Url s AndRoutes . Areas.Admi n. Models
@addTagHelper * , Mi cr osoft . As pNetCor e. Mvc . Ta gH elpe r s

Создание маршрута для области


Чтобы задействов ать обл асти , потребуется добавить в ф айл Startup . cs маршрут,
который содержит пер еменную сегм ента a r ea (листинг 16.23).
Листинг 16.23. Добавление маршрута для областей в файле Startup. cs

us ing Microsof t. AspN e tCore . Builder ;


usi ng Microsoft . Exte nsions . Dependency i njection ;
using Microsoft.AspNetCore . Routing . Constra i nts ;
using Microsoft.AspNetCore . Routing ;
using UrlsAndRoutes . Infras tru c t ure ;
Глава 16. Дополнительные возможности маршрутизации 493
namespace UrlsAndRoutes
puЫic class Startup {
puЫic void ConfigureServices(IServiceCollection services) {
services . Configure<RouteOptions>(options => {
optio11s . ConstraintMap.Add( weekday typeof(Wee kDayConstra i nt)) ;
11 11
,

options . LowercaseUrls = true ;


options . AppendTrail i ngSlash = true ;
}) ;
services .AddMvc() ;

puЫic void Configure(IAppl i cationBuilder арр) {


app . UseStatusCodePages() ;
app . UseDeveloperExceptionPage() ;
app . UseStat i cFiles() ;
app . UseMvc(routes => {
routes.MapRoute(
name: " areas 11 ,
template: 11 {area:exists}/{controller=Home}/{action=Index} 11 ) ;
routes.Rou t es . Add(new LegacyRoute(
app . ApplicationServices ,
11
/articles/Window s_ З . l _Overview .h tm l
11
,
11
/old/. NET 1. O_Class Library 11
));

routes . MapRoute(
name : "default 11
,

temp l ate : " {contro ll er=Home}/{acti on=Index}/{ i d?} " ) ;


routes . MapRoute(
name: " out ",
template : "outbound/ {controlle r=H ome}/{act ion=Index} 11
) ;

}) ;

Переменная сегмента area используется для сопоставления с URL, которые наце­


лены на контроллеры в специфических областях. В листинге задействован стандар­
тный шаблон URL, но сегмент area можно добавлять в любой требующийся шаблон.
Маршрут, который добавляет поддерж1tу областей, должен находиться перед менее
специфическими маршрутами для обеспечения корректного сопоставления с URL.
Ограничение exists применяется для гарантии того , что запросы соответствуют
только областям, которые были определены в приложении.

Заполнение области
Контроллеры, представления и м одели в области можно создавать точно так же,
как в главной части приложения MVC. Чтобы создать модель, щелкните правой кноп­
кой мыши на папке Areas/Admin/Models, выберите в контекстном меню пункт
Add <=:>Class (Добавить <=:> Класс) и создайте файл класса по имени Person . cs, содержи­
мое которого показано в листинге 16.24.
494 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

Листинг 16.24. Содержимое файла Person. cs из папки Areas/Admin/Models


namespace UrlsAndRoutes . Areas .Admin . Models
puЫic c l ass Person {
puЬlic string Name { get ; set ;
puЫic string City { get ; set ;

Для создания контроллера щелкните правой кнопкой мыши на папк е Areas/


Admin/Controllers, выберите в контекстном меню пункт AddqClass и создай­
те файл класса по имени HomeController. cs с определением контроллера из
листинга 16.25.

Листинг 16.25. Содержимое файла HomeController. cs


из папки Areas/Admin/Controllers
using Mic r osoft . AspNetCore . Mvc ;
us in g UrlsAndRoutes . Areas . Admin . Models;
namespace UrlsAndRoutes . Areas . Admin . Control l ers
[Area ( "Admin 11
) ]

puЫic class HomeController : Contro ll er {


pr i vate Person [ ] data = new Person[] {
new Person { Name Alice ", City = "London " },
11

new Person { Name " ВоЬ , Ci ty Paris 11 11 11


} ,

new Person { Name Joe Cit y = Ne w York


11 11
,
11 11

};

puЫic ViewResult Index () => View (data) ;

Новый контроллер в целом похож на стандартный кроме одного аспекта. Чтобы ас­
социировать контроллер с областью, к классу должен быть применен атрибут Area :

[Area ( 11 Admin 11 ) ]

puЫic class HomeController : Control l er {

Без атрибута Area контроллеры не будут принадлежать обл асти, даже если они не
определены в главной части приложения. Отсутствие атрибута Area может привести
к странным р езультатам. Это первое, что следует проверять , если при работе с облас­
тями получаются не те результаты, которые ожидались.

Совет. Если для настройки маршрутов используются атрибуты, как было описано в гла­
ве 15, тогда с применением маркера Rou te можно ссы­
[ area J в аргументе атрибута
латься на область , указанную с помощью атрибута
Area : [Route ( [area] /арр/ 11

[controller]/actions/[action] / {id : weekday?} 11


)].

Н аконе ц , добавьте в папку Areas/Admin/Views/Home файл представления Razor


по имени Index. csh t ml, содержимое которого показано в листинге 16.26.
Глава 16. Доnолнительные возмо ж ности маршрутизации 495
Листинг 16.26. Содержимое файла Index.cshtml из nanкиAreas/Admin/Views/Home

@model Person [ ]
@{ Layout = null ;
<!DOCTYPE html>
<html>
<head>
<meta name= " viewpo r t " content=" wi dth=de v i ce - wi dth " />
<t i tle>Areas</title>
<link re l =" stylesheet" a sp- href- inc lude=" liЬ/ boo ts trap/dist/ css/* .min. css " />
</head>
<body class =" pa nel - body" >
<tа Ы е c l ass= " taЫe taЫe-borde r ed t aЬl e -strip ed taЫe - condensed " >
<t r ><t h>Name</th><th>City< / t h>< / t r>
@f o r each (P e rson р in Mo de l) {
<tr><td>@p . Name</td><td>@p . Ci ty</ td></t r >
</tаЫе>
</body>
</html>

Моделью для данного представления является массив объектов Person. Благодаря


файлу импортирования представлений , содержимое которого бьmо приведено в листин­
ге 16.21, есть возможность ссылаться на тип Pe r son без указания пространства имен.
Запуск приложения и запрос URL вида /Admin даст результат. показанный на рис . 16.8.

Name City

Alice London

ВоЬ Paris

Joe New York

Рис. 16.8. Использование области

Влияние области на приложение MVC

Важно понимать влияние, которое области оказывают на остальные части приложения. Мы


создали область по имени Admin , но в главной части приложения имеется также контрол­
лер Admin. До создания области запрос /Admin направлялся бы действию Index конт­
роллера Admi n в главной части приложения; теперь он будет нацелен на действие I ndex
контроллера Home в области Admi n (корень области предоставляет стандартные значения
для переменных сегментов con troller и act i on ). Изменение такого вида может стать
причиной неожиданного поведения, и наилучший способ применения областей предусмат­
ривает встраивание их использования в первоначальную схему именования контроллеров

для проекта. Если возникла потребность добавить области в готовое приложение, то вы


должны тщательно обдумать их влияние на имеющиеся маршруты.
496 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

Генерирование ссылок на действия в областях


Для создания ссылок, указывающих на действия в той же области, к которой от­
носится текущий запрос, никаких дополнительных шагов предпринимать н е придет­
ся. Инфраструктура МVС обнаруживает, что текущий запрос относится к конкретной
области, и гарантирует, что средство генерации исходящих URL будет искать соот­
ветствие только среди маршрутов, определенных для этой области . Например , в лис­
тинге 16.27 демонстрируется добавление элемента а в файл Index. cshtml из папки
Areas/Admin/Views/Home.

Листинг 16. 27. Добавление якоря в файле Index. csh tml


из папки Areas/Admin/Views/Home
@mode l Pe rson []
@{ Layout = null ;
< ! DOCTYPE html >
<html >
<head>
<meta name="viewport" content= " width=devi ce-width " />
<title>Areas </t itle>
<link rel=" styl esheet " asp -hr e f-i nc l ude= "li Ь/bootstrap/dist/css/* . min . css " />
</ he ad>
<body class= "panel -body " >
<tаЫе c l ass= " taЫe taЫe - bordered taЬle-striped taЫe-condensed">
<tr><th>Name</th><th>C i ty</th></tr>
@foreach (Person р i n Model) {
<tr><td>@p .Name </ td><td>@p .Cit y</td></tr>

</tаЫе>
<а asp-action="Index" asp-controller="Home">Link</a>
</body>
</html >

Запустив приложение и запросив URL вида / a d.mi n, вы заметите, что ответ содер­
жит следующий элемент:

<а href=" /admin/ " >Link</a>


При генерации исходящей ссылки система маршрутизации выбрала маршрут
для области и учла стандартные значения, доступные для переменных сегментов
controller и action .
Чтобы создать ссьmку на действие в другой области или в главной части прило­
жения, вы должны снабдить систему маршрутизации значением для сегмента area
(листинг 16.28).

Листинг 16.28. Нацеливание на другую область в файле Index. cshtml


из папки Areas/Admin/Views/Home
@model Person []
@{ Layo ut = null;
<!DOCTYPE html >
<htm l >
<head>
Глава 16. Дополнительные возможности маршрутизации 497
<meta name= " viewport " content= " width=device - width " />
<title>Areas</title>
<link rel=" styl esheet " asp - href -i nc lude= " liЬ/bootstrap/dist/css/*.min . css " />
</head>
<body class= " panel-body " >
<tаЫе class= " taЫe taЫe-bordered taЫe -str ip ed taЫe-condensed " >
<tr><th>Name</t h ><th>C i ty</ th> </t r>
@foreach (Perso n р in Mode l) {
<t r ><td>@p.Name </ td ><td>@ p.City</ td>< / tr >

</tаЫе>
<а asp - action="Index" asp - co ntroll e r="Home " >Link </a>
<а asp-action="Index" asp-controller="Home" asp-route-area="">Link</a>
</body>
</html>

Атрибут asp - route - a r ea устанавливает значение для переменной сегмента area.


В данном случае эта переменная устанавливается в пустую строку, что указывает на
главную часть приложения и дает следующий НТМL-элем ент:

<а href =" / " >Link</a>


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

Полезные советы относительно схемы URL


После всего сказанного у вас может все-таки остаться вопрос: с чего начинать про­
ектирование собственной схемы URL? Вы можете просто принять стандартную схему,
но организация собственной схемы обеспечивает ряд пр еимуществ. В последние годы
проектное решение с URL приложения применяется все более широко, и поя вилось
несколько важных принципов проектирования . Следуя этим принципам проектиро­
ваниЯ , вы улучшите удобство использования, совместимость и поисковый рейтинг
своих прилож ений.

Делайте URL чистыми и понятными человеку


Пользователи обращают внимание на URL в приложениях. Просто вспомните, ког­
да вы в последний раз пытались отправить кому-то URL веб-сайта Amazon. Вот как
выглядит URL для предыдущего англоязычного издания этой книги :

http : //www.amazon . com/Pro - ASP - NET - Experts - Voice - ASP -Net/dp/1430265299


Отправить по электр онной почте такой URL - еще куда ни шло , но попробуйте
продиктовать его по телефону. Когда этим приходится заниматься часто, в итоге про-
ще сообщить номер ISBN книги и предложить самостоятельно найти ее страницу на
веб-сайте Amazon. Было бы неплохо получать доступ к странице книги с помощью
URL следующего вида:

http : //www . amazon.com/books/pro - aspnet - mvcS - framework


Такой URL вполне моJtено было бы диктовать по телефону, и он не выглядит так ,
будто при наборе почтового сообщения на клавиатуру что -то свалилось.
498 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

На заметку! Чтобы было предельно ясно: я глубоко уважаю компанию Amazoп, которая про ­
дает больше моих книг, чем все остальные вместе взятые. Мне известен факт, что все
сотрудники Amazoп - замечательно умные и приятные люди . Никто из ни х не окажется до
такой степени мелочным, что прекратит продажи моих книг из-за настолько несуществен­
ного, как критика применяемого в Amazon формата URL. Я люблю Amazon. Я преклоняюсь
перед Amazoп . Я просто х очу, чтобы они привели в порядок свои URL.

Ниже даются руководящие принципы построения дружественных к пользователям


URL.
• Проектируйте URL так, чтобы они описывали предоставляемое содержимое , а
не детали реализации приложения. Используйте /Articles/AnnualReport ,
а не /Websi te _ v2/CachedContentServer/FromCache/AnnualReport .
• Отдавайте предпочтение заголовкам содержимого п ер ед идентификационными
номерами . Применяйте /Artic le s/AnnualReport , а не /Articles/2392. Если
вы должны использовать идентификационные номера (для различения элемен­
тов с одинаковыми заголовками или во избежание дополнительных запросов к
базе данных, необходимых для поиска элемента по его заголовку). тогда указы­
вайте и номер, и заголовок (например, /Art ic le s/ 2392/AnnualReport). Таной
URL дольше набирать , но он имеет больше смысла для человека и улучшает по­
исковый рейтинг. Приложение может просто игнорировать заголовок и отобра­
жать элемент, соответствующий идентификатору.

• Не применяйте расширения имен файлов для НТМL-страниц (скажем, . aspx


или .mv c ), но используйте их для специализированных типов файлов (таких
как. jpg, .pdf и . zip). Веб-браузеры не придают значения расширениям имен
файлов, если тип MIME установлен корректно, но люди ожидают, что имена
файлов PDF заканчиваются на .pdf .
• Создавайте осмысленную иерархию (например. / Products /Menswear / Shirts /Red),
чтобы посетитель смог догадаться , юш выглядит URL родительской категории .
• Поддерживайте независимость от регистра символов . (Кто-то может пожелать
ввести URL. указанный на печатной странице.) По умолчанию система марш­
рутизации ASP.NET Core не чувствительна к регистру символов.

• Избегайте применения знаков, кодов и символьных последовательностей. Если


вам нужен разделитель слов , то используйте дефис (как в /my -great - article).
Подчеркивания не являются дружественными к пользоват елю, а URL-
кодированные пробелы выглядят либо странно (/my+great+article), либо пло­
хо (/my %20great %20art icl e ).
• Не изменяйте URL. Неработающие ссылки равнозначны потерям в бизнесе.
После изменения URL продолжайте поддерживать старую схему URL насколько
возможно долго посредством перенаправления .

• Поддерживайте согласованность. Примите один формат URL в масштабах всего


приложения.

URL должны быть короткими, легкими для набора, реда~пируемыми человеком и


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

https : //www .nngroup . com/articles/url-a s - ui/


Глава 16. Дополнительные возможности маршрутизации 499
ТИ:м Бернерс-Ли, изобретатель веб, предлагает аналогичные советы по адресу:

https : //www . wЗ . org/ P rov ide r /S t y l e/ URI

GET и POST: выбор правильного запроса


Эмпирическое правило гласит, что запросы GET должны применяться для извлече­
ния информации, предназначенной только для чтения, а запросы POS T - для любой
операции, которая изменяет состояние приложения. В терминах соответствия стан­
дартам запросы GET предназначены для безопасных взаимодействий (не имеющих
побочных эффектов помимо извлечения информации), а запросы POST - для небе­
зопасных взаимодействий (принятие решения либо изменение чего-либо). Такие со­
глашения установлены консорциумом WЗС (World Wide Web Consortium) и описаны
по адресу:

h ttp : //www . w3 . o rg /P r oto c o ls/rfc2616/rfc 2 616 - s ec9.html


Запросы GET являются адресуемыми: вся информация содержится в URL, поэтому
подобные адреса можно добавлять в закладки и ссылаться на них в будущем.
Не используйте запросы GET для операций, которые изменяют состояние. Многие
разработчики веб-приложений научились этому на горьком опыте в 2005 году, когда
было выпущено приложение Google Web Accelerator. Оно с упреждением выбирало все
содержимое, на которое ссылалась каждая страница, что в рамках протокола НТТР
вполне законно . поскольку запросы GET должны быть безопасными. К сожалению,
многие разработчики веб-приложений проигнорировали соглашения НТТР и разм е ­
щали в своих приложениях простые ссылки на операции типа "удалить элемент" или
"добавить в корзину" . В результате получился хаос.
В одной компании были уверены , что их система управления содержимым стала
целью повторяющихся атак злоумышленников, т.к . все содержимое постоянно удаля­

лось. Позже в компании обнаружили, что поисковый агент наткнулся на URL адми­
нистративной страницы и прошелся по всем ссылкам на удаление. Аутентификация
может защитить от таких действий со стороны пользователей, но не способна обеспе­
чить защиту от веб-акселераторов.

Р езюме
В этой глав е рассматривались дополнительные возможности системы маршру­
тизации. Вы узнали, как генерировать исходящие ссылки и URL, а также научились
настраивать систему маршрутизации. По ходу дела вы ознакомились с концепцией
областей и с рекомендациями относительно того, как должна создаваться удобная и
содержательная схема URL. В следующей главе мы перейдем к исследованию контрол­
леров и действий, которые являются центральной частью инфраструктуры ASP.NET
Core МVС. Будет подробно описана их работа и показано, как их применять для полу­
чения наилучших результатов в приложении.
ГЛА В А 17
Контроллеры и действия

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


Core MVC
В инфраструктуре ASP.NEТ .NET,
контроллеры являются классами
3
торые содержат логику, требуемую для обработки запроса. В главе
ко­
объяснялось, что
роль контроллера заключается в инкапсуляции логики приложения. Это значит, что
контроллеры отвечают за обработку входящих запросов, выполнение операций над мо­
делью предметной области и выбор представлений для визуализации пользователю.
Контроллер волен обрабатывать запрос любым способом, какой посчитает подхо ­
дящим, до тех пор , пока он не забирается в области ответственности , которые от­
носятся к модели и представлению. Таким образом, контроллеры не содержат и не
сохраняют данные, а также не генерируют пользовательские интерфейсы.
В настоящей главе мы рассмотрим реализацию контроллеров, а танже различные
способы применения контроллеров для получения и генерации вывода. В табл . 17.1
приведена сводка, позволяющая поместить контроллеры в конте кст.

Таблица 17 .1. Помещение контроллеров в контекст

Вопрос Ответ

Что это такое? Контроллеры содержат логику для получения запросов, обновления
состояния или модели приложения и выбора ответа, который будет от­
правлен клиенту

Чем они полезны? Контроллеры являются центральной частью проектов MVC и содержат
логику предметной области для веб-приложения

Как ОНИ Контроллеры - это классы С#, открытые методы которых вызываются
используются? для обработки НТТР-запроса. Методы могут брать на себя ответствен ­
ность за выдачу ответа клиенту напрямую, но более распространенный
подход предусматривает возвращение результата действия, который
сообщает MVC, как ответ должен быть подготовлен
Существуют ли Если вы - новичок в области разработки приложений MVC, то можете
какие-то скрытые легко создавать контроллеры, содержащие функциональность, которая
ловушки или больше подходит для модели или представления. Более специфичная
ограничения? проблема связана с тем, что любые открытые классы с именами, закан­
чивающимися на Con t r o ller, инфраструктура MVC считает контрол­
лерами; это означает возможность непредумышленной обработки НТТР­
запросов в классах, которые не планировались быть контроллерами
Существуют ли Нет, контроллеры - это основная часть приложений MVC
альтернативы?

Изменились ли они Способ определения и использования контроллеров был упрощен бла­


по сравнению с годаря объединению контроллеров Web API и обычных контроллеров
версией MVC 5? (контроллеры API рассматриваются в главе 20). Вдобавок любой откры­
тый класс , имя которого заканчивается на Control l er , считается кон­
троллером , если только он не декорирован атрибутом NonController
Глава 17. Контроллеры и действия 501
В табл. 17.2 приведена сводка для этой главы.

Таблица 17.2. Сводка по главе

Задача Решение Листинг

Определение контроллера Создайте открытый класс с именем, 17.1-17.10


заканчивающимся на Controller,
или унаследуйте его от класса
Contro l le r
Получение деталей НТТР-запроса Используйте объекты контекста или 17.11-17.14
определите параметры методов
действий

Выдача результата из метода Работайте напрямую с объектом кон­ 17.15-17.17


действия текста результата или создайте объект
результата действия

Выдача НТМL-результата Создайте результат действия 17. 18- 17. 25


Перенаправление клиента Создайте результат перенаправления 17.26-17.31
Возвращение содержимого клиенту Создайте результат содержимого 17.32-17.36
Возвращение кода состояния НТТР Создайте НТТР-результат 17.37-17.38

Подготовка проекта для примера


Для цел ей этой главы создайте новый проект типа Empty (Пустой) по имени
ControllersAndActions с применением шаблона ASP.NET Core Web Applicatioп (.NET
Core) (Веб - приложени е ASP.NET Core (.NET Core)). Добавьте требуемые пакеты NuGet
в раздел dependencies файла proj ect . j son и настройте инструментарий Razor в
разделе tools , как показ ано в листинге 17.1. Разделы, которые не нужны для данной
главы, понадобится удалить.

На заметку! Настоящая глава содерж ит модульные тесты для основных средств . Для крат­
кости проекты модульного тестирования в инструкции по созданию примера проекта не

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


главе 7, или загрузить код примеров из веб-сайта издательства.

Листинг 17. 1. Добавление пакетов в файле proj ect. j son

"dependenc ies": {
"Microsoft . NETCore . App ":
" version ": " 1 . 0 . 0 ",
" type ": "platform "
} ,
"Microsof t.AspN etCore . Diagnostics ": " 1 . 0.0 ",
"Microsoft . AspNetCore . Server . IISintegration ": "1.0 . 0 ",
"Microsoft . AspNetCore . Server . Kestrel ": " 1 . 0 . 0",
"Microsoft . Extensions .Logging.Console ": " 1 . 0 . 0",
"Microsoft. AspNetCore. Mvc" : "1. О. О" ,
"Microsoft.AspNetCore.StaticFiles": 11 1.0.0 11 ,
502 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

"Microsoft. AspNetCore. Session" : "1. О. О",


"Microsoft.Extensions . Caching.Memory": 11 1.0.0 11 ,

"Microsoft . AspNetCore.Razor.Tools": {
"version": "1. О. O-preview2-final",
"type": "build"

}/

"t ools ":


"Microsoft . AspNetCore.Razor . Tools": "l.0.0-preview2-final",
"Microsoft . AspNe t Cor e . Se r ve r .II Si ntegrat i on . Tools ": "l. 0 . 0- preview2 - f i na l"
}'
" frame wor k s ": {
" netcoreapp l. 0 ":
" imports " : [ " do t net5 . 6 ", " portaЫe - net45+win8 " ]

}'
" b u ildOptions ":
" emit Ent r yPoint ": t r u e ,
" pre s erve Comp i la ti o nCo nt ext ": t r u e

В дополн ение к базовым пак етам MVC были добавлены пакеты, тр ебующие ся для
хр анилища сеанса. В листинге 17.2 приведен класс Startup. которы й конфигуриру ет
приложение в соотв етствии с пакетами NuGet.

Листинг 17.2. Содержимое файла Startup. cs

using Microsoft . As pNe t Core . Bui l der ;


using Microsoft .Extensions . Dependency i njection ;
namespace Co n trol l ersAndActi o n s
p uЫi c cla ss Startup {
puЫic vo i d Co n figureSe r v i ces(ISe r v i c e Col l ection services) {
services.AddМvc();
services.AddМemoryCache();
services.AddSession();

p u Ыic void Co n fig u re( I App li cati o nBu i lder а рр ) {


app.UseStatusCodePages();
app.UseDeveloperExceptionPage();
app.UseStaticFiles();
app.UseSession();
app . UseMvcWithDefaultRoute();

М етоды AddMernoryCac h e ( ) и Add Ses s ion () создают службы , н е обходимые дл я


управления сеансом. Метод UseSession ( ) добавляет в конвейер ко м понент проме ­
жуточного ПО , который ассоциирует данные сеанса с запросами, и добавля ет cookie-
Глава 17. Контроллеры и действия 503
наборы к ответам, чтобы гарантировать возможность идентификации будущих за­
просов. Метод UseSession () должен вызываться перед вызовом метода UseMvc (),
так что компонент сеанса может перехватывать запросы, прежде чем они достигнут

пром ежуточного ПО MVC , и модифицировать ответы после того, как они были сгене­
рированы. Друтие методы настраивают стандартные пакеты, которые рассматрива­
лись в главе 14.

Подготовка представлений
Основное внимание в главе уделяется контроллерам и их методам действий , поэ­
тому классы контроллеров будут определяться в главе повсеместно. В качестве под­
готовки будут определены представления, которые помогут демонстрировать работу
контроллеров и методов действий. Созданные в настоящем разделе представления
находятся в папке Views/Sha r ed, что позволит их использовать в любых контрол­
лерах. которые будут определяться позже в главе. Создайте папку Views/Shared , до­
бавьте в нее файл представления Razor по имени Resul t . cshtml и поместите в файл
разметку, показанную в листинге 17.3.

Листинг 17.З. Содержимое файла Resul t. cshtml из папки Views/Shared


@model string
@{ Layout = null ;
<!DOCTYPE html>
<html>
<head>
<meta name= "viewpo r t " content= " width=device - width " />
<title>Controllers and Ac ti ons</ tit le>
<link rel= "stylesheet " asp-href- include= "lib/bootstrap/dist/css/* .min . css " />
</head>
<body class= "panel -b ody " >
Model Data: @Model
</body>
</html>

Моделью для данного представления является объект string, который позволит


отображать простые сообщения . Далее создайте в папке Views/Shared файл по име­
ни DictionaryResult . cshtml и поместите в него разметку, приведенную в листин­
ге 17.4. В качестве модели для этого представления выбран словарь, который даст
возможность отображать более сложные данные, чем в предыдущем представлении.

Листинг 17.4. Содержимое файла DictionaryResult.cshtml


из папки Views/Shared
@model IDictionary<string , string>
@{ Layout = null ;
<!DOCTYPE html>
<html>
<head>
<meta name= "v i ewport " content= "width=device - width " />
<title>Controllers and Actions</title>
<link rel= "stylesheet " asp - href - include =" liЬ/ b ootstrap/dist/css/* . min . css " />
</head>
504 Часть 11. Подробные сведения об инфраструктуре ASP. NEТ Core MVC

<body class= "panel -body " >


<tаЫе class= " taЫe taЬle - bordered taЬle-condensed taЬle-striped">
<tr><th>Name</th><th>Value</th></tr>
@foreach (string ke y in Model.Keys) {
<tr><td>@key</td >< td>@Model[key]</td></tr>

</tаЫе>
</body>
</html>

Затем со здайте в папке Views/Shared файл по имени SimpleForm .c shtml и оп­


ределите в нем представл е ние, показанное в листинге 17.5. Как следует из названия,
представление содержит простую НТМL- форму, которая будет получать данные от
пользователя.

Листинг 17.5. Содержимое файла SimpleForm. cshtml из папки Views/Shared


@{ Layout = nul l;
< !DOCTYPE html>
<htm l >
<head>
<meta name= " viewport " content= " ~Jidth=device - width " />
<tit le >Controllers and Actions</title>
<link rel=" stylesheet " asp-href-include= " liЬ/bootstrap/dist/ css/* .min. css" />
</head>
<body class= " panel-body">
<form method= "post " asp - action= " ReceiveForm " >
<div class= " form - group " >
<label for= " name " >Name : </labe l >
<input class= " form - control " name="name" />
</div>
<div class= " form -g rou p" >
<label for = "name " >City : </label>
<input class =" form - contro l" name= " city " />
</div>
<button class= "Ьt n Ьtn - primary center - Ыock " type= "submit " >Submit
</button>
</form>
</body>
</html>

В представлениях используются встроенные дескрипторные вспомогательные


классы для генерации URL из системы маршрутизации. Чтобы включить дескриптор­
ные вспомогательные классы, создайте в папке Views файл импортирования пред­
ставлений по имени _Viewlmports. cshtml и добавьте в него выражение . приведен­
но е в листинге 17.6.

Листинг 17.6. Содержимое файла_Viewimports. cshtml из папки Views


@addTagHelper * , Microsoft . AspNetCore.Mvc. TagHelpers
Глава 17. Контроллеры и действия 505
Все представления, созданные в папке Views/Shared, зависят от пакета CSS по
имени Bootstrap . Для добавления
Bootstrap в проект создайте файл bower . j son с
применением шаблона элемента Bower Configuration File (Файл конфигурации Bower)
и добавьте пакет Bootstrap (листинг 17.7).

Листинг 17. 7. Добавление пакета в файле bower. j son

" name ": " asp . net ",


"private ": true ,
"dependencies ": {
"bootstrap": 11 3.3.6 11

Понятие контроллеров
Контроллеры - это классы С#, открытые методы которых (известные как методы
дейсmвий или просто действия) отвечают за обработку НТТР-запроса и подготовку
ответа, возвращаемого клиенту. Для определения, какой класс контроллера и метод
действия должны обработать запрос, инфраструктура MVC использует систему мар­
шрутизации, описанную в главах 15 и 16. Затем она создает новый экземпляр класса
контроллера , вызывает метод действия и применяет результат метода для выпуска
ответа клиенту .

Инфраструктура MVC снабжает методы действий данными контекста, так что


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

Когда МVС вызывает метод действия, ответ из метода описывает ответ, который
должен быть послан клиенту . С амый распространенный вид ответа создается путем
визуализации представления Razor, поэтому метод действия использует свой ответ,
чтобы сообщить MVC, какое представление задействовать и какие данные модели
представления ему предоставить. Но имеются также другие виды ответов, и методы
действий могут делать все, что угодно, от требования у MVC отправки клиенту пере­
направления НТГР до отправки сложных объектов данных .
Таким образом, существуют три области функциональности , которые важны для
понимания контроллеров. Во-первых, важно понимать особенности определения кон­
троллеров, так чтобы инфраструктура MVC могла применять их для обработки запро­
сов. Контроллеры представляют собой просто классы С#, но создавать их можно раз­
ными способами , и важно уяснить отличия между ними. Определение контроллеров
рассматривается в разделе " Создание контроллеров" далее в главе.
Во-вторых, важно понимать, каким образом МVС снабжает методы действий дан­
ными контекста. Получение необходимых данных контекста важно для эффективной
разработки веб-приложений, а инфраструктура MVC облегчает его, определяя набор
классов, которые используются с целью описания всего , что требуется тому или ино­
му методу действия. В разделе "Получение данных контекста" далее в главе объясня­
ется, как МVС описывает запросы и ответы .
506 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

Наконец, в-третьих, важно понимать, каким образом методы действий выпускают


ответ. Методы действий редко нуждаются в самостоятельной генерации НlТР-ответа,
и вы должны знать, как инструктировать MVC о необходимости выпуска требуемых
ответов, что будет показано в разделе "Генерирование ответа" позже в главе.

Создание контроллеров
Вы сталкивались с применением контроллеров почти во всех предшествующих
главах. Наступило время заглянуть "за кулисы" и посмотреть, как они определяются.
В последующих разделах будут описаны различные способы создания контроллеров
и приведены объяснения отличий между ними.

Создание контроллеров РОСО


Инфраструктура MVC поддерживает соглашение по конфигурации, а это значит,
что контроллеры в приложении MVC обнаруживаются автоматически вместо того,
чтобы определяться в конфигурационном файле. Базовый процесс обн аружения
прост: любой класс puЫic с именем, заканчивающимся на Con t roller, является
контроллером, а любой определенный в нем метод puЫic - действием. Чтобы вы­
яснить, как это работает, добавьте в проект папку Controllers и поместите в нее
файл класса по имени PocoController . cs, содержимое которого приведено в лис­
тинге 17.8.

Совет. Хотя соглашение предусматривает помещение контроллеров в папку Controllers,


они могут находиться где угодно в проекте и MVC по-прежнему отыщет их.

Листинг 17.8. Содержимое файла PocoController. cs из папки Controllers


namespace ControllersAndActions . Controll ers
puЬlic class PocoController {
puЬlic string Index{ ) => " This is а РОСО controller";

Класс PocoController удовлетворяет простому критерию, который МVС ожидает


найти в контроллере. Он определяет единственный метод puЫic по имени Index () ,
который будет использоваться как метод действия и который возвращает объект
string.
Класс PocoController является примером контроллера РОСО, где РОСО озна­
чает "plain old CLR object" ("простой старый объект CLR") и опирается на тот факт,
что контроллер реализован с применением .стандартных средств .NET без какой-либо
прямой зависимости от АРI-интерфейса, предоставляемого инфраструктурой ASP.NET
Саге МVС.
Чтобы протестировать контроллер РОСО, запустите приложение и запросите URL
вида /Poco/Index/. Система маршрутизации будет сопоставлять запрос со стандар­
тным шаблоном URL и направлять запрос методу Index () класса PocoController
(рис. 17. 1) .
Глава 17. Контроллеры и действия 507

~ С [ ф localhost:61521/Poco/lndex

This i s а РОСО control l er

Рис. 17. 1. Использование контроллера РОСО

Применение атрибутов для корректировки идентификации контроллеров

Поддержка контроллеров РОСО не всегда работает желательным образом . Общая проблема


заключается в том, что MVC будет идентифицировать в качестве контроллеров фиктивные
классы, созданные для модульного тестирования. Избежать указанной проблемы проще
всего, уделяя внимание именам классов и не допуская имен вроде FakeCont r o ll er .
Если это невозможно, тогда применяйте к классу атрибут NonCon t r o ll e r, определенный
в пространстве имен Mi crosoft . AspN e t Co re. Mvc , чтобы сообщить MVC о том, что
класс не является контроллером . Имеется также атрибут NonAc ti o n, который может при­
меняться к методам , чтобы предотвратить их использование как методов действий.
В ряде проектов может отсутствовать возможность следования соглашению об именовании
для класса, который должен выступать в качестве контроллера РОСО. Инфраструктуре MVC
можно указать, что класс является контроллером, даже когда он не удовлетворяет критерию

выбора РОСО, применив атрибут Cont roller, который также определен в пространстве
имен Microso ft. AspNetCore . Mvc.

Использование АРl-интерфейса контроллеров инфраструктуры MVC


Класс PocoCo nt rol l er - это удобная демонстрация способа идентификации
контроллеров инфраструктурой MVC и возможной реализации простых контролле­
ров. Но чuсmы.е контроллеры РОСО. не имеющие зависимостей от пространств имен
Mi crosoft . As pn et Core , не особенно полезны. поскольку у них отсутствует доступ к
средствам , которые MVC предлагает для обработки запросов .
Доступ к определенным частям АРI-интерфейса МVС может осуществляться за счет
создания новых экземпляров классов из пространств имен Microsoft . Aspn etCore.
В качестве про стого прим ера класс РОСО может затребовать у MVC визуализацию
представления Razor, возвращая объект Vi ewRes ul t и з своих методов действий ,
как показано в листинге 17.9. (Класс Vi e wRes ul t будет рассматриваться в разделе
"Генерирование отв ета" далее в главе.)

Листинг 17.9. Использование АРl-интерфейса MVC в файле PocoController. cs

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
namespace Cont r o ll ersAndAc t i o ns.C o nt r oll e r s
puЫic c l ass PocoCo ntro ll e r {
508 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Саге MVC

puЫic ViewResul t Index () => new ViewResul t ()


ViewName = "Result",
ViewData =
new ViewDataDictionary (
new EmptyModelMetadataProvider(),
new ModelStateDictionary()) {
Model =
$ 11 This is а РОСО controller"
}
};

Класс PocoCont r o ller больше не является чистым контроллером РОСО. т.к . он


имеет прямые зависимости от АРI-интерфейса MVC. Однако, оставив в стороне чисто­
ту, данный класс намного более полезен, чем класс из предыдущего примера. потому
что он запрашивает у MVC Razor. К сожалению, код ока­
визуализацию представления
зывается сложным . Чтобы создать объект Vi e wRes u l t, необходимо создать объекты
Vi ewDataDi ct i onary , EmptyModelMetadat a Pr ovide r и Mode l StateDict ionary,
которые требуют доступа к трем разным пространствам имен. (Средства, к которым
относятся эти типы. будут описаны в последующих главах . ) Суть рассматриваемого
примера - демонстрация того, что к средствам, предлагаемым MVC, можно обра­
щаться напрямую, даже если в результате получается некоторая путаница.

Выделенные полужирным изменения в листинге 17.9 обеспечивают визуализацию


представления Re s u l t . cshtml с применением типа string в качестве модели пред­
ставления. Запустив приложение и запросив URL вида /Poco/Index , вы получите
ответ, приведенный на рис . 17.2.

f- С <D localhost:61521/Pocoilпdex

Model Oata: Тhis is а Р О СО controller

Рис. 17.2. Работа с АРl-интерфейсом MVC напрямую

Использование базового класса Controller


Пр едшествующие примеры проиллюстрировали, каким образом начать с контрол­
лера РОСО и обеспечить ему доступ к средствам МVС. Такой подход проливает свет
на то, как работает MVC; это полезное знание на тот случай, если вы обнаружите,
что невольно создаете контроллеры, но контроллеры РОСО неудобны в написании,
восприятии и сопровождении .

Более легкий способ создания контроллеров предусматривает наследование клас­


сов от класса Mic r o s oft . AspNetCore . Mvc . Cont r ol l er, в котором определены ме­
тоды и свойства, предоставляющие доступ к средствам MVC в сжатой и удобной ма ­
нере. Добавьте в папку Con t rolle r s файл класса по имени Deri vedCon t r ol l er . cs
с определением контроллера, приведенным в листинге 17. 1О .
Глава 17. Контроллеры и действия 509
Листинг 17.10. Наследование от классаController
в файле DerivedController. cs из папки Controllers

using Microsoft . AspNetCore . Mvc ;


namespace ControllersAndActions.Controllers
puЬlic class DerivedController : Controller {
puЫic ViewResult Index() =>
View(HResult", $"This is а derived controller");

Запустив приложение и запросив URL вида / Der i ved/ Index, вы получите резуль­
тат, показанный на рис. 17.3.

Model Data: This is а derived controller


1
L---------------------------
Рис. 17.3. Использование базового класса Controller

Контроллер в листинге 17. 1О делает то же самое, что и контроллер в листинге 17.9


(он требует у инфраструктуры MVC визуализировать представление с моделью пред­
ставления string), но применение базового класса Controller означает возмож­
ность получения результата более простым путем.
Основно е изменение касается того, что объект ViewResul t, т ребующийся для
визуализации представления Razor, можно создать с использованием метода View ()
вместо явного создания экземпляра этого класса (и необходимых ему других типов)
прямо в методе действия. Метод View () унаследован от базового класса Controller,
а объект ViewResul t по-прежнему создается тем же способом , просто не загромождая
метод действия. Наследование от класса Controller не изменяет манеру работы конт­
роллеров; оно всего лишь упрощает код, который пишется для решения общих задач.

На заметку! Инфраструктура MVC создает новый экземпляр класса контроллера для каждого
запроса, который ей предлагается обработать. Это значит, что вам не придется синхро­
низировать доступ к методам действий или свойствам и полям экземпляра. Разделяемые
объекты, в том числе базы данных и службы-одиночки, которые описаны в главе 18, могут
применяться параллельно и должны быть соответствующим образом реализованы.

Получение данных контекста


Независимо от того, как определяются контроллеры, они редко будут существовать
в изоляции и обычно нуждаются в доступе к данным из входящего запроса, таким
как значения строки запроса, значения формы и параметры, извлеченные из URL
51 О Часть 11 . Подробные сведения об инфраструктуре ASP.NET Саге MVC

систем ой маршрутизации, что все вместе называют данными контекста. Есть три
основных способа досrупа к данным контекста :

• извлечение данных из набора объектов конте1сста;

• получение данных как параметра метода действия;

• явное обращение к средству привязки моделей инфраструктуры .

Здесь мы рассмот рим подходы к получению входных данных для м етодов де йс­
твий, сосредоточившись на объектах контекста и параметрах методов действий .
Привязка моделей р а скрывается в глав е 26.

Получение данных из объектов контекста


Одно из главных преимуществ использования базового класса Co n tr o ll er для
создания контроллеров заключается в удобном досrупе к набору объ ектов контекста,
которые описывают текущий запрос, подготавливаемый ответ и состояние приложе­
ния. В табл. 17.3 описаны наиболее полезные свойства контекста Contr o l ler .

Таблица 17.З. Полезные свойства класса Controller для данных контекста

Имя Описание

Reque s t Это свойство возвращает объект HttpReque s t , который описывает


запрос, полученный от клиента (табл. 17.4)
Respons e Это свойство возвращает объект HttpResponse, который применя­
ется для создания ответа клиенту (табл . 17.7)
HttpContext Это свойство возвращает объект Ht tpContext , который является
источником многих объектов , возвращаемых другими свойствами ,
такими как Reque s t и Re spons e . Оно также предоставляет инфор­
мацию о доступных возможностях НТТР и доступ к низкоуровневым
средствам наподобие веб-сокетов

RouteData Это свойство возвращает объект RouteData , выпускаемый сис­


темой маршрутизации, когда имеется совпадение с запросом , как
описано в главах 15 и 16
Model State Это свойство возвращает объект Model Sta t eDictionary, кото­
рый используется для проверки достоверности данных, отправлен­
ных клиентом (глава 27)
Us e r Это свойство возвращает объект ClaimsPrincipa l, который опи­
сывает пользователя, сделавшего запрос, как объясняется в глава х
29 и 30

Многие контроллеры пишутся без применения свойств из табл. 17.3, потому что
данные контекста доступны через средства, рассматриваемые в последующих главах,

которые больше гармонируют со стилем разработки приложений MVC. Наприм ер ,


большинство контроллеров не нуждаются в использовании свойства Request для по­
лучения деталей обрабатываемого НТТР-запроса, поскольку та же самая информация
доступна через процесс привязки модели, который обсуждается в главе 26.
Но свойства, перечисленные в табл. 17.3, все же могут быть полезны для понима­
ния и работы с объектами контекста, а также для целей отладки . В листинге 17 .11
свойство Reques t применяется для досrупа к заголовкам в НТТР-запросе .
Глава 17. Контроллеры и де й ствия 511
Листинг 17 .11. Использование данных контекста в файле Deri vedCon troller. cs
using Microsoft . AspNetCore . Mv c ;
using System.Linq;
namespace Control l ersAndActions . Controllers
puЬlic c l ass DerivedController : Controller {
puЬlic ViewResult Index(} =>
View( " Result ", $" Thi s is а derived controller " };
puЬlic ViewResul t Headers () => View ( "DictionaryResul t" ,
Request.Headers.ToDictionary(kvp => kvp.Ke y,
kvp => kvp.Value.First()));

Раб от а с объ е ктам и контек ста означает перемеще ни е по диапа з ону различных
типов и простран ств имен. Свойство Contr o lle r. Request , которо е применяет ­
ся дл я получ ения данных контекста о НТГР-запросе в листинг е , во звраща ет объект
HttpRequest . В табл . 17.4 описаны сво йства HttpRequest, наибол ее полезные при
нап и сан ии классов к онтроллеров.

Таблица 17.4. Распространенные свойства HttpRequest


Имя Описание

Path Это свойство возвращает раздел пути URL запроса


QueryString Это свойство возвращает раздел строки запроса URL запроса
Headers Это свойство возвращает словарь заголовков запроса, индексированный
по именам

Body Это свойство возвращает поток, который может использоваться для чте­
ния тела запроса

Form Это свойство возвращает словарь данных формы в запросе, индексиро­


ванный по именам

Cookies Это свойство возвращает словарь сооkiе-наборов запроса, индексиро­


ванный по именам

Свой ство Request . Headers прим е ня ется для получ е н ия слов аря з аголовков, ко­
торый обр а батывается с п о м ощью LINQ:

View( " DictionaryResult ", Request . Headers .ToDictionary(kvp => kvp.Key,


kvp => kvp.Value.First() }} ;

Словарь , в озвращаемы й свойством Request . Headers, хр анит зна ч е ния всех з аго­
ловков с исполь з ованием структуры StringValues, которая приме ня ется в ASP.NET
для представления посл едовательности строковых з нач ени й . Клиент НТТР может от­
пр авить для загол овков НТГР нес к ол ько знач ений, но н ам нужно отобр аз ить тол ько
п е рво е з нач е ние. С помощью LINQ-м eтoдa ToDictionary () дл я каждого заголовка
получается объ е кт KeyValuePair<string , Str i ngValues > и выбирает ся первое
зн ач е ни е . Результато м явля ется слов ар ь , содержащий з начения string , к оторые мо­
гут о тображать ся пр едставлени ем DictionaryResult.
512 Часть 11 . Подробные сведения об инфраструктуре ASP. NET Core MVC

Запустив приложение и запросив URL вида /De ri v e d/Head er s , вы получите вы­


вод , подобный показанному на рис. l 7.4. (Набор заголовков и их значений будет от­
личаться в зависимости от используемого браузера.)

[j Contro r ~s and Actioru; )(

~- --~ [~~~~~~:~~~:_~~~~.~~-i~~=~:.~~~=~=;:;-:,-_~-=~~-
Name Value

Connection Keep-Alive

Accept

Accept-
=~::,::::::"""'""'mlopPl"'loo'"" '"''""""'~ь..·r '"'' 1
Encoding

Ассер\- en·US.en;q:O.B
Language

Host locall10st:61521

User-Agent Mozilla/5.0 (Windows NT 10.О; W0\"164) AppleWebKiV537.36 (KHTML. j1

.г- ~k~"k~,~~~~ ~4'""'


Рис. 17 .4. Отображение данных контекста

получение данных контекста в контроллере РОСО


Хотя контроллеры РОСО не особенно полезны в обычных проектах, они позволя­
ют заглянуть "за кулисы " и посмотреть, как функционирует МVС. Получение данных
контекста в контроллер е РОСО свя з ано с проблем ой , поскольку нельзя про сто со здать
собственные объекты Ht tpReque s t или Htt p Respons e ; необходимы объекты , кото­
рые были созданы ASP.NET и обновлены всеми компонентами промежуточного ПО,
заполняющими поля данных этих объектов по мере обработки запроса.
Чтобы получить данные контекста, контроллер РОСО должен запрос ить их пре­
доставление у инфраструктуры МVС . В листинге 17.12 класс PocoCon tro ll e r моди­
фицирован для добавл ения метода действия, который отображает заголовки НТГР­
запроса.

Листинг 17.12. Отображение данных контекста в файле PocoController. cs

u sing Microsoft . AspNetCore . Mvc ;


us ing Mi c r oso f t . AspNetCo r e . Mvc . Mode lBi ndin g ;
u si n g Mic r osoft . As p NetCo r e . Mvc . Vi ew Fea tur es ;
using System.Linq;
names p ace Control le r sAndActio ns. Cont r ol l ers
p uЫi c c l as s PocoCo ntr o ll e r {
[ControllerContext]
puЫic ControllerContext ControllerContext { get; set; }
puЫ ic Vi ewRes ul t Inde x (} => n e w ViewRes ul t(} (
ViewName " Re s ul t ",
Vi ewDa t a = n ew ViewDa t aDic ti o n ary( n ew EmptyMode l MetadataProvider(} ,
ne w Model Sta t e Di c ti o na r y(} } (
Mode l = $ "Thi s is а Р ОСО con trol l e r"

};
Глава 17. Контроллеры и действия 513
puЫic ViewResult Headers () =>
new ViewResult() {
ViewName = "DictionaryResult",
ViewData = new ViewDataDictionary(
new EmptyModelMetadataProvider(),
new ModelStateDictionary()) {
Model = ControllerContext.HttpContext.Request.Headers
.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.First())

};

Дllя получения данных контекста определяется свойство по имени Con trollerCon text
типаControllerContex t, которое декорировано атрибутом, также имеющим имя
Control l erContext .
Пол езно объяснить три разных случая применения термина ControllerContext .
Первое использование - класс ControllerContext из пространства имен
Microsoft . AspNetCore . Mvc, представляющий собой класс, который собирает вмес­
те все объекты контекста, требующиеся методу действия контроллера, с применением
свойств, описанных в табл. 17.5.

Таблица 17.5. Самые важные свойства ControllerContext

Имя Описание

Act i onDesc riptor Это свойство возвращает объект ActionDescriptor, который


описывает метод действия

Ht tpContext Это свойство возвращает объект HttpContext, который предо­


ставляет детали НТТР-запроса и отправляемого взамен НТТР-ответа
(табл. 17.6)
ModelState Это свойство возвращает объект Mode lSt ateDictionary, кото­
рый используется для провер ки достоверности данных , отправляе­
мых клиенту (глава 27)
RouteData Это свойство возвращает объект RouteData, описывающий способ,
которым система маршрутизации обработала запрос (глава 15)

Дос туп к данным, связанным с НТТР, осуществляется через свойство


Cont r o ll e rC ontext . HttpCo n text , которое возвращает объект Microsoft .
AspNetCore . Http . HttpCon text . Класс HttpContext объединяет несколько объек­
тов, описывающих различные аспекты запроса, которые доступны через свойства,
п еречисленные в табл . 17.6.
Второе использование атрибута Con tr oll erContext продемонстри ровано в лис­
тинге 17.12: с его помощью декорируется свойство, чтобы сообщить инфраструктуре
МVС о необходимости установки значения свойства в объект Contro ller Co ntext,
который описьmает текущий запрос. В этом случае применяется прием, назьmаемый
внедрением зависимостей, который рассматривается в главе 18, и MVC будет исполь­
зовать такое свойство для предоставления контроллеру данных контекста перед вы­
зовом какого-нибудь метода действия, чтобы обработать запрос.
514 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Соге MVC

Таблица 17.6. Распространенные свойства HttpContext

Имя Описание

Connection Это свойство возвращает объект Connectioninfo, который описывает


низ к оуровневое подключение к клиенту

Request Это свойство возвращает объект HttpRequest, который описывает


НТТР-запрос, полученный от клиента, как объяснялось ранее в главе

Response Это свойство возвращает объект HttpResponse, применяемый для со­


здания ответа, который будет возвращен клиенту, как описано в разделе
" Генерирование ответа" далее в главе

Session Это свойство возвращает объект ISession, описывающий сеанс,


с которым ассоциирован запрос

User Это свойство возвращает объект ClaimsPrincipal, который описыва­


ет пользователя, ассоциированного с запросом (глава 28)

Наконец , третье прим енение термина ControllerContext - имя самого свойс­


тва. В контроллерах РОСО можно использовать любые допустимые имена свойств С#,
но имя ControllerContext было выбрано из-за того, что оно прим еняется классом
Controller. "За кулисами" класс Controller для своих данных контекста полага­
ется на тот же самый класс ControllerContext, которы й декорирован тем же са­
мым атрибутом ControllerContext . Все свойства класса Controller, описанные в
табл. 17.3, являются просто более удобной и краткой альтернативой использованию
свойств класса ControllerContext напрямую, что в действительности происходит
внутри свойств, предоставляемых классом Controller . В качестве примера ниже
приведено определение свойства HttpContext из класса Controller:

puЫic HttpContext HttpContext {


get {
return ControllerContext . HttpContext ;

Свойство HttpContext - это всего лишь более удобный способ получить значе ­
ние свойства ControllerContext . HttpContext. Никакой "магии" в базовом классе
Controller нет: он дает в р езультате более простые и ясны е контроллеры по той при­
чине, что объединяет общие задачи в удобные методы и свойства, которые при необ­
ходимости можно воссоздать самостоятельно в контроллере РОСО . Если углубиться в
детали, то выяснится, что масса функциональности в инфраструктуре ASP.NEТ Core
MVC удивительно проста; здесь отсутствует какая-либо сп ециальная и зюминка -
только продуманная функциональность , предлагаемая тщательно спроектированным
набором пакетов NuGet. При наличии времени рекомендуется удостовериться в это м,
загрузив исходный код МVС из http : //github . com/aspnet и изучив его.

Использование параметров метода действия


Определе нные данны е контекста могут быть также получ ены через пара метры
метода действия, что позволяет создавать более естественный и элегантный код.
Распростран енн ый приме р касается ситуации , когда метод действия нуждается в по­
лучении значений данных формы, отправленной пользователем. Для сравнения далее
Глава 17. Контроллеры и де йствия 515
будет показано, как получить данные формы через объекты контекста и затем через
параметры метода действия.
Доступ к значениям данных формы осуществляется посредством свойс­
тваRequest . Forrn класса Controller. В целях демонстрации добавьте в папку
Controllers файл класса по имени HomeController. cs и определите в нем произ­
водный контроллер, приведенный в листинге 17.13.
Листинг 17.13. Содержимое файла HomeController. cs из папки Controllers
using Mic rosoft.AspNetCore.Mvc;
namespace ControllersAndActions.Controllers
puЬlic c la ss HorneController : Con troller {
puЫic Vi ewRes ult Index() => View( "Sirnpl eForrn " ) ;
puЫic ViewRes ult ReceiveForrn() {
var name = Request . Form["narne "];
var ci ty = Request . Form [ " ci ty"] ;
return View("Result ", $ " {name} lives in {city}");

Метод действия Index () в этом контроллере визуализирует представление


SimpleForm, которое было создано внутри папки Views/Shared в начале главы.
Нас интересует метод Recei veForm (), потому что он применяет объект контекста
HttpRequest для получения значений данных формы из запроса.
Как бьmо описано в табл. 17.4, свойство Forrn, определенное в классе HttpRequest,
возвращает коллекцию значений данных формы, индексированную по именам ассоци­
ированных НТМL-элементов. В представлении SimpleForm присутствуют два элемента
inpu t , narne и city, значения которых извлекаются из объекта контекста и использу­
ются для создания строки, передаваемой представлению Resul t в качестве его модели.
После запуска приложения и запроса URL вида /Ноте отобразится форма. Когда
вы заполните поля и щелкнете на кнопке Submit (Отправить), браузер отправит
данные формы как часть НТГР-запроса POST, который будет обработан методом
Recei veForm (), порождая показанный на рис. 17.5 результат.

Name: _ -~---?-~~~~~~~~'e1.R.:~eN~Fo_rn1 ---~- J


' Adam Model Data: Adam lives in London

1
City:

------------"·--11
~------"·--г-··--------·-------~

Гlond;~ -~----

___________· ____________!
Рис. 17 .5. Получение данных формы из объектов контекста
516 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

Проиллюстрированный в листинге 17. 13 подход работает велюилепно, но су­


ществует более элегантная альтернатива. В методах действий можно определять
параметры, которые MVC применяет для передачи данных контекста контроллеру,
в:ключая детали НТТР-запроса. Альтернативный подход более ла:коничен, чем из­
влечение данных :конте:кста непосредственно из объектов конте:кста, и дает методы
действий. которые легче в восприятии. Чтобы получить данные формы, объявите в
методе действия параметры, имена :которых соответствуют значениям данных формы
(листинг 17.14).

Листинг 17.14. Получение данных контекста как параметров


в файле HomeController. cs
using Microsoft .As pNetCore . Mvc ;
namespace ControllersAndActions . Controllers
puЬlic class HomeControl l er : Contro ll e r {
puЬlic ViewRe sul t Index () => View ( "S impleForm " ) ;
puЫic ViewResult Rece i veForm(str i ng name , string c i ty)
=> View( " Result ", $ " {name} lives in {city} " ) ;

Пересмотренный метод действия выпускает тот же самый результат, но он про­


ще в восприятии . Инфраструктура MVC будет предоставлять значения для пара ­
метров метода действия , автоматически проверяя объе:кты конте:кста, в том числе
Request . QueryString , Request . Form и RouteData. Va lu es . Имена параметров
трактуются :ка:к нечувствительные :к регистру символов, таи что параметр метода

действия ci ty может быть наполнен значением из Request . Form [ "Ci ty " ] , напри­
мер. Такой подход также дает методы действий, легче подцающиеся модульному тес­
тированию, пос:коль:ку значения , которыми оперирует метод действия , получаются
:как обычные параметры С# и не требуют имитации объектов контекста.

Генерирование ответа
После того как метод действия завершил обработку запроса , ему необходимо сге ­
нерировать ответ. Для генерирования вывода из методов действий доступно много
средств, которые будут описаны в последующих разделах.

Генерирование ответа с использованием объекта контекста


Самый низкоуровневый способ генерации вывода предусматривает применение
объекта контекста HttpResponse - именно так инфраструктура ASP.NET Core пре­
доставляет доступ к НТТР-ответу, который будет отправлен клиенту . В табл. 17. 7 пе­
речислены базовые свойства класса Htt pRespon se , определенного в пространстве
имен Microsoft . AspNetCore . Http.
В листинге 17.15 контроллер Ноте обновлен, чтобы его действие Recei vedForm
генерировало ответ с использованием объекта HttpResponse, возвращаемого свойс­
твом Controller. Request.
Глава 17. Контроллеры и действия 517
Таб лица 17.7. Распространенные свойства HttpResponse
Имя Описание

StatusCode Это свойство используется для установ к и кода состояния НТТР, связанного
с ответом

ContentType Это свойство применяется для установки заголовка Content-Type ответа

Headers Это свойство возвращает словарь заголов ков НТТР, которые будут включе­
ны в ответ

Cookies Это свойство возвращает коллекцию, которая используется для добавле­


ния сооkiе-наборов к ответу
Body Это свойство возвращает объект System . IO . Stream, который применя­
ется для записи данны х тела запроса

Ли стинг 17.15. Выпуск ответа в файле HomeController. cs


using Microsoft . AspNetCore . Mvc ;
using System.Text;
namespace ControllersAndActions.Controllers
puЫic c l ass HomeController : Controller {
puЫic ViewResu l t Index () => View ( " SimpleForm " ) ;
puЬlic void ReceiveForm(string name, string city)
Response.StatusCode = 200;
Response.ContentType = "text/html";
byte [] content = Encoding .ASCII
. GetBytes ($"<htmJ.><body>{name} li.ves i.n {ci.ty}</body>");
Response.Body.WriteAsync(content, О, content.Length);

Это плохой способ генерации ответа из-за жесткого кодирования НТМL-разметки в


методе действия с применением строк С# , что чревато ошибками и трудно поддается
модульному тестированию . Тем не менее, мы получаем отправную точку для выясне­
ния , каким образом ответы создаются "за кулисами".
Сушествуют эффективные альтернативы, которые предусматривают работу непос­
редственно с объектом HttpResponse . Инфраструнтура MVC строит низкоуровневый
ответ с помощью намно го более удобного средства, находящегося в центральной час­
ти функциональности контроллеров - результата действия.

Понятие результатов действий


Результаты действий используются в МVС для отделения заявлений о намерениях
от выполнения намерений. После освоения концепция покажется простой, но понима­
ние лежащего в основе подхода может занять не1юторое время по причине присушей
ему косвенности .

Вместо того чтобы иметь дело напрямую с объектом HttpResponse . методы дейс­
твий возвращают объект, который реализует интерфейс IActionResul t из про­
странства имен Microsoft . AspNetCore . Mvc. Объе кт реализации IActionResu l t,
518 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Саге MVC

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

другой URL. Но - и здесь в и гру вступает косвенность - ответ напрямую не генери­


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

На заметку! Система результатов действий является примером паттерна Команда. Этот


паттерн затрагивает сценарии, при которых сохра няются и передаются объе кты, описы­
вающие выполняе м ые операции . Дополнительные детали можно найти в к ниге Роберта
Мартина Гибкая разработка программ на Java и С++ : принципы, паттерны и методики
(Диалектика, 2016 год).

Ниже приведено определение интерфейса IActionResul t из исходного кода


МVС:

using System . Threading . Tasks ;


namespace Microsoft . AspNetCore . Mvc
puЫic interface IActionResult {
Task ExecuteResultAsync(ActionContext context) ;

Интерфейс IActionResult может показаться простым, но это потому, что MVC не


навязывает виды ответов, которые может выпускать результат действия. Когда метод
действия возвращает результат действия. инфраструктура МVС вызывает его метод
ExecuteResul tAsync (),который отвечает за генерирование ответа от имени метода
действия. Аргумент ActionContext пр едоставляет данные контекста для генерации
ответа, включая объект HttpResponse . (Клас с ActionContext является суперклас ­
сом coritro llerContext и определяет все свойства. описанные в табл. 17.5.)
Чтобы по смотреть, как работают результаты действий , добавьте в проект папку
Infrastructure и поместите в нее файл класса по имени CustomHtmlResult . cs с
определением результата действия из ли стинга 17 .16.

Листинг 17.16. Содержимое файла CustomHtmlResult.cs из папки Infrastructure


using Microsoft . AspNetCore .Mvc ;
using System .Te xt ;
using System .T hreading .T asks ;
namespace ControllersAndActions . Infrastructure
puЫic c lass CustomHtmlResu lt : IActionResu lt
puЫic string Conte nt { get; set ; }
puЫic Task ExecuteResultAsync(ActionContext context) {
context . HttpContext . Response . StatusCode = 200 ;
context.Ht tpC ontext.Response . ContentType = " text/html ";
byte (] content = Encoding . ASCII . GetBytes(Content) ;
return context .Ht tpContext . Response .Body. Wr it eAsync(content ,
О , content . Length);
Глава 17. Контроллеры и действия 519
Класс CustomHtmlResul t реализует интерфейс IActionResul t, а его метод
ExecuteResul tAsync () применяет объект HttpResponse для записи НТМL-ответа, ко­
торый содержит значение свойства по имени Content. Метод ExecuteResul tAsync ()
должен возвращать объект Task, так что ответ может строиться аси нхронно. Это
хорошо согласуется с его реализацией в классе CustomHtmlResul t, которая полага­
ется на метод WriteAsync () объекта Stream, представляющего тело ответа. Вызов
WriteAsync () возвращает объект Task, который можно использовать в каче стве ре­
зультата метода ExecuteResul tAsync () в классе CustomHtmlResul t .
В листинге 17.17 класс результата действия применяется к контроллеру Home, уп­
рощая метод действия Recei veForm () контроллера Home.

Листинг 17.17. Использование результата действия в файле HorneController.cs


using Microsoft . AspNetCore.Mvc ;
using System . Text ;
using ControllersAnc:!Actions.Infrastructure;
namespace ControllersAndActions . Controllers
puЫic class HomeController : Control l er {
puЫic ViewResul t Index () => View ( " SimpleForm " ) ;
puЬlic IActionResult ReceiveForrn(string narne, string city)
=> new CustornНtrnlResul t {
Content = $ {narne} lives in {city}"
11

} ;

Код, отправляющий ответ, теперь определен отдельно от данных, которые содер­


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

Модульное тестирование контроллеров и действий

Многие части ASP.NET Core MVC спроектированы для упрощения модульного тестирования,
и это особенно справедливо в отношении действий и контроллеров. Наличие такой подде­
р ж ки объясняется несколькими причинами.

• Тестировать действия и контроллеры можно за пределами веб-сервера .

• Для тестирования результата метода действия проводить разбор какой-либо НТМL­


разметки не понадобится. Чтобы удостовериться в получении ожидаемых результатов,
можно проинспектировать возвращаемый объект реализации IActionResul t .
• Эмуляция клиентских запросов не нужна . Система привязки моделей MVC позволяет пи­
сать методы де й ствий, которые получают входные данные в качестве своих параметров .
Для тестирования метода действия необходимо просто вызвать его напрямую и предо­
ставить интересующие значения параметров.

Далее в главе будет показано, как создавать модульные тесты для разных видов результа­
тов действий. В главе 7 приводились инструкции для настройки проекта модульного тести­
рования; можно также загрузить готовые проекты из веб - сайта издательства.
520 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

Генерирование НТМL-ответа
В предыдущем разделе была возможность изъять из класса контроллера код. ге­
нерирующий отв ет, с прим е н ением результата действия . Инфраструктура ASP.NET
Core MVC укомплектована более гибким подходом 1t выпуску ответов - классом
ViewResul t .
Класс Vi ewResu l t - это результат действия, предоставляющий доступ к механиз­
му визуализации Razor, который обрабатывает файлы . cshtrnl для внедрения данных
модели и отправляет результат клиенту через механизм контекста HttpResponse.
Работа механизмов визуализации объясняется в главе 21, а здесь мы сосредоточим
внимание на использовании класса ViewResult как результата действия.
В листинге 17 .18 специальный класс результата действия заменен классом
ViewResul t, экземпляр 1шторого создается посредством метода View (), предостав­
ляемого базовым классом Controller.

Листинг 17 .18. Применение класса ViewResul t в файле HomeController. cs


using Microsoft.AspNetCore .Mvc ;
using System . Text;
using Cont roll ersAn dActions .Infras tructure;
name s pace ControllersAndAct ions . Con tr o lle rs
puЫic class HomeController : Controller {
puЫic ViewResult Index() => Vi ew (" SirnpleForm");
puЬlic Vi e wResu l t Receive Forrn(string narne, string c ity )
=> View( " Res ul t ", $ " {name } lives in {c ity}" ) ;

Объекты ViewResul t можно создавать напрямую, как демонстрировалось в кон­


троллере РОСО в начале главы, но использование метода View () дает более простой
и краткий код. Класс Controlle r предлагает несколько версий метода View (), кото­
рые позволяют выбирать представление, подлежащее визуализации, и снабжать его
данными модели (табл. 17.8).

Таблица 17.8. Методы View () класса Controller

Метод Описание

View () Этот метод создает объект ViewRes ul t для стандартного представ­


ления, ассоциированного с методом действия, так что вызов
View () в
методе по имени MyAction () будет визуализировать представление
под названием MyAction. cshtrnl. Данные модели не применяются
View (vi ew) Этот метод создает объект ViewResul t, который визуализирует
указанное представление, так что вызов View ( "MyView ") будет
визуализировать представление по имени MyView . cs h trnl .
Данные модели не используются

View (rnodel) Этот метод создает объект ViewResul t для стандартного пред­
ставления, ассоциированного с методом действия, и применяет
указанный объект как данные модели

View (vi ew , rnodel) Этот метод создает объект Vi ewResul t для указанного представле­
ния и использует указанный объект как данные модели
Глава 17. Контроллеры и действия 521
Запустив приложение и отправив форму, вы увидите знакомый результат
(рис. 17.6).

D Controllers ~nd Action< Х

1 f- С) ControUers ond Actio11> Х ,


1 Name: ---·------------·- - · · - - -
,---- . -----·-- ·-------··- ______"]. ~ ; С [so_~-~lho~~;_::_:1rм~1_:~~eceiveF~-':'2J ;
i
1 Adam! -- -·-" -- -- - ·----- __"_ ------·-- --· ". -- - - " - - -··
/ ··--- - ------------ --- . Model Data: Adam lives in London j
C ity: [-------~-------------
! Londo11

1
1
L ____ _

Рис. 17 .6.
----- "'' ·--~·-------·-

Применение объекта ViewResul t


J для генерации НТМL-ответа

Поиск файла представления


Когда инфраструктура MVC вызывает метод ExecuteResul tAsync () объ­
екта ViewResul t, начинается поиск представления, которое было указано.
Последовательность каталогов, где МVС ищет представление , является примером со­
глашения по конфигурации. Вам не придется регистрировать файлы представлений с
помощью инфраструктуры . Вы просто помещаете их в одно из набора известных мес­
тоположений, и инфраструктура найдет их. По умолчанию MVC будет искать пред­
ставление в следующих местоположениях:

/Vi еws /<ИмяКонтроллера>/<ИмяПредставления> . сshtml


/Vi ews /Shared/<ИмяПpeдcтaвлeния> .cshtrnl

Поиск начинается с папки , которая содержит представления, выделенные для те­


кущего контроллера. В имени этой папки опускается частьContro ller имени клас­
HomeController будет Views/Home.
са, так что папкой для класса
Если имя представления в объекте ViewResul t не указано, тогда будет исполь­
зоваться значение переменной action из данных маршрутизации . Для большинс­
тва контроллеров это означает применение имени метода действия, следовательно,
стандартным файлом представления, ассоциированным с методом Index () , является
Index . cshtml . Однако если вы использовали атрибут Route, то имя представления,
связанн ое с методом действия, может быть другим.
Если контроллер принадлежит области, как было описано в главе 16, тогда место­
положения поиска отличаются:

/Аrеаs/<ИмяОбласти>/Viеws/<ИмяКонтроллера>/<ИмяПредставления> . сshtтl
/Аrеаs/<ИмяОбласти>/Viеw s/Sh аrе d/<ИмяПр едставления> . сsh tтl
/V iews/Shared/<ИмяПpeдcтaвлeния>.cshtml

Инфраструктура MVC проверяет, существует ли каждый из перечисленных фай­


лов. по очереди. Как только обнаруживается совпадение, она применяет найденное
представлен ие для визуализации результата метода действия. Области в примере
522 Часть 11. Подробные сведения об инфраструктуре ASP.NET Саге MVC

проекта не используются, поэтому метод действия из листинга 17. 18 заставит MVC


начать поиск с файла Views/Home/Result . cshtml . Такого файла н е существует, так
что MVC продолжит поиск для файла Views/Shared/Resul t . cshtml . который будет
найден и применен для визуализации НТМL-ответа.

Модульное тестирование: визуализация представления

Чтобы протестировать представление, которое визуализирует метод действия, можно про­


инспектировать возвращаемый им объект ViewResu l t . Это не совсем то же самое (в кон ­
це концов, вы не следуете процессу вплоть до проверки финальной НТМL-разметки, которая
была сгенерирована), но достаточно близко к реальности, т.к . позволяет удостовериться в
корректной работе системы представлений MVC . В проект модульного тестирования добав­
лен новый файл по имени ActionTests . cs для хранения модульных тестов, задейство­
ванных в настоящей главе .

Первой тестируемой ситуацией является момент, когда метод действия выбирает специфи­
ческое представление:

puЬlic ViewResult ReceiveForm(string name , string city)


=> View( " Result ", $ " {name} lives in {city} " ) ;
Можно определить, какое представление было выбрано, прочитав свойство ViewName объ­
екта ViewResul t, как показано в следующем тестовом методе:

using ControllersAndActions . Controllers ;


using Microsoft . AspNetCore . Mvc ;
using Xunit ;
namespace ControllersAndActions . Tests
puЬlic class ActionTests (
[Fact]
puЫic void Vi ewSelected()
11 Организация
HomeController controller = new HomeController();
11 Действие
ViewResult result = controller . ReceiveForm( "Adam ", " London " ) ;
11 Утверждение
Assert . Equa l ( " Result ", result .Vi ewName);

При тестировании метода действия , который выбирает стандартное представление, возни­


кает небольшая вариация:

puЫic ViewResult Result() => View();

В таких ситуациях необходимо удостовериться, что именем представления является nul l:

Assert .N ull(result .V iewName) ;

С помощью значения null объект ViewResul t сигнализирует MVC о том, что было вы­
брано стандартное представление, ассоциированное с методом действия .
Глава 17. Контроллеры и действия 523

Указание представления по его пути

Подход с соглашением об именовании для представлений удобен и прост, но он ограни­


чивает представления, которые можно визуализировать. Если требуется визуализировать
специфичное представление, то для этого можно предоставить явный путь и пропустить
стадию поиска. Ниже приведен пример:
using Microsof t. AspNetCo r e . Mvc;
namespace Control l e rsAndAc t ion s . Co nt ro l lers {
puЫic c l a s s ExampleCont r o ll er : Controlle r {
puЬl i c Vi e wRe sul t Inde x() {
return View("/Views/Admin/Index");

При таком указании представления путь должен начинаться с / или ~ / и может включать
расширение имени файла (которым будет . c shtml, если оно не указано).
Когда вы обнаруживаете , что пользуетесь этим средством, возьмите паузу и задайте себе
вопрос : чего вы стараетесь достичь? Если вы пытаетесь визуализировать представление , при ­
надлежащее другому контроллеру, тогда может быть лучше перенаправить пользователя на
метод действия в другом контроллере (пример ищите в разделе "Перенаправление на метод
действия" далее в главе). Если вы пытаетесь обойти схему именования файлов представле­
ний, поскольку она не соответствует способу организации вашего проекта, тогда обратитесь
в главу 21, в которой объясняется реализация специальной последовательности поиска.

Передача данных из метода действия в представление


Когда объект Vi e wRes ul t используется для выбора представления, из метода дейс­
твия можно передавать данные для применения при генерации НТМL-содержимого .
Инфраструктура MVC пр едлагает различные способы передачи данных из метода
действия в представление, которые описаны в последующих разделах. Эти средства
естественным образом касаются темы представлений, которая более подробно рас-
1tрывается в главе 21. В настоящей главе мы обсудим только ту функциональность
представлений , которой достаточно для демонстрации средств контроллеров.

Использование объекта модели представления


Отправить объе1tт представлению можно путем его передачи в качестве парамет­
ра методу View () , что приведет к установке свойства Vi ewData . Mod e l созданного
объекта ViewResul t . В листинге 17.9 указанное свойство устанавливалось напря­
мую , чтобы объяснить, как работают контроллеры РОСО, но метод View () позабо­
тится об этом более лаконичным образом. В листинге 17.19 п01~а з ан новый класс
Examp l e Controll er. добавленный в папку Con t rolle rs, который передает объект
модели представления методу View ( ).

Листинг 17.19. Содержимое файла ExampleController.cs из папки Controllers


using Micr osof t. AspNetC o re.Mvc;
using Sy s tem ;
namespace ControllersAn dAc ti o n s . Contro l l e rs
puЫic c l ass ExampleCont r o ll er : Cont r o ll er {
puЫic ViewRe su l t Index () => Vi ew ( Da t e Time . Now) ;
524 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Саге MVC

Объект DateTime передается методу View () для применения в качестве модели


представления . Для доступа к объекту изнутри представления используется ключевое

слово Model механизма Razor. Далее создается папка Views/Example, куда помеща­
ется файл представления по имени Index. cshtml, содержимое которого приведено
в листинге 17.20.

Листинг 17.20. Содержимое файла Index. csh tml из папки Views/Example

@{ Layout = null ;
< ! DOCTYPE html>
<html>
<head>
<meta name="viewport" content= " width=device - wi dth " />
<title>Controllers and Actions</title>
<link rel= "stylesheet" asp-href-include="liЬ/bootstrap/dist/css/*.min.css" />
</head>
<body class= "panel-body " >
Model : @(( (DateTime)Mode l) .DayOfWeek)
</body>
</html >

Представление в листинге 17.20 называется нетuпuзированным или слабо типи­


зированным. Такому представлению ничего не известно об объекте модели представ­
ления, и оно трактует его как экземпляр obj ect. Чтобы получить значение свойства
DayOfWeek, экземпляр objec t понадобится привести к типу DateTime:

Model : @(( (DateTime) Model) .DayOfWeek )

Прием работает, но порождает запутанные представления. Навести порядок мож­


но за счет создания строго типизированных представлений, когда представление
включает детали типа объекта модели представления, как демонстрируется в лис­
тинге 17.21.

Листинг 17 .21. Добавление строго типизированного представления


в файл Index. csh tml из папки Views/Exarnple
@model DateTime
@{ Layout = null ;
< ! DOCTYPE html>
<html>
<head>
<meta name="viewport " content= "width=device - width" />
<title>Contro ll ers and Actions</title>
<link rel= " stylesheet " asp -h ref - inc lude="li Ь/bootstrap/dist/css/* . min . css " />
</head>
<body class= "panel-body " >
Model: @Model.DayOfWeek
</body>
</html>
Глава 17. Контроллеры и действия 525
тип модели представления указывается с применением ключевого слова mo del
синтаксиса Razor. Обратите внимание на использование буквы m нижнего регистра
при указании типа модели и буквы М верхнего регистре при чтении значения.
Строгая типизация не только помогает привести в порядок представление , но так­
же позволяет Visual Studio поддерживать средство IntelliSense (рис. 17.7).

·lJ<t1eao ...
j <meta narre• " vie\'lpoгt " content •",,..idth•devi ce- \'lidth" />
1 <title >Controllers • nd Act i ons </title>

.
I
; <link reJ •"stylosheet" osp·href-include•"liЬ/Ьcotstrap/dist/cso/ • . css" />
</l1ead>
-.- .; <body class• "p-ll · l">
~>od ol : ёf'lodel .j
; </Ьоdу > ф AddYeщ
.. </html>
Ф ComparoTo
,1- Dщ
/' Day
-1' •. D•yOIWocl: Dot•Тinle. D •yOfWeek ( get;}
/' DayOfYear Gets the d•y of the >vtek 1'prt<ented Ьу t hi~ instanco.
(i' Equals
Ф GetDateTimeFormats
~ GetHa<hCode

Рис. 17. 7. Поддержка средства lntelliSense для строго типизированных представлений

Модульное тестирование: объекты модели представления

Объекты модели представления присваиваются свойству ViewResul t . ViewDa ta . Model,


а это означает возможность проверки того, что метод действия отправляет ожидаемые дан­
ные, когда вызывается метод View (). Вот тестовый метод, который проверяет тип модели
для метода действия из листинга 17.20 :

[Fact]
puЫic void ModelOb jectType() {
11 Организация
Examp l eController controller = new ExampleController();
11 Действие
ViewResu lt result = controller .I ndex() ;
11 Утверждение
Assert .IsType<System.DateTime >(res ult.ViewData . Model) ;

Метод Assert . IsType () применяется для проверки того, что объект модели представ­
ления является экземпляром DateTime .

С использованием метода View () связано одно затруднение , которое возникает,

когда нужно задействовать стандартное представление, ассоциированное с действи­


ем, и снабдить его объектом модели string (листинг 17.22).
526 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

Листинг 17.22. Использование метода View () в файле ExampleController. cs


u s i ng Mi cro s oft . AspNetCore . Mvc ;
us i ng System ;
namespace Co ntrollersAndActions . Controlle r s
puЫic class Examp l eController : Co ntroller {
p u Ьlic ViewResu l t Index() => View(DateT ime.Now) ;
puЫic ViewResult Result() => View("Hello World");

В новом методе действия Resul t () необходимо применить метод View (), кото­
рый ви зуал изирует стандартно е представление для действия , и указ ать д анны е мо­
дели, чт о соответствует треть ей версии этого метода из табл. 17.8. Но посл е з апус­
ка приложения и запроса URL вида /Example /Resu l t будет получ ено сообще ни е об
ошибке вроде показанного ниже :

InvalidOpe rationExceptio n: The view 'H e l lo , Wor l d ' was not found.
The f ol lowi ng loca t ions we re searched :
/V i ews/Example/Hel l o , Wo rl d . cs h tml
/Vi ews/Shared/He l lo , Worl d. cshtml
InvalidOpera t ionException : Предст а вление ' Hello , World ' не найдено .
П оиск производился в следующих мес т оположениях :

/Views/Example/Hello , World . cshtml


/Views/Shared/Hello , World . cshtml
Пробл ема в том, что вызов метода Vi e w ( ) с объектом st ri ng дал совпаде ние со
второй версией данного метода из табл. 17.8, т. е . аргумент s tr ing был инте рпрети­
рован как имя пр едставления , подлежащего ви зуализации, поэтому инфраструктура
МVС пыталась найти файл представления под названием Hel l o , World . cshtml , а н е
Re sul t . cs html. Это распространенная проблема , но ее легко исправит ь , выполнив
приведение данных модели к типу obj ect (листинг 17.23).

Листинг 17.23. Выбор корректного метода View () в файле ExampleController . cs


using Microsoft . AspNe t Core . Mvc ;
using System ;
namespace Contro l lersAndActions . Contro l lers
p uЫ ic class Examp l eContro l ler : Co ntrol l e r {
puЬlic ViewResult Index() => View(DateT i me . Now) ;
puЫic ViewResult Result() => View( (object) "Hello World");

Явное приведение данных модели к object гарантирует соотв етствие вы з ова пра­
вильной версии метода Vi ew () и обеспечит визуализацию представления и з файла
Re su l t . cshtml.
Глава 17. Контроллеры и действия 527
Передача данных с помощью ViewBag
Объ ект ViewBag был описан в главе 2. Это средство позволяет определять свойс­
тва в динамическом объекте и получать доступ к ним в представлении. Динамический
объект доступен через свойство ViewBag, предоставляемое классом Controller, как
демонстрируется в листинге 17.24.

Листинг 17 .24. Использование объекта ViewBag в файле ExarnpleCon troller. cs


using Microsoft.AspNetCore.Mvc;
using System ;
namespace ControllersAndActions . Controllers
puЫic class ExampleController : Contro l ler {

puЫic ViewResult Index( ) {


ViewBag.Message = "Hello";
ViewBag.Date =
DateTirne.Now;
return View () ;

puЫic ViewResult Result() => View( (object) " Hello World " ) ;

Свойства Message и Date объекта ViewBag определяются путем присваивания им


з начений . До этого момента указанные свойства не существовали, и никакая подгото­
вительная работа для их создания не выполнялась. Чтобы прочитать данные в пред­
ставлении , извлекаются значения из тех же самых свойств , которые устанавливались
в методе действия (листинг 17.25) .

Листинг 17.25. Чтение данных из объекта ViewBag в файле Index. cshtrnl


из папки Views/Exarnple
@model DateTime
@{ Layout = null ;
<IDQCTYPE html>
<html>
<head>
<meta name= "viewport " content= "width=device-width " />
<title>Controllers and Actions</title>
<link rel="stylesheet" asp - h ref-in clude= "li Ь/bootstrap/dist/css/* . min . css " />
</head>
<body class= "p anel - body " >
<p>The day is: @ViewBag. Date. DayOfWeek</p>
<p>The message is: @ViewBag . Message</p>
</body>
</html>

Преимущество применения объекта ViewBag перед объектом модели представле­


ния связано с легкостью отправки множества объектов представлению. Если бы инф­
раструктура МVС поддерживала только модели представл ений, то для получения того
же результата пришлось бы создавать новый тип с членами string и DateTime .
528 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

Внимание! Среда Visual Studio не способна предоставить поддержку lntelliSense для любых
динамических объектов, включая ViewBag , и ошибки не будут обнаружены до тех пор ,
пока не произойдет визуализация представления .

Модульное тестирование: объект ViewBag


Свойство ViewResu l t . ViewDa ta возвращает словарь, ключами которого являются име­
на свойств ViewBag, определяемых методом действия . Вот тестовый метод для метода
действия из листинга 17.24:

[Fact]
puЫic void Mode l ObjectType()
11 Организация
ExampleController controller = new ExampleController();
11 Действие
ViewResult result = controller . Index{) ;
11 Утверждение
Assert.IsТype<string>(result.ViewData["Message"]);
Assert.Equal("Hello", result.ViewData["Message"]);
Assert.IsType<System.DateTime>(result.ViewData["Date"]);

Этот тестовый метод проверяет типы свойств Message и Date , используя метод
Assert. IsType () , и проверяет значение свойства Message с применением метода
Assert . Equal ().

Выполнение перенаправления
Обычный результат из метода действия предназначен не для выпуска какого-ли­
бо вывода напрямую, а для перенаправления клиента на другой URL. Большую часть
времени таким URL является другой метод действия внутри приложения, который
генерирует вывод, подлежащий отображению пользователям. Когда выполня ется пе­
ренаправление , браузеру отправляется один из следующих двух кодов НТГР.

• Код НТГР 302, который означает временное перенаправл ение. Это наиболее час­
то используемый тип перенаправления и в случае применения паттерна Post/
Redirect/Get необходимо посылать данный код .

• Код НТГР 301, который означает постоянное перенаправление. Этот код должен
использоваться с осторожностью, т.к. он инструктирует получат еля не запраши­

вать снова исходный URL, а применять новый URL, вмюченный вместе с кодом
перенаправления. В случае сомнений используйте временное перенаправление,
т.е. отправляйте код 302.
Для выполнения перенаправления могут применяться и другие р езультаты дейс­
твий, которые описаны в табл. 17.9.
Глава 17. Контроллеры и действия 529
Табл и ца 17.9. Р езультаты действ ий для перенаправления

Имя М етод класса Controller Описание

Redi rectResult Redirect Этот результат действия


Redi rectPermanent посылает ответ с кодом со­

стояния НТТР 301 или 302,


выполняя перенаправление

клиента на новый URL


LocalRedirectResult LocalRedirect Этот результат действия вы­
Local Redi r ectPermanent полняет перенаправление

клиента на локальный URL


RedirectToActionResul t Redi rectToAction Этот результат действия
RedirectionToActionPerrnanent выполняет перенаправле­
ние клиента на указанные

действие и контроллер

RedirectToRouteResult RedirectToRoute Этот результат действия


RedirectToRoutePermanent выполняет перенаправ­

ление клиента на URL,


сгенерированный из спе­
цифичес кого маршрута

Перенаправление на буквальный URL


Наибол ее базовый способ пер енаправле ния браузе ра пр едусматривает вы зов мето­
да Redirect () ,пр едоставляем ого классом Controller , которы й возвращает экзем ­
пляр класса RedirectResul t (листинг 17 .26).

Листи нг 17.26. Перенаправление на буквальный URL в файле ExampleController. cs


using Microsoft.AspNetCore . Mvc ;
us i ng System ;
namespace ControllersAndActions . Control l ers
puЬlic class Examp l eController : Controller {
puЫic ViewResult Index() {
ViewBag . Message = " He ll o ";
ViewBag.Date = DateT i me . Now ;
return View() ;

puЬlic ViewResult Result() => View( (object) "Hello World " );


puЬlic Re directResul t Redirect () => Redirect ( "/Example/Index") ;

Зде сь URL пе р енаправления выражается к ак аргумент string м етода Redi rect (),
к оторый об е сп е чив а ет вр е м е нно е п е р е направл е ни е . Постоянное п е рен апр авлени е
м ож но выполнить с помощью м етода RedirectPermanent (), как по казано в лис­

тинге 17.27.
530 Часть 11. Подробные сведения об инфраструктуре ASP. NET Core MVC

Совет. Объект LocalRedirectionResul t - это альтернативный результат действия,


который будет генерировать исключение, если контроллер попытается выполнить пере­
направление на любой URL, не являющийся локальным . Он полезен в случае, если осу­
ществляется перенаправление на URL, предоставляемые пользователями, когда пред­
принимается попытка атаки открытым перенаправлением для перенаправления другого

пользователя на ненадежный сайт. Такой вид результата действия может быть создан пос­
редством метода LocalRedirect (), унаследованного от класса Controller.

Листинг 17.27. Постоянное перенаправление на буквальный URL


в файле ExampleController. cs

using Microsoft . AspNetCore . Mvc;


using System ;
namespace ControllersAndActions . Controllers
puЫic class ExampleController : Controller {
puЫic ViewResult Index() {
ViewBag.Message = "Hello";
ViewBag.Date = DateTime . Now ;
return View () ;

puЫic ViewResult Result () => View ( (object) " Hello World " );
puЬlic RedirectResult Redirect{) => RedirectPermanent("/Example/Index");

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

Перенаправление на буквальный URL тестировать легко. С помощью свойств ur 1 и


Permanent класса RedirectResult можно прочитать URL и вид перенаправления (пос­
тоянное или временное). Вот тестовый метод для постоянного перенаправления из листин­
га 17.27:

[Fact]
puЫic void Redirect i on () {
11 Организация
ExampleController controller = new Examp l eController();
11 Действие
RedirectResult result = controller . Redirect() ;
11 Утверждение
Assert .Equal( " /Example/Index ", result.Url) ;
Assert . True(result . Permanent);

Обратите внимание , что тест обновлен для получения объекта RedirectResul t при вы­
зове метода действия .
Глава 17. Контроллеры и действия 531
Перенаправление на URL системы маршрутизации
При перенаправлении пользователя на другую часть прилож е ния необходимо
удостовериться в том, что отправляемый URL является допустимым в рамках схемы
URL. Проблема, связанная с использованием для перенаправления буквальных URL, в
том, что любое изменение в схеме маршрутизации означает необходимость пересмот­
ра кода и обновления таких URL. К счастью, можно задействовать систему маршрути­
зации для генерирования допустимых URL с помощью метода RedirectToRoute (),
который создает экземпляр класса RedirectToRouteResult (листинг17.28).

Совет. Если вы последовательно прорабатываете примеры из настоящей главы, тогда для


нормального функционирования кода в листинге 17.28 может понадобиться очистить хро­
нологию браузера. Причина в том, что браузер запомнит постоянное перенаправление в
листинге 17.27, и будет транслировать запрос URL вида /Example/Redirect в запрос
/Example / Index, не контактируя с сервером.

Листинг 17 .28. Перенаправление наURL системы маршрутизации


в файле ExampleController. cs
using Microsoft.AspNetCore . Mvc ;
using System ;
namespace ControllersAndActions.Controllers
puЬlic class ExampleController : Controller {
puЫic ViewResult Index() {
ViewBag . Message = "H el lo";
ViewBag.Date = DateTime .Now ;
return View () ;

puЫic ViewResult Result() => View( (object) "Hello World");


puЫic RedirectToRouteResult Redirect() =>
RedirectToRoute (new { controller "Example",=
action = "Index",
ID = "MyID" }) ;

Метод RedirectToRoute () выпускает временное перенаправление . Для постоян­


ного перенаправления применяйте метод RedirectToRoutePermanent (). Оба мето­
да принимают анонимный тип, свойства которого затем передаются системе марш­
рутизации для генерирования URL, как объяснялось в главе 16.

Модульное тестирование: перенаправление на URL системы маршрутизации

Ниже приведен модульный тест для метода действия из листинга 17.28:

[E'act]
puЫic void Redirection() {
11 Организация
ExampleController controller new ExampleController() ;
532 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC

11 Действие
RedirectToRouteResult result = controller.Redirect();
11 Утверждение
Assert.False(result.Permanent);
Assert.Equal("Example", result.RouteValues["controller"]);
Assert.Equal("Index", result.RouteValues["action"]);
Assert.Equal("MyID", result.RouteValues["ID"]);

Результат проверяется косвенным образом за счет просмотра информации о маршруте,


предоставляемой объе ктом RedirectToRouteResul t , поэтому нет необходимости в
разборе URL, что потребовало бы допущений в модульном тесте о схеме URL, используе­
мой приложе нием.

Перенаправление на метод действия


Выполнить перенаправление на метод действия можно более элегантно с приме­
нением метода RedirectToAction () (для временного перенаправления) или мето­
да RedirectToAct i onPermanent () (для постоянного перенаправления). Это просто
оболочки вокруг метода RedirectToRoute (), которые позволяют указывать значе­
ния для метода действия и контролл е ра без необходимости в создании анонимного
типа (листинг 17.29).

Листинг 17.29. Перенаправление с использованием метода RedirectToAction ()


в файле ExampleController. cs
using Microsoft.AspNetCore.Mvc ;
using System ;
namespace ControllersAndActions . Control le rs
puЫic class ExampleController : Controller {
puЫic ViewResult Index() {
ViewBag.Message = " Hel l o ";
ViewBag . Date = DateTime.Now;
r eturn View () ;

puЬlic RedirectToActionResul t Redirect () => RedirectToAction (" Index") ;


)

Если указан только метод действия , то предполагается , что он относится к те1<у­


щему контроллеру. Для перенаправления на другой контроллер понадобится предо ­
ставить имя контроллера как параметр:

p uЬlic RedirectToActionResult Redirect() =>


RedirectToAction("Index", "Home");

Доступны и другие перегруженные версии, с помощью которых можно предостав­


лять дополнительные значения для генерации URL. Они выражаются с примен е нием
анонимного типа , который отнюдь не умаляет пользу от удобного метода, но может
облегчить восприятие кода.
Глава 17. Контроллеры и действия 533

На заметку! Значения, предоставляемые методу действия и контроллеру, не проверяются


перед их передачей системе маршрутизации. Вы сами отвечаете за то, что указываемые
цели действительно существуют.

Модульное тестирование: перенаправления на метод действия

Вот модульный тест для метода действия из листинга 17.29:

[Fact]
puЫic void Re direc tion() {
//Ор г анизация
Examp l eControl ler control l er = ne w Exampl e Controller() ;
11 Действ и е
Red i rectToActio nResult resul t = c ontrolle r .Redi r ect() ;
11 У т ве р ждени е
Asse r t .False (r e sul t .Perma ne nt ) ;
Asser t. Equ al ("Index ", result.ActionName);

В классе Redirec tT o Act i o n Res ul t определены свойства Cont r o l l erName и


Ac t ionNarne , которые упрощают инспектирование перенаправления, созданного контрол­
лером, и не требуют разбора URL.

Использование паттерна Post/Redirect/Get


Перенаправление чаще всего применяется в методах действий, которые обраба­
тывают НТТР-запросы POS T. Как объяснялось в предыдущей главе. запросы POST
используются, когда нужно изменить состояние приложения. Если после обработки
запроса POST просто возвращать НТМL-ответ, то есть риск того, что пользователь
щелкнет на кнопке перезагрузки страницы в браузере и отправит форму во второй
раз, приводя к непредсказуемым и нежелательным результатам .

Наблюдать такую проблему можно в контроллере Home внутри примера приложе­


ния. Метод Rece i veForrn ( ) принимает параметры, значения которых получаются из
данных формы. и применяет метод Vi ew () для возвращения объекта Vie wResul t:

p uЬlic Vi ewRes u lt ReceiveFo rm(s t ring name , s tring c it y )


=> View("Result", $"{narne} lives in {city}");

Чтобы взглянуть на проблему, запустите приложение и запросите URL вида /Home .


Отправьте форму и затем щелкните на кнопке перезагрузки страницы в браузере.
С помощью инструментов, доступных по нажатию клавиши <F12>. изучите НТТР­
запросы. сделанные браузером; вы заметите, что серверу был отправлен новый за­
прос POST. В таком простом приложении он не окажет какого-либо влияния, но эта
проблема может вызвать разрушения, если запросы PO ST в итоге удаляют данные,
отправляют заказы или выполняют другие важные задачи, которые пользователь не

намеревался предпринимать.
534 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

Чтобы избежать возникновения описанной проблемы, можно следовать паттерну


под названием Post/Redirect/Get. В соответствие с паттерном Post/Redlrect/Get вы
получаете запрос POST, обрабатываете его и затем выполняете перенаправление бра­
узера, так что он делает запрос GET для другого URL. Запросы GET не долж ны моди­
фицировать состояние приложения, поэтому случайные повторные отправки данного
запроса н е вызовут никаких проблем. В листинге 17.30 добавлено п е ренаправление
браузера на другой URL с помощью запроса GET.

Листинг 17.30. Реализация паттерна Post/Redirect/Get в файле HomeController.cs

u sing Mi c r o s oft . Asp NetCo re. Mvc ;


u si ng Sys tern . Tex t ;
u si ng Contro l lersAn dActions .Infrast ru ct ure;
na rnes p ace Co n t r ol l er s AndAc tions . Co ntr o ll ers
pu Ыi c c l ass HorneCon tro ll e r : Co n t r ol l e r {
puЫ ic Vie wRe s ult I nde x () => Vie w ( " Sirnpl e Fo r rn " ) ;
[HttpPost]
puЫic RedirectToActionResult ReceiveForm(string name, string city)
=> RedirectToAction(nameof(Data));
puЫic ViewResult Data() => View("Result");

Метод Redirect To Act i o n Resu l t получает данные от пользователя через запрос


POST и выполняет перенаправление клиента на метод действия Data (). Если пользо­
ватель перезагрузит страницу, тогда методу действия Data () посыла ется безвредный
запрос GE T. Атрибут Http Po s t , который будет описан в главе 20, гарантирует воз­
можность отправки действию Rece ive Fo r () только запросы POS T.

Использование объекта TempDa ta


Перенаправление заставляет браузер послать полностью новый НТТР-запрос , что
означает отсутствие доступа к данным формы из исходного запроса. Таким образ ом,
методу Da t a ( ) ничего не известно о значениях n a me и ci ty, которы е должны быть
отображены пользователю.
Если необходимо предохранить данные между запросами, тогда можно применить
объект Te mpData . Объект TempData похож на данные сеанса, которые использовались
в главе 9, за исключением того, что значения TempDa ta помечаются для удаления,
когда они прочитаны , и удаляются из хранилища данных после обработки запроса.
Это идеальный вариант для краткосрочных данных, которые необходимы для того,
чтобы перенаправление работало в паттерне Post/Redirect/Get. Объект TempData до­
ступен через свойство TernpDat a класса Co nt rol ler (листинг 17.31).

На заметку! Объект TempData полагается на промежуточное ПО сеанса . В начале главы


приводился список требуемых пакетов NuGet в файле p r oj ec t. j son и операторы кон­
фигурирования для класса S t ar tup .
Глава 17. Контроллеры и действия 535
Листинг 17. 31. Использование объекта TempDa ta в файле HomeCon trol l er . cs

using Microsoft . AspNetCore . Mvc ;


using System . Text ;
u s ing ControllersAndActio ns.I nfra s t ru cture;
namespace Con t rollersAndAct i o ns. Co n tr o ll e r s
puЫic class HomeControl l er : Con tr ol ler
puЫic ViewResu l t I n dex() (
r e t u r n Vi e w( " Simple Fo rm" ) ;

[Ht t p Po s t ]
p uЫ ic Redir ect ToAct i onRe s u lt ReceiveForm(st r i ng name , s tr ing c ity) {
TempData [ "name"] = name;
TempData["city"] = city;
r eturn Red i rectToAct i on( n a me o f (Data )) ;

puЫic ViewResul t Da t a() {


string name = TempData [ "name"] as string;
string city =
TempData["city"] as string;
return View("Result", $" {name} lives in {city} ");

Метод Rece i ve Form () применяет свойство Temp Da t a . возвращающее словарь.


для сохранения значений name и c i ty перед перенаправлением кли ента на действие
Data. Метод Data () использует то же самое свойство Te mpData для извлеч е ния зна­
чений данных и их применения при создании данных модели, которые будут отобра­
жаться представлением.

Совет. Словарь TempDa t a также предоставляет метод Pe e k: () , который позволяет полу­


чить значение данных, не помечая его для удаления, и метод Ке ер (), который можно ис­
пользовать, чтобы предотвратить удаление ранее прочитанного значения . Метод Кеер ()
не защищает значение навсегда. Если значение прочитано снова, оно будет помечено для
удаления еще раз . Если вы хотите хранить элементы, чтобы они не удалялись, когда за­
прос будет обработан, тогда применяйте данные сеанса.

В оз вр ащение разных типов содержимого


HTML - не единственны й вид отв ета , который способны генерировать методы
действий, и в табл. 17.10 описаны встроенные результаты действий, которые можно
использовать для разных типов данных.

Генерирование ответа JSON


Формат JSON (JavaScript Object Notation - система обозн а чений для объектов
JavaScript) стал стандартным способом передачи данных между веб-приложением и
ero клиентом. Форм ат JSON в значительной степени заместил ХМL в кач естве форма­
та обмена , поскольку с ним проще работать, особенно при написании кода JavaScript
клиентской стороны, т.к. JSON тесно связан с синтаксисом, который JavaScript ис­
пользует для опр еделения лите ральных значений данных.
536 Часть 11. Подробные сведения об инфраструктуре ASP.NET Саге MVC

Таблица 17 .1 О. Результаты действий для содержимого

Метод класса
Имя Описание
Controller
J sonRes ult J son Этот результат действия сериализирует объект в
формат JSON и возвращает его клиенту

Conten t Resul t Content Этот результат действия отправляет ответ, тело


которого содержит указанный объект

ObjectResult Этот результат действия будет применять согласо­


вание содержимого для отправки объекта клиенту

OkOb j ectRe sult Ok Этот результат действия будет использовать со­


гласование содержимого для отправки объекта
клиенту с кодом состояния ~ПР 200, если со­
гласование содержимого успешно

NotFoundOb jectResult Not Found Этот результат действия будет применять со­
гласование содержимого для отправки объекта
клиенту с кодом состояния НТТР 404, если со­
гласование содержимого успешно

Тема формата JSON и его роли в веб - приложенилх раскрывается в главе 20, а в
листинге 17.32 демонстрируется применени е метода J son ( ) для создания объекта
J s onRes ul t .

Листинг 17 .32. Генерирование ответа JSON в файле ExampleCon troller. cs


using Micro s oft . AspNe t Core . Mvc ;
us i ng Systern;
narnesp ace Co ntro l lersAndActi on s . Co nt r oller s
puЫ ic c las s Exarnpl eCo nt r o ll e r : Cont r oll e r {
puЬlic JsonResult Index() => Json(new[] { "Alice", "ВоЬ", "Joe" }) ;

Запустив приложение и запросив URL вида / Exampl e , вы получите ответ. который


выражает строковый мас с ив С# из метода действия в формате JSON:
[ "Al ice '', " ВоЬ ", " Joe " ]
Некоторые браузеры будут отображать результаты JSON встроенным образом . тог­
да как другие, включая Microsoft Explorer, требуют сохранения данных в файл, прежде
чем их можно будет инспектировать.

Модульное тестирование: результаты действий, отличающиеся от HTML


Важно помнить, что модульные тесты для метода действия должны быть сосредоточены на
данных, возвращаемых с целью последующего форматирования, а не на самом формати­
ровании, которое поддерживается инфраструктурой MVC и обычно выходит за рамки боль­
шинства проектов тестирования . В качестве примера ниже показан модульный тест для ме­
тода действия из листинга 17.32:
Глава 17. Контроллеры и действия 537

[Fact]
puЫic void JsonActionMethod()
11 Организация
ExampleController controller new ExampleController();
11 Действие
JsonResult result = controller.GetJson();
11 Утверждение
Assert.Equal(new[J { "Alice", "ВоЬ", "Joe" }, result.Value);

Класс JsonResult предлагает свойство Value, возвращающее данные, которые будут


преобразованы в формат JSON для формирования ответа клиенту. В модульном тесте про­
изводится сравнение свойства Value с данными, которые ожидается получить.

Использование объектов для генерации ответов


Многие приложения нуждаются только в ответах HTML и JSON от контроллеров,
а при доставке других типов содержимого, таких как изображения, файлы JavaScript
и таблицы стилей CSS, рассчитывают на поддержку для статических файлов. Тем не
менее, могут возникать ситуации, когда необходимо возвращать в ответе содержимое
специфического типа, и для помощи в этом доступны соответствующие результаты
действий. Простейшим является класс ContentResul t, создаваемый посредством
метода Content (),который применяется для отправки значения string с дополни­
тельным типом содержимого MIME. В листинге 17.33 метод Content () используется
для воссоздания вручную результата JSON из предыдущего раздела.

Листинг 17.33. Ручное создание результата JSON в файле ExampleController. cs


using Microsoft.AspNetCore.Mvc;
namespace ControllersAndActions.Controllers
puЫic class ExampleController : Controller {
puЫic ContentResult Index()
=> Content (" [\ "Al.ice\", \ "ВоЬ\", \ "Joe\"] ", "appl.icat.ion/json") ;

Такой тип результата действия полезен, когда имеется содержимое, которое удобно
представлять в формате string, и известно, что клиент способен принимать указы­
ваемый тип MIME. Опасность этого подхода в том, что клиенту может быть отправлен
ответ в формате. который он не в состоянии обработать. Более надежный подход по­
лагается на согласование содержимого, которое выполняется классом Obj ectResul t
(листинг 17.34).

Листинг 17 .34. Применение согласования содержимого


в файле ExampleController. cs
using Microsoft.AspNetCore.Mvc;
namespace ControllersAndActions.Controllers
538 Часть 11 . Подробные сведения об инфраструктуре ASP.N ET Core MVC

puЫic class ExampleController : Controller {


puЫic ObjectResult Index() => Ok(new string[) { "Alice", "ВоЬ", "Joe" }) ;

Термин согласование содержимого намекает на сложную систему выявления об­


щего формата между браузером и приложением, но в действительности представляет
собой простой процесс. Когда браузер делает НТТР-запрос, он включает в него заго­
ловок Accept, который указывает, какие форматы он может обрабатывать. Вот как
выглядит такой заголовок в версии Google Chrome, используемой для тестирования
примера:

Accept : text/html,application/xhtml+xml,application/xml ;
q=0.9,image/webp,*/*;q=0 . 8
Поддерживаемые форматы выражаются как типы MIME. Инфраструктура MVC
имеет набор форматов, которые она может применять для значений данных, и срав­
нивает их с форматами, поддерживаемыми браузером. Пр едпочтительным форма­
том, используем ым инфраструктурой MVC, является JSON, и он будет применяться
в большинстве случаев кроме ситуации, когда действие возвращает значение string
и используется простой текст. Процесс согласования содержимого и особенности его
реализации более подробно рассматриваются в главе 20.

Реагирование с помощью содержимого файлов


Большинство приложений при доставке содержимого файлов рассчитывают на
промежуточное ПО статических файлов, но есть также набор результатов действий.
которые можно применять для отправки файлов клиенту {табл. 17.11).

Внимание! Будьте осторожны при использовании этих результатов действий, чтобы не создать
приложение, которое позволит запрашивать содержимое произвольных файлов . В частнос­
ти, не получайте путь к подлежащему отправке файлу из любой части запроса либо из лю­
бого хранилища данных , которое пользователь может модифицировать через запрос.

Таблица 17 .11. Результаты действий для файлов

Метод класса
Имя Описание
Controller
FileContentResult File Этот результат действия посылает клиенту
байтовый массив с указанным типом MIME
FileStreamResult File Этот результат действия читает поток
и отправляет содержимое клиенту

VirtualFileResult File Этот результат действия читает поток из


виртуального пути (относительного к ката­
логу, где размещается прило жение )

PhysicalFileResult PhysicalFile Этот результат действия читает содер жи­


мое файла из указанного пути и посылает
его клиенту
Глава 17. Контроллеры и действия 539
В листинге 17.35 метод Fil e (), унаследованный от класса Controller, приме­
няется для возвращения СSS-файла Bootstrap в качестве результата метода действия
Index () контроллера Examp le .

Листинг 17.35. Использование содержимого файла в качестве ответа


в файле ExampleCon troller. cs
using Microsoft . AspNetCo r e . Mvc ;
namespace ControllersAndActions.Controllers
puЬlic class ExampleController : Controller {
puЬlic VirtualFileResul t Index ()
=> File("/lib/Ьootstrap/dist/css/bootstrap . css", "text/css");

Для применения этого метода действия понадобится модифицировать элемент


link в файле Simp l eForm . cshtml, чтобы в нем использовался вспомогательный
кл асс Url (листинг 17.36).

Листинг 17.36. Нацеливание на метод действия в файле SimplerForm. cshtml


@{ Layout = null;
< !DOCTYPE html >
<html>
<head>
<meta name =" viewport " content= " width=device - width " />
<title>Control lers and Actions</title>
<link rel="stylesheet" href="@Url.Action("Index", "Example")" />
</head>
<body class= " pane l- body " >
<form method= " post " asp - action= " ReceiveForm " >
<d iv class= "form- group " >
<label for= " name " >Name : </ l abel>
<input c l ass= " form -c ontrol " name= " name " />
</div>
<div class= " form-g r oup " >
<label for= "name " >City : </lab el>
<input class= " form -c ontrol " name= " city " />
</div>
<button class= " btn Ьtn - pr i mary center - Ыock " type="submit">Submit
</button>
</form>
</body>
</html>

По сле запуска приложения и запрашивания URL вида / Н оте отправленный брау­


зеру НТМL-ответ будет включать следующий элемент:

<link rel= " stylesheet " href=" /Example " />


Он заставит браузер послать НТГР-запрос. нацеленный на метод действия из лис­
тинга 17 .35, который отправит файл CSS, требующийся для стилизации содержимого
представления .
540 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Саге MVC

На заметку! Как будет показано в главе 25 , дескрипторные вспомогательные классы являют­


ся намного более удобным инструментом для доставки файлов CSS.

Возвращение ошибок и кодов НТТР


Финальный набор встроенных клас сов ActionResul t может применяться для от­
правки клиенту специфичных сообщений об ошибках и результирующих кодов НТГР
(табл. 17. 12). Большинство приложений не нуждается в таких ср едствах, поскольку
ASP.NET Core и MVC генерируют эти виды результатов автоматически. Однако они
м огут быть полезны в ситуациях, когда необходимо получить более прямой контроль
над ответами, отправля ем ыми клиенту .

Таблица 17.12. Результаты действий для кодов состояния

Метод класса
Имя Описание
Controller
StatusCodeResult StatusCode Этот результат действия отправ­
ляет клиенту у казанный код со­
стояния НТТР

OkResult Ok Этот результат действия от­


правляет клиенту к од состояния

НТТР 200
CreatedResult Created Этот результат действия от ­
правляет клиенту код состояния

НТТР 201
Created.AtActionResult Created.At Action Этот результат действия отправ­
ляет клиенту код состояния НТТР
201 вместе с URL в заголовке
Location, которы й нацелен на
действие и контроллер

Created.AtRouteResult Created.AtRoute Этот результат действия от­


правляет клиенту код состояния

НТТР 201 вместе с URL в заго­


ловке Location, который сге­
нерирован из специфического
маршрута

BadRequestResult BadRequest Этот результат действия от­


правляет клиенту код состояния
НТТР 400
UnauthorizedResult Unauthorized Этот результат действия от­
правляет клиенту код состояния
НТТР 401
NotFoundResult NotFound Этот результат действия от­
правляет клиенту код состояния

НТТР 404
UnsupportedMediaTypeResult Этот результат действия от­
правляет клиенту код состояния
НТТР 415
Глава 17. Контроллеры и действия 541

Отправка специфического результирующего кода НТТР

Отправить браузеру сп ецифичный код состояния НТГР можно с использованием


метода StatusCode () ,который создает объект StatusCodeResul t (листинг 17.37).

Листинг 17.37. Отправка специфического кода состояния в файле


ExarnpleController.cs
using Microsoft . AspNetCore.Mvc;
using Microsoft .AspNetCore .H ttp ;
namespace ControllersAndActions . Contro ll ers
puЫic class · ExampleController : Controller {
puЬlic StatusCodeResul t Index ()
=> StatusCode(StatusCodes . Status404NotFound);

Метод StatusCode () принимает значение int, в котором можно напрямую

указывать код состояния. Класс StatusCodes из пространства имен Microsoft .


AspNetCore . Http определяет поля для всех кодов состояния, поддерживаемых НТГР.
В листинге 17.37 применяется поле Status404NotFound для возвращения кода 404,
который означает, что запрошенный ресурс не существует.

Отправка результата 404 с использованием удобного класса


Другие результаты действий, описанные в табл. 17. 12, расширяют или основаны
на классе StatusCodeResult , который предлагает более удобный способ отправки
специфических кодов состояния. Того же самого результата , что и в листинге 17.37,
м ожно достичь с применением более удобного класса NotFoundResul t, который яв­
ляется производным от класса StatusCodeResul t и может быть создан с использо ­
ванием удобного метода NotFound () класса Controller (листинг 17.38).

Листинг 17.36. Генерирование результата 404 в файле ExarnpleController. cs


using Microsoft . AspNetCore . Mvc ;
using Microsoft . AspNetCore .H ttp ;
namespace ControllersAndActions.Contro l lers
puЫic class ExampleController : Controller {
puЬlic StatusCodeResult Index () => NotFound ();

Модульное тестирование: коды состояния НТТР

Класс StatusCodeResul t следует шаблону, который вы уже видели для други х ти­
пов результатов, и делает свое состояние доступным через набор свойств. В этом
StatusCode возвращает числовой код
случае сво йство состояния НТТР, а свойство
StatusDescription - связанную описательную строку. Приведенный ниже тестовый
метод предназначен для проверки метода действия из листинга 17.28:
542 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC

[ Fact ]
p uЬlic vo i d NotFoundActionMe thod ()
11 Ор г ан и за ци я
Exampl eCo ntrol l e r cont r ol ler = new Examp le Con t rol l e r () ;
11 Д е й стви е
St atusCodeRes ult result = con t roll e r.Index () ;
11 Утвержден и е
Asser t. Equal(404 , res ul t . StatusCode);

Другие классы результатов действий


Некоторые дополнительные классы результатов действий тесно связаны со средства­
ми MVC , рассм атриваемыми в других главах. В табл. 17 .13 эти Юiассы кратко описаны
со ссылками на главы, где обсуждаются средства, к которым они имеют отношен ие.

Таблица 17 .13. Другие классы результатов действий

Метод класса
Имя Описание
Controller
Pa r t i a l Vi ewResult Pa rt i alView Этот результат действия применяется для
выбора частичного представления , как
объясняется в главе 21
ViewComponent Resul t Vi ewComponent Этот результат действия используется для
выбора компонента представления , как
описано в главе 22
EmptyResult Этот результат действия ничего не делает
и производит пустой ответ для клиента

ChallengeResult Этот результат действия применяется для


обеспечения соблюдения политик безо­
пасности в запросах . Подробные сведения
приведены в главе 30

Резюме
Контролл еры являются одним из основных строительных блоков в паттерне проек­
тирования MVC и находятся в це нтральной части разработки приложений MVC . В на­
стоящей глав е было показ ано. ка к создавать контроллеры РОСО с испол ьзованием
обычных классов С# и получать преимущество от удобства, предлагаемого базовым
классом Cont r ol l er. Вы узнали, какую роль результаты действий играют в контрол­
лерах МVС, и выяснили , каким образом они облегчают модульное тестирование. Были
продемонстрированы различные способы получения ввода и генерации вывода и з ме ­
тода действия, а также встроенные результаты действий, которые делают этот про­
цесс простым и гибким. В следующей главе будет описано одно из средств, кото ро е
вызывает наибольшую путаницу у разработчиков, использующих ASP.NET, но очень
важно для эффективной разработки приложений МVС - внедрение зави сим остей.
ГЛАВА 18
Внедрение зависимостей

в настоящей
injection -
главе рассматривается внедрение зависимостей
DI) - методика, которая помогает создавать гибкие приложения и уп­
(dependency

рощает модульное тестирование. Внедрение зависимостей может оказаться трудной в


понимании темой, как в план е того, почему оно может быть полезным, так и в отноше­
нии того, каким образом оно выполняется. В связи с этим мы будем двигаться медлен ­
но , начав с традиционного способа построения компонентов приложения и постеп е нно
объясняя, как работает внедрение зависимостей и почему оно важно. В табл. 18.1 при ­
ведена сводка, позволяющая поместить внедрение зависимостей в контекст.

Таблица 18.1. П омещение внедрения зависимостей в контекст

Вопрос Ответ

Что это такое? Внедрение зависимостей облегчает создание слабо связанных компо­
нентов , что обычно относится к компонентам, которые потребляют функ­
циональность, определяемую интерфейсами, и не обладают непосредс­
твенным знанием о том, какие классы реализации используются

Чем оно полезно? Внедрение зависимостей упрощает изменение поведения приложения пу­
тем изменения компонентов, которые реализуют интерфейсы, определя­
ющие фун к циональные средства приложения . Оно та кже дает в результате
компоненты, которые легче изолировать для модульного тести рования

Как оно С по м ощью класса Startup указывается, какие классы реализации


используется? применяются для доставки функциональности, определяемой интер­
фейсами, которые использует приложение . Когда для обработ ки запро­
сов создаются новые объекты, такие как контроллеры , они автомати­
чески снаб жа ются требуемыми экземплярами классов реализации

Существуют ли Главное ограничение заключается в том, что классы объявляют о своем


какие-то скры­ использовании служб в виде аргументов конструктора. Это может при­
тые ловушки или вести к появлению конструкторов, единственной ролью которых являет­
ограничения? ся получение зависимостей и и х присваивание полям экземпляра

Существуют ли Вы не обязаны применять внедрение зависимостей в собственном


альтернативы? к оде, но полезно знать, каким образом оно работает, поскольку внедре­
ние зависи м остей используется инфраструктурой MVC для предостав­
ления фун к циональных средств разработчикам

Изменилось ли Предшествующие версии ASP.NET MVC были спроектированы так,


оно по сравнению чтобы сделать возможным внедрение зависимостей, но для его запуска
с версией MVC 5? в работу требовалось выбрать и установить сторонний инструмент.
В ASP.NET Core MVC полная реализация DI в ключена как часть ASP.NET
и широ ко применяется внутренне инфраструктурой MVC , хотя ее м ож но
заменить сторонним пакето м
544 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC

В та бл. 18.2 приведена сводка для этой главы.

Таблица 18.2. Сводка по главе

Задача Решение Листинг

Создание слабо связанных Изолируйте классы посредством интер­ 18.1-18.18


компонентов фейсов и соедините их вместе с исполь­
зованием внешних отображений

Объявление зависимости Определите аргумент конструктора с ти­ 18 .19


в компоненте, таком как пом, который требует компонент
контроллер

Конфигурирование отображе­ Добавьте отображение в класс Startup 18.20,


ния службы 18.22-18.28
Модульное тестирование ком­ Создайте имитированную реализацию 18.21
понента с зависимостью интерфейса службы и передайте ее как
аргумент конструктора при создании ком­
понента в модульном тесте

Указание способа для созда­ Создайте отображение службы с приме­ 18.29 , 18.30,
ния объектов реализации нением метода жизненного цикла, кото­ 18 .32, 18.33
рый подходит управляемой службе

Изменение класса реализа ­ Используйте метод ж изненного цикла, 18.31


ции во время выполнения который принимает фабричную функцию

Получение зависимостей для Примените внедрение в действия 18.34


отдельных методов действий
в контроллере

Запрос вручную объекта реа­ Используйте свойство 18.35


лизации в контроллере HttpContext.RequestServices

Подготовка проекта для примера


Для целе й э той главы создайте новый проект типа Empty (Пустой) по име­
ни Dependencyinj ection с применением шаблона ASP.NET Core Web Application
(.NET Core) (Веб-приложение ASP.NET Core (.NET Core)). Добавьте требуемые пакеты
NuGet в раздел dependencies файла proj ect . j son и настройте инструментарий
Razor в разделе tools, как показано в листинге 18.1. Разделы, которые не нужны для
данной главы, понадобится удалить.

Листинг 18.1. Добавление пакетов в файле proj ect. j son

"dependencies ": {
"Microsoft.NETCore . App ":
"version ": "1. 0 . 0 ",
" type ": "platform "
} ,
"Microsoft . AspNetCore . Diagnostics" : "1.0.0",
"Microsoft . AspNetCore . Se rver . IIS in tegration ": "1. 0 . 0 ",
"Microsoft . AspNetCore. Server . Kestrel" : "1. О . О ",
"Microsoft . Extensions .Logging . Console ": " 1 . 0 . О ",
Глава 18. Внедрение зависимостей 545
"Microsoft. AspNetCore. Mvc" : "1. О. О" ,
" Мicr osoft. AspNetCore. StaticFiles" : "1. О. О",
"Microsoft.AspNetCore.Razor .Tools":
"version": 11 1. О. 0-preview2-final",
"type": "Ьuild"

}'
" tools " :
"Microsoft .AspNetCore . Server .II Sintegration . Tools ": "1 . 0 . 0- previ ew2 - f i nal ",
"Microsoft.AspNetCore.Razor.Tools": "1.0.0-preview2-final"
) '
" frameworks ": {
"netcoreappl . 0 ":
" irnports ": [ "dotnet5 . 6", " portaЫe-net4 5+win 8 " ]

}'
"buildOptions ": { " ernitEntryPoint ": true ,
"preserveCompilationContext ": true },
" runtimeOptions ": {
"conf i gProperties ": { " System . GC .S erver ": true}

В листинге 18.2 приведен класс Startup, который конфигурирует средства, пр е­


доставляемые пакетами NuGet.

Листинг 18.2. Содержимое файла Startup. cs

using Microsoft . AspNetCore . Builder ;


using Microsoft . Extensions.Dependencyinjection ;
narnespace Dependency in jection
puЫic class Startup {
puЫic vo id ConfigureServices(IServiceCollection services) {
services.AddМvc();

puЫic vo id Conf i gure( I App li cationBui lder арр) {


app.UseStatusCodePages();
app. UseDeveloperExceptionPage();
app .UseStaticFiles();
app.UseМvcWithDefaultRoute();

Создание модели и хранилища


Для примеров этой главы необходима простая модель. Создайте папку Models и
добавьте в нее файл класса по имени Product . cs с определением, показанным в лис­
тинге 18.3.
546 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

Листинг 18.3. Содержимое файла Product. cs из папки Models


namespace Dependencyinjection.Models
puЬlic class Product {
puЬlic string Name [ get ; set ; }
puЫic decimal Price { get ; set;

Для управления моделью добавьте в папку Models файл класса по имени


IReposi tory . cs и опр еделите в нем интерфейс, представленный в листинге 18.4.

Листинг 18.4. Содержимое файла IReposi tory. cs из папки Models

using System . Collections . Generic ;


namespace Dependencyinjection.Models
puЫic interface IRepository {

IEnumeraЫe<Product> Produc t s { get;


Product this [string name] [ get ; }
void AddProduct(Product product) ;
void DeleteProduct(Product product);

В интерфейсе IReposi tory определены операции , которые могут выполняться


над коллекцией объектов Product. Чтобы предоставить реализацию этого интерфей­
са, добавьте в папку Models файл класса по им ени MemoryReposi tory . cs и помес ­
тите в него содержимое из листинга 18.5.

Листинг 18. 5. Содержимое файла MemoryReposi tory. cs из папки Model s

using System . Collections . Generic ;


namespace Dependencyinjection . Models
puЬlic class MemoryRepository : IRepository
private Dictionary<string , Product> products;
puЬlic MemoryRepository() {
products = new Dictionary<string , Pr oduct>() ;
new List<Product> {
new Product { Name " Kayak ", Price = 275 М } ,
new Product { Name = " Lifejacket ", Price = 48 . 95 М },
new Product { Name = " Soccer ball ", Price = 19 . 50 М)
} . ForEach ( р => AddProduct ( р) ) ;

puЫic IEnumeraЬle<Product> Products => products.Values;


puЬlic Product this[string name] => products [ name] ;
puЫic void AddProduct(Product product) =>
products[product.Name] = product ;
puЬlic void DeleteProduct(Product product) =>
products . Remove(product . Name) ;
Глава 18. Внедрение зависимостей 547
Класс MemoryReposi tory хранит свои объекты модели в памяти , используя сло­
варь. Это значит, что постоянное хранилище отсутствует, и останов либо перезапуск
приложения сбросит модель к примерам объектов данных, которые создаются в конс­
трукторе. В реальном про екте такой подход нецелесообразен, но его будет достаточ­
но для настоящей главы, внимание которой сосредоточено на другом аспекте работы
приложения.

Создание контроллера и представления


Создайте папку Controllers и добавьте в нее файл класса по имени
HomeCo ntroller . cs с содержимым, показанным в листинге 18.6.
Листинг 18.6. Содержимое файла HomeController.cs из папки Controllers

using Microsoft . AspNetCore . Mvc ;


namespace Dependencyinjection . Contro llers
puЫic class HomeController : Controller
puЫic ViewRes ult Index() => View ();

Контроллер Ноте имеет только один метод действия, применяющий метод Vi ew ()


для создания объекта ViewResu l t, который будет визуализировать стандартно е
представление . Чтобы создать пр едставление, ассоциированное с методом действия,
создайте папку Views/Home и добавьте в нее файл представления Razor по имени
Index . cshtml . Разметка, помещаемая в это представление, приведена в листинге 18.7.
Листинг 18.7. Содержимое файла Index.cshtml из папки Views/Home

@model IEnumeraЫe<Product>
@{ Layout = null ;
< ! DOCTYPE html>
<html>
<head>
<meta name= " viewport " content= " width =de v ic e-width " />
<title>Dependency Injection</title>
<link rel= "styles heet " asp-href - include= "li Ь/bootstrap/dist/css/* .min . css " />
</head>
<body class= "panel-bod y" >
@if (ViewData .C ount > 0)
<tаЫе class= " taЫe taЬle - bordered taЫ e - condensed taЫe - striped">
@foreach (var kvp in ViewData) {
<tr><td>@kvp . Key</td><td>@kvp . Va lu e</td></tr>
}
</tаЫе>

<tаЫе class= " taЫe taЫe-bordered taЬle - co n densed ta Ьl e -strip ed " >
<thead>
<tr><th>Name </th ><t h > Price</t h ></t r >
</thead>
<tbody>
@if (Model == n ull) {
<tr><td colspan= " З " class= "text - center " >No Model Data</td></tr>
} else {
548 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

@f or e ach (var р i n Mode l )


<tr>
<td>@p . Name</td>
<td>@str i ng . Format ("{ O:C2 }", p . Price)</td>
</tr>

</ t body>
</tаЫе>
</ b ody >
</html >

Представление строго типизировано с использованием пер е числения объектов


Pr o d u ct , и основны м содержимым представления является НТМL-таблиц а . Если
контроллер не пр едоставляет какие-либо данные модели, тогда в таблице будет отоб­
ражаться только соответствующее сообщение. При наличи и данных модели для каж ­
дого объекта Product из перечисления к таблице добавляется строка. Им еется таюке
таблица, в которо й будут перечи слены ключи и зн ачения и з объ е кта ViewBag , если
они есть; в противном случае она будет скрыта. Данная табл ица будет задействована
позже в главе.

При стилизации НТМL-элементов представление полагается на п а кет CSS из


Bootstrap. Bootstrap в проект, создайте файл bower . j son с
Чтобы добавить при м ене­
нием шаблона элемента Bower Configuration File (Файл конфигурации Bower) и добавь­
те пакет Bootstrap (листинг 18.8).

Листинг 18.8. Добавление пакета Bootstrap в файле bower. j son

" name ": " a s p . net ",


"pr i vate ": true ,
"dependencies ": {
"bootstrap": "3.3.6"

Последний подготовительный шаг предусматривает со здание в паш'е Views фай­


ла _ View i mp orts . csh t ml, который устанавливает вст роенны е дескрипторные вспо­
могательные классы для использования в представлениях Razor и импортирует про­
странство имен модели (листинг 18.9).

Листинг 18.9. Содержимое файла_Viewimports. cshtml из папки Views


@using Dependen cy inj ection . Models
@addTagHelpe r * , Microsoft . AspNetCo r e . Mvc . TagHelpers

Создание проекта модульного тестирования


Добавьте в решение Visual Studio папку te s t и с применен ие м шаблона Class
Library (.NET Core) (Библиотека классов (.NET Core)) создайт е проект по и мени
Dep e n d e ncyi n j ect i o n. Te sts. следуя процессу , который был описан в гл а в е 7.
Замените стандартное содержимое файла proj e ct . j s on конфигу рацией, прив еден­
ной в листинге 18. 10.
Глава 18. Внедрение зависимостей 549
Листинг 18.1 О. Содержимое файла proj ect . j son из проекта
Dependencyinjection . Tests

"version ": " 1 . 0 . 0- * ",


"testRunner ": "xuni t ",
"dependencies ": {
"Microsoft . NETCore . App ":
" type": "platform",
"version ": " 1 . 0 . 0"
},
"xunit": " 2 . 1 . 0",
"dotnet - test-xunit ": " 2 . 2 . 0- preview2 -bui ld1029 ",
"Dependencyinjection ": "1. 0 . 0" ,
"moq . netcore" : "4.4.0-betaB ",
"System . Diagnostics . TraceSource ": "4.0.0"
},
" frameworks ": {
"netcoreappl . 0":
" imports ": [ "dotnet5 . 6", "p orta Ьle-net 45+win8 "]

Запустив приложение , вы увидите результат, показанный на рис. 18.1 .

С (_2._locall1~:_s_9_4_39_ __ --~
Name Price

No Model Data

L ___
_"___···--·------
Рис . 18.1. Запуск примера приложе ния

Создание слабо связанных компонентов


Как видно на рис. 18.1. данные модели при запуске приложения отсутствуют.
Причина в том, что не установлено отношение между классом HomeController .
который должен передавать данные модели своему представлению, и классом
MemoryReposi tory, содержащим данные модели. Целью соединения вместе компо­
нентов в приложении MVC является обеспечение возможности легк ой замены компо­
нента альтернативной реализацией той же самой функциональности.
Наличие возможно сти замены компонентов позволяет эффективно проводить мо­
дульное тестирование, облегчает изменение поведения приложения в разных средах
размещения (таких как сервер разработки и производственный сервер) и упрощает
долгос рочно е сопровождение приложения.
550 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

В последующих разделах начнете.я объяснение альтернативного подхода и про ­


блем, которые он привносит. Это может выглядеть как непрямой способ объяснения
средства внедрения зависимостей, но одна из сложностей , присущих DI. заключаете.я
в том , что Dl решает проблему, которая не всегда очевидна при написании кода и
про.являете.я только на более поздней стадии цима разработки.

Мнение о внедрении зависимостей

Внедрение зависимостей - одна из тем, по поводу которых читатели связываются со мной


наиболее часто. Примерно в половине сообщений электронной почты выражается недо­
вольство относительно того, что я "навязываю" 01. Как ни странно, во второй половине со­
общений сетуют на то, что я недостаточно сильно делаю акцент на преимуществах 01, и
другие читатели могут не осознать, насколько полезным способно быть это средство.
Внедрение зависимостей может оказаться сложной для понимания темой, и ценность его
спорна. Средство DI может быть удобным инструментом, но далеко не всем оно нравится
и не всем требуется.
Средство 01 приносит лишь ограниченную пользу, если вы не проводите модульное тести­
рование или выполняете небольшой, самодостаточный и устойчивый проект. Понимать, как
работает 01, по-прежнему полезно, поскольку DI используется для доступа к ряду важных
средств MVC, но вы далеко не всегда обязаны применять 01 в своих контроллерах и других
классах.

Я использую DI в своих проектах главным образом потому, что проекты часто двигаются в
неожиданных направлениях, а наличие возможности легкой замены компонента новой реа­
лизацией мож ет уберечь от внесения большого числа утомительных и чреваты х ошибками
изменений. Я предпочитаю приложить определенные усилия в начале проекта , чем иметь
дело со сложным набором правок на более позднем этапе. Я совершенно не категоричен в
отношении внедрения зависимостей, т.к. оно решает проблему, которая возникает не в каж­
дом проекте. Только вы можете решить, нужно ли DI в вашем проекте, и только вы способны
оценить получаемые выгоды и сопутствующие затраты.

И сследование сильно связанных компонентов


Е стественной склонностью большинства разработчиков .является выбор наиболее
прямого пути к решению задачи. В примере приложения это озн ачает применение
мюч е вого слова new для создания объекта хранилища, который требуется контрол­
леру, чтобы получить данные модели (листинг 18.11).

Листинг 18.11. Создание объекта хранилища в файле HomeCon troller. cs

using Mic r osoft . As pNetCo r e . Mvc ;


using Dependencyinjection.Models;
namespace Depe nde nc y inj ec ti on. Controll e r s
p u Ьlic c l as s HomeCo nt ro ll e r : Co ntro ll er
puЬlic ViewResult Index() => View(new MemoryRepository() . Products);

Хорошая новость об этом коде состоит в том, что он работает. Запустив приложе ­
ние, вы увидите детали объектов модели, отображаемые в браузере (рис. 18.2).
Глава 18. Внедрение зависимостей 551

CJ Dep1mdency lnjection Х

- С [ <D l~all1ost:59439

Name Price

Kayak $275.00

Lifejacket $48.95

Soccer ball $19.50

-----·------------··---------
Рис. 18.2. Отображение данных модели

Плохая новость в т ом, что контроллер Н оте и класс Meтor yR e p osi t or y теперь
сильно связаны, т.е. заменить хранилище , не изменяя класс HoтeCo n t r o ll e r, не удас­
тся. Как объяснялось в главе 7, выполнение эффективного м одульного тестирования
означает возможность изоляции одиночного компонента , но протестировать метод

действия Index () в листинге 18.11 невозможно бе з неявного тестирования также и


класс а хранилища. Если модульный тест не пройдет, то нельзя будет утверждать, где
конкретно присутствует проблема - в контроллере, хранилище или каком-то другом
компоненте, от которого зависит хранилище . Во всех практических смыслах конт­
ролл е р Нот е и класс M eтo ryR e p o si t o r y формируют единственный индивидуальный
модуль, как иллюстрируется на рис. 18.3.

1Ко"'ролмр Н Хр'"илище
Р ис. 18.З. Эффект от сильно связанных компонентов

Развязывание компонентов для модульного тестирования

В гл аве 7 использовалось свойство для хранения ссылки на класс хранилища через


реализуемый им интерфейс . что позволило создавать имитированное хранилище для
целей модульного тестирования. В листинге 18.12 такой подход применяется к конт­
роллеру в текуще м прим е ре приложения .

Листинг 18.12. Использование свойства для хранилища в файле HomeController . cs


using Microsoft. AspNetCo r e .Mvc ;
usi ng De pe nde ncyi njec t i on.Model s;
names pace Depende ncy i n je ct i on.C ont r ol lers
puЫ i c c l as s Home Contro l l e r : Contro lle r
puЫic IRepository Repository { get; set; } = new MemoryRepository();
puЫic ViewResul t Index () => View (Reposi tory. Products) ;
)
552 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

Этот прием прекрасно подходит, если вы хотите провести модУльное тестирование,


т.к. он позволяет изолировать класс контроллера, устанавливая свойство Repos i tory
перед вызовом метода действия внутри модульного теста.
Добавьте в проект Dependencyinj ection . Test s файл класса по имени
DI Tests. cs и определите в нем модульный тест, показанный в листинге 18.13, J{О­
торый применяет свойство Repository для настройки фиктивного хранилища перед
взаимодействием с контроллером.

Листинг 18.1 З. Тестирование контроллера в файлеDITests. cs внутри


проекта Dependencyinjection. Tests
using Dependency inj ection.Cont r ol l ers ;
using Dependencyinjection.Models;
using Microsoft . AspNetCore . Mvc;
using Moq;
using Xunit;
namespace Tests
puЫic class DITests
[Fact]
puЬlic void ControllerTest()
11 Организа ция
va r data = new [] { new Product { Name = "Test ", Price 100 } } ;
var mock = new Mock<IRepository>();
mock.SetupGet(m => m.Products) .Retu rns(da ta);
HomeCont roller controller = new HomeController
Repository = mock . Object
};
11 Д ействие
ViewResult result = controller.Index();
11 Утверждение
Assert.Equal(data , result . ViewData.Model);

Свойство Repository позволяет изолировать контроллер и предоставить тестовые


данные, которые можно инспектировать в объекте ViewResul t, созданном методом
действия. В итоге мы обеспечиваем только частичное решение проблемы сильно связан­
ных компонентов, потому что установить свойство Reposi tory, когда приложение фун­
кционирует. невозможно. В главе 17 было указано, что инфраструктура МVС отвечает за
создание э1<земпляров контроллеров для обработки запросов, но ей ничего не известно
об особой важности, придаваемой свойству Reposi tory. Результатом такого приема
является то. что контроллер и хранилище слабо связаны при проведении модульного
тестирования и сильно связаны во время вьmолнения приложения (рис. 18.4).

Использование брокера типов


Следующий логический шаг предусматривает вынесение кода, в котором решает­
ся, какую реализацию интерфейса хранилища применять, за пр еделы класса контрол­
лера и его помещение куда-то в другое место приложения. Чтобы посмотреть, как это
может выглядеть, добавьте в проект приложения папку Infrastructure и создайте в
ней файл класса по имени TypeBroker . cs с содержимым из листинга 18.14.
Глава 18. Внедрение зависимостей 553
Тестирование

Контроллер Хранилище
"

Развертывание

1Ко"'роллер Н И"'ерфейс Н ХР'"'"'ще


Рис . 18.4. Эффект от добавления свойства Reposi tory

Листинг 18.14. Содержимое файла ТypeBroker.cs из папки Infrastructure


using Dependency i njection.Models;
using System;
narnespace Dependencyinjection.Infrastructure
puЫic static class TypeBroker {
private static Туре repoType = typeof(MemoryRepository);
private static IRepository testRepo ;
puЫic static IRepository Repository =>
testRepo ?? Activator .Createinstance(repoType) as IRepository;
puЫic static void SetRepositoryType<T>() where Т: IRepository =>
repoType = typeof (Т);
puЫic static void SetTestObject(IRepository repo) {
testRepo = repo ;

В классе TypeBroker определено свойство


Reposi tory , которое возвращает но­
вые объекты, реализующие интерфейс IReposi tory. Класс реализации, использу­
емый свойством Reposi tory, определяется значением поля repoType, которое по

умолчанию установлено в MemoryReposi tory, но может быть изменено вызовом ме­


тода SetReposi toryType () .
Для поддержки модульного тестирования метод SetTestObj ect () позволяет при­
менять специфический объект. В листинге 18.15 контроллер обновлен, чтобы полу­
чать объект хранилища от брокера типов.

Листинг 16.15. Использование брокера типов в файле HomeController. cs

using Microsoft . AspNetCore . Mvc;


using Dependencylnjection . Models ;
using Dependencyinjection.Infrastructure;
name space Dependencyinjection.Controllers
puЫic class HomeControl l er : Controller
puЫic IRepository Repository { get; } = TypeBroker.Repository;
puЫic ViewResult Index() => View(Repository . Products) ;
554 Часть 11. Подробные сведения об инфраструктуре ASP.NET Соге MVC

Теперь в примере приложения имеется более сложный набор отношений, как де­
монстрируется на рис. 18.5. Важно отметить, что между классом контроллера и клас­
сом хранилища никакого прямого отношения нет - все реш а ется при посредстве

интерфейса и брокера . Это значит, что класс хранилища можно изменять без необхо ­
димости во внесении изменений в класс контроллера.

Хранилище
~
Контроллер "'-
Интерфейс .....

~ Брокер ~

Рис. 18.5. Эффект от добавления брокера типов

Чтобы посмотреть, как применя ется брокер типов , добавьте в папку Models файл
класса по имени Al ternateRepos i tory . c s с еще одной реали з ацией интерф е йса
IRepos i tory, приведенной в листинге 18.16.

Листинг 18.16. Содержимое файла Al terna teReposi tory. cs из папки Model s

using System . Co ll ections . Generic ;


names p ace Depe n dencyinjection . Models
puЫ i c c l ass Al ter n ateRepos i to r y : IRe p os i tory
p ri vate Dictionar y <string , Produ c t> product s;
puЫ ic Alte rn ateRe p osito ry ( ) {
pro du cts = n ew Di ctionary<st r i n g , Pr odu ct> ( ) ;
n e w List<Produ c t > {
ne w Product { Name = " Corner Fl a gs ", Price = 34 . 95 М },
new Product { Name = " Stadium ", Price = 79500 М }
} . Fo rE ach ( р => AddProduct (р)) ;

p u Ыi c IEn um e r a Ы e< Pr oduc t > Products => p r oduct s . Va l ues ;


puЫic Product t his [ string name ] => p r oduc ts[ name ] ;
puЫ i c void AddProduct( Produc t product) =>
produ c t s[produ ct . Name] = p r odu ct;
puЫic void Delete Product( Product product) =>
p roducts . Remove( pr oduct . Name );

В реальном приложении альтернативное хранилище могло бы хранить данные


в другом формате либо использовать постоянство другого вида. В текущем приме ­
ре отличие между классами Al terna te Re pository и MemoryRepository каса ­
ется данных модели, которые они создают при создании их экз е мпляров. Для при­
менения класса Al te rn at e Reposito ry сконфигурируйте брокер типов в м етоде
Conf i g u reServ i ces () класса Startup, как пок азано в листинге 18. 17.
Глава 18. Внедрение зависимостей 555
Лист инг 18.17. Конфигурирование брокера типов в файле Startup. cs
u s i ng Micro s o f t . As pNetCore . Builder ;
using Microsoft . Extensions . Dependency i njection ;
using Dependencyinjection.Infrastructure;
using Dependencyinjection.Models;
namespace Dependencyinjection
puЫic class Start up {
puЫic void Conf i gureServi ces(ISe r viceCol l ectio n services)
TypeBroker.SetRepositoryType<AlternateRepository>();
services . AddМvc() ;

puЬlic void Con figu re( IAp pl ica ti o n Builde r арр) {


app . UseStatusCodePages() ;
app . UseDeveloperExceptionPage() ;
app .U seS t aticFi l es() ;
app . UseMvcW i th Defau l tRo u te() ;

Результат этого изм енения можно увидеть, запустив приложение, которо е отобра­
зит данны е , пр едоставля емые новым I<Лассом хранилища (рис . 18.6).

[J Dependency lnjection

1------С -~- 1oc-; -1 1o~:s9'439


Name Price

Corner Flags $34.95

Stadium $79,500.00

L___________"__________" .________________ . __

Р ис. 18.6. Изменение класса хранилища

Брокер типов по зволяет использовать специфический объект в качеств е хранили ­


ща, что делает возможным написание модульных тестов , подобных представленному
в л истинге 18.18.

Листинг 18.18. Тестирование контроллера через брокер в файле DITests. cs


using Depende n cyin j ec t ion . Co n trol l ers ;
using Dependencyinject i on .I n f ra s truct u re ;
using Dependencyin j ection . Mod els ;
using Microsoft.As p Ne tCore . Mvc ;
using Moq ;
556 Часть 11. Подробные сведения об инфраструктуре ASP.NET Соге MVC

using Xunit ;
n amespace Tests
puЫic class DITests
[Fact]
puЬlic void Contro l lerTest()
11 Организация
var data = new[] { new Pr oduct { Name = " Test ", Pr ice 100 } } ;
var mock = n ew Moc k<IRe p osi t or y > {);
mock . SetupGe t (m => m. Products) . Returns(data) ;
TypeBroker.SetTestObject(mock.Object);
HomeController controller = new HomeController();
11 Действие
ViewResu l t resul t = controller . Index() ;
11 Ут верж д е н ие
As s ert . Equal(data , resul t. Vi e wDat a . Model) ;

В ведени е в средство в н едр ен ия


зави с и м остей ASP.NET
В пр едыдущем разделе вы ознакомились с процессом отделения класса контролл е ­
ра от хранилища. которое поставляет данные модели. Теперь класс HomeContro ll er
может получать реализацию интерфейса I Re p o s itory, ничего не зная о том. какой
rшасс применя ется или I{ак создается его эr<земпляр. Знание о классе реализации
IReposi tory соде ржится в кл а ссе TypeBroker . который может испол ьзовать ся лю­
бым другим контроллером. нуждающимся в доступе к хранилищу. а также для при­
менения тестового объекта .
Общим р е зультатом явля ется более гибкое приложение , но остались е ще н е кото­
рые шероховатости. Самый крупный недостаток с вязан с тем, что для каждого но ­
вого типа, которым должен управлять брокер. пон адобится добавлять новые методы
и свойства. Можно было бы пер еписать класс TypeB r o ke r , сделав его бол е е уни­
версальным. но в этом нет необходимости. поскольку инфраструктура ASP.NET Core
предлагает изящную версию той же самой функциональности , пакетированную та­
ким способом, который облегчает ее применение и не требует ник аких специальных
классов.

Подготовка к внедрению зависимостей


Термин вн едрение зависимостей (DI) описывает альтернативный подход к созда ­
нию слабо связанных компонентов, который интегрирован в ASP.NET Core и исполь­
зу ется инфраструктурой МVС автоматически, а это означает, что контроллеры и дру­
гие компоненты не обязаны знать, каким образом создаются требующиеся и м типы .
В листинге 18.19 продемонстрирована подготовка контроллера Home к внедрению
зависимостей.
Глава 18. Внедрение зависимостей 557
Листинг 18.19. Подготовка к внедрению зависимостей в файле HomeCon troller. cs
using Microsoft.AspNetCore.Mvc ;
using Dependencyinjection.Models;
using Dependencyinjection .I nfrastructure ;
namespace Dependencyinjection.Controllers
puЫic class HomeController : Controller
private IRepository repository;
puЫic HomeController(IRepository repo) {
repository = repo;

puЫic ViewResul t Index () => View (reposi tory. Products) ;


}

Контроллер объявляет свои зависимости в виде аргументов конструктора. Таким


образом, здесь учитывается вторая часть термина: зависимости во "внедрении зави­
симостей" - это объекты, которые требуются для создания нового экземпляра класса.
В рассматриваемом случае класс контроллера объявляет зависимость от · интерфейса
IReposi tory.
В инфраструктуре ASP.NET Core компонент под названием поставщик служб от­
вечает за отображение интерфейсов на типы реализации, которые применяются для
удовлетворения зависимостей.
Когда требуется новый контроллер, инфраструктура MVC запрашивает у постав­
щика служб создание нового экземпляра класса HorneController. Поставщик служб
инспектирует конструктор HorneController для выяснения е го зависимостей, созда­
ет требуемые объе1пы служб и внедряет их внутрь конструктора, чтобы создать но­
вый контроллер, который может использоваться для обработки запроса. Это основной
процесс внедрения зависимостей, поэтому для ясности его стоит повторить.

1. Инфраструктура МVС получает входящий запрос к методу действия в контрол­


лере Horne.
2. Инфраструктура МVС требует у компонента поставщика служб ASP.NET новый
экземпляр класса HomeController .
3. Поставщик служб инспектирует конструктор HomeController и обнаруживает,
что он имеет зависимость от интерфейса IRepository.
4. Поставщик служб обращается к своим отображениям , чтобы найти класс ре­
ализации, который подлежит применению для зависимостей от интерфейса
IRepos i tory.
5. Поставщик служб создает новый экземпляр найденного класса реализации.

6. Поставщик служб создает новый объект HomeController, используя объект ре­


ализации в качестве аргумента конструктора.

7. Поставщик служб возвращает созданный объект HomeController инфраструк­


туре MVC, который она прим еняет для обработки входящего НТТР-запроса.

В целом результат будет таким же, как в случае использования специального клас­
са брокера типов, но важное преимущество заключается в том, что процесс внедре­
ния зависимостей интегрирован в МVС, т.е. компонент поставщика служб будет при-
558 Часть 11. Подробные сведения об инфраструктуре ASP.NET Соге MVC

меняться всякий раз, когда создается экземпляр класса контроллера. Таким образом ,
классам контроллеров разрешено объявлять зависимости, не имея никакого понятия
о том, как они будут распознаваться. Вы просто пишете классы контроллеров, кото ­
рые объявляют свои зависимости как параметры конструкторов, и позволяете инфра­
структуре вместе с компонентом поставщика служб позаботиться об остальном.

На заметку! Во всех примерах в этой главе используется встроенная система внедрения


зависимостей, являющаяся частью ASP.NET Саге . Существуют сторонние пакеты, которые
допускается применять в качестве замены встроенной функциональности и которые могут
предлагать усовершенствования и дополнительные средства . В число популярных пакетов
входят Autofac и StгuctuгeMap, хотя на время написания книги для их интеграции в ASP.NET
Саге требовались дополнительные па кеты. Подробные сведения доступны по адресу
http://github . com/aspnet/Dependencyinjection/ЫoЬ/dev/README.md,
но перечисленные там па кеты, скорее всего, выйдут из употребления по мере интегриро­
вания поддержки ASP.NET Core в главные пакеты 01.

Конфигурирование поставщика служб


Объявление зависимости через конструктор HomeController нарушает рабо­
ту приложения, в чем легко удостовериться, запустив проект. Когда MVC пытается
создать экземпляр класса HomeControl ler для обслуживания запроса, возникает
ошибка, показанная на рис. 18.7.

'-----
'1
~ lnter·мl Serve1 Error Х

С lф-=-~~~~-------=-----~~-===-~=о~~}
Ап unhandled exception occurred while pгocessing the request.
__:
-11
J

lпva li clOpeгationExceptio11:
Unable to гeso lve seгvice for type
j ' Depeпdeпcylnjectio11. Models.IReposito1·y' \Vhile atterпp<iпg to activate /
1 'Depeпdeпcylп1ectюп.Coпёrollers . HorпeCoпtro!ler'. i
j Microsoft.Extensions.lnternal.Actf.1atcrU tilrties.GetService((St>rvkeProvider sp. Туре type-, Турё' requiredBy. Boolean 1
1 isDefaul tParameterRequired) j

! Query Cookies Heade1s 1


1 1

!№l]~?1pe1~qti~x~tif~~~T~'~~~#,,,-_..~
Рис. 18. 7. Запуск примера приложения

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


так, чтобы он знал , каким образом распознавать зависимости от служб. В настоящий
момент поставщик служб не располагает такой информацией и генерирует исключе ­
ние, когда ему предлагается создать объект HomeController, потому что ему ничего
не известно о том, как распознавать зависимость от интерфейса IReposi tory.
Конфигурация для поставщика служб определяется в классе Startup, так что
служба будет на месте к моменту запуска приложения для получения запросов. В лис­
тинге 18.20 поставщик служб конфигурируется так , чтобы ему было известно, каким
образом иметь дело с зависимостями от интерфейса IReposi to ry.
Глава 18. Внедрение зависимостей 559
Листинг 18.20. Конфигурирование поставщика служб в файле startup. cs

using Microsoft . AspNetCore .Builder ;


using Microsoft.Extensions.Dependencyinjection ;
using Dependencyinjection.Infrastructure;
using Dependencyinjection . Models ;
namespace Dependencyinjection
puЫic class Startup {
puЫic void ConfigureServices(IServiceCollection services)
services.AddTransient<IRepository, MemoryRepository>();
services.AddMvc() ;

puЬlic void Configure(IApplicationBuilder арр) {


app . UseStatusCodePages() ;
app . UseDeveloperExceptionPage() ;
app . UseStaticFiles() ;
app . UseMvcWithDefaultRoute() ;

Внедрение зависимостей конфигурируется с использованием расширяющих мето­


дов, которые вызываются на объекте реализации IServiceCollection, полученном
с помощью метода ConfigureServices () .Расширяющий метод AddTransient (),
который применялся в листинге 18.20, сообщает поставщику служб, как обрабаты­
вать зависимость (он будет описан более подробно позже в главе). Отображение выра­
жается с использованием параметров типов, где первый тип является интерфейсом,
а второй - классом реализации.

services . AddTransient< IRepository, MemoryRepository>() ;

Этот оператор указывает поставщику служб о том, что распознавать зависимости


от интерфейса IReposi tory необходимо путем создания объекта MemoryReposi tory.
Запустив приложение , вы увидите, что зависимость, объявленная конструктором
HomeController, распознана и контролл еру предоставлен доступ к данным модели
(рис. 18.8).

D D•pendency lnjection Х
.---
с [_SO~lhost:59439 ,-,........__---~--,----~--=-=~

Name Price

Kayak $275.00

1 Lifejacket $48.95

Soccer ball $19.50


!1____ ·------------------~

Рис. 18.8. Конфигурирование внедрения зависимостей


560 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

Модульное тестирование контроллера с зависимостью


Применение конструктора для получения зависимостей облегчает модульное тес­
тирование контроллеров. В листинге 18.21 приведен модульный тест для контроллера
из листинга 18.20.

Листинг 18.21. Тестирование контроллера в файлеDITests. cs внутри


проекта Dependencyinjection. Tests

using Dependencyinjection.Controllers;
using Dependencyinjection.Models;
using Microsoft.AspNetCore.Mvc;
using Моq;
using Xunit;
namespace Tests
puЫic class DITests
[Fact]
puЫic void ControllerTest()
11 Организация
var data = new[] { new Product { Name = "Test", Price 100} };
var mock = new Мock<IRepository> ();
mock . SetupGet(m => m.Products) . Returns(data);
HomeController controller = new HomeController(mock.Object);
11 Действие
ViewResult result = controller.Index();
11 Утверждение
Assert.Equal(data , result.ViewData . Model);

Контроллер ничего не знает, да и не заботится о том, какой вид объекта передает­


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

Использование цепочек зависимостей


Когда поставщику служб необходимо распознать зависимость, он инспектирует
тип, который был сконфигурирован для применения, чтобы выяснить, имеет ли он
в свою очередь зависимости, подлежащие распознаванию. Результатом является воз­
можность создания цепочки зависимостей, которые все распознаются во время вы­
полнения и могут управляться поср едством конфигурации в классе Startup. Чтобы
посмотреть на образование цепочки зависимостей, добавьте в папку Models файл
класса по имени IМodelStorage. cs с определением интерфейса, представленным
в листище 18.22.

Листинг 16.22. Содержимое файла IModelStorage. cs из папки Models

using System . Collections.Generic;


namespace Dependencyinjection . Мodels
puЫic interface IModelStorage {
IEnumeraЫe<Product> Items { get;
Глава 18. Внедрение зависимостей 561
Product t his [string key] { get ; set ; }
boo l ContainsKey(string key) ;
void Removeitem(string key);

Интерфейс IModelStorage определяет поведение простого механизма хранилища


для объектов Produc t. Для реализации этого интерфейса добавьте в папку Models
файл класса по имени DictionaryStorage . cs с определением из листинга 18.23.

Листинг 18.23. Содержимое файла DictionaryStorage. cs из папки Models


using System . Co ll ections.Ge neric ;
namespace Dependencyinjection.Models
puЫic c l ass DictionaryStorage : IMode lSt o r age
private Dictionary<string , Produ c t > items
= new Dictionary<string , Product> ();
p u ЬlicProduct this [str in g key]
get { return i tems [ key] ; }
set { items[key ] = value ; }

puЬlic IEnumeraЫe<Product> I tems => items . Values ;


puЫic bool ContainsKey(string key) => items . Conta in sKey(key) ;
puЬlic void Removeitem(string key) => items.Remove(key);

Класс DictionaryStorage реализует интерфейс IModelStorage, используя стро­


го типизированный словарь для хранения объе1пов модели. Такая функциональность
в текущий момент содержится внутри класса MemoryReposi tory , и отделение с при­
менением интерфейса имеет малую ценность в реальном про екте, но мы получаем по ­
лезный пример того, как внедр ение зависимостей может использоваться, не привнося
слишком много дополнительной сложности в приложение.
В листинге 18.24 класс MemoryReposi tory модифицирован так, что он объявляет
зависимость от интерфейса IModelStorage , но без какого -либо знания о классе реа­
лизации, который будет применяться во время выполнения.

Листинг 18.24. Объявление зависимости в файле MemoryReposi tory. cs

using System . Collections . Gener i c ;


namespace Dependencyinjection . Models
puЫic class MemoryRepos i tory : IRepos it ory
private IModelStorage storage;
puЫic MemoryRepository(IModelStorage modelStore)
storage = modelStore;
new List<Product> {
new Produ c t Name "Kayak ", Price = 275 М ),
new Product { Name "Lifejacket ", Price = 48. 95 М } ,
new Product { Name " Soccer ball ", Price = 19.50 М}
}. ForEach(p => AddProduct(p)) ;
562 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Соге MVC

puЬlic IEnumeraЫe<Product> Products => storage. Items;


puЫic Product this[string name] => storage[name];
puЫic void AddProduct(Product product) =>
storage[product.Name] = product;
puЫic void DeleteProduct(Product product) =>
storage.Removeitem(product.Name);

Запустив приложение , вы увидите, что поставщик служб сгенерирует исключение


со следующим сообщением :

InvalidOperationException : UnaЬle to resolve service for type


' Dependencyinjection . Models .I ModelStorage' while attempting to activate
' Dependencyin j ection.Models . MemoryRepository '.
InvalidOpera tionException : не удается распознать службу для типа
Dependencyin je ction .Models . IModelStorage при попытке активации
Dependencyinjection . Models . MemoryRepository.

Это демонстрирует прохождение поставщика служб через цепочку зависи м ос­


тей. Когда у н е го было затребовано создание нового объекта контроллера, он про­
инспектировал конструктор HomeController и нашел зависимость от интерфей­
са I Repos i tory , которая, как ему и з вестно, должна распознаваться посредством
объекта MemoryRepos i tory. Затем поставщик служб исследовал конструктор
MemoryRepos i to r y , который имеет зависимость от интерфейса IModelStorage. Но
в конфигурации не ука з ано, каким образом должны распознаваться зависимости от
IModelStorage . т.е. объект Memo ryRepository не может быть создан, равно как не
может быть создан и объект HomeController. Поставщик служб не в состоянии пре­
доставить инфраструктуре MVC объект, необходимый для обработки з апроса, потому

было сгенерировано исключение.


Необходимо добавить отображение типа , которое сообщит поставщику служб о
том, I<ак должны распознаваться зависимости от I Mode lS torage, что и делается в

конфигурации приложения, показанной в листинге 18.25.

Листинг 18.25. Конфигурирование дополнительного отображения типа


в файле Startup. cs

using Microsoft . AspNetCore . Builder ;


using Microsoft . Extensions . Dependencyinjection ;
using Dependencylnjection . Inf r astructure ;
using Dependencyinjection.Models;
namespace Dependencyinjection
puЬlic class Startup {
puЫic void Conf i gureServi ces(IServiceCollect i on services) {
services.AddTransient < IRepository , MemoryRepository>() ;
services.AddTransient<IModelStorage, DictionaryStorage>();
services .AddMvc();
Глава 18. Внедрение зависимостей 563
puЫic void Configure(IApplicat i onBuilde r арр) [
app . UseStatusCodePages() ;
app . OseDeveloper ExceptionPage() ;
app . OseStaticFi l es() ;
app . OseMvcWithDefaultRoute() ;

С таким добавлением поставщик служб может удовлетворить обе зависимости в це­


почке и способен создать набор объектов, требуемых для обслуживания запроса: объек­
та DictionaryStorage, внедряемого внутрь конструктора класса Memo r yReposi tory,
объект которого внедряется внутрь конструктора HomeController . Цепочки зави­
симостей - не просто искусный трюк; они позволяют формировать сложную функ­
циональность, объединяя компоненты, которые могут быть легко изолированы для
тестирования и очень просто изменены, чтобы удовлетворять развивающимся требо ­
ваниям проекта по мере обретения им зрелости.

Использование внедрения зависимостей для конкретных типов


Внедрение зависимостей может также применяться для конкретных типов, доступ
к которым не производится через интерфейсы. Несмотря на то что здесь не обеспе­
чиваются преимущества слабой связанности, присущие использованию интерфейсов,
сам по себе это удобный прием, поскольку он позволя ет получать доступ к объектам
где угодно в приложении и помещает конкретные типы под управление жизненным

циклом, как описано далее в главе.

Добавьте в папку Models файл l(Jlacca по имени ProductTotalizer. cs с соде р­


жимым, приведенным в листинге 18.26.

Листинг 18.26. Содержимое файла ProductTotalizer. cs из папки Models

using Systern . Linq ;


namespace Dependencyinjection . Mode ls
puЫic class ProductTotalizer [

puЬlic ProductTota li zer(IRepositor y repo) {


Repository = repo ;

puЬlic IReposito r y Repository { get ; set ; }


puЫic decimal Total => Repository.Products . Surn(p => p . Pr i ce) ;

Класс ProductTota liz er не дела ет ничего особо полезного, но имеет зависимость


от интерфейса IReposi tory, а это значит, что прим енение внедрения зависимостей
распознает данную зависимость, используя конфигурацию, которая также применя­
ется к остальной части приложения. В листинге 18.27 показано объявление класса
ProductTotalizer как зависимости для класса HomeContro ll e r.
564 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC

Листинг 18. 27. Добавление зависимости в файле HomeCon troller. cs

using Microsoft . AspNetCore . Mvc;


using Dependencyinjection . Models ;
using Dependencyinjection . Infrastructure ;
namespace Dependencyinjection . Contro l lers
puЫic class HomeController : Controller
private IRepository repository;
private ProductTotalizer totalizer;
puЫic HomeController(IRepository repo, ProductTotalizer total) {
repository = repo;
totalizer = total;

puЫic ViewResult Index()


ViewBag.Total = totalizer.Total;
return View(repository.Products);

Действие I ndex добавляет свойство ViewBag , содержащее обшую сумму, выпус­


каемую классом ProductTotalizer, которая отобразится в таблице для значений
ViewBag , добавленной к представлению I ndex . cshtml в начале главы. Финальный
шаг заключается в сообщении поставщику служб о том, как поступать с запросами
ProductTotalizer (листинг 18.28).

Листинг 18.28. Конфигурирование поставщика служб в файле Startup. cs

puЫ i c void Con figu reServices(IServiceCollection services) {


services . AddTransient<IRepository , MemoryReposi tory>() ;
services . AddTransient< I ModelStorage , DictionaryStorage>() ;
services.AddTransient<ProductTotalizer>();
services . AddMvc() ;

В данной ситуации отсутствует отображение между типом службы и типом р е­


ализации , поэтому используется перегруженная версия расширяюще го метода

AddT r ansient (), принимающая единственный параметр типа, который сообщает


поставщику служб. что для распознавания зависимости указанного типа он должен
создавать экземпляр класса ProductTotalizer .
Преимущества такого подхода - в противоположность простому созданию экземп­
ляра конкретного класса внутри контроллера - связаны с тем, что поставщик служб
будет распознавать любые зависимости, объявляемые конкретным классом, а также с
тем, что появляется возможность изменять конфигурацию для распознавания зависи­
мостей от конкретного класса с применением более специализированных подмассов .
Конкретные классы управляются поставщиком служб и подвергаются воздействию
со стороны средств жизненного цикла, которые рассматриваются в следующей главе .
Запустив приложение, вы увидите, что оно отображает обшую сумму для объектов
Product в модели (рис . 18.9).
Глава 18. Внедрение зависимостей 565

D Dep~ndency lnjection Х

j,_ С [ CD ~:all~ost59~3!_ ==-===-----·-- *-/


Total 343.45

i
J Name Price

i ~ ......_..,..."6,-
il!!!1fjiilt_....,.41>. $275.00
. ~~
Рис. 18.9. Использование внедрения зависимостей для классов

Жизненные циклы служб


В предыдущем разделе для сообщения поставщику служб о том, как он должен
обрабатывать зависимости от интерфейсов IRepository и IModelStorage, приме­
нялся расширяющий метод AddTransient () .Метод AddTransient () - один из че­

тырех способов определения отображений типов. В табл. 18.3 описаны расширяющие


методы, которые указывают поставщику служб, каким образом распознавать зави­
симости. Все методы в табл. 18.3 используют параметры типов, но доступны также
расширяющие методы, которые взамен принимают в качестве аргументов объекты
Туре, что может быть полезно, если необходимо генерировать отображения во время
выполнения.

Таблица 18 .З. Расширяющие методы, сообщающие поставщику служб о том,


как распознавать зависимости

Имя Описание

AddTransient<service , implType>() Этот метод указывает поставщику служб на


необходимость создания нового э кземпляра
типа реализации для каждой зависимос-
ти от типа службы , как описано в разделе
"Использование переходного жизненного цик­
ла" далее в главе

AddTransient<service>() Этот метод применяется для регистрации


одиночного типа, экземпляр которого будет
создаваться для каждой зависимости, как
объяснялось в разделе "Использование внед­
рения зависимостей для конкретных типов "
ранее в главе

AddTransient<service>(factoryFunc) Этот метод применяется для регистрации


фабричной функции, которая будет вызывать­
ся с целью создания объекта реализации для
каждой зависимости от типа службы, как по­
казано в разделе "Использование фабричной
функции" далее в главе
566 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC

Окончание табл. 18.3


Имя Описание

AddScoped<s ervi ce , implT ype>( ) Эти методы сообщают поставщику о том,


AddScoped<se r vi ce>( ) что экземпляры типа реализации должны
AddScoped<servi ce> (fa c tory Func ) использоваться повторно. Таким образом,
все запросы к службе со стороны компонен­
тов, ассоциированных с общей областью
действия, которой обычно является оди­
ночный НТТР-запрос, разделяют один и тот
же объект. Данные методы следуют тому
же самому шаблону, что и соответствую-
щие методы AddTran s i e nt (). См. раздел
"Использование жизненного ци кла, ограни­
ченного областью действия" далее в главе

AddSi ng l eton<se r vice , impl Type >() Эти методы указывают поставщику служб на
AddSing l eton<service>() необходимость создания ново~о экземпляра
AddSi ngl eton<s ervi ce> (facto ryFunc ) типа реализации для первого запроса к служ­

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


всех последующих запросов к службе . См .
раздел "Использование жизненного цикла
одиночки" далее в главе

AddSing l eton<se r vi ce>( ins tance ) Этот метод предоставляет поставщику служб
объект, который должен применяться для об ­
служивания всех запросов к службе. Поставщик
служб не будет создавать новые объекты

Использование переходного жизненного цикла


Простейший способ приступить к применению внедрения зависимостей предус­
матривает использование метода Add Tran sie nt ( ) , который сообщает поставщику
служб о том, что он должен создавать новый экземпляр типа реализации всякий раз ,
когда нужно распознать зависимость. Это конфигурация, которая уж е присутствует
в классе St a r t up :

puЫ i c vo i d ConfigureServic es(IServi ceCollect i on servi ces) {


services .AddTrans1ent<I Repository , MemoryRepository>() ;
s ervic e s.AddTransient<IModelStor age , Di c ti onary Sto r a ge> ();
services .AddTrans1ent<P r odu ctTotalizer>() ;
se r vices . AddMvc() ;

Со всеми жизненными циклами, описанными в табл. 18.3, связаны компромиссы.


Переходный жизненный цикл влечет за собой затраты по созданию нового экземпляра
класса реализации каждый раз, когда распознается зависимость, но его преимущест­
во в том , что не приходится беспокоиться об управлении параллельным доступом или
обеспечении безопасного многократного применения объекта множеством запросов.
Для демонстрации переходного жизненного цикла в классе MemoryRepository
переопределяется метод ToS tring (), чтобы он генерировал глобально уникальный
идентификатор (globally unique identifier - GUID), как показано в лист инге 18.29.
Глава 18. Внедрение зависимостей 567
Листинг 18.29. Переопределение метода ToString () в файле MernoryReposi tory. cs

using System . Collections .Generic ;


namespace Dependencyinjection.Models
puЫic class MemoryRepository : IRepository
private IModelStorage storage;
private string guid = Systern.Guid.NewGuid() .ToString() ;
puЫic MemoryReposito ry (IModelStorage modelStore) {
storage = modelSto re;
new List<Product> {
new Product { Name " Kayak ", Price = 27 5 М ) ,
new Product { Name "Lifejacket ", Price = 48 . 95 М ) ,
new Product { Name " Soccer ba ll", Price = 19 . 50 М}
} .ForEach(p => AddProduct(p)) ;

puЫic IEnumeraЫe<Prod uct > Pro ducts => storage.Items;


puЫic Product this[st ri ng name] => storage[name] ;
puЬlic void AddProduct(Product product) =>
storage[product.Name ] = product;
puЬlic void DeleteProduct(Product product) =>
storage.Removeitem( pr oduct .N ame) ;
puЫic override string ToString() {
return guid;

Идентификатор GUID облегчит опознание специфического эк земпляра класса


MemoryRepository и покажет, как различные методы жизненного цикла изменяют

поведение поставщика служб. В листинге 18.30 метод действия Index () 1юнтроллера


Ноте модифицирован так, чтобы создавать свойство Contro l ler в объекте ViewBag
и устанавливать его в GUID из хранилища.

Листинг 18.30. Использование объекта ViewBag в файле HomeController. cs

using Microsoft . AspNetCo re. Mvc ;


using Dependencyinjection . Models;
using Dependencyinjection . Infras tructure;
namespace Dependencyinjection . Controllers
puЫic class HomeCont r o ll er : Contro l ler
private IRepository repository;
private ProductTotalizer totalizer ;
puЫic HomeControlle r ( IRepository repo , ProductTotalizer total) (
repository = repo;
totalizer = total;

puЫic ViewResult Index()


ViewBag.HorneController = repository . ToString();
ViewBag.Totalizer = totalizer.Repository.ToString();
return View(repository . Products);
568 Часть 11. Подробные сведения об инфраструктуре ASP.NEТ Core MVC

Метод действия Index () добавляет значения в объект Vi ewBag, которые содер­


жат идентификаторы GUID для объектов хранилища, получаемых самим конструк­
тором HomeController и посредством конструктора класса Product To t al i zer ,
что можно увидеть, запустив приложение . Два идентификатора GUID отличаются
друг от друга , поскольку поставщик служб был сконфигурирован с помощью м етода
AddTra ns ient () , т.е . он со здает н овый объект Memor yReposi t ory для распознава­
ния зависимости класса HomeCon t r o lle r и еще один для класса ProductTotal izer
(рис. 18.10).

1 Dependency lnJeclion Х

1 +- ...;; С' ~-CJ l oca'ih~~ts8з31---··---·-·--·----··--·--75; Е J


_ _ _ _ _ _ _ _ _ :_ __" _ _;с __._ _________ ._ ____ __,_, _с_" ••. ~:_-' ·"'-----·
. -----·---r
HomeController 999 4С40З-4d6Ь-4с6а-9З79·С8ае 1 72аЗеd4 ~ i.
'
v otalizer
r F
1 ij

Name
Kayak 1 +- ""1:" С r 'tl

1 ;::~~ Ji-~:::;;~"" :::::::~=:~~::~--т1


·1 ~ Totalizer 9а9fе37с-504З-4524-9Зfе-1 f8551 7a7068 ' 1
1 j
1 Name Price 1

~~....,,... ~'--".... F~~ ..,..~


Рис. 18.1 О. Эффект от переходного жизненного цикла

При каждой пер езагрузке веб-страницы в браузере новый НТГР- запрос приводит
к тому, что инфраструктура MVC создает новый объект HomeContro ll er , вызывая
создание двух новых объектов Memo r yRepo si tory , каждый с собственным иденти­
фикатором GUID.

Совет. Идентификаторы GUID уникальны (или настолько близ ки к уникальным, что фактичес­
кая разница отсутствует), поэтому при запуске приложения на своей машине вы увидите
другие значения.

Использование фабричной функции


Одна из версий метода Add Tran s i en t () принимает фабричную функцию, кото­
рая вызывается каждый раз, когда встречается зависимость от тип а службы. Это
позв оляет варьировать создаваемый объ е кт, так что разные зависим ости получают
экземпляры разных типов или экземпляры . которые по-другому с1юнфигурированы.
В листинге 18.31 фабричная функция применяется для выбора разных реализаций
интерфейса I Repos i tory на основе среды размещения, в которой выполняется
приложение .
Глава 18. Внедрение зависимостей 569
Листинг 18.31. Использование фабричной функции в файле startup. cs

using Microsoft.AspNetCore.Builder;
using Microsoft .Extensi ons . Dependencyinjection;
using Dependencyinjection .I nfrastructure ;
using Dependencyinjection.Models;
using Microsoft.AspNetCore.Hosting;
namespace Dependencyinjection {
puЫic class Startup {
private IHostingEnvironment env;
puЬlic Startup(IHostingEnvironment hostEnv)
env = hostEnv;

puЫic void ConfigureServices(IServiceCollection services)


services . AddTransient<IRepository>(provider => {
if (env.IsDevelopment()) {
var х = provider.GetService<МemoryRepository>();
return х;
else {
return new AlternateRepository();
}
}) ;
services.AddTransient<МemoryRepository>();
services . AddTransient< IModelStorage , DictionaryStorage>();
servi ces . AddTransient<ProductTotalizer>() ;
services . AddMvc() ;

puЫic void Configure(IApplicationBuilder арр) {


app . UseStatusCodePages() ;
app . UseDeveloperExceptionPage();
app .UseStaticFiles() ;
app . UseMvcWithDefaultRoute() ;

В главе 14 было описано, как инфраструктура ASP. NEТ снабжает класс Startup
службами, содействуя в настройке приложения; в число этих служб входит реализа­
ция интерфейса IHostingEnvironment, предназначенная для определения среды
размещения. Службы можно получать как аргументы метода Configure (), но не
метода ConfigureServices () , поэтому в класс
Startup был добавлен конструктор.
который предоставляет доступ к объекту реализации IHostingEnvironment и при­
сваивает его полю по имени env.

Внутри метода ConfigureServices () с помощью метода AddTransient () оп­


ределяется фабричная функция с указанием лямбда-выражения. Лямбда-выражение
получает объект System. IServiceProvider, который может использоваться для
создания экземпляров других типов, зарегистрированных посредством поставщика

служб. с применением методов из табл . 18.4.


570 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC

Таблица 18.4. Методы IServiceProvider и расширяющие методы

Имя Описание

GetService<service>() Этот метод использует поставщик служб для созда­


ния нового экземпляра типа службы. Он возвращает
null, если для запрошенного типа отсутствует
отобра ж ение
GetRequi redService<service>() Этот метод применяет поставщик служб для созда­
ния нового экземпляра типа службы . Он генерирует
исключение, если для запрошенного типа отсутству­

ет отображение

Внутри фабричной функции объект реализации IHostingEnvironment ис­


пользуется для выяснения, функционирует ли приложение в среде разрабопш. и
если это так, тогда посредством метода GetServ i ce () создается экземпляр клас­
са MemoryRepo si tory, который возвращается из фабричной функции как объект,
подлежащий применению для зависимости от IRep o sitory. Метод GetService ()
используется для создания объекта из-за того , что класс MemoryRepository имеет
собственную зависимость от интерфейса IModelStorage , а применение поставщика
служб означает автоматическое управление обнаружением и распознаванием зави­
симости, но это также означает необходимость указания жизненного цикла, I<оторый
должен использоваться для объектов MemoryRepo si tory , например:

services.AddTransient<MernoryRepository>();

Без такого оператора поставщик служб не будет располагать информацией, I<ото­


рая ему нужна для создания и управления объектами MemoryReposi t or y.
Если приложение выполняется не в среде разработки. тогда фабричная функция
возвращает новый экземпляр класса Al ternateReposi tory. Экземпляр этого класса
может быть создан напрямую с применением ключевого слова new , потому что в его
конструкторе какие-либо зависимости не объявлены.

Использование жизненного цикла,


ограниченного областью действия
При таком жизненном цикле создается единственный объект класса реализации,
который применяется для распознавания всех зависимостей, ассоциированных с од­
ной областью действия, что обычно означает одиночный НТТР-запрос . (Вы можете
создавать собственные области действия, но в большинстве приложений от них мало
пользы.)
Поскольку стандартной областью действия является НТТР-запрос, рассматрива­
емый жизненный цикл позволяет единственному объекту разделяться всеми ком­
понентами, которые обрабатывают запрос. Чаще всего это удобно при совместном
использовании общих данных контекста, когда пишутся специальные классы, такие
как маршруты. Жизненный цикл, ограниченный областью действия, создается путем
применения расширяющего метода AddScoped ( ) для конфигурирования поставщика
служб (листинг 18.32).
Глава 18. Внедрение зависимостей 571

Совет. Как было описано в табл. 18.4, доступны также версии метода AddScoped () , кото­
рые принимают фабричную функцию и могут использоваться для регистрации конкретно­
го типа. Такие методы работают аналогично методу AddTransient () , который демонс ­
трировался в предыдущем разделе , с вполне очевидной разницей в том, что ж изненный
ци кл создаваемы х ими объектов отличается.

Листинг 18.32. Использование жизн енного цикла, ограниченного областью действия,


в файле Startup. cs

using Microsoft.AspNetCore . Builder ;


using Microsoft . Extensions . Depende nc yinjection ;
using Dependencyinjection . Infrastructure ;
using Dependencyinjection.Models ;
using Microsoft . AspNetCore . Hosting ;
namespace Dependencyinjection {
puЫic class Startup {

pr i vate IHostingEnvironment env ;


puЫic Startup(IHostingEnvironment hostEnv) {
env = hos tEnv ;

puЬlic void ConfigureServices(IServic eCollection services) {


services.AddScoped<IRepository, MemoryRepository>();
services . AddTransient< IMode l Sto rag e , DictionaryStorage>();
services . AddTransient<ProductTotalizer >() ;
services . AddMvc() ;

puЬlic void Configure( IApplicationBuilder арр) {


app . UseStatusCodePages() ;
app .UseDeveloperExceptionPage() ;
app . UseStaticF il es() ;
app . UseMvcW i thDefaultRoute() ;

В примере приложения экземпляры классов HomeController и ProductTotalizer


создаются вместе. чтобы обрабатывать запрос, и оба они требуют от поставщика
служб распознавания зависимости от интерфейса IReposito r y . Применение метода
AddScoped () гарантирует, что зависимости обоих объектов рас по знаются посредс ­
твом единственного объекта MemoryReposi tory. Увидеть результат можно, запус ­
тив пример приложения; браузер отобразит два одинаковых идентификатора GUID
(ри с . 18. 11). П ерезагрузка страницы приводит к созданию нового НТТР- запроса, о з­
начая создание нового объекта MemoryRepos i tory.

Использование жизненного цикла одиночки


Жи з н енный цикл одиночки гарантирует, что для распознавания всех зависимос­
тей для заданного типа службы применяется единственный объект. При исполь зо­
вании такого жизненного цикл а вы обязаны обеспечить безопасность параллельно­
го доступа к классам р еализации, применяемым для распознавания зависимостей.
В ли сти нг е 18. 33 изменена область действия для конфигурации IReposi tory.
572 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Саге MVC

6dba7caa-e118-4798-b26c-aabcdeBd90C6
56912сЬО-2ес а-4ЬЗ5-Ьбаd-5с1383426163
Bdba7caa-e 118-4798-b26c-aabcde8d90c6

Price

Рис. 18.11. Эффект от жизненного цикла, ограниченного областью действия

Листинг 18.33. Использование жизненного цикла одиночки в файле Startup. cs

p u Ыic void Conf i gureServices(IS e rvi c eCollectio n services) {


services.AddSingleton<IRepository, MemoryRepository>();
se r vices . AddTr ansient<IMode l Sto r age , Di ctionaryStorage>() ;
services . AddTransient<Produc tT ot a l izer>() ;
services . AddMvc() ;

Метод AddS ingle ton () создает новый экземпляр класса Me mo r yRepositor y в


первый раз , когда он долж ен распознавать зависимость от интерфейса I Reposi tory,
и затем повторно использует этот экземпляр для всех последующих зависимостей.
даж е если они ассоциированы с другими НТТР - запросами (рис. 18.12).

[') Dependency lnjection Х


D Dependency ln1ection Х

HomeController d761dae4-2104-4 1dc-


·_ -~-.lE~~~::==~-:=:-:::-~1_ !_I
HomeC011troller d761dae4-2104-41 dc-80fb-6f448de87ca9 1!

Totalizer d761dae4-2104-41 dc-


Totalizer d761dae4-2 104-41dc-80fb-6f448de87ca9
i
Name Pri
Name Price 1

""" .._.,,,.,,,..._ ~ ,,,,.--~,.,.,.. ~


Рис. 18.12. Эффект от жизненного цикла одиночки

И с пользов ан и е вн едрен ия в действия


Стандартный способ объявления зависимости - через конструктор , что представ ­
ляет собой прием , который можно применять в любом классе и который полагает­
ся н а средства внедрения зависимостей , являющиеся частью основно й пл атфор м ы
ASP. NEТ.
Глава 18. Внедрение зависимостей 573
Инфраструк1УРа MVC дополняет стандартную функциональность альтернативным
подходом, называемым внедрением в действия, который позволяет объявлять зави­
симости через парам е тры методов действий. Строго говоря , внедрение в действия
предоставляется систе мой привязки моделей , 1<0тор ая обсуждается в гл аве 26, но оно
р ассматривается в настоящей главе , т.к . позволя ет использовать службы по-другому .
Внедрение в дей ствия выполняется с помощью атрибута FromServices , который
прим еня ется к параметру метода дей ствия, как показано в листинге 18.34.

Листинг 18.34. И спользование внедрения в действия в ф а йле HomeController. cs


using Microsoft . AspNetCore . Mvc;
using Dependencyinjectio n. Mode l s ;
usi ng Dependency i njection .I nfrastruc t ure ;
namespace Dependencyinj e ct i on . Co ntroll e rs
puЫic class HomeCo nt ro l l e r : Cont ro l ler
private I Repository r e pos it or y;
puЫic HomeControlle r (I Repository r epo) {
r epository = repo ;

puЬlic ViewResult Index([FromServices]ProductTotalizer totalizer) {


ViewBa g. HomeControll e r = reposi t ory. ToS t ring() ;
ViewBag.Totalizer = totalizer.Repository.ToString();
return View(repos i to r y .Products) ;

Инфраструктура MVC с применением поставщика служб получает экземпляр клас­


са ProductTotalizer и пр едоставляет его в качестве аргумента при вы з ове метода
действия Index (). Использовани е вн едрения в действия мене е рас пространено , чем
стандартно е внедрение в конструкторы, но может быть удобны м в ситуации , когда
имеется з ависимость от объекта, которы й является дорогостоящим в создании и тре ­
буется только в одном из методов де й ствий, определяемых контроллером. Применение
вн едр е н ия в конструкторы распознает зависимость для всех методов действий, даже
е сли метод действия, вызываемый для обработки запроса, не исполь зует объект ре ­
ал и зации. Декор и рование метода действия атрибутом FromServ i ces сужает сферу
влияния з ависи м ости и гарантирует, что экзе м пляр типа реализации будет созда­
ваться, тол ько когда он необходим .

Использование атрибутов внедре н ия в с во й ства


В гл ав е 17 объяснялось , к ак получ ать данны е конт екста в контролл е ре РОСО за
сч ет объявления свой ства и его декорирования атрибутом Control l erContext.
Теп е рь , ч итая настоящую гл аву, вы должны понимать , что это было сп ециальной фор­
м о й вн едр е ния зав и си м о сте й. Она называется внедр е нием в свойства .
Инфраструктура MVC предлагает набор специализированных атрибутов, которые
мо жно применять для получения сп е цифических типов через внедр ение в свойства
внутри контроллер ов и компонентов представлений (рассматривае мых в главе 22).
В случа е наследования контроллеров от базового класса Cont r oller использовать
574 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC

такие атрибуты не обязательно, потому что информация контекста доступна через


удобные свойства, но в табл. 18.5 приведен список атрибутов, предназначенных для
применения в контроллерах РОСО.

Таблица 18.5. Специализированные атрибуты для внедрения в свойства

Имя Описание

ControllerContext Этот атрибут устанавливает свойство ControllerContext,


которое предоставляет надмножество функциональности
класса ActionContext, как описано в главе 31
ActionContext Этот атрибут устанавливает свойство ActionContext для
предоставления информации контекста методам действий .
Классы Controller открывают доступ к информации кон­
текста через свойство ActionContext, а также набор удоб ­
ных свойств, описанных в главе 31

ViewContext Этот атрибут устанавливает свойство ViewContext, чтобы


предоставить данные контекста для операций с представ­
лениями, включая дескрипторные вспомогательные классы

(глава 23)
ViewComponentContext Этот атрибут устанавливает свойство
ViewComponentContext для компонентов представлений,
которые обсуждаются в главе 22

ViewDataDictionary Этот атрибут устанавливает свойство


ViewDataDict ionary, чтобы предоставить доступ к дан­
ным привязки модели, как описано в главе 26

Запрашивание объекта реализации вручную


IЛавное средство внедрения зависимостей ASP.NET и дополнительные атрибуты,
которые инфраструктура МVС пр едлагает для внедрения в свойства и действия, пре­
доставляют всю поддержку. требуемую в большинств е приложений для создания слабо
связанных компонентов. Однако могут быть ситуации, когда удобно получать объект
ре ализации для интерфейса, не пол агаясь на внедрение. В таких ситуациях можно
работать напрямую с поставщиком служб, как демонстрируется в листинге 18.35.

Листинг 18.35. Использование поставщика служб напрямую в файле


HomeController . cs
using Microsoft . AspNetCore . Mvc ;
using Dependencyinjection.Models ;
using Dependencylnjection.Infrastructu r e ;
using Microsoft . Extensions . Dependencyinjection ;
namespace Dependencyinjection . Controllers
puЫic class HomeController : Cont r oller
puЫic ViewResu lt Index([FromServices]ProductTotalizer totalizer)
IRepository repository =
HttpContext.RequestServices.GetService<IRepository>();
Глава 1В . Внедрение зависимостей 575
ViewBag . HomeController = repository.ToString() ;
ViewBag . Totalizer = totalizer . Repository.ToString() ;
return View(repository.Products) ;

Объект HttpContext , возвращаемый свойством с таким же именем, опре­


деляет свойство Re qu е s t S е r v i се s , которое возвращает объект реализации
IServiceProvider , разрешая вызов на нем методов из табл. 18.4. В листинге 18.35
было удалено свойство Repos i tory , которое устанавливалось с применением внед­
р е ния в свойства , а взамен задействовано свойство HttpContext . RequestServices
для получения реализации интерфейса IRepo si tory .
Это известно как nammepн Локатор служб (Service Locator), использования ко­
торого, по мнению ряда разработчиков, следует избегать. Марк Симен сделал хо ­
рошее описание проблем, к которым он может привести (ht tp : / /Ьlog . ploeh . dk/
201О/02 / 03/ ServiceLoca tor isanAnti - Pa ttern). Моя точка зрения отличается
меньшей строгостью в том, что получение служб подобным способом совершенно
обоснованно, когда обычный прием получения зависимостей через конструктор по
некоторым причинам применять невозможно.

Совет. Если вам нужен доступ к службам в методе Configure () класса Startup, тогда
можете использовать свойство Appl ica tionServ ices, предлагаемое интерфейсом
IApplicationBuilder .

Резюме
В э той главе объяснялась роль, которую внедрение зависимостей играет в при ­
ложении MVC. Оно помогает создавать слабо связанные компоненты, 1<оторые лег­
ко заменять и просто изолировать в целях тестирования. Было продемонстрировано
средство внедрения зависимостей ASP.NET, а также ат рибуты, которые инфраструк­
тура MVC предоставляет для внедрения зависимостей в свойства и методы действий.
Кроме того, были описаны варианты жизненных циЮiов , доступные при конфигури­
ровании поставщика служб , и показано, как они влияют на способ, котор ым созда­
ются объекты. В следующей главе рассматриваются фильтры, добавляющие дополни­
тельную логику в процесс обработки запросов.
ГЛАВА 19
Фильтры

ф ильтры внедряют дополнительную логику в процесс обработки запросов МVС.


Они предлагают простой и элегантный способ реализации сквозной обязаннос­
ти; таким термином обозначаете.я функциональность, которая используете.я по всему
приложению и не может быть сосредоточена в одном месте, поскольку это нарушило
бы принцип разделения обязанностей. Классическими примерами скво з ной обязан­
ности .явл.яютс.я регистрация в журнале, авторизация и кеширование. В настоящей
главе мы рассмотрим различные категории фильтров, поддерживаемые МVС, и пока­
жем, как создавать и применять фильтры, а также каким образом управлять их выпол­
нением. В табл. 19.1 приведена сводка, позволяющая поместить фильтры в контекст.

Таблица 19.1. Помещение фильтров в контекст

Вопрос Ответ

Что это такое? Фильтры используются для применения логики к методам


действий , не добавляя код в класс контроллера

Чем они полезны? Фильтры позволяют использовать код, который не является


частью определения действия в классическом паттерне MVC.
Результатом будут более простые классы контроллеров и мно ­
гократно используемая функциональность, которая может при­
меняться повсюду в приложении

Как они используются? Существуют разнообразные типы фильтров, которые использу­


ются инфраструктурой MVC различными способами . Наиболее
распространенный способ создания фильтра предусматривает
создание класса, который унаследован от атрибута, предостав ­
ляемого MVC для требуемого типа фильтра

Существуют ли какие-то Функциональность, предлагаемая разными типами фильтров,


скрытые ловушки или перекрывается, поэтому выяснение, какой тип необходим, мо­
ограничения? жет быть затруднено

Существуют ли Нет, фильтры являются основным средством MVC и использу­


альтернативы? ются для реализации обычно требующейся функциональности,
такой как авторизация

Изменились ли они по Фильтры ведут себя по большому счету так же, как в предшес­
сравнению с версией твующих версиях MVC. Существует несколько незначительных
мvс 5? изменений.
Функциональность , ранее предоставляемая фильтрами аутен­
тификации , была помещена в фильтры авторизации.
Атрибут Authorize больше не является фильтром (детали
применения этого атрибута приведены в главе 29) .
Все типы фильтров могут определяться с использованием от­
дельны х синхронных или асинхронных интерфейсов.
Глава 19. Фильтры 577
В табл. 19.2 приведена сводка для этой главы .

Таблица 19.2. Сводка по главе

Задача Решение Листинг

Внедрение дополнительной ло­ Примените фильтры к контроллерам 19.1-19.11


гики в обработ ку запросов либо их методам действий

Ограничение доступа к Используйте фильтры авторизации 19.12, 19.13


действиям

Внедрение универсальной логи­ Используйте фильтры действий 19.14-19.16


ки в процесс обработки запросов

Инспектирование либо измене­ Используйте фильтры результатов 19.17-19.21


ние результатов, выпускаемых

методами действий

Обработка ошибо к Используйте фильтры исключений 19.22, 19.23


Использование служб в фильтрах Объявите зависимости в конструкторе 19.24-19.28
фильтра, зарегистрируйте службу в
классе Startup и примените фильтр с
помощью атрибута TypeFil ter

Помещение фильтров под управ­ Используйте жизненные циклы внед­ 19.29-19.31


ление жи зненным циклом рения зависимостей для регистрации
фильтров в классе Startup и приме­
ните фильтры с использованием атри­
бута ServiceFil ter

Применение фильтров к каждому Используйте глобальный фильтр 19.32-19.34


методу де й ствия в приложении

Изменение порядка, в котором Используйте параметр Order 19.35-19.37


применяются фильтры

Подготовка проекта для примера


В этой главе при создании примера приложения будет применяться тот же самый
подход, что и в предшествующих главах. Создайте новый проект типа Empty (Пустой)
по имени Fil ters с использованием шаблона ASP.NET Core Web Application (.NET Core)
(Веб-приложение ASP. NEТ Core (.NЕТ Core)). Добавьте требуемые пакеты NuGet в раз­
дел dependencies файла proj ect . j son и настройте инструментарий Razor в разде ­
ле tools, как показано в листинге 19.1.

Листинг 19.1. Добавление пакетов в файле project. j son

"dependencies ": {
"Microsoft.NETCore.App" :
" version ": " 1 . 0.О ",
"type": "platforrn"
}'
578 Часть 11. Подробные сведения об инфрастру ктуре ASP.NET Core MVC

"Microsoft .AspNetCore . Diagnost i cs ": "1 . О . О ",


"Microsoft . AspNetCore . Server . IISintegration ": " 1 . 0 . 0 ",
"Microsoft . AspNetCore . Server . Kestrel ": " 1 . 0 . 0 ",
"Microsoft . Extensions . Logging . Console ": " 1 . 0 . О ",
"Microsoft . AspNetCore . Mvc" : "1. О. О" ,
"Microsoft . AspNetCore . StaticFiles": "1.0.0",
"Microsoft.AspNetCore.Razor.Tools": {
"version": "1.0 . 0-preview2-final",
"type": "Ьuild"

) '
" tools ":
"Microsoft . AspNetCore.Razor.Tools": "1.0.0-preview2-final",
"Microsoft.AspNetCore . Server .I ISintegration.Tools ":
" 1 . 0 . 0-preview2 - final "
}'
" frameworks 11
: {

netcoreappl . O":
11

" imports " : [ "dotnet5. 6 11


,
11
portaЬle - net 4 5+win8 11
]

) 1

"buildOptions ":
emitEntryPoint
11 11
: true , "preserveCompilationContext 11
: true
) '
" runtimeOptions" : {
configProperties ": {
11 11
System . GC . Server ": true)

В листинге 19.2 приведен кл асс Startup, который конфигурирует средства , пре­


доставляем ы е пак етами NuGet.

Листинг 19.2. Содержимое файла Startup. cs


using Microsoft . AspNetCore . Builder ;
using Microsoft . Extensions .Dependencyinjection ;
namespace Fil ters {
puЫic class Startup
puЫic void ConfigureServices(IServiceCollect ion services) {
services.AddМvc();

puЫic void Configure(IApplicationBuilder арр) {


app.UseStatusCodePages();
app.UseDeveloperExceptionPage();
app.UseStaticFiles();
app.UseMvcWithDefaultRoute();
Глава 19. Фильтры 579

Включение SSL
Для ряда примеров в настоящей главе требуется применение протокола SSL, ко­
торый по умолчанию отключен. Чтобы включить SSL, выберите пункт Filter Properties
(Свойства фильтров) в меню Project (Проект) среды Visual Studio и отметьте флажок
ЕnаЫе SSL (Включить SSL) на вкладке Debug (Отладка), как показано на рис. 19. l.
Примите к сведению назначенный номер порта, который в каждом проекте будет
отличаться.

Арр URL: '1ttp:/Лocгlhost51000/


-- '"]
~ Enable SSL https:!Лocalhost44З18f

~ ЕnаЫе Anonymous Authentication

О Enabk \л/indows Authenьcation

Рис . 19. 1. Включение SSL

Создание контроллера и представления


Контроллеры в этой главе будут простыми, т.к. внимание сосредоточено на по­
мещении логики в другое место внутри приложения. Создайте папку Controllers,
добавьте в нее файл класса по имени HomeController. cs и определите в нем конт­
роллер, как продемонстрировано в листинге 19.3.

Листинг 19.З. Содержимое файла HomeController.cs из папки Controllers

using Microsoft . AspNetCore . Mvc;


namespace Filters . Controllers {
puЫic c lass HomeController : Controller
puЫic ViewResul t Index () => View ( "Mess age ",
"This is the Index action on the Home controller");

Метод действия Index () визуализирует представление по имени Message, пере­


давая ему в качестве данных представления строку. Создайте папку Views/Shared
и поместите в нее файл представления Razor по имени Message . cshtml с разметкой
из листинга 19.4.

Листинг 19.4. Содержимое файла Message. cshtml из папки Views/Shared

@{ Layout = null;
< ! DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device - width" />
<tit l e>Filters</title>
<link asp-href-include="liЬ/bootstrap/dist/css/* .min.css" rel="stylesheet" />
</head>
580 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC

<body class= "p anel - body " >


@if (Model is string) {
@Model
else if (Model is IDictionary<string, string>) {
var dict = Model as IDicti onary<string , string> ;
<tаЫе c l ass= " ta Ы e taЫe - condensed taЫe - striped taЫe-bordered " >
<thead><tr><th>Name</th><th>Value</th ></tr></thead>
<tbody>
@foreach (var kvp in dict) {
<tr><td>@kvp.Key</td><td>@kvp.Value</td></tr>

</tbody>
</tаЫе>

</body>
</html>

Представление является слабо типизированным и отображает либо значение string,


либо объект Dict i onary<st rin g , string> ; во втором случае выводится таблица.
При стилизации НТМL-элементов представление полагается на пакет CSS из
Bootstrap. Для добавления Bootstrap в проект создайте файл bower . j son с использо­
ванием шаблона элемента Bower Configuration File (Ф айл конфигурации Bower) и до­
бавьте пакет Bootstrap в раздел dependenc i es (листинг 19.5).
Листинг 19.5. Добавление пакета Bootstrap в файле bower. j son

" name ": " asp . net ",


" private" : true,
" dependencies ": {
"bootstrap": 11 3.3.6 11

Последний подготовительный шаг предУсматривает создание в папке Views файла


_ Viewimports . cshtml, который устанавливает встроенные дескрипторные вспомо­
гательные классы для применения в пр едставлениях Razor (листинг 19.6).
Листинг 19.6. Содержимое файла _Viewimports. cshtml из папки Views
@a ddT ag Help er * , Mic r osoft.AspNetCore . Mvc . TagHelpers

Запустив приложение, вы увидите вывод, показанный на рис. 19.2.

This is the lndex action 011 the Ноте controller

-------------·-------------~--·-.J
Рис. 19.2. Запуск примера приложения
Глава 19. Фильтры 581

Использование фильтров
Фильтры позволяют изъять логику, которая иначе применялась бы в методе дейс­
твия, из контроллера и определить ее в многократно используемом классе. В качестве

примера предположим, что нужно обеспечить возможность доступа к методам дейс­


твий только по НТГРS, а не по обычному незашифрованному протоколу НТГР. Объект
контекста HttpRequest предоставляет информацию, необходимую для выяснения,
применяется ли НТГРS (листинг 19.7).

Листинг 19.7. Проверка на предмет использования НТТРS в файле HomeController. cs


using Microsoft . AspN etCore.Mvc;
using Microsoft.AspNetCore.Http;
namespac e Filters . Controllers {
puЫic class HomeController : Controller
puЬlic IActionResult Index() {
if ( !Request. IsHttps) {
return new StatusCodeResult(StatusCodes.Status403Forbidden);
} else {
return V:i.ew ( "Message",
"This is the Index action on the Ноте controller");

Именно так решалась бы задача с НТГРS без фильтров. После запуска приложения
браузер будет запрашивать стандартный URLдля проекта, отличный от НТГРS, который
метод действия Index () обработает путем возвращения объекта
StatusCodeResul t ,
отправляющего код состояния НТГР 403 17). Если за­
в ответе (как описано в главе
просить стандартный URL для НТГРS , например, https: //localhost : 44318 , тогда
метод действия I ndex () отреагирует визуализацией представления Message (пр ежде
чем браузер отобразит результат, может понадобиться подтверждение в окне предуп­
реждения безопасности) . Оба исхода иллюстрируются на рис. 19.3.

Совет. Очистите хронологию в браузере, если вы не получаете ожидаемые результаты при


выполнении примеров из данного раздела. Браузеры часто отклоняют отправку серверу
запросов, которые сгенерировали ошибки SSL, что является хорошей практикой защиты,
но может мешать во время разработки.

[j lcx:alhost:51000 Х

1 С [(D-locall~~-st-:5-100......:::0=
1
1
Status Cod~~-~3; - Fo1·Ьi~~:-1 ..: .. ____ ? г~~!!~~~t~44318 -·--~-=~
L _________ __j This is the lndex action on the Home controlfer
1
i
L--------·------
Рис. 19.З. Ограничение доступа только запросами HTTPS
582 Часть 11 . Подробные сведения об инфраструктуре ASP.NEТ Соге MVC

Код в листинге 19.7 работает. но в нем присутствуют проблемы . Первая пробле­


ма заилючается в том, что метод действия содержит код, который имеет отношение
больше к реализации политики безопасности, чем к обработ1<е запроса, обновлению
модели и выбору ответа. Более серьезная проблема связана с тем , что решение с ко­
дом обнаружения НТТР внутри метода действия плохо масштабируется и этот код
должен быть продублирован в каждом методе действия, определяемом в контролл е ре
(листинг 19.8).

Листинг 19.8. Добавление метода действия в файле HomeCon trol ler. cs

u s ing Mi c r osoft . AspNe t Core . Ht tp;


us i n g Mi c r osof t. AspNe t Core . Mvc ;
n amespac e Fi l ter s. Cont r o l ler s {
p uЫi c class HomeCon t ro l ler : Co n tro l ler
p uЫi c IActio nRe s u l t Index () {
if ( ! Request . Is Htt ps) {
r etur n new Stat us CodeResult( S ta t usCodes . Stat u s403Forbidden);
e l se {
r eturn View ( " Mes sage ",
"Thi s i s t h e I ndex ac t io n o n t he Ноте con t r oller " ) ;

puЫic IActionResult SecondAction() {


if ( !Request. IsHttps) {
return new StatusCodeResult(StatusCodes.Status403Forbidden);
else {
return View("Message",
"This is the SecondAction action on the Home controller");

Нужно помнить о реализации той же самой проверки внутри каждого метода дейс ­
твия в каждом контроллере, для которого планируется требование протокола HTTPS.
Код реализации политики безопасности занимает значимую часть довольно простого
контроллера и затрудняет его понимание. Кроме того , забыть добавить его в н овый
м етод действия и тем самым создать брешь в политике безопасности - дело тол ько
времени. Проблемы такого рода способны решать фильтры, как демонстрируется в
листинге 19.9.

Листинг 19.9. Применение фильтра в файле HomeController. cs

u si n g Mi c r osoft. AspNe t Core . Http ;


u sing Microsoft . As p Net Co r e . Mvc ;
namespace Filters . Contro l lers {
pu Ы i c c l ass HomeCo n tro lle r : Contr o ll er
[RequireHttps]
puЬlic ViewResul t Index () => View ( "Message" ,
"This is the Index action on the Home controller");
Глава 19. Фильтры 583
[Requireнttps]

puЫic ViewResul t SecondAction () => View ( "Message",


"This is the SecondAction action on the ноте controller");

Атрибут Re q uireHtt p s применяет один из встроенных фильтров J\ классу


HomeContro l l er. Он ограничивает доступ к метолам действий, так что поддержива­
ются только запр о сы НТТРS, и позволяет удалить из всех м етодов код, относящийся
к б езопасности, и сосредоточиться на обработке усп е шных запросов.

На заметку! Фильтр Requi re Https работает не совсем так, как специальный код из листин­
га 19.7. Для запросов GET атрибут Requi r eHttp s обеспечивает перенаправление кли­
ента на первоначально запрошенный URL, но делает это с использованием схемы
ht t ps ,
в результате чего запрос
h t tp : / / l ocalho st/H ome/ Index будет перенаправлен на
https: / / localhost/Home/ Inde x . Такой подход имеет смысл в большинстве развер­
нутых приложений, но не на стадии разработки, поскольку протоколы НТТР и НТТРS обслу­
живаются на разных локальных портах. В классе Requ i r eHttp sAt t r i bute определен
защищенный метод по имени Handle NonH ttps Re q ue s t (), который можно переопре­
делить для изменения поведения. В качестве альтернативы в разделе "Использование
фильтров авторизации" исходная функциональность воссоздается с нуля.

Разумеется, по-прежнему необходимо помнить о применении атрибута


RequireH t tps к каждому методу действия, о чем впол не можно забыть. Но филь­
тры поддерживают полезный трюк: применение атрибута к классу контролл е ра дает
такой же эффект, как и его применение ко всем индивидуальным методам действий
(листинг 19. 10).

Листинг 19.10. Применение фильтра ко всем методам действий


в файле HomeController. cs

using Microsoft . AspNet Core .H ttp ;


using Mi crosoft . AspNetCore . Mvc;
namespace Filters . Cont r olle rs {
[RequireHt tps]
puЫic c l ass HomeContro l ler : Contro l ler
puЫic ViewResult I ndex () => Vi e w ( "Me ssage ",
" This i s the Index action on the Home cont r oller " ) ;
p u Ыic Vi ewRe su l t Sec o ndAct i o n () => View ( "Me s s age ",
" Th i s is the SecondAction act i on оп t he Home contro l ler " ) ;

Фильтры могут применяться с разными уровнями детализации. Если вы хотите


ограничить доступ к одним действиям, но не ограничивать его к другим, тогда приме­
няйте атрибут RequireHttps только к нужным методам. Чтобы защитить все методы
действий, в том числе любые методы, которые будут добавл ены в будущем, приме­
няйте атрибут RequireHt t p s к классу . Если же вы желаете применить какой-нибудь
фильтр к любому действию в приложении, то может е использовать глобалы-tые фш~ь­
тры. которые будут описаны далее в глав е.
584 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC

П онятие фильтров
Теперь, когда вы видели, как используются фильтры , на ступило время объяснить .
что происходит " за кулисами". Фильтры реализуют интерфейс IFi l terMetadata , ко­
торый находится в пространстве име н Mi c r o s oft . As pNetCore . Mvc . Fi l ters . Вот
его опред ел е ние :

names pace Microsoft . AspNetCore . Mvc . Fi lters


pu Ыi c interface IFilterMetadata { }

Интерф ейс пуст и не требу ет от класса фильтра реализации какого-либо специфи ­


ческого поведения. Причина в том, что существует несколько отдельных типов филь­
тров, каждый из которых работает по-разному и служит для своих целей.
В табл. 19.3 описаны типы фильтров , определяющие их инте рфейсы и то, что
они делают. (Инфраструктура MVC поддерживает и другие типы фильтров. но они н е
используются напрямую . Взамен они интегрированы в ср едства, рассматривае мы е
в других главах, и применяются ч е рез специфические атрибуты, включая атрибуты
Produce s и Cons umes , 1юторы е обсуждаются в главе 20.)

Таблица 19.З. Различные типы фильтров

Разновидность Интерфейсы Описание

Фильтры IAuthorizationFilter Этот тип фильтров используется для


авторизации I AsyncAuthori zationFilter применения политики безопасности
прило жения , включая авторизацию

пользователей

Фильтры IActionFilter Этот тип фильтров используется для


действий IAsyncActionFilter выполнения работы немедленно перед
или после выполнения метода действия

Фильтры I Res ult Fi lter Этот тип фильтров используется для


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

Фильтры IExcept i onFi l ter Этот тип фильтров используется для


исключений I AsyncExcept ionFi l t er обработки исключений

В т а бл. 19.3 приведены неч еткие описания, потому что фильтры можно исполь­
зовать для широкого спектра задач. ограниченного только вашим воображением и
проблемами, которые необходимо решить. По мере погружения в детали их работы
все станет яснее , а пока следует уяснить два важных момента .

Во-п е рвых, для каждого типа фильтра в табл. 19.3 пр едусмотр ены два разных ин­
терфейса . Фильтры могут выполнять свою работу синхронно или асинхронно, так что
синхронный фильтр результатов, например, реализует инт е рф е йс IResu l tFilter,
тогда как ас инхронный - инте рфейс IAsync Re s u ltFil ter.
Во-вторых, фильтры прим е няются в особом порядке. Фильтры авторизации вы­
полняются первыми, з а ними идут фил ьтры действий, а з атем фильтры результатов.
Фильтры исключений выполняются только в случае генер ации какого-либо исключе­
ния , которо е наруша ет нормальную последовательность .
Глава 19. Фильтры 585
Получение данных контекста
Фильтры обеспечиваются данными контекста в форме объекта Fil terContext.
Класс Fil terContext является производным от
ActionContext, который также
представляет собой базовый класс для юшсса ControllerContext, рассмотренного
в главе 17. Из соображений удобства в табл. 19.4 перечислены свойства, унаследо­
ванные от класса ActionContext , наряду с дополнительными свойствами, которые
определены в FilterContext.

Таблица 19.4. Свойства Fil terContext


Имя Описание

ActionDescriptor Это свойство возвращает объект ActionDescriptor, который


описывает метод действия

HttpContext Это свойство возвращает объект HttpContext, который предо­


ставляет детали НТТР-запроса и отправляемого взамен НТТР-ответа

ModelState Это свойство возвращает объект ModelStateDictionary, кото­


рый используется для проверки достоверности данных, отправлен­
ных клиентом (глава 27)
RouteData Это свойство возвращает объект RouteData, который описывает
способ обработки запроса системой маршрутизации (глава 15)

Filters Это свойство возвращает список фильтров, которые


были применены к методу действия, выраженный как
IList<IFilterMetadata>

Использование фильтров авторизации


Фильтры авт оризации применяются для реализации политики безопаснос­
ти приложения. Фильтры авторизации выполняются перед фильтрами других ти­
пов и перед запуском метода действия. Вот как выглядит определение интерфейса
IAuthorizationFilter :
namespace Microsoft . AspNetCore . Mvc.Filters {
puЫic interface IAuthorizationFilter : IFilterMetadata

void OnAuthorizat ion(Authori zat ionFilterContext con text ) ;

Метод OnAuthorization () вызывается для предоставления фильтру возможнос­


ти авторизовать запрос. Вот определение интерфейса IAsyncAuthorizationFilter,
используемого асинхронными фильтрами авторизации:

using System.Threading.Tasks;
namespace Microsoft.AspNetCore . Mvc.Filt ers
puЫic interface IAsyncAuthorizationFi l ter : IFilterMetadata {
Task OnAuthorizati onAsync(AuthorizationFilterContext context);
586 Часть 11 . Подробные сведения об инфрастру ктуре ASP.NET Core MVC

Метод OnAuthorizationAsync () вызыв ается, чтобы фильтр мог авторизов ать за­
прос. КШ{ОЙ бы интерф е йс ни пр име нялся, фильтр получает данны е конте кста , опи­
сывающие з апрос , чер ез экз е м пляр класса Aut horization FilterContext , которы й
является производны м от класса Fil te r Context и добавляет одно в аж ное свойство,
приведенное в табл . 19.5.

Таблица 19.5. Свойство AuthorizationFilterContext


Имя Описание

Result Это свойство I ActionResul t устанавливается фильтрами авторизации,


когда запрос не удовлетворяет политике авторизации прило жения . Если оно
установлено, тогда вместо вызова метода действия инфраструктура MVC ви­
зуализирует объект реализации IAct i onResul t

С оздание фильтра а вторизации


Чтобы посмотр еть , как работают фил ьтры авторизации , со здайт е в проекте папку
Infrastruct u re , поместите в нее файл класса по имени HttpsOnlyAttribute . cs
и определит е в нем фильт р, как пок азано в листинге 19. l l.

Листинг 19.11. Содержимое файла HttpsOnlyAttribute. cs


и з папк и Infras tructure

using System ;
us i ng Microsoft . AspNetCo r e . Http ;
using Micro s of t .AspNetCo r e. Mvc ;
using Microsoft . AspNetCore . Mvc . Filters ;
namespace Filters . Infrastructure {
puЬl i c class HttpsOnl yAtt r ibute : Attribute , IAuthorizationFilter

puЫic void OnAut horization(Au th oriza t io nFi lterContex t context)


i f ( !context.HttpContext . Request . IsHttp s ) {
contex t. Result =
new StatusCodeResult(StatusCodes . Status403Forbidden);

Если запрос удовлетворяет политике авторизации , то фил ьтр автор изации нич ег о
не делает; такая пассивность позволяет инфраструктур е MVC перейти к следующему
фильтру и в конечном итоге выполнить метод дей ствия.

На заметку! Атрибут Authorize , который может использоваться для ограничения доступа


специфическими пользователями и группами, был реализован в виде фильтра , но в ASP.NET
Core MVC это больше не так. Атрибут Author i ze по-прежнему применяется, но работа­
ет по-другому. Внутренне имеется глобальный фильтр (глобальные фильтры обсуждаются
позже в главе) , который обнаруж ивает атрибут Au t horize и обеспечивает соблюдение
политик, определенны х системой ASP.NET Authorize не яв­
Саге ldeпtity, однако атрибут
ляется фильтром и не реализует интерфейс IAuthorizationFil ter . Использование
системы ASP.NET Core ldeпtity и атрибута Au thor i ze рассматривается в главе 29.
Глава 19. Фильтры 587
При н а л и чи и пробл ем ы фильтр устанавливает свойство Resu l t объекта
AuthorizationFi l terContext, который передается методу OnAuthor i zat i on () .
Это препятствует дальне й ш ему выполнению и предоставля ет инфраструктур е MVC
р езульт ат для возвращения кли е нту. В листинге 19.11 классHttpsOn l yAt t rib u te
инспектирует свойство I sHttps объекта контекста Htt p Request и устанавливает
свойство Resu l t, чтобы пр е рвать выполнение, если запрос был сделан без НТГРS .
В листинг е 19. 12 новый фильтр приме ня ется к контроллеру Home .

Листинг 19.12. Применение специального фильтра в файле HomeCon troller. cs

using Microsoft .As p NetCore .Mvc ;


using Filters.Infrastructure;
namespace Fil ters . Co n t r o l ler s {
[HttpsOnly ]
puЫic class HomeControll er : Control l er
puЫic ViewRes ul t Index () => Vi ew ( " Message ",
" This is the Index action on the Home controller " ) ;
puЫic ViewResult SecondAc tion () => Vi ew ( " Message ",
" This i s the SecondAction ac ti on on the Home con t roller " ) ;

Э т от фильтр воссоздает функциональность , которая был а вкл юч е на в м етоды


действий из листинга 19.8. Такой прием м енее полезен в реальных проектах, чем вы ­
пол н ени е перен аправления подобно встроенному фильтру Requ ireHttp s, поскольку
пол ьзователи могут не понять смысл кода состояния 403, но демонстрирует хороший
прим е р того . как работают фильтры авторизации.

Модульное тестирование фильтров

Большая часть работы при модульном тестировании фильтра связана с настройкой объекта
к онтекста, который передается методам фильтра. Объем требующейся имитации зависит
от информации контекста, используемой фильтром. В качестве примера ниже приведен мо­
дульный тест для фильтра HttpsOnl y из листинга 19.11 .
using System . Linq ;
using Filters .I n fr astruct u re ;
using Microsoft . As pNetCore . Http;
using Microsoft . AspNetCo r e . Mvc ;
using Microsoft . AspNetCore . Mvc . Abst ra ction s ;
using Microsoft . Asp NetCo r e .Mvc . Filte r s ;
u s ing Moq ;
using Xunit ;
namespace Tests
puЬlic class FilterTests
[ Fact ]
puЫ i c void TestHttpsFi lt er()
11 Организация
var httpRequest = new Mock<HttpRequest>() ;
httpRequest . SetupSequence (m => m. Is Ht tps ) .Returns(true )
. Returns( f al s e) ;
588 Часть 11. Подробные сведения об инфраструктуре ASP.NET Соге MVC

var httpContext = new Mock<HttpContext>() ;


httpContext.SetupGet(m => m.Request) .Returns(httpRequest.Object);
var actionContext = new ActionContext(httpContext.Object,
new Microsoft . AspNetCore . Routing . RouteData() ,
new ActionDescriptor()) ;
var authContext = new AuthorizationFilterContext(actionContext ,
EnumeraЫe.Empty<IFilterMetadata>() .ToList()) ;
HttpsOnlyAttribute filter = new HttpsOnlyAttribute();
11 Действие и утверждение
filter.OnAuthorization(authContext);
Assert . Null(authContext . Result) ;
filter . OnAuthorization(authContext) ;
Assert . IsType(typeof(StatusCodeResult) , authContext . Result) ;
Assert . Equal(StatusCodes.Status403Forbidden ,
(authContext.Result as StatusCodeResult) .StatusCode) ;

Тест начинается с создания имитированных объектов контекста Н t tpReque s t и


HttpContext , которые позволяют представить запрос с или без НТТРS . Необходимо про­
тестировать оба условия, что делается следующим образом:

httpRequest .SetupSequence (m => m. IsHttps) .Returns (true) . Returns (false) ;

Этот оператор настраивает свойство HttpRequest. IsHttps таким образом, что оно
возвращает последовательность значений: свойство возвращает true, когда читает­
ся в первый раз, и false - во второй раз. Имеющийся объект HttpContext можно
применить для создания объекта ActionContext, который позволит создать объект
AuthorizationContext , необходимый модульным тестам. За счет инспектирования
свойства Resul t объекта Au thor i za tionFil terCon text выполняется проверка, как
фильтр реагирует на запросы, отличные от HTTPS, и затем проверка того , что происходит
с запросами НТТР. Для настройки объекта AuthorizationFilterContext требуется
много типов, которые полагаются на многочисленные пространства имен ASP.NET Соге и
MVC, но после получения объекта контекста написание оставшейся части теста будет от­
носительно простым.

Использование фильтров действий


Понять фильтры действий проще всего, взглянув на интерфейс, который их опре­
деляет. Вот юш выглядит интерфейс IActionFil ter:
namespace Microsoft.AspNetCore.Mvc . Filters
puЬlic int erface IActi on Filter : IFilterMetadata
void OnActionExecuting(ActionExecutingContext context) ;
void OnActionExecuted(ActionExecutedContext context) ;
Глава 19. Фильтры 589
Когда к методу действия применен фильтр действий, метод OnActionExecut ing ()
вызывается прямо перед вызовом метода действия, а метод OnActionExecuted () -
сразу после вызова метода действия. Фильтры действий снабжаются данны ­
ми 1<онтекста чер ез два класса контекста: ActionExecutingContext для метода
OnAc ti onExecuting () и
ActionExecutedContext для метода OnActionExecuted ( ) .
Оба класса контекста расширяют класс Fil terContext, свойства которого бьmи пе­
речислены в табл. 19.4.
В классе ActionExecutingContext, используемом для описания действия, кото­
рое планируется вызвать, определены дополнительные свойства, представленные в
табл . 19.6.

Таблица 19.6. Свойства ActionExecutingContext

Имя Описание

Controller Это свойство возвращает объект контроллера, методы действий


которого дол ж ны быть вызваны. (Детали метода действия до­
ступны через свойство ActionDescriptor, унаследованное от
базовых классов.)

ActionArguments Это свойство возвращает индексированный по имени словарь


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

Result Если фильтр присваивает этому свойству объект реализации


IAct i onResul t , тогда произойдет обход процесса обработки
запросов, и результат действия будет применяться для генера­
ции ответа клиенту без обращения к методу действия

Класс ActionExecutedContext используется для представления действия, кото­


рое было выполнено, и определяет свойства, описанные в табл. 19.7.

Таблица 19. 7. Свойства ActionExecutedContext

Имя Описание

Controller Это свойство возвращает объект Controller, чей метод


действия будет вызван

Cance led Это свойство типа bool устанавливается в true, если дру­
гой фильтр действий предпринял обход процесса обработки
запросов за счет присваивания результата действия свойству
Result объекта ActionExecutingContext
Exception Это свойство содержит любой объект Exception, который
был сгенерирован методом действия

ExceptionDispatchinfo Это свойство возвращает объект ExceptionDispatchinfo,


который содержит информацию трассировки стека для любо­
го исключения, сгенерированного методом действия

ExceptionHandled Установка этого свойства в true указывает, что фильтр обрабо­


тал исключение, которое дальше распространяться не будет
Result Это свойство возвращает объект реализации IActionResul t ,
возвращенный методом действия. При необходимости фильтр
может изменить или полностью заменить результат действия
590 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

Создание фильтра действий


Фильтры действий являются универсальным инструментом и могут применяться
для реализации любой сквозной обязанности в приложении. Фильтры действий можно
использовать для прерывания обработки запроса перед вызовом действия и для изме­
нения результата после выполнения действия. Простейший способ создания фильтра
действий предусматривает наследование класса от класса ActionFil terAttribute,
который реализует интерфейс IActionFilter. Добавьте в папку Infrastructure
файл класса по имени ProfileAttribute. cs и определите в нем фильтр, как проде­
монстрировано в листинге 19.13.

Листинг 19.1 З. Содержимое файла ProfileAttribute. cs из папки Infrastructure

using System . Diagnostics;


using System.Text;
using Microsoft.AspNetCore.Mvc.Filters;
namespace Filters.Infrastructure
puЫic class ProfileAttribute : ActionFilterAttribute
private Stopwatch timer;
puЫic override void OnAct ionExe cuting(ActionExe cutingContext context) {
timer = Stopwatch.StartNew();

puЫic override void OnActionExecuted(ActionExecutedContext context) {


timer .Stop () ;
string result = " <div>Elapsed time: "
+ $"{timer.Elapsed . Tota1Milliseconds} ms </div>";
byte[J byte s = Encoding.ASCII.GetBytes(result);
context.HttpContext . Response . Body . Write(bytes , О , bytes . Length) ;

В листинге 19.13 объект Stopwa tch применяется для измерения количества


миллисекунд. в течение которых выполнялся метод действия, запуская таймер в
методе OnActionExecuting () и останавливая его в методе OnActionExecuted ().
Чтобы пометить результат, с использованием объекта контекста получается ответ
HttpResponse, в который включается простой фрагмент НТМL-р азметки .
В листинге 19.14 иллюстрируется применение атрибута Profile к контроллеру
Н оте . (Кроме того, предыдущий фильтр удален, так что будут приниматься запросы
по стандартному протоколу НТТР.)

Совет. Как ни странно, контроллеры являются также и фильтрами действий. Базовый класс
Controller реализует интерфейсы IActionFil ter и IAsyncActionFil ter , а это
значит, что можно переопределить методы, определяемые упомянутыми интерфейсами,
и создать функциональность фильтра действий. Для контроллеров РОСО инфраструктура
MVC инспектирует классы и проверяет, реализуют ли они любой из интерфейсов фильтра
действий, и если это так, то автоматически использует их в качестве фильтров действий .
Глава 19. Фильтры 591
Листинг 19. 14. Применение фильтра в файле HomeCon troller . cs
using Microsoft . AspNetCore . Mvc;
using Filters.Infrastructure;
namespace Filters.Controllers {
[Profile]
puЫic class HomeController : Controller
puЬlic ViewResul t Index () => View ( "Message",
"This is the Index action on the Home controller");
puЫic ViewResul t SecondAction () => View ( " Message",
" This is the SecondAction action on the Home controller " ) ;

Запустив приложение, вы увидите сообщение, подобное показанному на рис. 19.4.


Количество миллисекунд будет варьироваться в зависимости от скорости машины
разработки.

На заметку! При записи фрагментов НТМL-разметки прямо в ответ производится расчет на то­
лерантность браузера к неправильно оформленным НТМL-документам: элемент di v, гене­
рируемый в фильтре , находится в начале тела ответа перед элементами DOCTYPE и html,
которые указывают на начало НТМL-документа , выпускаемого представлением Razor. Такой
прием работает и может быть удобным для отображения диагностической информации, но
на него не следует полагаться при реализации производственных функций .

1 Elapsed tiшe: 0.3354 шs

Рис. 19.4. Использование фильтра действий

Создание асинхронного фильтра действий


Интерфейс IAsyncActionFilter применяется для определения фильтров
действий, которые оперируют асинхронно. Ниже показано определение этого
интерфейса:

using System . Threading . Tasks;


namespace Microsoft.AspNetCore . Mvc . Filters
puЫic interface IAsyncActionFilter : IFilterMetadata
Task OnActionExecutionAsync(ActionExecutingContext context ,
ActionExecutionDelegate next);
592 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

В интерфейсе имеется единственный метод, который посредством продолжения


задачи позволяет фильтру запускаться до и после выполнения метода действия. В лис­
тинге 19.15 используется метод OnActionExecutionAsync () из фильтра Profile.

Листинг 19.15. Создание асинхронного фильтра действий в файле ProfileAttriЬute. cs

using System . Diagnostics ;


usin g System.Text;
using System.Threading .T asks ;
usin g Microsoft .AspNetCore .Mvc . Filters;
namesp ace Filters.Infrastructure
puЬlic class ProfileAttribute : ActionFilterAttribute
puЫic override async Task OnActionExecutionAsync(
ActionExecutingContext context,
ActionExecutionDelegate next) {
Stopwatch timer = Stopwatch.StartNew();
awai t next () ;
timer.Stop();
string result = "<div>Elapsed time: "
+ $ 11 {timer.Elapsed.TotalMilliseconds} ms</div>";
Ьуtе[] Ьytes = Encoding.ASCII.GetBytes(result);
await context.HttpContext .Response.Body.WriteAsync(bytes,
О, bytes. Length);

Объект ActionExecutingContext снабжает фильтр данными контекста, а объект


ActionExectionDelegate представляет метод действия (или следующий фильтр),
подлежащий выполнению. Фильтр выполняет свою подготовительную работу перед
вызовом делегата и затем завершает работу, когда делегат заканчивает функциони­
рование. Делегат возвращает объект Task, поэтому в листинге применяется ключевое
слово awai t.

Использование фильтров результатов


Фильтры результатов применяются до и после того, как инфраструктура MVC об­
работала результат, возвращаемый методом действия. Фильтры результатов способ­
ны изменять или заменять результат действия либо полностью аннулировать запрос
(даже если метод действия уже был вызван). Вот интерфейс IResultFilter, который
определяет фильтр результатов:

namespace Microsoft.AspNetCore.Mvc.Filters
puЬlic interface IResultFilter : IFilterMetadata
void OnResultExecuting(ResultExecutingContext context);
void OnResultExecuted(ResultExecutedContext context);
Глава 19. Фильтры 593
Фильтры ре зультатов следуют то му же самому шаблону , что и фильтры действий .
Метод OnResultExecut ing () вызывается до того, кан результат де йствия, выпущен­
ный м етодом действия, будет обработан, и снабжается информацией контекста через
объект Resul tExecutingContext. Класс Res ul t ExecutingCon text является про­
изводным от класс а FilterContext и определяет дополнительные свойства , описан­
ные в табл . 19.8.

Таблица 19.8. Свойства Resul tExecutingContext


Имя Описание

Controller Это свойство возвращает объект контроллера, чей метод


действия был выполнен

Cance l Установка этого свойства типа bool в true остановит обра­


ботку результата действия для генерации ответа

Result Это свойство возвращает объект реализации I ActionResu l t,


возвращаемый методом действия

Метод OnResu l tExecuted () вызывается после обработки инфраструктурой


MVC результ ата действия и снабжается данными контекста через экземпляр класса
Resul tExecutedContext, который в дополнение к свойствам, унаследо в анным от
Fi lterContext, определяет свойства, перечисленные в табл . 19.9.

Таблица 19.9. Свойства Resul tExecutedContext

Имя Описание

Controller Это свойство возвращает объект контроллера, чей метод дейс­


твия был выполнен

Canceled Это свойство типа boo l указывает, был ли аннулирован запрос

Except i on Это свойство содержит любой объект Except i on, который был
сгенерирован методом действия

Except ionDispatchi nfo Это свойство возвращает объект ExceptionDispatchinfo,


который содержит информацию трассировки стека для любого
исключения, сгенерированного методом действия

ExceptionHandled Установка этого свойства в true указывает, что фильтр обработал


исключение, которое дальше распространяться не будет

Result Это свойство возвращает объект реализации IActionResul t,


который использовался для генерации ответа клиенту

С оздание фильтра результатов


Класс Re s и 1 t Fi1 t е r А t t r ibu t е р е ализует интерфейсы фильтров результатов и
предлагает самый л егкий способ создания фильтра результатов , который может при­
меняться как атрибут. Чтобы посмотреть на фильтр результатов в работе , добавьте в
папку Infrastructure файл класса по имени ViewRes ul t Deta il sAttribu te . c s с
определени е м фильтра и з листинга 19.16.
594 Часть 11. Подробные сведения об инфраструктуре ASP.NET Соге MVC

Листинг 19. 16. Содержимое файла ViewResul tDetailsAttribute. cs


из папки Infrastructure

us i ng System . Collections . Ge ne r ic ;
u si ng Microsoft . AspNetCore . Mvc ;
u s ing Microsoft . AspNetCore . Mvc . Filter s;
usi n g Microsoft . AspNetCore . Mvc.ModelBinding ;
u si n g Mic r o s oft . Asp Ne tCore . Mv c . View Featu r es ;
namespace Filters . Infrastructur e {
p u Ьl ic class Vi ewRe s u ltDeta i lsAttribute : ResultFilterAttribute {
p uЫic override void OnRe s ultExec u ting(Resu l t ExecutingContext context)
Dict i o n ary<string , st rin g> dic t = n ew Dict ionar y <string , s tr i ng> {
[ " Resu l t Туре " ] = context . Re s ult . GetType() . Name
};
ViewResult vr ;
if ( (vr = co n text . Resul t a s Vi e wResu lt ) ! = n u ll ) {
dict [ " View Name " ] = v r. ViewName ;
dict[ " Mo d el Ту р е " ] vr . Vi ew Data . Mode l . Get Type ( ) . Name ;
dict [" Model Data " ] = vr . ViewData . Model . ToSt r ing ( ) ;

contex t. Result = new Vi e wRes ul t {


ViewName = " Message ",
Vi ewData = new View Da t aDictiona r y(
new EmptyMo d e l Metad ataProvi d e r() ,
new ModelStateDic tio nary () ) { Mode l d i ct }
};

Класс Vi e wRes ul tDetai ls At tri bu t e переопределяет единств е нный м етод


OnResultExecuting () и исполь зует объект контекста для и з мен е ния результа­
та действия, применяемого при генерации ответа 1шиенту. Фильтр создает объект
Vie wResul t, который визуализирует представление Me s sage , используя словарь с
простой диагностической информацией в качестве модели представления.
Метод OnResul tExecuting () вызывается после того, как метод действия вы­
пустил результат действия , но перед е го обработкой с целью генерации результата.
Изменение значения свойства Re s ul t объекта контекста позволяет предо ставлять
другой тип резул ьтата из метода действия, к которому фильтр был прим енен . В лис ­
тинге 19. 17 фильтр результатов применяется к контроллеру Home.

Листинг 19.17. Применение фильтра результатов в файле HomeController.cs


us i ng Microsoft . AspNe tCore . Mvc ;
us i ng Filt er s . Infras t ructu r e ;
namespace Fil t ers . Controlle rs {
[ViewResultDetails]
puЫi c class HomeContro l ler : Controller
p u Ыic Vi ewResu l t I n dex() = > View( " Me s sa ge",
" Thi s i s the Index ac tio n on the Home c on tro l l er" ) ;
puЬlic ViewResul t SecondAct i on () = > View ( " Message ",
" Th i s is the SecondAct i on act i on on the Home con t roller " ) ;
Глава 19. Фильтры 595
Запустив приложение, вы увидите э ффект от фильтра результатов (рис. 19.5).

Name Value

Result Туре ViewResult

View Name Messa.ge

Model Туре String

L M~-d._e__' _o_a_ta- -This is the lnd-~ action-on th~:.ome -~ontroll:____J

Рис. 19.5. Эффект от фильтра результатов

Создание асинхронного фильтра результатов


Интерф ей с IAsyncResul tFil te r используется для создания асинхронных филь­
тров резул ьтатов. Далее приведено определение этого интерфейса:

using Systern . Threading . Tasks ;


narnespace Microsoft . AspNetCo r e . Mvc .Filters
puЫic interface IAsyncResultFilter : IFilterMetadata

Task OnResultExecutionAsync(Resu ltExecut i ngContext co ntext,


ResultExecutionDelegate next);

Интерфейс I AsyncResul tFi 1 ter похож на интерфейс , предназначенный для асин ­


хронных фильтров действий. В листинге 19.18 класс ViewResul tDetailsAttribute
переписан с целью реализации интерфейса IAsyncResul tFil ter.

Листинг 19.18. Создание асинхронного фильтра результатов


в файле ViewResul tDetailsAttribu te. cs
using Systern . Co l lections.Generic;
using Systern . Threading . Tasks ;
using Microsoft.AspNetCore.Mvc ;
using Microsoft . AspNetCore.Mvc.Fil ters ;
using Mi crosoft.AspNetCore . Mvc .ModelBinding ;
using Microsoft . AspNetCore . Mvc . Vi ewFeatures ;
namespace Filters . Infrastructure {
puЫic class ViewResu lt Det ailsAttribute : ResultFilterAttribute
puЫic override async Task OnResultExecutionAsync(
ResultExecutingContext context,
Resu1tExecutionDe1egate next) {
596 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

Di ctionary< s tring , str ing> dict = new Di ct i onary<str in g , string> {


[" Result Туре " ] = context . Re s ult . GetType() .Name ,
};

Vi ewRe s ult vr;


if ( (vr = context . Resul t as Vi ewRe sul t) ! = nu l l) {
d i ct[ "View Name "] = vr . ViewName ;
d i ct[ " Мodel Тур е" ] vr . ViewData . Model . GetType() . Narne ;
dict [" Model Data " ] = vr . Vi ewData . Model . ToString() ;

context . Resul t = new ViewResu lt {


ViewNarne = "Message ",
Vie wData = new ViewDataD i cti onary (
new ErnptyModelMetadataProvide r () ,
new ModelStateDictionary()) {
Model = di ct

};

await next ();

Обратите внимание, что вы несете ответственность за вызов делегат а , получен­


ного в виде аргумента методом OnRe su l tExecutionA s ync () . Если не вызвать деле ­
гат, тогда конве йер обработки запросов не будет з авершен и р езульт ат действия н е
ви зуализируется .

Создание гибридного фильтра действий/результато в


Проводить различи е между стадиями действия и р езул ьтата процесса обр аботки
запросов не всегда удобно. Так может случиться из - за того , что вы х отите трактовать
обе стадии как один шаг, либо потому, что ваш фильтр реагирует на способ выпол­
нения действия , н о дел ает это, вм ешиваясь в р е зультат. Поле зн о распол агать во з ­
можностью создания фильтра , который является фильтром одновреме нно действий и
фильтром результатов и способен выполнять работу на каждой стадии.
Требование настолько распространенное, что класс ActionFi l t e rAttribute ре ­
ализует интерф е йсы для обеих разновидностей фильтров, т. е . в единственном атрибу­
те можно смешивать и сочетать типы фильтров. Для демонстрации в листинге 19.19
приведен пер е смотренны й класс Pr of il eAt tr ibute , в кот ором фил ьтр дей ствий
объединяется с фильтром результатов.

Листинг 19.19. Создание гибридного фильтра в файле ProfileAttribute. cs


usi ng Systern . Di agnostics ;
usi ng Systern . Text ;
using System . Threading . Tasks ;
us i ng Microso f t . AspNetCore . Mvc . Filte rs ;
narnespace Filters .I nfrastr uct ur e
puЫ i c c la ss Pr ofi l eAttr ib ut e Acti on Fi l terAt t ribute
private Stopwatch timer;
private douЫe actionTime;
Глава 19. Фильтры 597
puЫic override async Task OnActionExecutionAsync(
ActionExecutingContext context ,
ActionExecutionDelegate next) {
timer = Stopwatch.StartNew();
await next () ;
actionTime = timer.Elapsed.TotalMilliseconds;

puЫic override async Task OnResultExecutionAsync(


ResultExecutingContext context,
ResultExecutionDelegate next) {
awai t next () ;
timer.Stop();
string result = 11 <div>Action time: 11
+ $ 11 { actionTime} ms</ di v><di v>Total time: 11
+ $"{timer.Elapsed.Tota1Milliseconds} ms</div>";
byte [] bytes = Encoding. ASCII. GetBytes (resul t) ;
await context.HttpContext.Response.Body.WriteAsync(Ьytes,
О, bytes.Length);

Здесь асинхронные методы применяются для фильтров обоих типов, но вы можете


получить требуемую функциональность за счет смешивания и сочетания, поскольку
стандартные ре ализ ации этих методов вызывают свои синхронные аналоги. Внутри
фильтра с помощью Stopwatch измеряется время выполнения действия и общее вре­
мя, а результаты з аписываются в ответ. В листинге 19.20 комбинированный фильтр
прим еняется к контроллеру Horne.

Листинг 19.20. Применение гибридного фильтра в файле HomeController. cs

using Microsoft . AspNetCore . Mvc ;


using Filters . Infrastructure ;
namespace Filters . Control l ers
[Profile]
[ViewResultDetails]
puЫic class HomeController : Control l er {

puЬlic ViewResul t Index () => View ( 11 Message ",


" This is the Index action on the Home controller " ) ;
puЫic ViewResul t SecondAction () => View ( "Message ",
" This is the SecondAction action on the Home con t rol l er " ) ;

Запустив приложение, вы увидите вывод , подобный показанному на рис . 19.6. Вывод


появля ется после содержимого, предоставляемого атрибутом ViewResul tDetails,
т.к . он за писывается на стадии постобработки фильтра результатов, а не получается
из метода фильтра действий, который использовался в предыдущей версии.
598 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC

Name Value

Resutt Туре ViewResult

View Name Message

Model Туре String

Model Data This is the lndex action on the Ноте controller

Action time: 0.2576 ms


Total time: 2.1627 ms
____________________J
Рис. 19.6. Вывод из гибридного фильтра действий/результатов

Использование фильтров исключений


Фильтры исключений позволяют реагировать на исключения без необходимости
в написании блоков try ... catch в каждом методе действия. Фильтры исключений
могут применяться к классам контроллеров или к методам действий. Они вызывают­
ся, когда исключение не обработано методом действия либо фильтрами действий или
результатов , которые были применены к методу действия. (Фильтры действий и ре­
зультатов могут иметь дело с необработанным исключением, устанавливая свойство
ExceptionHandled своих объектов контекста в true.) Фильтры исключений реализу­
ют интерфейс IExceptionFilter, который определен следующим образом:

name space Micros o ft.AspNetCore.Mvc.Filters {


puЫic interface IExceptionFilter : IFilterMetadata
void OnException(ExceptionContext con text);

Метод OnException () вызывается при столкновении с необработанным исклю­


чением. Интерфейс IAsyncExceptionFil ter может использоваться для создания
асинхронных фильтров исключений, которые удобны, когда необходимо реагировать
на исключения с применением асинхронного АРI-интерфейса. Вот определение ин­
терфейса I AsyncExcep t i onFil ter:
using System .Threading.Tasks;
namespace Microsoft.AspNetCore .Mvc .Filters
puЫic interface IAsyncExceptionFilter : IFil te rMetadata
Task OnExceptionAsync(ExceptionContext context);
Глава 19. Фильтры 599
М е т о д OnExcept i o n A s y n c () является асинхронным а н алогом метода
On Except i o n () из интерфейса I Exc ep t i o nFil t er и вызывается при наличии необ­
работанного исключ ения. Для обоих интерфейсов данные контекста предоставляются
чер ез кл асс ExceptionCon t e xt, производный от Fi l terContext , в котором опреде­
л ены дополнительные свойства, описанные в табл. 19. 1О .
Таблица 19.10. Свойства ExceptionContext

Имя Описание

Exception Это свойство содержит любой объект Except i o n,


который был сгенерирован
ExceptionDispatchinfo Это свойство возвращает объект ExceptionDi spatchinf o ,
содержащий информацию трассировки стека для исключения

Except i onHandl ed Это свойство типа boo l используется для указания, обработа ­
но ли исключение

Result Это свойство устанавливает объект реализации I ActionRe s u l t ,


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

Создание фильтра исключений


Класс Ex c eptionFilterAttrib u te реализует оба интерфейса фильтров исклю­
ч ений и является с амым легким способом создания фильтра, так что он может быть
прим е нен как атрибут. Наиболее распространенное использование фильтра исключ е ­
ний свя з ано с отображением специальной страницы ошибки для специфическ ого типа
исюпоч ения , снабжая пользователя более полезной информацией, ч е м способны обес­
печить стандартные возможности обработки ошибок . Чтобы взглянуть на это, добавь­
те в папку Inf r astructure файл клас са по имени RangeExceptionAttribut e . cs и
определите в нем фильтр , как продемонстрировано в листинге 19.21.
Листинг 19.21. Содержимое файла RangeExceptionAttribu te. cs
из папк и Infrastructure
using System ;
u s ing Microsoft . AspNetCo r e . Mvc ;
using Microsoft . AspNe t Core . Mvc . Filte r s;
us i ng Microsoft . AspNetCore. Mv c . Mode lBind i ng ;
using Microsoft . AspNetCo r e . Mvc .Vi ewFeatu r es ;
namespace Fi lters . Infrastructure {
puЫic class RangeExceptionAttribu t e : Excep ti onFi l terAttribu t e
puЫic overr i d e void OnExcepti on (E xcep ti onCo n text context) {
if (context . Exceptio n is ArgumentOu tOfRangeException) {
context . Resu l t = ne w Vi ewRe su lt () {
Vi ewN ame = " Me ssa ge",
ViewData = new Vi ewDataDictionar y (
new EmptyModelMe t a d ata Provi de r() ,
new Mode l StateDictionary()) {
Model = @" The data r eceived Ьу the application cannot Ье processed "
)
};
600 Часть 11. Подробные сведения об инфраструктуре ASP.NET Саге MVC

Этот фильтр применяет объект Excepti onCon t ex t для получения типа необра­
ботанного исключ ения. Если типом является Argumen t OutOfRangeExcep tion , то

создается результ ат действия, который отобр аж ает сообщение пользователю . В л ис­


тинге 19.22 в контролл е р Ноте добавляется метод действия , а к 1юнтроллеру приме­
няется фильтр исмюч ений.

Листинг 19.22. Применение фильтра исключений в файле Homecontroller. cs

usi n g Fi lters .In frastructure ;


using Mic r osoft . AspNetCore . Mvc ;
using System;
naтespace Fi l ters.Controllers
[Profile]
[ ViewResu l tDetails]
[RangeException]
puЫic class H o т eCon t rol l er : Contr o ll e r {
pu Ы ic Vi ewRes u lt I n d ex() => Vi e w ("Message ",
" This is the Index act i on on the Ноте contro l ler " ) ;
pu Ы ic ViewRes ul t Secon dAction ( ) => Vi ew ( " Message ",
"This is the SecondAction action on t h e Ноте controller " ) ;
puЬlic ViewResult GenerateException(int? id) {
if (id == null) {
throw new ArgwnentNullException(nameof(id));
else if (id > 10) {
throw new ArgumentOutOfRangeException(nameof(id));
else {
return View ( "Message", $ "The value is { id} ") ;

При получении из URL запроса значения int, допускающего nu l l, метод де й ствия


GenerateExcept i on () полага ется на стандартный шаблон маршрутизации . Этот
м етод действия генерирует исключ ени е Ar gume n tNu l l Exception, е сли соотв етству­
ющий сегм ент URL отсутствует, и исмючение ArgumentOu tO f RangeExceptio n , если
его значение больше 10. При наличии сегмента со значением м е нее 10 м етод дейс­
твия возвращает объект ViewRes ult .
Протестировать фильтр ис ключ ений можно , з апустив приложение и з апросив URL
вида /Home/Ge nerateException/ 100. Значение в последнем сегм енте выходит за
пределы, ожидаемые методом действия. который сгенерирует исмючение с типом , об­
р абатываемым фильтром, что даст ре зультат, представленный на рис . 19.7. В случае
запроса /Home/GenerateException метод действия сгенерирует исключение, которое
не будет обработано фильтром, по этому задействуется стандартная обработка ошибок.

И спользо вание в недрения


за в исимостей для фильтров
Когда фильтр делается производным от одного и з удобных классов атрибутов, та ­
!{ИХ как Ex ceptionFil terAttr i bute , для обработки каждого запроса инфраструкту­
ра MVC создает новый экземпляр класса фильтра.
Глава 19. Фильтры 601

Рис. 19. 7. Использование фильтра исключений

Это рациональный подход, поскольку он позволяет изб ежать возможных проблем,


свя з анных с повторным использованием или параллелизмом, и удовлетворяет нуж­

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


Альтернативный подход предусматривает применение системы внедрения зависи­
мостей с целью выбора другого жизненного цикла для фильтров. Использовать внед­
рение зависимостей в фильтрах можно двумя способами, которые будут описаны в
последующих разделах .

Распознавание зависимостей в фильтрах


Первый подход заключается в применении внедрения зависимостей для управле­
ния данными контекста, которые предназначены фильтрам. Это позволяет фильтрам
разных типов совместно использовать данные или одиночному фильтру разделять
данные между своими экземплярами, применяемыми для обработки других запросов.
Чтобы посмотреть, как все работает, добавьте в папку In fra structure файл класса
по имени Fil t e r Diagno s ti cs. cs с определениями интерфейса и класса реализации
из листинга 19.23.

Листинг 19.23. Содержимое файла Fil terDiagnostics. cs из папки


Infrastructure
using Sys t em . Colle c ti ons . Gene r ic ;
namespace Fil ter s.Inf ras tru ct ur e {
puЬlic int e r face I FilterDi ag n o stics
IEnumeraЫe< string > Messages { get;
vo i d AddMessa ge(string me ss a g e );

p u Ыic class De fa ul t Fi l terDiag n o stics : IFi l t erD i agno sti c s


priva t e Lis t <s tr ing> me ssages = n e w Lis t <str ing>() ;
puЫ i c IEnume r aЫe<s t r ing > Messa ges = > mes s ages ;
puЫic void Add Me ss age(s trin g me ssag e) =>
messages . Add(mess age) ;

Интерфейс IFil terDia gnost i c s определяет простую модель для сбора диагности­
ческих сообщений во время выполнения фильтра. Класс Defaul tFil t erDiagnost i c s
является реализацией. которая будет использоваться. В листинге 19.24 показан класс
S t artup , обновленный для конфигурирования поставщика служб с целью примене­
ния нового инте рфейса и его реализации.
602 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC

Листинг 19.24. Конфигурирование поставщика служб в файле Startup. cs

using Microsoft . AspNetCore . Builder ;


using Microsoft . Extensions.Dependency l njection ;
using Filters.Infrastructure;
namespace Filters {
puЫic class Startup
puЫic void ConfigureServices(IS e rviceCollection services) {
services.AddScoped<IFilterDiagnostics, DefaultFilterDiagnostics>();
services . AddMvc() ;

puЫic void Configure(IAppl i cationBuilder арр) {


app.UseStatusCodePages() ;
app.UseDeveloperExceptionPage() ;
app.UseStaticFiles() ;
app . UseMvcWithDefaultRoute() ;

Поставщик служб конфигурируется с использованием расширяющего метода


AddScoped () , т.е. все фильтры, создаваемые для работы с одиночным запросом, по­
лучат тот же самый объект Defaul tFil terDiagnostics . Это является основой для
разделения специ альных данных контекста между фильтрами.

Соэдание фильтров с зависимостями


На следующем шаге создаются фильтры , которые объявляют зависимости от ин­
терфейса IFilterDiagnostics . Добавьте в папку Infrastructure файл класса по
имени TimeFilter . cs с содержимым, приведенным в листинге 19.25 .

Листинг 19.25. Содержимое файла TimeFil ter. cs из папки Infrastructure

using System . Diagnostics;


using System . Threading.Tasks ;
using Microsoft . AspNetCore . Mvc.Filters;
namespace Filters . Infrastructure {
puЫic class TimeFil ter : IAsyncActionFil ter, IAsyncResul tFil ter {
private Stopwatch timer ;
private I FilterDiagnostics diagnostics ;
puЫic TimeFilter(IFilterDiagnostics diags)
diagnostics = diags;

puЬlic async Task OnActionExecutionAsync(


ActionExecutingContext context ,
ActionExecutionDelegate next) {
timer = Stopwatch . StartNew() ;
await next() ;
diagnostics . AddMessage($@ "Action time :
{timer . Elapsed . TotalMilliseconds} " ) ;
Глава 19. Фильтры 603
puЫic async Task OnResultExecutionAsync(
ResultExecutingContext context,
ResultExecutionDelegate next) {
await next () ;
tirner.Stop();
diagnostics.AddMessage($@"Result tirne:
(timer.Elapsed.TotalMilliseconds}");

Класс TimeFil ter представляет собой гибридный фильтр действий/результатов,


который воссоздает функциональность таймера из предыдущего примера, но хранит
информацию о времени с применением реализации интерфейса IFil terDiagnostics,
которая объявлена нак аргумент конструктора и будет предоставлена системой внед­
рения зависимостей при создании фильтра.
Обратите внимание, что класс TimeFilter явно реализует интерфейсы фильтров,
а не наследуется от удобного класса атрибута. Вы увидите, что фильтры, которые
опираются на внедрение зависимостей, применяются через другой атрибут и не ис­
пользуются для декорирования контроллеров или действий напрямую.
Чтобы взглянуть, каким образом фильтры задействуют внедрение зависимостей
для разделения данных контекста, добавьте в папку Infrastructure файл клас ­
са по имени DiagnosticsFil ter. cs и определите в нем фильтр, показанный в
листинге 19.26.

Листинг 19.26. Содержимое файла DiagnosticsFil ter. cs из папки Infrastructure


using Systern.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore . Mvc.Filters;
narnespace Filters.Infrastructure {
puЬlic class DiagnosticsFilter : IAsyncResultFilter
private IFilterDiagnostics diagnostics;
puЫic DiagnosticsFilter(IFilterDiagnostics diags) {
diagnostics = diags;

puЬlic async Task OnResultExecutionAsync(


ResultExecutingContext context,
ResultExecutionDelegate next) {
await next();
foreach (string rnessage in diagnostics?.Messages)
byte [] bytes = Encoding . ASCII
.GetBytes($" <div>{rnessage}</d iv>");
await context.HttpContext . Response.Body
.WriteAsync(bytes , О, bytes.Length);
604 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

Класс DiagnosticsFil ter - это фильтр результатов, который получает реализа­


цию интерфейса IFil terDiagnostics в виде аргумента конструктора и записывает
содержащиеся в ней сообщения в ответ.

применение фильтров
Финальный шаг заключается в применении фильтров к классу контроллера.
Стандартные атрибуты С# не располагают всеобъемлющей поддержкой для распоз­
навания зависимостей конструктора, из-за чего фильтры, которые были определены
в предшествующих разделах , не являются атрибутами. Взамен применя ется атрибут
TypeFilter, сконфигурированный с необходимым типом фильтра (листинг 19.27).

Совет. Порядок применения фильтров в листинге 19.27 важен, как будет объясняться в разде­
ле "Порядок применения фильтров и его изменение " далее в главе .

Листинг 19.27. Применение фильтров с зависимостями в файле HorneController. cs

using Microsoft . AspNetCore . Mvc;


using Filters .I nfrastructure ;
using System;
namespace Filters . Controllers
[TypeFilter(typeof(DiagnosticsFilter))]
[TypeFilter(typeof(TirneFilter))]
puЫic class HomeController : Controller

puЫic ViewResult Index() => View("Message",


" This is the Index action on the Home controller " ) ;
puЬlic ViewResul t SecondAction () => View ( "Message ",
" This is the SecondAction action on the Home controller " ) ;
puЫic ViewResult Generate Exception(int? id) {
i f (id == null) {
throw new ArgumentNullException(nameof(id)) ;
else i f (id > 10) {
throw new ArgumentOutOfRangeException(nameof(id)) ;
else {
return View ( "Message ", $ " The val ue is { id} " ) ;

Атрибут TypeFil ter создает новый экземпляр класса фильтра для каждого запро­
са, но делает это с использованием средства внедрения зависимостей, которое поз­
воляет создавать слабо связанные компоненты и помещать объекты, участвующие в
распознавании зависимостей, под управление жизненным циклом.
В текущем примере сказанное означает, что оба фильтра , примененные в лис­
тинге 19.27, получат тот же самый объект реализации IFilterDiagnostics, а
сообщения, сохраненные классом TimeF il ter, будут записаны в ответ классом
DiagnosticsFi l ter. Запустив приложение и запросив стандартный для него URL.
можно увидеть результат (рис. 19.8).
Глава 19. Фильтры 605

This is the lndex action on the Ноте controller


I Action time: 0.0114
Resulttime: О . 1561

Рис. 19.8. Использование фильтров с зависимостями

Управление жизненными циклами фильтров


В случае применения атрибута TypeFil ter новый экземпляр класса фильтра со­
здается для каждого запроса. Такое поведение ничем не отличается от поведения при
использовании фильтра напрямую как атрибута за исключ ением того , что атрибут
TypeFil ter позволяет массу фильтра объявлять зависимости, которые распознают­
ся посредством поставщика служб.
Атрибут ServiceF il ter продвигается на шаг дальше и применяет поставщик
служб для создания объекта фильтра. Это также дает возможность помещать объек­
ты фильтров под управление жизненным циклом. В целях демонстрации в листин­
ге 19.28 приведен модифицированный класс TimeFilter, который сохраняет сред­
нее арифметическое записанных значений времени.

Листинг 19.28. Сохранение средних арифметических значений в файле TimeFil ter. св

using System . Collections . Concurrent ;


using System . Diagnostics ;
using System.Linq;
using System . Threading . Tasks ;
using Microsoft . AspNetCore . Mvc . Filters ;
namespace Filters . Infrastructure {
puЫic class TimeFilter : IAsyncActionFilter, IAsyncResultFilter
private ConcurrentQueue<douЫe> actionTimes
new ConcurrentQueue<douЫe>();
private ConcurrentQueue<douЫe> resultTimes
new ConcurrentQueue<douЫe>();
private IFi lterDiagnos ti cs diagnos t ics ;
puЬlic TimeFilter(IFilterDiagnostics diags)
diagnostics = diags ;

puЫic async Ta sk OnActionExecut i onAsync(


ActionExecutingContext context , ActionExecutionDelegate next) {
Stopwatch timer = Stopwatch.StartNew();
await next() ;
timer . Stop() ;
actionTimes.Enqueue(timer.Elapsed.TotalМilliseconds);
606 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC

diagnostics . AddMessage($@"Action time :


{timer.Elapsed.TotalMilliseconds}
Average: { actionTimes. Average () : F2} ") ;

puЫic async Task OnResultExecutionAsync(


ResultExecutingContext context , ResultExecutionDelegate next) {
Stopwatch timer = Stopwatch.StartNew();
await next ();
timer.Stop();
resultTimeз.Enqueue(timer.Elapsed.TotalMilliseconds);
diagnostics . AddMessage($@ " Result time :
{timer . Elapsed .T otalMi l liseco nd s}
Average: {resultTimes.Average() :F2}");

Фильтр те перь использует безопасную к потокам коллекцию для хранения значе ­


ний времени, которые он записывает для стадий действия и р езультата обработки
запроса, и прим еняет отдельный объект Stopwatch каждый раз , когда по ступает тре­
бовани е обработать зап рос. В листинге 19.29 классTimeFilter регистрируется как
одиночка с помощью поставщика служб в классе Startup.

Листинг 19.29. Конфигурирование поставщика служб в файле Startup. сз

using Microsoft . AspNetCore . Builder ;


using Microsoft . Extensions . Dependencyinjection ;
using Filters.Infrastructure ;
namespace Filters {
puЫic class Startup
puЫic void ConfigureServices(IServiceCollection services) {
зervices.AddSingleton<IFilterDiagnostics,
DefaultFilterDiagnostics>();
services.AddSingleton<TimeFilter>();
services . AddMvc() ;

puЫic void Configure(IApplicationBuilder арр) {


app . UseStatusCodePages() ;
app . UseDeveloperExceptionPage() ;
app . UseStaticFiles() ;
app . UseMvcWithDefaultRoute() ;

Обратите внимание, что жизненный цикл для класса реализации IFil terDiagnostics
также изменен, чтобы он стал одиночкой. Если бы сохранилось создан ие нового эк­
земпляра для каждого запроса, то объект-одиночка TirneFil ter получал бы раз ные
объекты реализации IFilterDiagnostics от фильтра Diagnost i csFilter, который
продолжил бы создаваться через атрибут TypeFil ter и создавался бы для каждого
запроса.
Глава 19. Фильтры 607
Применение фильтра

Последний шаг касается применения фильтра к контроллеру с использованием ат­


рибута ServiceType (листинг 19.30).
Листинг 19.30. Применение фильтра в файле HomeController. cs
using Microsoft.AspNetCore.Mvc ;
using Filters .I nfrastructure;
using System;
namespace Filters.Controllers
[TypeFilter(typeof(DiagnosticsFilter) )]
[ServiceFilter(typeof(TimeFilter))]
puЫic class HorneController : Controller
puЫic ViewResul t Index () => View ( "Message",
"This is the Index action on the Home controller " );
puЫic ViewResult SecondAction() => View( " Message ",
"This is the SecondAction action on the Home controller " );
puЫic ViewResult GenerateException(int? id) {
if (id == null) {
throw new ArgumentNullException(nameof (id));
else i f (id > 10) {
throw new ArgumentOutOfRangeException(nameof(id) );
else {
return View( " Message" , $"The value is {id}");

Запустив приложение и запросив стандартный URL, можно увидеть результат.


Поскольку для распознавания всех зависимостей используется единственный объ­
ект реализации интерфейса IFil terDiagnostics, набор отображаемых сообщений
строится с каждым запросом (рис. 19.9).

This is the lndex action on the Ноте controller


Action time: 9.9636 Average: 9.96
Result time: 2442.0765 Average: 2442.08 /
Action time: 0.0564 Average: 5.01
Result time: 2.069 Average: 1222.07
Action time: 0.012 Average: 3.34 ,
1

Resulttime:.0.1501 Avera.ge: 814.7.7 . ~..1


Action time: 0.0114 Average: 2.51
'~t· . :1 . це· ~ ~ ...~~ •. . '

Рис. 19.9. Применение поставщика служб для управления жизненным циклом фильтра
608 Часть 11 . Подробные сведения об инфраструктуре ASP.N ET Core MVC

Создание глобальных фильтров


В начале главы было указано, что вы можете применять фильтры к целому клас­
су контроллера, чтобы не применять их к индивидуальным методам действий.
Глобальны е фш~ьтры продвигаются еще дальше и применяются один раз в классе
Startup, после чего. как и следует из их названия, автоматически применяются к

каждому методу действия в каждом контроллере внутри приложения. В качестве гло­


бального фильтра может использо ваться любой фильтр . Для примера добавьте в пап­
ку Infrastructure файл класса по имени ViewResul tDiagnostics. cs с определе­
нием фильтра, показанным в листинге 19.31.
Листинг 19.31. Содержимое файла ViewResul tDiagnostics. cs
из папки Infrastructure
using Microsoft.AspNetCore.Mvc;
using Microsoft . AspNetCore . Mvc.Filters;
narnespace Filters . Infrastructure {
puЫic class ViewResultDiagnostics : IActi onF ilt e r {
private IFilterDi agnostics diagnostics;
puЫic ViewResu ltDiagnostics(IFilterDiagnosti cs diags)
diagnostics = diags ;

puЫic void OnActionExecuting(ActionExecutingContext context)


11 ничего не дел ать - метод в этом фильтре не используется

puЫic void OnActionExecuted(ActionExecutedContext context)


ViewResult vr ;
if ( (vr = context .Result as ViewResult) != null) {
diagnostics . AddMessage($ " View narne : {vr . ViewNarne} " ) ;
d iagnosti cs . AddMessage($@ " Mode l type:
{vr.V i ewData . Mode l.GetType() . Narne}") ;

Фильтр использует объект реализации IFil terDiagnostics для сохране­


ния сообщений об имени представления и типе модели результатов действий
ViewResul t . В листинге 19.32 этот фильтр применяется глобально наряду с клас­
сом DiagnosticsFilter, на который он полагается пр и записи диагностических
сообщений.

Листинг 19.32. Регистрация гл обальных фильтров в файле Startup. cs


u sing Microsoft . AspNetCore . Builder;
using Microsoft . Extensions . Dependencyin j ection ;
using Filters . Infrastructure ;
n arnespace Filters {
puЫic c lass Startup
puЫic void ConfigureServices(IServiceCollection services) {
Глава 19. Фильтры 609
services.AddScoped<IFilterDiagnostics, DefaultFilterDiagnostics>();
services.AddScoped<TimeFilter>();
services.AddScoped<ViewResultDiagnostics>();
services.AddScoped<DiagnosticsFilter>();
serv ices.Add.Мvc() .Add.МvcOptions(options => {
options.Filters.AddService(typeof(ViewResultDiagnostics));
options.Filters .AddService(typeof(DiagnosticsFilter));
}) ;

puЬlic void Configure(IApplicationBuilder арр) {


app . UseStatusCodePages();
app.UseDeveloperExceptionPage() ;
app . UseStaticFiles() ;
app . UseMvcWithDefaultRoute();

Dюбальные фильтры настраиваются путем конфигурирования пакета служб МVС.


В рассматриваемом примере для регистрации фильтров как глобальных использу­
ется метод MvcOptions . Fil ters . AddService (). Метод AddService () принимает
тип . NЕТ, экземпляр которого будет создан с применением правил жизненного цикла ,
указанных где-то в другом месте метода ConfigureServices () . Жизненный цикл
других типов фильтров изменен на ограниченный областью действия, так что новые
экземпляры создаются для каждого запроса. В результате новые экземпляры филь­
тров ViewResul tDiagnostics и DiagnosticsFil ter создаются и применяются к
каждому запросу, адресованному каждому контроллеру.

Совет. Добавлять глобальные фильтры можно также с использованием метода Add () вмес­
то AddService () , который позволяет регистрировать объект фильтра в качестве гло­
бального фильтра, не полагаясь на внедрение зависимостей и поставщика служб. Метод
Add () применяется в следующем разделе .

Добавьте в папку Controllers файл класса по имени GlobalController . cs с


определением контроллера из листинга 19.33.

Листинг 19.ЗЗ. Содержимое файла GlobalController. cs из папки Controllers

using Microsoft.AspNetCore . Mvc;


namespace Filters.Controllers {
puЫic class GlobalController : Controller {
puЬlic ViewResult Index () => View ( " Message ",
"This is the global controller");

Ника~{ие фильтры к контроллеру Global не применялись, но после запуска при­

ложения и запрашивания URL вида /global отобразится вывод из двух глобальных


фильтров (рис. 19.10).
610 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

r-:---~~----~~--~~~~~~-~~.

С [ <D localhost:51000/global

This is the global controller


View name: Message
Model type: String
----·---·--·--·----·-----·-----
Рис. 19.1 О. Использование глобальных фильтров

Порядок применения фильтров и его изменение


Фильтры запускаются в специфической очередности: авторизации, действий и
результатов. Но при наличии нескольких фильтров заданного типа порядок их при­
менения управляется областью действия, через которую фильтры были применены.
Добавьте в папку Infrastructur e файл класса по имени MessageAttribute. cs и
определите в нем фильтр, приведенный в листинге 19.34.

Листинг 19.34. Содержимое файла MessageAttribute. cs из папки Infrastructure

using System . Text;


using Microsoft.AspNetCore.Mvc.Filters ;
namespace Filters.Infrastructure
puЬlic class MessageAttribute : ResultFilterAttribute
private string message ;
puЫic MessageAttribute(string msg)
message = msg;

puЫic overr ide void OnResultExecuting(ResultExecutingContext context)


WriteMessage(context, $"<div>Before Result : {message)</div>");

puЬlic override void OnRe sultExecuted (Res ult ExecutedContext context)

WriteMessage(context, $"<div>After Result : {mes sage)</div> " ) ;

private void WriteMessage(FilterContext context , string msg) {


byte[] bytes = Encoding.ASCII
.GetByte s($ " <div>{msg}</div> ");
context.HttpContext . Response
.Body . Wr ite (bytes , О, bytes.Length) ;
Глава 19. Фильтры 611
В листин ге 19.34 определ ен фильтр результатов , который записывает фрагменты
НТМL- р азметки до и после обработки результата действия . Сообщение , записываемое
фил ьтром , конфигурируется поср едством аргумента конструктора , который мож ет
использ оват ься, 1<0гда применя ется ат рибут. В листинге 19.35 контроллер Home уп­
рощен, а фильтры из предш е ствующих примеров заменены множеством экземпляров
фильтр а Message .

Листинг 19.35. При м енение фильтра в файле HorneController. cs


using Microsoft . AspNetCore . Mvc ;
using Filters . Infrastructure ;
namespace Filters . Controllers {
[Message( " This i s t h e Contro l ler - Scop ed Fi l t er " )]
puЫic class Home Co n tro l le r : Controll e r {
[Message ( "This is the First Action-Scoped Fil ter")]
[Message("This is the Second Action-Scoped Filter")]
puЫic ViewResu l t Index () = > View ( " Mess age ",
" Th i s is the I n dex act i o n o n t h e Home c o n t r ol l er " ) ;

Н а бор глобальных фильтров изменен с целью исполь з ов а ния также фильт ра


Message (л исти нг 19.36).

Листинг 19.36. Создание глобального фильтра в файле Startup. cs


using Microsoft . AspNetCore . Bui l de r;
using Microsoft . Exten s ions . Dependency in je c t i on ;
using Filters . Infra s tr u ctu r e ;
namespace Filters {
puЫic class Startup
puЫic void Conf i gureSe r v i ces(IService Col l ection servi ces) {
services . AddS co ped<IF i lter Di a g no sti cs , De f a ul t Fil t er Di a gno s tics>() ;
services . AddScoped<T i me Fi l ter> ();
services . Add Sco ped<V i ewRes u lt Di a gn o sti cs> ( ) ;
services . AddScoped<Diagnost i cs Fi l te r >() ;
services . AddMvc() . AddMvcOptions (op ti o n s => {
options.Filters.Add(new
MessageAttribute("This is the Globally-Scoped Filter"));
) ) ;

puЫic void Conf i gure(IApp li cation Buil der арр) {


app . OseStatusCodePages() ;
app . UseDe v eloper Excep t io n Page ();
app . OseStat icFi les( );
app . UseMvcWit h De f aultRou t e() ;
612 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

При реагировании метода действия I n d ex () на запрос будут использоваться че ­


тыре экземпляра фильтра . Запустив приложение и запросив стандар тный URL, вы
увидите следующий вывод , отображаемый в брауз ер е :

Before Result : This is the Globally- Scoped Filter


Befo r e Result:This is the Contro lle r - Scoped Filter
Bef ore Res ult:This is the First Action - Scoped Filter
Before Result : This i s the Second Action - Scoped Filter
Afte r Result : This i s the Second Action -S coped Fi lter
After Result : This i s the Fi rst Action - Scoped Filter
After Result : Th i s is the Controller - Scoped Filter
After Resu l t : Th i s is the Gl obally- Scoped Filter
По умолчанию инфраструктура MVC запускает глоб ал ьны е фильтры. зат ем филь­
тры, примененные к классу контроллера, и в з аключ ение фильтры , которы е при мен е ­
ны к методам действий. После того, как метод действия вызван ил и р е зультат де йс­
твия обработан, стек фильтров раскручивается, из - за чего сообщения After Resul t
в выводе расположены в обратном порядке .

Изменения порядка применения фильтро в


Стандартный порядок м о ж ет быть изменен з а счет реализаци и интерфейса
I Ordere dF ilter , который MVC ищет при выяснении, как пом ещать фильтры в пос­
ледовательность. Вот определение этого интерфейс а:

name s pace Microsoft . AspNetCore . Mvc . Filters {


pu Ы ic interface I OrderedFilte r : IF i lt erMeta data
int Orde r { get ; }

Свойство Order возвращает значение i nt ; низко е значени е указывает инфраструк­


туре MVC на необходимость применения фильтр а пер ед фильтрам и с бол ее высокими

значениями Order . Удобны е атрибуты уже реализуют интерфейс IOrderedFilter ,


и в листинг е19.37 свойство Or d er устанавливается для фильтров , прим ен ен н ых к
контроллеру Home .

Совет. Атрибуты TypeFil ter и Servi ceFil tеrтоже реализуют интерфейс IOrderedFil ter,
т. е. в случае использ о вания внедрения зависимостей порядок применения фильтров так­
же можно изменять.

Листинг 19.37. Установка порядка применения фильтров в файле HomeController. cs

using Fil ters .I nfrastructu r e ;


us i ng Mi c r oso f t . AspNetCore . Mvc ;
namespace Filters.Contro l lers {
[Message("This is the Controller-Scoped Filter", Order = 10)]
puЫic class HomeController : Contr o l ler {
[Message ("This is the First Action-Scoped Filter", Order = 1)]
[Message ("This is the Second Action - Scoped Filter", Order = -1 )]
puЫ i c View Result I ndex() => Vie w( "Mes s age ",
" This is the Index action on th e Home controller " ) ;
Глава 19. Фильтры 613
Значения Orde r могут быть и отрицательными, что дает удобный способ гаранти­
ровать применение фильтра перед любыми глобальными фильтрами со стандартным
порядком (хотя при создании глобальных фильтров можно также устанавливать поря­
док). Запустив приложение, вы увидите, что порядок следования сообщений в выводе
изменился , чтобы отразить новые приоритеты:

Before Result : This is the Se c ond Ac t i on - Scoped Filter


Before Result : This is the Globally- Scope d Filter
Before Resu l t : This is the First Action - Scoped Filter
Before Result : Th i s i s the Contro ll e r-S coped Fil ter
After Re su l t : Thi s is t he Con t rol l er -Scoped Fi lt e r
After Result : Th is is th e Fi r s t Ac t ion - Sc op ed Filter
After Result : Th i s is the Globally - Scoped Filte r
After Result : This is the Second Act i o n- Scoped Fil ter

Рез ю м е
В настоящей главе было показано, как инкапсулировать логику сквозной ответс­
тв е нности в виде фильтров. Вы узнали о доступных типах фильтров и о том, как их
р е ализовать. Были приведены объяснения, каким образом применять фильтры в
форме атрибутов к контроллерам и методам действий, а также в качестве глобальных
фильтров. В сл едующей гл аве будет поrшзано, как использовать контроллеры для со­
здания веб-служб.
ГЛАВА 20
Контроллеры API

н е все контроллеры используются для отправки НТМL-документов клиентам.


Существуют также контроллеры API, которые применяются для предоставл е ­
ния доступа к данным приложения. Это средство, которое ранее предлагалось через
отдельную инфраструктуру Web API, но теперь оно интегрировано в ASP.NET Core
МVС. В настоящей главе объяснена роль контроллеров API в веб-приложении, опис а­
ны задачи. которые они решают, и продемонстрированы способы их создания, тес­
тирования и использования . В табл . 20. l приведена сводка, позволяющая поместить
контроллеры АР! в контекст.

Таблица 20.1. Помещение контроллеров API в контекст

Вопрос Ответ

Что это такое? Контроллеры API подобны обычным контроллерам за исключени­


ем того, что ответы , порождаемые их методами действий -
это объекты данных, которые отправляются клиенту без НТМL­
разметки

Чем они полезны? Контроллеры API позволяют клиентам иметь доступ к данным в
приложении, не получая также и НТМL-разметку, которая требу­
ется для представления содержимого пользователю. Не все кли­
енты являются браузерами , и не все клиенты отображают данные
пользователям. Контроллер API делает приложение открытым
для поддержки новых типов клиентов или клиентов, разработан­
ных третьей стороной

Как они используются? Контроллеры API применяются аналогично обычным контролле­


рам HTML
Существуют ли какие­ Наиболее распространенные проблемы связаны со способом
то с к рытые ловушки сериализации объектов данных, чтобы и х можно было отправить
или ограничения? клиенту. За подробными сведениями обращайтесь в раздел
"Форматирование содержимого" далее в главе

Существуют ли Вы не обязаны использовать контроллеры API в свои х проектах ,


альтернативы? однако их применение может увеличить ценность вашей плат­
формы для клиентов

Изменились ли они по Контроллеры API ранее предоставлялись через инфраструктуру


сравнению с версией Web API,но теперь они интегрированы в ASP.NET Саге MVC и со­
MVC 5? здаются подобно обычным контроллерам

В табл. 20.2 приведена сводка для этой главы.


Глава 20. Контроллеры АР! 615
Таблица 20.2. Сводка по главе

Задача Решение Листинг

Предоставление доступа к дан­ Создайте контроллер АР! 20.1-20.11


ным в приложении

Запрос данны х из контроллера Используйте запрос Ajax, либо напрямую 20.12-20.14


API работая с АРl-интерфейсом браузера,
либо через библиотеку вроде jQuery

Предоставление клиентам неко­ Добавьте в проект MVC дополнительные 20.16-20.17


торого диапазона разных форма­ пакеты сериализации
тов данны х

Переопределение процесса со­ Применяйте атрибут Produces 20.18


гласования содер ж имого

Разрешение клиентам переопре­ Добавьте в класс Startup отображ ения 20.19-20.20


делять заголовок Accept за счет форматеров, добавьте переменную сег-
указания формата данны х в URL мента, которая зах ватывает формат дан-
ны х, и дополнительно примените атрибут
ForrnatFilter
Предоставление полной подде­ Включите форматер Ht tpNotAcce 20.21-20.22
р жк и для процесса согласования ptaЬleOutputForrnatter и уста -
содерж имого новите конфигурационное свойство
RespectBrowserAcceptHeader
Получение данны х в разнообраз­ Примените атрибут Consumes 20.23
ны х форматах , используя разные
методы действий

Подготовка проекта для примера


Создайте новый проект типа Empty (Пустой) по имени ApiControllers с исполь­
зованием шаблон а ASP.NET Саге Web Applicatioп (.NET Саге) (В е б-приложе ние ASP.NET
Core (.NET Core)).

Создание модели и хранилища


Начните с создания папки Models , добавьте в не е файл класса по имени
Reservation . cs и определите класс модели, как показано в листинге 20.1.

Листинг 20.1. Содержимое файла Reservation. cs из папки Models


namespace ApiControllers . Models {
puЬlic class Reservation {
puЫic int Reservationid { get ; set ;
puЫic string ClientName { get ; set ;

puЬlic string Location { get ; set ; }


61 б Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

Затем добавьте в папку Models файл класса по имени I Reposi tory . cs и опреде­
лите в нем интерфейс для хранилища объе1tтов м одели (листинг 20.2).

Листинг 20.2. Содержимое файла IReposi tory. cs из папки Models


using System . Col l ections.Generic ;
namespace ApiControllers.Models
puЫic interface IRepository {
IEnumeraЬle<Reservation> Reservations { get ; }
Reservation t his [int id] { get ; }
Reservation AddReservation(Reservation reservation);
Reservation UpdateReservation(Reservation reservation);
void DeleteReservation(int id};

Далее добавьте в п апку Models файл 1wacca по имениMemoryReposi tory. cs с оп­


ределением непостоянной реализации интерфейса IReposi tory из листинга 20.3.

Листинг 20.З. Содержимое файла MemoryReposi tory. cs из папки Models


using System . Collections . Generic;
namespace ApiControllers . Models {
puЬlic class MemoryRepository : IRepository
private Dictionary<int, Reservation> it ems ;
puЫic MemoryRepository() {
items = new Dictionary<int , Reservation>() ;
new List<Reservation> {
new Reservation { ClientName "Alice ", Location = " Board Room " } ,
new Reservation { ClientName " ВоЬ ", Location " Lecture Hall " } ,
new Reservation { ClientName " Joe ", Loc at i on = " Meeting Room 1 "

} . ForEach (r => AddReservation (r)) ;

puЫic Reservation this[int id] => items.ContainsKey(id) ? items[id] null ;


puЬlic IEnum eraЫe<Reservation> Reservations => items . Values ;
puЫic Reservation AddReservation(Reservation reservation) {
if (reservation . Reservationid == 0) (
int key = items.Count ;
while (items . ContainsKey(key)) key++ ; };
reservation . Reservationld = key ;

items[reservation . Reservationid] reservation;


return reservation;

puЫic void DeleteReservation(int id) => items . Remove(id) ;


puЫic Reservation UpdateReservation(Reservation reservation)
=> AddReservation(reservation);
Глава 20. Контроллеры API 617
При создании экземпляра хранилища строится простой набор объектов модели,
а поскольку постоянство не поддерживается, то в случае останова или перезапуска

приложения все внесенные изменения будут утрачены. (Пример создания постоянно­


го хранилища приводился в главе 8 как часть про екта приложения SportsStore.)

Создание контроллера и представлений


По зже в главе будут созданы контроллеры REST, но при подготовке нужно создать
обычный контроллер, чтобы обеспечить основу для дальнейших примеров. Создайте
папку Controllers , поместите в нее файл класса по имени HomeController . cs и
опр еделите в нем контроллер, код которого приведен в листинге 20.4.
Листинг 20.4. Содержимое файла HomeController. св из папки Controllers

usi ng Microsoft.AspNetCore .Mvc ;


using Api Controllers . Models;
namespace Ap iControl lers .Controllers
puЫic class HomeController : Controller
private IRepository repository { get; set;
puЫic HomeCont rol ler(IRepository repo) {
repos it ory = repo;

puЫic ViewResult Index() => View(repository.Reservations);


[Http Post]
puЫic IActionResult AddReservation(Reservation reservation)
repository . AddReservation(reservation) ;
return RedirectToAction( " Index");

В контроллере определено действие Index, которое является стандартным для


приложения и визуализирует модель данных. В нем также определено действие
AddReservation, которое доступно только НТГР-запросам POST и применяется для
получения данных формы от пользователя. Упомянутые действия следуют паттерну
Post/Redirect/Get, описанному в главе 17, так что перезагрузка веб-страницы небу­
дет создавать повторную отправку формы.
Чтобы можно бьmо отделить НТМL-содержимое от заголовка документа, необходимо
создать компоновку, которая упростит изменения. вносимые позже в главе. Создайте
папку Views/Shared и добавьте в нее файл компоновки по имени _Lауоut. cshtml с
содержимым, показанным в листинге 20.5.
Листинг 20.5. Содержимое файла_Layout. cshtml из папки Views/ Shared

< !DOCTYPE html>


<html>
<head >
<meta name ="viewport " content= " width=device-width" />
<title>RESTfu l Contro llers </title>
<link asp -href- inc l ude= "liЬ/b ootstrap/dist/css/*.min .css" rel= " styl esheet" />
</head>
<body class= "panel-body">
@RenderBody ()
</body>
</html>
618 Часть 11. Подробные сведения об инфраструктуре ASP.NET Саге MVC

Теп ерь создайте папку Views/Home , добавьте в нее файл представления по имени
Index . cshtml и поместите в него содержимо е из листинга 20.6.

Листинг 20.6. Содержимое файла . cshtml из папки Views/Home


@mode l IEnumeraЬle<Reservation>
@{ Layout = " Layout "; }
<form id= " addform " asp-ac ti on= "AddReservation " method= " post " >
<div class= " form - group " >
<l abel for= " ClientName " >Name : </ l abel>
<input class= " form-control " name= " Cl ientName " />
</div>
<div class =" form - group " >
<label for= "Location " >Location:</label>
<input class= " form-control " name =" Location" />
</div>
<div class ="t ext - center panel - body " >
<button type= " submit " class= " btn btn-sm btn-primary " >Add</button>
</div>
</form>
<tаЫе class= " taЫe taЫe - condensed taЬle - striped taЫe - bordered " >
<thead><tr><th>ID</th><th>Client</th><th>Location</th></tr></thead>
<tbody>
@foreach (var r in Model) {
<tr>
<td>@r . Reservationid</td>
<td>@r . ClientName</td>
<td>@r . Location</td>
</tr>

</tbody>
</tаЫе>

Это стр ого типизированное представление получает последовательно сть объектов


Reservation в качестве своей модели и с помощью Rаzоr-цикл а
foreach запол няет
ими таблицу. Предусмотр ен также элемент form , который сконфигурирован для от­
правки запросов POST к действию AddReservation.
Примеры в настоящей главе зависят от СSS-пак ета Bootstrap. Чтобы добавить
Bootstrap в проект, создайте в корневой папке проекта файл bower . j s on с использо­
ванием шаблона элемента Bower Configuration File (Файл конфигурации Bower) и до­
бавьте пакет Bootstrap в ра здел dependencies этого файла (листинг 20 .7) .

Листинг 20. 7. Добавление пакета в файле bower. j son

"name ": "asp . net ",


"private ": true ,
"dependencies ": {
"bootstrap": 11 3.3.6 11
Глава 20. Контроллеры API 619
Создайте в папке Views файл _Viewimports . cshtml и настройте в нем встроен­
ные дескрипторные вспомогательные классы для применения в представлениях Razor,
а также обеспечьте импортирование пространства имен модели (листинг 20.8).

Листинг 20.8. Содержимое файла_Viewirnports. cshtrnl из папки Views

@using ApiControllers . Models


@addTagHelper * , Microsoft . AspNetCore . Mvc.TagHelpers

Конфигурирование приложения
Доб ав ьте в ра здел depende n cies файла proj ect . j son требуемы е пакеты NuGet
и настройте в разделе tools инструментарий Razor, как демонстрируется в листин­
ге 20.9. Разделы, которые не нужны для данной главы, понадобится удалить .

Листинг 20.9. Добавление пакетов в файле proj ect. j son

"dependenc i es ": {
"Microsoft.NETCore.App":
"version ": "1 .0 . 0 ",
" type ": " platform "
} ,
"Microsoft . AspNetCore . Diagnos ti cs ": " 1 . О . 0 ",
"Microsoft . AspNetCore . Server.IISintegration ": " 1 . 0 . О ",
"Microsoft . AspNetCore . Server . Kestre l": "1. 0 . 0 ",
"Microsoft . Extensions . Logging . Conso l e ": "1. 0.0 ",
"Microsoft.AspNetCore.Mvc": 11 1.0 . 0 11 ,
"Microsoft.AspNetCore.StaticFiles": "1.0.0",
"Microsoft.AspNetCore.Razor.Tools":
"version": "1.0.0-preview2-final",
"type": "build"

},

" tools ":


"Microsoft . AspNetCore . Razor.Tools": "1 . 0.0-preview2-final",
"Microsoft . AspNetCore . Server . IISintegration . Tools": " l . 0 . 0-preview2 -final "
}'
" frameworks ": {
" netcoreappl.0 ":
" imports ": ["dotne t 5 . 6", " portaЫe - net45+win8 "]

},
"buildOptions ":
"emitEntryPoint ": true , "preserveCompilationContext ": true
}'
" runtimeOptions ": {
" configProperties ": { " System . GC . Server ": true }
620 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC

В листинге 20.10 приведен класс Sta r tup, который конфигурирует средства, пре ­
доставляемые пакетами NuGet, и использует метод AddS ing leton () ,чтобы настро­
ить отображение службы для хранилища объектов модели.

Листинг 20.1 О. Содержимое файла Startup. cs

usi ng Mi crosoft .As pNetCo r e . Builde r ;


using Microsof t. Ex t ensions . Dep e ndencyinj e ction ;
using ApiControllers.Models;
namespace Ap i Co nt rollers {
puЬl i c c l ass Start up (
puЫic vo i d Config ure Se rvi ce s (IServic e Collect ion servi c e s )
services.AddSingleton<IRepository, MemoryRepository>();
services.AddМvc();

pu Ы ic v oid Config ure (IApplica t ionBui l der арр) {


a pp . UseStatusCode Pages() ;
ap p. Use Devel op e rExc e p t ionPa ge () ;
app . UseSta ti cFi l e s () ;
app . UseMv cWithDefaultRoute ( ) ;

Установка порта НТТР


Н екоторые примеры в этой главе тестируются путем набора URL вручную. Чтобы
облегчить опис а ние , понадобится установить порт, который будет получать НТГР­
запросы. Выбе рите пункт ApiControllers Properties (Свойства ApiCon t ro ll ers) в меню
Project (Проект) средыVisual Studio, перейдите на вкладку Debug (Отладка) и и зме­
ните значени е в пол е Арр URL (URL приложения) на http : / / localhost : 7000/, как
показано на рис . 20.1. После установки номера порта сохраните изменения.
Запустите приложение, заполните форму и щел кните на кнопке Add (Добавить) ;
приложение добавит в модель новый объект Re servat i o n (рис. 20.2). Вноси мы е в
хранилище изменения не будут постоянными, утрачиваясь при останове или п ере­
запуске приложения.

Арр UR : [hitP:/11ocalhost:7000/
0 EnaЫe SSL

URL:
~ ЕnаЫе Anonymous Aulhentication

О ЕnаЫе \"Jindows Authentication

Рис. 20.1. Установка URL приложения


Глава 20 . Контроллеры API 621

D i!ESTfulC0ttttol!~rs х

~ f\ES1ful Coritrol:ers

Namo;
!· :.m~~~C~~:'h~~~~~-~:-:= ~:~ ~-::;-:::~~~·-:::-=:._::~ J
Jacqu1

l ocalion:
Mooting Rooni 7 1 Loci11ion:

I~
ll __,.._

!
'
ю

1
Clicnl

Аl1св
ВоЬ
l ocalion

Вoard Room
Leclur• Hall
j
1 ID

l ocation

ВoordRoom !

l. ~ -----~~-------Moolin:oom ~1 ...-'___:ь_._ __,.._____~ес-•~-~ -·н-R~-n


1
1
_
m_, - - - - - - - - .
!
Jacqui MeeUng Room 7

1 1
_ _ •.... !
L.--

Рис . 20.2. Выполнение примера приложения

Роль контроллеров REST


Рассматриваемое приложение является примером классического веб-приложения.
Вся логика приложения находится на сервере и содержится в классах С# , что дела­
ет ее лепюй в управлении, тестировании и сопровождении. Но приложению, спроек­
тированному подобным образом, могут быть присущи серьезные недостатки в плане
скорости, эффективности и открытости.

Проблема скорости
В настоящий момент пример приложения представляет собой сuнхрон.н.ое веб­
приложение . После того как пользователь щелкает на кнопке Add, браузер отправля­
ет серверу запрос POST, ожидает ответа и затем визуализирует полученную НТМL­
разметку. В течение этого промежутка вр емени пользователь ничего не может делать,
а только ждать. Период ожидания может быть незначительным на этапе разработки,
когда браузер и сервер располагаются на одной машине; тем не менее, развернутые
приложения подпадают под реальные характеристики ограничений и задержек , по­
этому время, в течение которого синхронное приложение вынуждает пользователя

ждать , может оказаться ощутимым.

Синхронное приложение не всегда имеет проблему со скоростью. Например, если


вы пишете производственное приложение для применения в единственном местопо­

ложении, где все клиенты подключаются через быструю и надежную локальную сеть,
тогда проблема может не возникать. С другой стороны, если пишется приложение для
мобильных клиентов на площадках со скудной инфраструктурой, то проблема скоро­
сти может быть существенной.
622 Часть 11. Подробные сведения об инфраструктуре ASP.NET Саге MVC

Совет. Некоторые браузеры позволяют эмулировать разные типы сетей, что является удоб­
ным инструментом для выяснения, склонны ли пользователи согласиться на работу с син­
хронным приложением в определенном диапазоне сценариев. Скажем, браузер Google
Chrome предлагает средство под названием сетевое регулирование, которое доступно в
разделе Network (Сеть) инструментов, вызываемых по нажатию <F12>. Доступен диапа­
зон предопределенных сетей или же можно создать собственную сеть, указав скорости
выгрузки и загрузки, а также задержку.

Проблема эффективности
Проблема эффективности проистекает из способа, которым синхронное веб-прило­
жение трактует браузер как механизм визуализации НТМL-разметки, используемый
только для отображения отправляемых сервером НТМL-документов.
Когда пользователь впервые запрашивает стандартный URL примера приложения,
отправленный обратно НТМL-документ содержит все, в чем браузер нуждается для
отображения содержимого приложения, включая перечисленную ниже информацию:

• содержимое, основанное на СSS-файле Bootstrap, которое должно быть загруже­


но, если не доступна кешированная копия;

• содержимое, имеющее в своем составе форму, которая сконфигурирована для


отправки запроса POST к действию AddReservation;
• содержимое, имеющее в своем составе таблицу, тело которой содержит три за­
полненных строки.

Пример приложения прост, и начальный запрос приводит к тому, что сервер по­
сылает клиенту около 1,3 Кбайт данных. Однако когда пользователь отправляет
форму, клиент перенаправляется снова на действие Index, в результате давая еще
1,3 Кбайт данных, чтобы отразить добавление одиночной строки таблицы. Браузер
уже визуализировал форму и таблицу, но они были отброшены и заменены полно­
стью новым представлением того, что является практически совершенно тем же са­

мым содержимым.

Может показаться, что объем в 1,3 Кбайт данных не особенно велик и, несомнен­
но, вы будете правы. Но если учесть соотношение между полезным и дублированным
содержимым, тогда вы увидите, что подавляющее большинство данных, посылаемых
браузеру, оказывается излишним. К тому же пример приложения намеренно упро­
щен; очень немногие реальные приложения требуют настолько мало НТМL-разметки,
и по мере возрастания сложности приложения объем дублированного содержимого
значительно увеличивается.

Проблема открытости
Последняя проблема традиционного веб-приложения связана с тем, что его про­
ектное решение закрыто, означая возможность доступа к данным в модели только

через действия, которые предоставляет контроллер Home . Закрытые приложения ста­


новятся проблемой, если лежащие в основе данные необходимо использовать в дру­
гом приложении, особенно когда такое приложение разрабатывается другой коман­
дой или даже другой организацией. Разработчики зачастую уверены, что ценность
приложения кроется во взаимодействиях, которые оно предлагает пользователям, в
значительной степени из-за того, что они являются теми частями, на обдумывание и
Глава 20. Контроллеры API 623
реализацию которых разработчики тратят немалое время. Но как только приложение
установилось и накопило активную пользовательскую базу, содержащиеся в нем дан­
ные часто становятся важными.

Введение в REST и контроллеры API


Контроллер API - это контроллер MVC, который отвечает за предоставление до­
ступа к данным в приложении, не инкапсулируя их в НТМL-разметку. Это позволяет
извлекать или модифицировать данные в модели без необходимости в применении
действий, предлагаемых обычными контроллерами, такими как контроллер Home в
примере приложения.

Самый распространенный подход к доставке данных из приложения предусмат­

ривает использование паттерна REST (Representational State Тransfer - передача со­


стояния представления). Детальная спецификация REST отсутствует, что привело к
появлению многочисленных подходов, позиционирующих себя как согласованные с
REST. Тем не менее, существует несколько унифицированных идей, которые полезны
в клиентской части разработки веб-приложений.
Основная предпосылка веб-службы REST связана с тем, чтобы заключать в себе
характеристики НТГР, поэтому методы запросов (также называемые командами) ука­
зывают серверу операцию, подлежащую выполнению, а URL запроса определяет один
или большее число объектов данных, к которым операция будет применена.
В качестве примера взгляните на URL, который может ссылаться на специфичес­
кий объект Reservation в рассматриваемом приложении:

/api/reservations/1
Первая часть URL - api - используется для отделения части данных приложе­
ния от стандартных контроллеров, которые генерируют НТМL-размет1<у. Следующая
часть - reservations - указывает коллекцию объектов, с которой будет произво­
диться работа. Финальная часть - 1- задает индивидуальный объект внутри коллек­
ции reservations. В примере приложения это значение свойства Reservationid,
которое уникальным образом идентифицирует объект, и будет применяться в URL.
Идентифицирующие объект URL объединяются с методами НТГР для указания
операций. В табл. 20.3 перечислены наиболее часто используемые методы НТГР и
описано, что они представляют, когда комбинируются с URL примера. Также приведе­
ны детали о том, какие данные (т. е . полезная нагрузка) включаются в запрос и ответ
для каждой комбинации метода и URL. Контроллер АР!, который обрабатывает такие
запросы, с помощью кода состояния ответа сообщает об исходе запроса.
Следование соглашению REST не является требованием, но содействует упроще­
нию работы с приложением, потому что один и тот же подход широко прим еняется
во многих уже установившихся веб-приложениях.

Создание контроллера API


Процесс создания контроллера АР! основан на подходе, используемом для стан­
дартных контроллеров, с рядом дополнительных средств, которые помогают указы­

вать АРI-интерфейс, представляемый клиентам. Добавьте в папку Controllers файл


класса по имени ReservationController. cs с определением, приведенным в лис­
тинге 20.11. Функциональность, предлагаемая этим контроллером, анализируется в
последующих разделах.
624 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

Таблица 20.З. Объединение методов НТТР с URL для указания веб - службы REST
Ко м анда URL Описание Полезная нагрузка

GET /api/reservations Эта комбинация из- Ответ содер жит пол-


влекает все объекты ную колле кцию объе ктов
Reservation
GET /api /rese r vations/ l Эта комбинация Ответ содерж ит указанный
извле к ает объект объект Reservation
Reser vation, свойс-
тво Reservat i on i d
к оторого имеет зна-

чение 1
POST /api/reservation Эта комбинация со- За п рос содер ж ит значения
здает новый объект для друг их свойств, которые
Reservat i on требуются для создания
объекта Reservation.
Ответ содержит объект, ко-
торый был сохранен , гаран-
тируя получение клиентом

сохраненны х данны х

PUT /api/reservation Эта комбинация Запрос содер ж ит значения ,


обновляет сущее- требуемые для изменения
твующий объект свойств указанного объекта
Reservation Reservat i on. Ответ со-
держ ит объект, который был
сохранен, гарантируя полу-
чение клиентом сох раненны х

данных

DELETE /api/reservation/1 Эта комбинация Полезная нагруз к а в запросе


удаляет объект или ответе отсутствует
Rese r vation, свойс-
тво Reservationid
которого имеет зна-

чение 1

Совет. Вспомните , что классы контроллеров могут определяться где угодно в прое кте, а не
только в папке Con t ro ll ers. Для крупных и сложных проектов может быть удобно опре­
делять контроллеры API отдельно от обычных контроллеров HTML и помещать их в какую­
нибудь подпап ку и ли даже в выделенную папку.

Листинг 20.11. Содержи м ое файла ReservationController. cs и з папки Controllers

using System . Collections . Gener i c ;


using Microsoft . AspNetCore . Mvc ;
using ApiControl l ers . Models ;
namespace ApiControllers . Controllers
[Route( "api/ [controller] " ) ]
c l ass Rese r vationCo ntrol l e r : Control l er
p u Ьlic
Глава 20 . Контроллеры API 625
private IRepository repository ;
puЫic ReservationControl l er(IRepository repo) {
repository = repo ;

[HttpGet]
puЫic IEnumeraЫe<Reservation> Get( ) => repository . Reservat i ons ;
[HttpGet ( 11 { id} 11 ) ]
puЫic Reservation Get(int id) => repository[id ];
[HttpPost]
puЫic Reservation Post( [ FromBody ] Reservation res) =>
repository . AddReservatio n (new Reservation {
ClientName = res . Cli entName ,
Location = res . Location
}) ;
[HttpPut]
puЫic Reservation Put( [FromBody ] Reservat i on r es) =>
repository . UpdateReservation(res) ;
[HttpDelete ( " { id} " ) J
puЬlic void Delete(int id) => repository . DeleteReservation(id) ;

Контролле ры АР! работают в той же самой манер е , что и обычны е контроллеры. т. е .


можно создать контроллер РОСО или унасл едовать класс от базового класса Con troller,
который пр едост авляет бол ее удобный доступ к данным контеJ{СТа запроса.

Адаптаци я паттерна REST


Паттерн REST поощряет определенную долю догматиз м а относительно того, каким образом
АРl-интерфейсы веб-прило ж ения дол ж ны быть представлены клиентам . Паттерн REST не
является стандартным и ли хотя бы четко определенным , и существует несколько полезных
подходов, которые облегчают адаптацию REST в приложениях ASP.NEТ Core MVC, но имеют
тенденцию к крушению планов у тех программистов, кто обладает установившимися пред­
ставлениями о том, что считается соответствующим REST.
В табл. 20.З URL, перечисленные для операций POS T и PU T, не идентифицируют ресурс
уни кальным образом, что не которые считают неотъемлемой хара ктеристикой RESТ. В слу­
чае операции POST уникальный идентификатор объекта Reservation назначается моде­
лью, т. е . клиент не м ожет предоставить его как часть
URL. В случае операции PUT средство
привязки моделей MVC (которое описано в главе 26 и является причиной применения ат­
рибута FromBody в листинге 20 .11) упрощает получение деталей подле жащего модифи­
кации объе кта Reserva tion из тела запроса . Таким образом, именно здесь контроллер
Reservation ож идает найти значение Rese r vat i onid, идентифицирующее объект мо­
дели, который дол ж ен быть модифицирован .

Подобно всем паттернам REST представляет собой отправную точку, предлагающую удоб­
ные и полезные идеи . Это не жесткий стандарт, который дол жен соблюдаться любой ценой ;
единственная важ ная цель в том, чтобы писать понятный, тестируемый и сопровождаемый
код. Учет природы приложений MVC и проектного решения, положенного в основу храни­
лища , способствует получению более простого приложения и по-пре ж нему предоставляет
практичный АРl-интерфейс для потребления клиентами. Я советую рассматривать паттерны
к ак ру ководящие принципы, которые вы подгоняете под собственные нужды, что справед­
ливо в отношении как REST, так и MVC в целом .
626 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

Определение маршрута
Маршрут, посредством которого достигаются контроллеры АР!, может определяться толь­
ко с использ ованием атрибута Route , но не конфигурации приложения в классе Start up.
По соглашению для контроллеров API применяется маршрут, предваренный префиксом
ap i , за которым следует имя контроллера, так что контроллер Reservat i onCont r oller
из листинга 20.11 достигается через URL вида / api / r eserva t i on :
[ Route( " ap i / [ controller ]" )J
puЬlic cla ss ReservationController Contro l le r {

Объявление зависимостей
Объекты контроллеров API создаются так же, как и объекты обычных контролле ­
ров, а это значит, что они могут объявлять зависимо сти , которые будут распознав ать ­

ся с использованием поставщика служб. Класс Res e r va t i onCon troll e r объявляет в


конструктор е зависимость от интерфейса I Reposi tory, которая будет распо знавать­
ся для пр е доставления доступа !{ данным в модели:

puЫic ReservationController(IRepository repo)


repository = r epo ;

Определение методов действий


Каждый метод действия декорируется атрибутом . указывающим метод НТГР, кото­
рый он принимает, например:

[HttpGet]
pu Ы ic IEn u m era Ьl e<Re s erva tio n> Ge t() => r epo sit or y. Rese r vations ;

Атрибут HttpGet входит в набор атрибутов, применяемых для ограничения досту­


па к методам действий запросами, которые имеют специфич е ский м етод или команду
НТГР. Полный набор атрибутов представлен в табл. 20.4.
Таблица 20.4. Атрибуты методов НТТР

Имя Описание

HttpGet Этот атрибут указывает, что действие может быть вызвано только
НТТР-запросами, которые используют команду GET
HttpPos t Этот атрибут указывает, что действие может быть вызвано только
НТТР-запросами, которые применяют команду POST
Http Dele t e Этот атрибут указывает, что действие может быть вызвано толь ко
НТТР-запросами, которые используют команду DELETE
Ht t p Pu t Этот атрибут указывает, что действие может быть вызвано только
НТТР-запросами, которые применяют команду PUT
HttpPatch Этот атрибут указывает, что действие может быть вызвано только
НТТР-запросами, которые используют команду РАТСН

HttpHead Этот атрибут указывает, что действие может быть вызвано только
НТТР-запросами, которые применяют команду HEAD
AcceptVerbs Этот атрибут используется для указания множества команд НТТР
Глава 20. Контроллеры API 627
Маршруты могут дополнительно конкретизироваться за счет включения фраг­
мента маршрутизации в качестве аргумента для атрибута метода НТГР:

[HttpGet (" { id} ")]


puЫic Reservation Get(int id) => repository[id] ;

Фрагмент маршрутизации { id} объединяется с маршрутом. определенным в ат­


рибуте Route, который применен к контроллеру, и ограничением, основанным на
методе НТГР. В этом случае действие может быть достигнуто путем отправки запроса
GET с URL. соответствующим шаблону маршрутизации /api/reservations/ { id}.
где сегмент id затем используется для идентификации объекта Reservation, подле­
жащего извлечению.

Обратите внимание, что маршруты, генерируемые для контроллера API, не вклю­


чают переменную сегмента {act ion}, т.е. имя метода действия не является частью
URL, требующейся для нацеливания на специфический метод. Все действия в контрол­
URL (/api/reservation
лере АР! достигаю тся посредством одного и того же базового
в рассматриваемом примере), а метод НТГР и дополнительные сегменты п рименяются
для их различения.

Определение результатов действий


При пр едставлении ре зультатов методы действий для контроллеров АР! не пола­
гаются на объекты ViewResult, поскольку доставка данных не требует каких-ли­
бо представлений. Взамен методы действий контроллера АР! возвращают объекты
данных:

[H ttpGet]
puЫic IEnumeraЫe<Reservation> Get() => repository . Reservations ;

Пр иведенное действие возвращает последовательность объектов Reservation и


возлагает на инфраструктуру MVC ответственность за их сериализацию в формат,
который может быть обработан клиентом. В разделе "Форматирование содержимого"
далее в главе этот процесс объясняется более подробно.

Настройка результатов, возвращаемых действиями контроллеров API


Один из наиболее привлекательных аспектов контроллеров API заключается в том, что вы
можете просто возвращать из методов действий объекты С# и позволить инфраструктуре
MVC самостоятельно выяснить, что с ними делать. Инфраструктура MVC довольно хорошо
с этим справляется . Например , если вы возвратите из метода действия контроллера API
значение null , то клиенту будет отправлен ответ 204 - No Content (204 - содержим ое
отсутствует) .

Но контроллеры API способны использовать также и средства, доступные обычным конт­


роллерам. Это означает, что стандартное поведение можно переопределить, возвращая из
методов действий тип IActionResul t, который указывает вид результата для отправ ки.
Ниже показана реализация метода действия из примера контроллера, отправляющая ответ
404 - Not Found (404 - не найдено) для запросов, которые не соответствуют каким-либо
объе ктам в модели:
628 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Соге MVC

[HttpGet( " {id}")]


puЬlic IActionResult Get(int id) {
Reservation result = repository[id] ;
if (result == null) {
return NotFound() ;
else {
return Ok(result) ;

Если в хранилище не обнаруживается объект с указанным идентификатором , тогда вызы­


вается метод NotFound () , создающий объект NotFoundResul t, который приводит к
отправке клиенту ответа 4 О 4 - Not Found. Если объект в хранилище найден , тогда вы­
зывается метод Ok () , чтобы создать объект Ob j ectResul t. Метод Ok () позволяет от­
правлять объект клиенту внутри действия, которое возвращает IActionResul t , как было
описано в главе 17. Потребность в переопределении стандартны х ответов, возвращаемы х
действиями контроллеров API, возникает нечасто , но если она все же появляется, то для
этого доступен полный диапазон результатов действий.

Тестирование контроллера API


Существует множество инструментов, которые по мога ю т тести ровать АРI­
интерфейсы веб-приложения. В числе хороших примеров можно назвать Fiddler
(www . telerik . com/fiddler), представляющий собой автономный инструмент от­
ладки НТГР-запросов , и Swashbuckle (http : / / gi thub . com/domaindr i vendev /
Swashbuckle), являющийся пакетом NuGet, который добавляет в приложение стра­
ницу сводки с описания ми его операций АР! и позволяет их тестировать .
Но самый простой способ удостовериться в работоспособности контроллера АР!
пр едусматривает прим енение PowerShell, который облегчает создание НТГР-запросов
в командной строке Windows и позволяет сосредоточиться на результатах опера­
ций АР!, не погружая сь в детали. В последующих разделах будет пока зано, как ис­
пользовать PowerShell для тестирования операций, предо ст авл я емых контроллером
Reservation . Для выполнения тестовых команд можно открыть новое окно PowerShell
или работать в окне консоли ди сп етче ра пакетов Visual Studio, которое само взаимо­
действует с PowerShell.

Тестирование операций GET


Чтобы протестиров ать операцию GET , предлагаемую контроллером Reservation,
откройте окно PowerShell и наберите такую команду:

Invoke - RestMethod http : // localhos t : 7000/api /reservation - Method GET


Эта команда применяет командлет PowerShell по имени Invoke - RestMethod для
отправки запроса GET на URL вида /api/rese rvati on . Результат разбирается и фор­
матируется для удобства восприятия данных:

reservationid clientName lo cation

О Alice Board Room


1 ВоЬ Lecture Hall
2 Joe Meeting Room 1
Глава 20 . Контроллеры API 629
С е рвер отвечает на зап рос GET представлением JSON объектов Reservat i on , со­
дер жащихс я в модели, которое командлет Invok:e - RestMethod выводит в табличном
формате.

Формат JSON
Формат JSON (JavaScript Object Notatioп - система обозначений для объектов JavaScript)
стал ста ндартным форматом данных для веб-приложений . Формат JSON популярен бла­
годаря простоте, лаконичности и легкости в работе с ним . Данные JSON особенно легко
обрабатывать в коде JavaScript, потому что формат JSON подобен способу выражения ли ­
теральных объектов на языке JavaScript. Современные браузеры включают встроенную под­
держку для генерации и разбора данных JSON , а популярные библиотеки JavaScript, такие
как jQuery, будут автоматически преобразовывать в и из формата JSON.
Хотя формат JSON эволюционировал из JavaScript, его структура легка в чтении и понима­
нии для разработчиков на языке С# . Вот как выглядит ответ из контроллера API в примере
приложения:

[ { " reservationid ": О , " c l ie n tName " : " Alice ", " location ": "Board Room " } ,
{ " reservation i d " : 1 , " c li entName ": " ВоЬ ", " location ": " Lectur e Ha ll"},
{" reservationid ": 2 , " cl ientName ":" Joe ", " location ":" Meeting Room 1 " }]

Здесь описан массив объектов, который обозначается символами [ и J , а каждый объект


отмечается символами { и } . Объекты являются коллекциями пар "ключ-значение", где каж­
дый ключ отделяется от своего значения с помощью двоеточия ( : ), а пары разделяются
запятыми (, ). Это отдаленно напоминает синтаксис литералов С# , который использовался
в классе MernoryReposi tory для определения данных в листинге 20.3:

new Lis t <Reservat i on> {


new Reservation { ClientName "Alice ", Loca ti on = " Board Room " } '
new Reservation { ClientName " ВоЬ ", Location " Lecture Hall " } '
new Reservation { Cli entNarne "Joe ", Location = "Meeting Room 1 " }

Однако обратите внимание, что MVC заменяет соглашение С# по записи имен свойств , на­
чиная с заглавной буквы (например, ClientNarne), соглашением JavaScript (clientNarne
начинается со строчной буквы).

Несмотря на то что форматы не идентичны, имеющегося подобия вполне достаточно для


того , чтобы разработч ик С# мог читать и понимать данные JSON, не прикладывая больших
усилий. В большинстве веб-приложений вам не придется погруж аться в детали JSON, т.к.
всю рутинную работу берет на себя инфраструктура MVC , но вы можете почитать о формате
JSON на веб-сайте www . j son . org.

Контроллер Reservation пр едоставляет две операции GET. Когда запрос GET от­
правляется на URLвида /api/reservation , возвращается ответ, содержащий все
объекты в м одели . Чтобы извлечь одиночный объект, его значение Reservationid
указывается как финальный сегмент в URL, наприм е р:

Invoke - RestMethod http://localhost : 7000/api/reservation/1 -Method GET


Эта команда запр ашивае т объ ект Reserva t ion со значением 1 в свойстве
Reservationid и выводит следующий результат:
630 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Соге MVC

reservationid clientName location

1 ВоЬ Lecture Hall

Тестирование операции POST


Все операции, предоставляемые контроллером АР!, могут быть протестированы
с применением PowerShell, хотя формат команд может оказаться слегка неудобным.
Вот как выглядит команда, которая отправляет контроллеру АР! запрос POST для со­
здания нового объе1па Reservation в хранилище и записывает полученные данные
в ответ:

Invoke-RestMethod http://localhost:7000/api/reservation
-Method POST -B ody (@
{ clientName = "Anne "; location = "Meeti ng Room 4"} 1 ConvertTo-Json)
- ContentType " application/json"
В приведенной команде с помощью аргумента -Body указывается тело запроса,
которое кодируется как JSON. Аргумент -ContentType используется для установки
заголовка Content-Type в запросе. Команда даст следующий результат:
reservationid clientName locati on

3 Anne Meeting Room 4


Операция POST применяет значения clientName и location, чтобы создать объект
Reservation и возвратить клиенту представление JSON нового объекта, которое вклю­
чает присвоенное ему значение Reservationid. Может показаться, что клиент прос­

то получает значения данных, которые он отправил серверу в запросе. Тем не менее,


такой подход гарантирует, что клиент работает с теми же самыми данными, которые
использует сервер, и учитывает любое форматирование или трансляции, вьшолняемые
сервером в отношении полученных от клиента данных. Чтобы взглянуть на эффект от
запроса POST, отправьте еще один запрос GET на URLвида /ap i /reservation:
Invo ke-RestMet hod http://localhost:7000/api/reservation - Method GET
Возвращенные данные отражают добавление нового объекта Reservation :
reservationid clientName location

О Alice Board Room


1 ВоЬ Lecture Ha ll
2 Joe Meeting Room 1
3 Anne Meeting Room 4

Тестирование операции PUT


Метод PUT применяется для изменения существующих объектов в модели.
Значение Reservationid объекта указывается как часть URL запроса, а значения
clientName и l ocation предоставляются в теле запроса. Вот команда PowerShell,
которая отправляет запрос PUT для модификации объекта Reservation:

Invoke -RestMethod http://localhost:7000/api/reservation


-Method PUT -Body (@
{ reservationid = " l"; clientName = " ВоЬ "; location =
" Media Room"} 1 Convert To-Js on) - ContentType " app li cat i on/j son "
Глава 20. Контроллеры API 631
Этот з апрос изменяет объ е кт Re s e r vation со значением 1 в свойств е
Reservat i onid и указывает новое значение для свойства Location . Выполнив ко­
манду , вы увидите следующий ответ, который отражает внес енное изменение:

reservationid c l ientName location

1 Во Ь Medi a Room
Чтобы взглянуть на эффект от запроса POST , отправьте еще один запрос GE T на
URL вида /api/reservation:
Invo ke - RestMe t hod http : //localho s t : 7000/api/reservation - Method GET
Возвраще нны е данные содержат добавленный новый объект Res ervation:
reser·vationid clientName l ocatio n
------------- ----------
--------
о Al i ce Board Room
1 ВоЬ Media Room
2 Joe Meeti ng Room 1
3 Anne Meet i ng Room 4

Тестирование операции Del.et;e


Последний тест заключается в отправке запроса DE LETE, который удал ит объект
Reservation из хранилища:

Invoke-RestMethod http : // l oca l hos t: 7000/api/reservation/2 - Me thod DELETE


Де йствие, которое принимает запросы DE LE TE в контроллере Re s ervat i on , не
возвращает результатов, поэтому после завершения команды никакие данные не отоб­
раж аются . Чтобы просмотреть эффект от удаления, запросите содержимое хранили­
ща с использованием такой команды:

Invoke - RestMethod http : // l oca l hos t: 7000/api/reservation - Method GET


Объект Res e rvation со значением 2 в свойстве Rese r vat i on id был удален из
хранилища :

reservationid c l ientName location

О Al ice Board Room


1 В оЬ Me di a Room
3 An ne Meeting Room 4

Использование контроллера API в браузере


Определение контроллера API решает проблему открытости в приложении, но ни­
чего не делает в плане решения проблем скорости и эффективности . Для этого по­
надобится обновить часть HTML приложения, чтобы при отправке НТТР-запросов
к 1юнтроллеру API с целью выполнения операций над данными она полагалась на
JavaScript .
В бр аузере асинхронные НТТР-запросы обычно известны как запросыАjах, где Ajax
представляет собой аббревиатуру для Asynchronnus JavaScript and ХМL (асинхронный
JavaScript и ХМL) . В последние годы формат данных ХМL подрастерял свою популяр­
ность . но наз вание Ajax по-прежнему применяется для ссылки на асинхронны е НТТР­
запросы, даже когда они возвращают данные JSON. Выражаясь более широко , опи-
632 Часть 11. Подробные сведения об инфраструктуре ASP.NET Саге MVC

санный в настоящем разделе прием является основой одностраничных приложений,


где код JavaScript внутри единственной НТМL-страницы используется для извл е чения
данных. которые предназначены множеству разделов прилож ения, с динамической
генерацией подлежащего отображению содержимого.

На заметку! Разработка клиентской стороны является отдельной темой и выходит за рам­


ки настоящей книги . Здесь мы создадим только базовый асинхронный НТТР-запрос без
подробных объяснений, просто чтобы дать вам представление о том, как это делается .
Детальное объяснение использования JavaScript и jQuery для создания одностранич­
ных приложений, которые потребляют службы из контроллеров API, ищите в моей книге
Pro ASP.NET Core MVC Client Development (издательство Apress).

Браузеры предоставляют АРl-интерфейс JavaScript для выполнения запросов Ajax,


но работать с ним несколько неудобно. к тому же есть отличия в том, как браузеры
реализуют некоторые дополнительные средства. Выполнять запросы Ajax проще все­
го с применением библиотеки jQuery, которая является бесконечно полезным инстру­
ментом для разработ1ш клиентс1юй стороны. В листинге 20.12 показано содержимое
файла bower . j son с добавленным пакетом jQuery.

Листинг 20.12. Добавление пакета jQuery в файле bower. j son

" name ": " asp .net ",


" private ": true,
" dependencies ": {
"b ootst r a p": " З . 3 . 6 ",
"jquery": "2. 2. 4"

На самом деле. поскольку средства Bootstrap зависят от jQuery. инструмент Bower


уже установил пакет в папку wwwroot/ lib. Добавление в листинге 20.12 просто де­
лает такую зависимость явной.

На заметку! При добавлении пакетов в проект с использованием Bower вы не всегда будете


получать ожидаемые номера версий . Диспетчер пакетов NuGet загрузит несколько версий
пакетов и организует их бок о бок, чтобы гарантировать предсказуемую работу всех функ­
ций , однако Bower не может это делать, т.к. браузеры не способны иметь дело с разными
версиями того ж е кода во время выполнения. На момент написания главы последней вер­
сией библиотеки jQueгy была 3.0.0, но в листинге 20.12 указана версия 2.2.4, потому что
с ней работает Bootstrap 3.3.6. Указание jQuery 3.0.0 в файле bower . j son не приведет к
обновлению jQuery инструментом Bower, поскольку это вызовет несоответствие заданной
версии и требуемой версии Bootstrap .

Чтобы задействовать средства, предлагаемые jQuery, создайте папку wwwroot/j s


и добавьте в нее файл по имени c li ent . j s с содержимым из листинга 20.13.
Глава 20. Контроллеры API 633
Листинг 20.13. Содержимое файла client.js из папки wwwroot/js

$(document) . ready(function () {
$( " form " ) .submit(function (е)
e . preventDefault() ;
$ . ajax({
url: " api/reservation ",
content Type: " application/json ",
method: " POST "'
data : JSON.stringify({
clientName : this.elements [" ClientName "] . val ue,
location : this.elements[ " Location"] . value
}) '
success : function(data)
addTaЬleRow(data) ;
}
})
}) ;
}) ;

var addTaЬleRow = function (reservation)


$ ( " tаЫе tbody" ) . append ( " <t r><td>" + reservation. reservationid
+ " </td><td> "
+ reservation . clientName + " </td><td>"
+ reservat ion.location + " </ td></tr>");

Когда пользователь отправляет форму в браузере, файл JavaScript создает ответ,


кодирует данные формы как JSON и отправляет их серверу с применением НТТР­
за прос а POST. Данные JSON, возвращаемые сервером, автоматически разбирают­
ся jQuery и затем используются для добавления строки в НТМL-таблицу. В листин­
ге 20.14 обновлена компоновка с целью включения элементов script для библиотеки
jQuery и файла client . j s.

Листинг 20.14. Добавление ссылок на файлы JavaScript в файле _ Layou t. csh tml
<html>
<head>
<meta name= "viewport " content="width=device - width " />
<t itle >RESTful Controllers</title>
<link asp-href - include= "l iЬ/bootstrap/dist/css/*.min . css "
rel= " stylesheet" />
<script src="l.iЬ/jquery/dist/jquery.js"></script>
<script src="js/client.js"></script>
</head>

<body class= " panel -b ody " >


@RenderBody ()
</body>
</html>
634 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC

Первый элемент script сообщает браузеру о необходимости загрузки библиотеки


jQuery, а второй элемент script указывает файл, который будет содержать специ­
альный код.
Запустив приложение и создав с помощью НТМL-формы объект Reservation в
хранилище приложения, вы не заметите каких-либо визуальных отличий, но если вы
просмотрите НТТР-запрос, отправленный браузером, то увидите, что он требует на­
много меньше данных, чем бьmо в синхронной версии приложения. При простом тес­
тировании асинхронный запрос потребовал 480 байтов данных, что составляет около
40% объема, необходимого в случае синхронного запроса. В реальных приложениях
улучшения будут более существенными, т.к. размер данных имеет тенденцию быть го­
раздо меньше размера НТМL-документа, который применяется для их отображения.

Форматирование содержимого
Когда метод действия возвращает объект С# в качестве своего результата, инф­
раструктура МVС обязана выяснить, какой формат данных должен использоваться
для кодирования объекта и отправки его клиенту. В настоящем разделе объясняется
стандартный процесс, а также влияние на него запроса, отправляемого клиентом, и
конфигурации приложения. Для содействия прояснению работы процесса добавьте в
папку Contro lle rs файл класса по имени ContentController . cs и определите в
нем контроллер, приведенный в листинге 20 . 15.

Листинг 20.15. Содержимое файла ContentController.cs из папки Controllers


using Microsoft.AspNetCore.Mvc;
using ApiControllers . Models ;
namespace ApiControllers . Controllers
[Route ( "api/ [controller] " )]
puЫic class ContentController : Controller
[HttpGet("string")]
puЬlicstring GetString() => "Thi s is а string response";
[ HttpGet ( " obj ect " )]
puЫic Reservation GetObject() => new Reservation {
Reservationid = 100 ,
ClientName = " Joe ",
Location = " Board Room "
};

Для двух действий в контроллере Con ten t указаны статические переменные


сегментов как аргументы атрибута HttpGet, что означает возможность достичь их
посредством URL вида /api/contro l ler/string и /api/controller/object .
Контроллер Content даже близко не следует паттерну REST, но он облегчит понима­
ние , каким образом работает согласование содержимого.
Формат содержимого, выбираемый MVC, зависит от четырех факторов: форматов,
которые клиент будет принимать, форматов, которые МVС может выпускать, поли­
тики содержимого, указанной действием, и типа, возвращаемого методом действия.
Процесс выяснения , каким образом все сочетается друг с другом, может выглядеть
Глава 20 . Контроллеры АР! 635
пугающим , но хорошая новость в том, что стандартная политика прекрасно работает
для большинства приложений . Вы должны понимать , что происходит "за кулисами" ,
только при необходимости внесения изменений или в случае . если результаты полу­
чаются н е в том формате , который ожидался.

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


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

1. Если м етод действия возвращает объект s t ring , то он отправляется клиенту


не модифицированным, а заголовок Content - Type ответа устанавливает ся в
text/p l a i n .
2. Для всех остальных типов данных, включая прочие простые типы вроде i nt ,
данные форматируются как JSON, а заголовок Content- Type ответа устанав­
ливается в a ppl icat i on / j s o n.
Строки получают специальную интерпретацию из-за того, что они могут вызвать
проблемы при кодировании в JSON. В результате кодирования других простых типов ,
таких как значение 2 типа in t языка С#. получается строка в кавычках, подобная " 2 ".
Когда кодируется строка, итогом будет два набора кавычек, так что "He l l o " ста­
новится "" He ll o "". Не все клиенты хорошо стравляются с таким кодированием,

поэтому надежнее применять формат t e xt / pl ai n и полностью обойти проблему .


Проблема возникает редко, потому что лишь немногие приложения отправляют зна­
чения stri n g; гораздо чаще посылаются объекты в формате JSON. Оба исхода мож ­
но просмотреть с использованием PowerShell. Вот команда. которая вызывает метод
GetString () ,который возвращает значение st ring :
Invoke-WebReque s t http : // l ocal ho s t : 7000/api/content/s tri ng
select @{n = ' Content -T ype '; e = {
$ . Headers ." Con ten t-T ype " } }, Conte nt
Данная ком анда отправля ет запрос GE T на URL вида /api/content/string и об­
рабатывает ответ для отображения з аголовка Con tent -Type и содержимого отв ет а.

Совет. Во время применения командлета In v oke-W e b Re qu es t может быть получена


ошибка, если не была выполнена начальная настройка для lnternet Explorer. Это особенно
вероятно на машине Windows 10, где lnternet Explorer заменен браузером Edge. Проблему
легко устранить, запустив IE и выбрав требуемую начальную конфигурацию.

Ко м анда выдает следующий вьmод:

Content - Type Con tent

text/plain ; c ha r s e t = ut f - 8 Th i s is а s tr i ng r es pon se
Ту ж е с амую команду можно также использовать для отображения результата в
формате JSON. изменив лишь запрашиваемый URL:
Invoke - WebRe ques t http : //loca l ho s t :7 000/ api/content/object
select @{n = ' Content-T ype '; e = {
$ .Headers ." Con t e nt - Type " }}, Conten t
636 Часть 11. Подробные сведения об инфраструктуре ASP.NET Саге MVC

Команда генерирует вывод, сформатированный для ясности, который показывает.


что ответ был закодирован в JSON:
Content - Type Content

appl i cation /j so n; cha r set utf-8 {" rese r va ti onid ": l OO,
'' clientName '' : '' Joe '',
" loca t ion ":" Board Room " }

Согласование содержимого
Большинство клиентов будут включать в запрос заголовок Accept , ука з ывающий
набор фор м атов, которы е они готовы получать в ответе , выр аженный в виде множ ес­
тва типов MIME. Ниже прив еден заголовок Accep t, который посылает в з апросах бр а ­
узер Google Chrome:
Acc ept : text / html, appl ic ation/xhtml + xml , app licat i on/xml ;
q = 0 . 9,image/webp , */* ; q = 0 . 8
Заголовок указывает, что Chrome может обраб атывать форматы HTML и XHTML
(ХНТМL - это совместимый с ХМL диалект НТМ L), ХМL . а также формат и з ображени й
WEBP. Значения qв заголовке задают относительные предпочт ения, где зн ачение 1. О
является стандартным. Указание знач ения q, равного О . 9, для application/xml со­
общает серверу о том, что Chrome будет прини м ать данные XML. но пр едпочита ет
и м еть дело с HTML или XHTML. Последний элем е нт, * / *. уведомляет сервер о том,
что Chrome будет принимать любой формат, но его значение q указывает наименьшее
предпочтени е с реди п е р е численных типов. В итоге это означает, что заголово1t Accept.
отправляемый браузером Chrome, снабжает сервер следующей информацией.

1. Бр аузер Chrome предпочитает получать данные HTML или ХНТМL л ибо изобр а ­
жения WЕВР.

2. Если эти форматы не доступны, тогда следующим наиболее пр едпочтит ельным


форматом является ХМL.

3. Если ни один и з предпочтительных форматов не доступен, то Chrome будет при ­


нимать любой формат.

Вы мож ете предположить . что для изменения формата з апроса, получ е нного от
приложения MVC, достаточно установить заголовок Ac cept , но он не работает подоб­
нь1м образом - точнее он не работает подобным образом из-за того, что требуется
определенная подготовка. Для начала вот команда PowerShell, посылающая GET м е ­
тоду GetObj ect () запрос с заголовком Accept , в котором указано, что клиент будет
принимать только данные ХМL:

Invo ke - WebRequest http : //loca l host : 7000/ap i /cont e nt/object


-Headers @{Accept="application/xml"} 1
se l ect @{ n = ' Content -Type '; е = { $ . Headers. "Content- Type " }} , Content
Ниже показаны результаты, где видно, что сервер отправил ответ applica t ion/
j s on:
Content - Type Con t en t

a pplication/ j son ; char s et utf - 8 {" reservationi d": lOO ,


" c l ientName ":"Joe ",
"l ocation ":"Board Room " }
Глава 20. Контроллеры АР! 637
Включение заголовка Accept никак не влияет на формат, хотя сервер отправил
клиенту ответ в формате, 1шторый не был указан. Проблема в том, что по умолчанию
инфраструктура МVС сконфигурирована для поддержки только формата JSON, поэ­
тому нет других форматов, которые можно было бы применять. Вместо возвращения
ошибки инфраструктура MVC посылает данные JSON в надежде на то, что клиент
сумеет их обработать, даже при отсутствии такого формата в числе указанных в за­
головке Accept запроса.

Конфигурирование сериализатора JSON


Для сериализации объектов в формат JSON инфраструктура
ASP.NET Core MVC использует
популярный сторонний пакет JSON Js on . Net. Стандартная конфигурация
под названием
подходит большинству проектов, но может быть изменена, если данные JSON необходимо
создавать специфичес ким образом . Расширяющий метод AddMvc () . AddJsonOptions () ,
вызываемый в классе Startup, позволяет получить доступ к объекту MvcJsonOptions,
посредством которого конфигурируется пакет Json . Net. Описание доступных конфигура­
ционных параметров ищите по адресу www . newtonsoft.com/j son.

Включение форматирования XML


Чтобы увидеть согласование содержимого в работе, понадобится предоставить
инфраструктуре MVC определенный выбор в форматах, которые она применяет для
кодирования данных ответа. Несмотря на то что JSON стал стандартным форматом
для веб - прилож ен ий , MVC мож ет также поддерживать 1<одирование данных как ХМL.
Добавьте пакет NuGet, содержащий поддержку ХМL, в раздел dependen ci es файла
proj ec t . j son , как продемонстрировано в листинге 20.16.

Листинг 20.16. Добавление пакета форматирования XML в файле project. j son

" dependencies ": {


"Microsoft.NETCore . App ":
"version": " 1 . 0 . 0 ",
" type ": " platforrn "
} ,
"Microsoft . AspNetCore . Diagnostics ": "1. 0 . 0 ",
"Microsoft . AspNetCore . Server. II Sinteg ra tion ": "1. О . О",
" Microsoft . AspNetCore . Server . Kestre l": " 1 . О . О ",
"Microsoft . Extensions . Logging . Console ": " 1 . 0 . 0 ",
"Microsoft . AspNe tCore. Mvc": " 1 . О. О ",
"Mic r osoft . AspNetCore .S taticFiles ": " 1 . 0 . О ",
"Microsoft .Asp NetCore .Ra zor .Tools ":
" version ": "1. 0 . 0- preview2-final" ,
" type ": "bui ld "
} ,
"Мicrosoft.AspNetCore .Mvc. Formatters .Xml": "1. О. 0"
} 1

Добавление пакета Microsoft. AspNetCore. Mvc . Formatters . Xml предостав­


ляет доступ к расширяющим методам, которые можно использовать для включения

форматирования ХМL в файле Startup . cs (листинг 20. 17).


638 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

Совет. Можно создать собственный формат содержимого , унаследовав его от к ласса


Microsoft . AspNetCore. Mvc . Forma tters. OutputFormatter . Это ред к о при­
носит пользу, потому что создание специального формата данных не является удобным
способом открытия доступа к данным в приложении, а са м ые распространенные форма­
ты - JSON и XML - уже реализованы.

Листинг 20.17. Включение форматирования XML в файле Startup. cs

using Microsoft . AspNetCore . Builder;


u sin g Microsoft.Extensions . Dependencyin je ct i on ;
using ApiControllers . Models;
namespace ApiControl lers {
puЬl i c class Startup {
puЬlic void ConfigureServices(IServiceCo ll ection services) {
services . AddSingleton<IRepository , MemoryRepository>() ;
services.AddМvc() . AddXmlDataContractSerializerFormatters();

puЫic void Conf i gure ( IApplicationBuilder арр) {


app . UseStatusCodePages() ;
app . UseDeveloperExceptionPage() ;
app . UseStaticFiles() ;
app . UseMvcWithDefaultRoute() ;

Когда инфраструктуре MVC доступен только формат JSON , у нее нет никакого вы­
бора кроме кодирования ответов как JSON. Теперь . когда выбор есть, м о жно пон аблю­
дать за более полной работой процесса согласования содержимого.

Совет. В листинге 20.17 применялся расширяющий метод AddXmlDataContract


SerializerFormatters (), но м о ж но так же использовать расширяющий ме тод
AddXmlSerializerFormatters () , который предоставляет доступ к более старо­
му классу сериализации. Это мо жет быть полезно при необходимости генерации ХМL­
содер жи мого для более старых клиентов .NET.

Вот команда PowerShell, которая снова запрашивает данные ХМL:

Invoke - Web Request http : //localhost : 7000/api/content/object


-Headers @{Accept = " app li cation/xml " } 1
se l ect @{ n = ' Content - Type '; е = { $ . Headers. " Content - Type " } } , Content
Запустив такую команду, вы увидите, что теперь с ерв ер возвращает данные ХМL,
а не JSON (для краткости атрибуты пространств имен ХМL опущены):

Content - Type Content

application/xml ; charset=utf - 8 <Reservation>


<Cl i entName>Joe</ClientName>
<Location>Board Room</Location>
<Reservationid>lOO</Reservationid>
</Reservation>
Глава 20. Контроллеры API 639

Указание формата данных для действия


Систему согласования содержимого можно переопределить и указать формат дан­
ных прямо на методе действия, применяя атрибут Produces (листинг 20.18) .

Листинг 20.18. Указание формата данных в файле ContentController.cs


using Microsoft . AspNetCore . Mvc;
using ApiControll er s . Mode l s ;
namespace ApiControllers . Controllers
[Route( "api/[controller]")]
puЫic class ContentControl l er : Contro ll er
[HttpGet( " string " )]
puЫic string GetString () => " This is а string response ";
[HttpGet( " object ")]
[Produces("application/json")]
puЫic Reservat ion GetObject() => new Reservation {
Reservation id = 100,
ClientName = "Jo e ",
Location = " Board Room "
};

Атрибут Produces - это фильтр, изменяющий тип содержимого объектов


Obj ectResu l t , которые используются инфраструктурой МVС "за кулисами" для пред­
ставления результатов действий в контроллерах API. В аргументе атрибута указыва­
ется формат, который будет применяться для результата, возвращаемого действием, и
допускается также указание дополнительных разрешенных типов. Атрибут Pr oduces
принудительно устанавливает формат, используемый ответом, который можно про­
смотреть с помощью следующей команды PowerShell:
( Invoke - WebRequest http : //localhost : 7000/api/content/object
- Headers @{Accept = " application/xml " }). Headers. " Content - Type "
Эта команда отображает значение заголовка Content-Type из ответа на запрос
GET к
URL вида /api/content/object. Выполнение команды показывает, что при­
меняется JSON, как указано в атрибуте Produces, хотя заголовок Accept запроса
определя ет, что должен использоваться ХМL.

Получение формата данных из маршрута или строки запроса


Заголовок Accept не всегда находится под контролем программиста, пишущего
код клиента, особенно если разработка проводится с применением старого браузера
либо инструментального набора. В таких ситуациях может быть удобно разрешить
указание формата данных ответа через мар шрут, используемый для нацеливания на
метод действия, или в разделе строки запроса в URL. Первым делом необходимо оп­
ределить в классе Startup сокращенные значения, которые могут применяться для
ссылки на форматы в маршруте или в строке запроса . По умолчанию имеется од~-ю
отображение, в котором j son используется в качестве со1<ращения для application/
j son. В листинге 20.19 добавляется дополнительное отображение для ХМL.
640 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

Листинг 20.19. Добавление сокращения формата в файле Startup. cs


usi ng Microso f t . As p Ne tC o r e . Bu il der;
using Microsoft . Exten si ons . Dep e n d e n cyin j ection ;
using ApiContro ll e r s . Mode l s ;
using Microsoft.Net.Http.Headers;
namespace ApiControl l ers {
puЬlic c l ass Sta r t u p {
p uЫi c vo i d Co n f i g ur eServices (I ServiceCo ll ect i o n servi ces)
services . AddSi n gleton<IRepository , MemoryRe p ository>() ;
services . AddMvc()
. AddXmlDataCont r actSeria l ize r Formatters()
.AddМvcOptions(opts => {
opts.FormatterMappings.SetмediaTypeМappingForFormat("xml",
new MediaTypeHeaderVa1ue("app1ication/xm1"));
}) ;

puЫic void Config u re(IApp l icationBuilder арр) {


app . UseSta t usCodePages() ;
app . UseDeveloperException Page() ;
ap p. UseStaticFi l es() ;
app . UseMvcW ith Defa u ltRoute() ;

Свойство MvcOption s. Fo rmatte rMa p p in g s служит для установки и управл е­


ния отображ е ниями. В листинге 20. 19 с использованием метода
Se t MediaType
Mappi n gForFormat () создается новое отображение, так что сокращени е xml бу­
дет ссылаться на формат applicat i o n/ xml . Дале е понадобится применить атрибут
Format Fil ter к методу действия и дополнительно подкорр ектировать маршрут для
действия, чтобы он включал переменную сегмента format (листинг 20.20).

Листинг 20.20. Использование атрибута FormatFilter в файле ContentController . cs

usi n g Mi crosoft . AspNetCore . Mvc ;


u s ing ApiCo n tro ll er s. Models ;
names p ace ApiContro ll ers . Controlle r s
[Route( " api/[controller] " ) ]
puЬl i c class Conte nt Controller : Con t roller

[ Htt p Get( " str i ng " )]


p uЫic string GetS t ring() => "Thi s is а s t ring response ";
[HttpGet("object/{format?}")]
[FormatFilter]
[Produces("application/json", "application/xml")]
puЬl i c Reservation Ge t Objec t () => new Reserva ti on {
Reservation i d = 1 00 ,
Cl ientName = "Joe ",
Location = " Board Room"
);
Глава 20 . Контроллеры API 641
К методу GetObj ect () бьm применен атрибут Forrna tFil ter, а маршрут для данно­
го действия модифицирован так , что он включает необя зательный сегмент forrnat . Вы
не обя заны использовать атрибут Produ ces в сочетании с атрибутом ForrnatFi l ter,
но е сли дела ете это, тогда будут работать только з апросы, у1<азывающие форматы, для
которых бьm с1юнфигурирован атрибут Produ ces. Запросы, указывающие формат, для
которого атрибут Produ ces н е был СI<онфигурирован , получат ответ
404 - Not Foun d .
Если вы не применяли атрибут Produ ces , то запрос может задавать любой форм ат,
который инфраструктура МVС сконфигурировала для использования.
Кроме того , к атрибуту Pr oduces добавлен формат application/xrnl, чтобы ме­
тод де йствия поддерживал запросы JSON и ХМL . В следующей команде PowerShell
фор мат xrnl указывается как часть URL запроса:
(Invoke - WebRequest http : //loca l host : 7000/api/content/object/xml) .
Headers. " Conten t -Type "
Запус1< этой ко м анды приводит к отображению типа содержимого ответа:

application/xml ; charset = utf - 8


Атрибут ForrnatFil ter ищет переменную с е гмента маршрутизации по имени
format, получ а ет содержащееся в ней сонращенное знач е ние и извлекает ассоции­
рованный формат данных из конфигурации приложения . Затем этот формат приме ­

няется для ответа . Если данные маршрутизации от сутствуют, тогда инспентируется


строка з апроса. Вот команда PowerShell, которая требует использования формата
ХМL посредством строк и запрос а :

(Invoke - WebRequest http : // l ocal h ost : 7000/api/content/


o bj ect?forma t =xml ) . Heade rs." Cont e n t - Typ e "
Формат, найденный атрибутом Forma tF il t er , переопр еделяет любые форматы,
указанны е в заголовк е Accept , что п е р едает выбор формата в руки разработчика
клиентов, даже когда работа производится с инструм е нтальными наборами и браузе­
рами , 1юторы е не позволяют устанавливать заголовок Accept.

В ключение полного согласования содержимого


Дл я большинства приложений отправка данных JSON . когда нет других доступных
форматов, является практичной политикой, поскольку клиент в еб-приложения скорее
некорр е ктно установит свой заголовок Acce pt, ч е м не сумеет обработать JSON. Тем
не мен е е , определенным приложениям придется иметь дело с клиентами , которые вы­

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


указано в заголовке Accept . Обеспечение работоспособности согласования содержи­
мого тр е бует вн ес ения в конфигурацию внутри класса Startup двух и з мен ений , как
иллюстриру ется в листинге 20.21.
Листинг 20.21. Включение полного согласования содержимого в файле Startup. cs
using Microsoft . As pNetCore . Bu i l de r;
using Microsoft .E xtensions .Depe nde n cy inj ection ;
using ApiContro l lers . Models ;
using Microsoft . Ne t.H t t p . Hea de rs ;
using Microsoft . AspNetCo r e . Mvc .Forrna t te r s ;
namespace ApiCon t r ollers {
p u Ыic class Startu p {
puЬlic vo i d Configu r eSe r vices( I ServiceCo ll ection services) {
642 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC

services.AddSingleton<IRepository, MemoryRepository>() ;
services . AddMvc()
. AddXmlDataContractSerializerFormatters()
. AddMvcOptions(opts => {
opts . FormatterMappings . SetMediaTypeMappingForFormat( " xml ",
new MediaTypeHeaderVa l ue( "appli cation/xml" )) ;
opts.RespectBrowserAcceptHeader = true;
opts.ReturnHttpNotAcceptaЬle = true;
}) ;

puЫic void Configure(IApplicat i onBuilder арр) {


app . UseStatusCodePages() ;
app.UseDeveloperExceptionPage() ;
app . UseStaticFiles();
app . UseMvcWithDefaultRoute() ;

Параметр RespectBrowserAcceptHeader применяется для управления тем. пол­


ностью ли соблюдается заголовок Accept . Параметр ReturnHttpNotAcceptaЬle
используется для управления тем, будет ли клиенту отправляться отв ет 406 - Not
АссерtаЫе (406 - н е приемлемо) . если подходящий форм ат отсугствует.
Нужно также удалить атрибут Produces и з метода действия, чтобы процесс согла­
сования содержимого не переопр еделялся (листинг 20.22).
Листинг 20.22. Удаление атрибута Produces в файле Contentcontroller. cs

using Microsoft.AspNetCore . Mvc ;


using ApiControllers . Models ;
namespace ApiControllers.Controllers
[Route( " api/[controller] " ) J
puЫic class ContentController : Controller
[HttpGet ( " string " )]
puЫic string GetString () => "T his is а string response ";
[HttpGet( " object/{format?} " )]
[ FormatFilter]
//[Produces("application/json", "application/xml")]
puЫic Reservation GetObject() => new Reservation {
Reservationid = 100 ,
ClientName = " Joe ",
Location = " Board Room "
};

Ниже показана команда PowerShell, которая отправляет запрос GET на URL вида
/ap i /content/object с заголовком Accept , указывающий тип содержимого, кото­
рый приложение не мож е т предоставить:

Invoke-WebRequest http://localhost : 7000/ap i /content/object


- Headers @{Accept = " application/custom " }
В ре зультате запуска этой команды отображается сообщение об ошибке 406, ука­
зывающее клиенту на то , что сервер не смог предоставить затребованный формат.
Глава 20. Контроллеры API 643

Получение разных форматов данных


Когда клиент посылает данные контроллеру, скажем, в запросе POST, с помощью
атрибута Consumes можно указывать разные методы действий для обработки специ­
фических форматов данных (листинг 20.23).
Листинг 20.23. Обработка разных форматов данных в файле ContentController. cs

using Microsoft . AspNetCore . Mvc;


using ApiControllers . Models ;
namespace ApiControllers .Controllers
[Route("api/[controller]")]
puЫic class ContentController : Controller
[HttpGet("string")J
puЬlic string GetString () => "This is а string response";
[HttpGet( " object/{format?} " )]
[FormatFi lter]
//[Produces( "application/json ", " application/xml " )]
puЫic Reservation GetObject() => new Reservation {
Reservationid = 100,
ClientName = "Joe",
Location = "Board Room "
};
[HttpPost]
[Consumes("application/json")]
puЬlic Reservation ReceiveJson([FromВody] Reservation reservation) {
reservation.ClientName = "Json";
return reservation;

[HttpPost]
[Consumes("application/xml")]
puЫic Reservation ReceiveXml([FromВody] Reservation reservation) {
reservation.ClientName = "Xml";
return reservation;

Действия ReceiveJson и ReceiveXml принимают запросы POST. Отличие между


ними связано с форматом данных, указанным в атрибуте Consumes, который иссле­
дует заголовок Content -Type, чтобы выяснить, может ли метод действия обработать
Content -Type, установ­
запрос. В результате, когда поступает запрос с заголовком
ленным в application/j son, будет применяться метод Recei veJson (),но если за­
головок Content-Type установлен в application/xml , тогда будет использоваться
метод Recei veXml () .

Резюме
В настоящей главе объяснялась роль, которую контроллеры API играют в при­
ложениях МVС. Было продемонстрировано, каким образом создавать и тестировать
контроллер АР! и как выполнять НТТР-запросы с применением jQuery. Вдобавок был
описан процесс форматирования содержимого. В следующей главе более подробно
рассматривается работа представлений и механизмов визуализации.
ГЛАВА 21
Представления

главе 17 было показано. что методы действий могут возвращать объекты


в ViewResul t, которые сообщают инфраструктуре MVC о необходимости визуа­
лизации представления и возвращения НТМL-ответа клиенту. Вы уже видели исполь­
зование представле ний во многих примерах книги , поэтому должны приблизительно
понимать, что они делают, но в этой главе мы погрузимся в детали.
Мы начнем с того, что покажем , как инфраструктура MVC обрабатывает объекты
ViewResul t с примен ени ем механизмов вuзуалuза4uи, и продемонстрируем созда­
ние специального механизма визуализации. Кроме того, мы опишем приемы эффек­
тивной работы со встроенным механизмом визуализации Razor, в том числе использо­
вание частичных представлений и разделов компоновки, которые являются важными
темами для разработки эффективных приложений MVC. В табл . 21.1 приведена свод­
ка, позволяющая пом е стить представл е ния в контекст.

Таблица 21.1. Помещение представлений в контекст

Вопрос Ответ

Что это такое? Представления - это часть паттерна MVC, применяемая для отобра­
жения содержимого пользователю. В приложении ASP.NET Core MVC
представление является файлом, содержащим НТМL-элементы и код
С#, который обрабатывается для генерации ответа

Чем они полезны? Представления позволяют отделять презентацию данны х от логики,


которая обрабатывает запросы. Представления также дают возмож ­
ность применять ту же самую презентацию повсюду в приложении , т.к.

многие контроллеры могут использовать одно и то же представление

Как они В большинстве приложений MVC применяется механизм визуализа-


используются? ции Razor, который облегчает смешивание содержимого HTML и С#.
Представления выбираются за счет возвращения объекта ViewResul t
в качестве результата метода действия, как было описано в главе 17
Существуют ли какие­ Привыкание к использованию Razor и его смеси HTML и С# может
то скрытые ловушки занять какое-то время. В этой главе объясняется работа механизма
или ограничения? Razor, что поможет в прояснении ряда его операций
Существуют ли Для инфраструктуры MVC доступно несколько сторонних механизмов
альтернативы? визуализации , но их применение ограничено

Изменились ли они Razor остается стандартным механизмом визуализации в MVC, но


по сравнению с вер­ во внутренние АРl-интерфейсы были внесены некоторые изменения.
сией MVC 5? Самое важное изменение связано с тем, что представления, частичные
представления и разделы визуализируются асинхронным образом,
чтобы улучшить показатели производительности. Крупнейшим измене­
нием является прекращение поддержки дочерних действий и их замена
компонентами представлений (которые рассматриваются в главе 22)
Глава 21. Представления 645
В т а бл. 21.2 приведена сводка для настоящей главы .

Таблица 21.2. Сводка по главе

Задача Решение Листинг

Соэдание специального механизма Реализуйте интерфейсы 21.1-21.6


визуализации I ViewEngine и I Vi ew
Определение областей содержимо- Используйте разделы Razor 21.7-21 .19
го для применения в компоновке

Соэдание многократно используе- Применяйте частичные 21.20-21.23


мых фрагментов разметки представления

Добавление содержимого JSON Используйте выражение 21.24-21.26


в представления @Js on. Seri a l ze
Изменение местоположений, в ко - Создайте расширитель местополо- 21.27-21 .31
торых Razor ищет представления жений представлений

П одготов ка проекта дл я приме р а


Создайте новый проект типа Empty (Пустой) по имени Vi ews с использованием шаб­
лона ASP.NET Core Web Application (.NET Core) (Веб-приложение ASP.NEТ Core (.NЕТ Core)).
Добавьт е пакеты МVС в раздел dependencies файла pro j ect . j son (листинг 21 . 1).

Листинг 21.1. Добавление пакетов в файле project. j son

"dependencies ": {
"Microsoft . NETCore . App ":
"version ": "1. 0 . 0",
" type ": "platforrn"
} 1

"Microsoft . AspNe t Core . Diagnost ics ": " 1 . О. О ",


"Microsoft . AspNetCore . Server . II Sin t egr ation ": "1. 0 . 0",
"Microsoft . As pNetCore . Serve r. Kes t r e l": " 1 . 0 . О ",
"Microsoft . Extens i ons . Logg in g . Console": " 1 . 0 . 0",
"Microsoft. AspNetCore. Mvc" : "1 . О. О" ,
11
Microsoft.AspNetCore.StaticFiles": "1.0.0 11 1
"Microsoft.AspNetCore.Razor.Tools":
"version": "1.0.0-preview2-final",
"type": "build"

} 1

" tools ":


"Microsoft .AspNetCore . Server .II Sintegrat i on. Tool s ": "1. 0 . 0- pr evi ew2 -final ",
"Microsoft.AspNetCore.Razor.Tools": "1.0.0-preview2-final"
} 1

" frarneworks ": (


" netcoreappl . O":
" irnports ": [ "dotn e tS . 6 11 1 " por t a Ыe- n et 45+win8 " ]

} '
646 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

" bui l dOptio n s ": {


" e rn itEntryPoint ": tr ue , "p reserveCornp i lationContext ": t r ue
} 1

"ru n t irneOption s ": {


" conf i gProperties ": { " Syst em . GC . Serve r": true }

В ли стинге 21 .2 пока зан кл асс Startu p , 1юторый конфигурирует с р едства . п р едо ­

ставляемые пакетами NuGet .

Листинг 21.2. Содержимое файла Startup. cs


u s i ng Microsoft . AspNetCore . Bu il der ;
using Microsoft . Extensions . De pendencyinjection ;
n amespace Views {
pu Ы ic class S t art up
puЫic void ConfigureServi ces(IServiceCol l ect i on services ) {
services . AddМvc();

puЫic void Configure(IAppl icationBu i lder арр) {


app.UseStatusCodePages();
app.UseDeveloperExceptionPage();
app.UseStaticFiles();
app.UseмvcWithDefaultRoute();

Создайте папку Cont r ollers, добавьте в нее файл класса по имени HomeController.
cs и определите контролл е р. приведенный в листинге 21.3.

Листинг 21.З. Содержимое файла HomeController. cs из папки Controllers


using System ;
u sing Microsoft . AspNetCore.Mvc ;
narne space Vi e ws . Controllers {
puЫic class HorneContro l ler : Controlle r
p u Ьlic Vi ewRes ul t I ndex() {
ViewBag . Message = " Hel l o , Wor l d ";
Vi ewBag . Tirne = DateTirne . Now . ToStr i ng( "HH: mrn:ss " );
ret u rn View( " Deb u gDa t a " ) ;

puЫic ViewResu l t List() => View() ;


Глава 21. Представления 647

Создание специального механизма визуализации


Мы собираемся впасть в крайность и создать специальный механизм визуализа­
ции. В большинстве проектов делать это не придется, т.к. инфраструктура MVC со­
держит встроенный механизм визуализации Razor, синтаксис которого был описан в
главе 5и который применялся во всех примерах, рассмотренных до сих пор (и вскоре
будет использоваться снова).
Ценность создания специального механизма визуализации состоит в том, что вы
увидите происходящее "за кулисами" и расширите свои знания работы МVС. Вдобавок
вы поймете, насколько большую свободу имеют механизмы визуализации при транс­
ляции объектов ViewRe su l t в ответы, предназначенны е клиентам.
Механизмы визуализации - это классы, реализующие интерфейс IViewEngine,
который определен в пространстве имен As pNet Core . Mvc . ViewEngines. Вот опре­
деление интерфейса IViewEngine:
namespace Microsoft . AspNetCore . Mvc . ViewEngines
puЫic interface IViewEngine {
ViewEngineResult GetView(string executingFilePath, string viewPath,
bool isMainPage);
ViewEngineResult FindView(ActionContext context , string viewName ,
bool isMa inPage) ;

Роль механизма визуализации заключается в трансляции запросов к представле­


ниям в объекты ViewEngineResult. Когда инфраструктура MVC нуждается в пред­
ставлении, она начинает с вызова метода GetView (),который дает механизму визу­
ализации возможность предоставить представление, просто используя его имя.

Если методу GetView () не удалось предоставить представление, тогда вызывает­


ся метод FindView (),так что механизм визуализации получ ает шанс поискать пр ед­
ставление с применением объекта ActionContext, который содержит информацию о
методе действия, создавшем объект ViewResu l t .
Работа механизма визуализации связана со снабжением инфраструктуры MVC
объектами ViewEngineResult , которые могут использоваться для генерации ответов.
Создавать экземпля ры класса ViewEngineResul t н апр ямую нельзя, но для создания

э1,земпляров он предлагает статические методы, которые перечислены в табл. 21 .3 .

Таблица 21.З. Статические методы класса ViewEngineResul t

Имя Описание

Found (name , v i ew) Вызов этого метода предоставляет инфраструктуре MVC


запрошенное представление, которое указывается в пара­

метре view . Представления реализуют интерфейс IView


NotFound(name , locations) Вызов этого метода создает объект ViewEngineResul t ,
который сообщает инфраструктуре MVC о том , что за­
прошенное представление не удалось найти. Параметр
l ocat i ons - это перечисление значений string, описы­
вающих местоположения, в которых механизм визуализа­

ции искал представление


648 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

При написании механизма визуализации выбирается один из методов, описан­


ных в табл. 21 .3, для указания исхода запроса к представлению. Метод Found ()
создает объект ViewEngineResul t, который обозначает успешный запрос и пре­
доставляет MVC представление для обработки. Метод NotFound () создает объект
ViewEngineResu l t, который отражает неудачный запрос и снабжает инфраструк­
туру MVC списком местоположений, просмотренных механизмом визуализации во
время поиска представления (они будут отображаться разработчику как часть сооб­
щения об ошибке).
Еще одним строительным блоком системы механизма визуализации является ин­
терфейс IView , который применяется для описания функциональности, обеспечи­
ваемой представлениями вне зависимости от того, какой механизм визуализации их
создал. Интерфейс I View определен следующим образом:

using Microsoft . AspNetCore . Mvc .Rendering ;


using System.Threading .T asks ;
namespace Microsoft.AspNetCore . Mvc . ViewEngines
puЫic interface IView (
string Path { get ; )
Task RenderAsync(ViewContext context) ;

Свойство Ра t h возвращает путь к представлению, что предполагает определение


представлений как файлов на диске. Метод RenderAsync () вызывается инфраструк­
турой MVC для генерации ответа клиенту. Данные контекста пер едаются представле­
нию через экземпляр класса ViewContext, который является производным от класса
ActionContext. В дополнение к свойствам контекста, унаследованным от родитель­
ского класса (которые обеспечивают доступ к запросу, данным маршрутизации, кон­
троллеру и т.д.), класс ViewContext предлагает свойства, удобные для использова­

ния при визуализации ответов: наиболее полезные свойства такого рода приведены
в табл. 21.4.

Таблица 21.4. Полезные свойства класса ViewContext

Имя Описание

ViewData Это свойство возвращает объект ViewDataDictionar y, который


содержит данные представления, предоставленные контроллером

TempData Это свойство возвращает словарь, содержащий временные данные


(как описано в главе 17)
Writer Это свойство возвращает объект TextWri ter , который должен
применяться для записи вывода из представления

Самым интересным из всех перечисленных является свойство ViewData, воз­


вращающее объект ViewDataDictionary. В классе ViewDataDictionary определе­
но несколько полезных свойств, которые предоставляют доступ к модели представ­
ления, данным ViewBag и метаданным модели представления. В табл. 21.5 описаны
наиболее полезные из этих свойств.
Глава 21 . Представления 649
Таблица 21.5. Полезные свойства класса ViewDataDictionary
Имя Описание

Model Это свойство типа object возвращает данные модели, предостав­


ленные контроллером

ModelMetadata Это свойство возвращает объект ModelMetadata, который может


использоваться для рефлексии типа данных модели

ModelState Это свойство возвращает состояние модели, которое рассматри­


вается в главе 27
Keys Это свойство возвращает перечисление значений ключей, которые
могут применяться для доступа к данным ViewBag

Простейший способ посмотреть, как все это работает - IViewEngine,


ViewEngineResul t и IView - предусматривает создание механизма визуализации.
Мы построим простой механизм визуализации, который возвращает один вид пред­
ставления. Представление будет визуализировать результат, содержащий информа­
цию о запросе и данные представления, которые сгенерированы методом действия.
Такой подход позволяет продемонстрировать способ оперирования механизмов визу­
ализации, не увязая в разбор е шаблонов представлений и не занимаясь воссозданием
других средств. которые поддерживает Razor.

Создание специальной реализации интерфейса IView


Начнем с создания реализации интерфейса IView . Добавьте в проект папку
Infrastructure и создайте в ней файл класса по имени DebugDataView. cs, содер­
жимое которого показано в листинге 21.4.
Листинг 21.4. Содержимое файла DebugDataView. cs из папки Infrastructure
using System ;
using System . Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore . Mvc . Rendering ;
using Microso ft.AspNetCore.Mvc.ViewEngines;
namespace Views . Infrastructure {
puЫi c class DebugDataView : IView {
puЫic str ing Path => String .Empty ;
puЫic async Task RenderAsync(Vi ewContext context) {
context .HttpContext . Response . ContentType = "text /plain";
Str ingBui lder sb = new StringBui lder() ;
sb . AppendLine(" --- Routing Data---"); //Данные маршрутизации
foreach (var kvp in context . RouteData . Values) {
sb . AppendLi ne( $ " Key: {kvp . Key} , Value: {kvp.Value} " ) ;

sb . AppendLi ne("---View Data- --" ) ; //Данные представления


foreach (var kvp in context .ViewData) {
sb . AppendLine($ "Key : {kv p.Key}, Value: {kvp.Value} ");

await context.Writer . WriteAsync(sb .T oString() ) ;


650 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Соге MVC

Когда это представление визуализируется, оно записывает детали данных мар­


шрутизации и данных представления, полученные с использовани е м аргумента

ViewCont e x t метода RenderAs ync () . Ответом является простой текст, поэтому с


помощью объекта контекста заголовок Cont en t -T yp e запроса устанавлив ается в
te xt/ pl a in. Без такого присваивания ASP.NET по умолчанию применит t e xt/h t ml ,
что заставит браузер отображать данные в виде единственной неразбитой строки
символов.

Создание реализации интерфейса rviewEngine


Цель механизма визуализации -выпуск объекта Vi ewEngineResult , который со­
держит либо экземпляр реализации I View, либо список мест, где производился поиск
подходящего представления. Теперь, имея рабочую реализацию интерфейса IV i ew,
можно создать механизм ви зу ализации. Доб авьте в папку Infrastructure файл
класса по имени DebugDa t aVi ewEng ine . cs с содержимым из листинга 21.5.

Листинг 21.5. Содержимое файла DebugDataViewEngine. cs из папки Infrastructure


usi ng Mi crosof t. AspNetCore . Mvc ;
using Microsoft . AspNetCore . Mvc.ViewEngines ;
namespace Views . Infrastru ct ur e {
puЫic class Debug DataViewEn gi ne : I Vie wEng ine {
puЬlic ViewEngineRe sult GetV i ew(string executingFi lePath ,
string vi ewPath , bool i sMainPage) {
ret u rn ViewE ngineResu l t. NotFou nd( v iew Path ,
new st ri ng[] { " (Debug View Engine - Ge tV i ew) " }) ;

p u ЬlicViewEngineResult Fin dV i ew(Act i onCo n t e xt cont ex t ,


string v i ewName , boo l is Mai nPage) {
if (vi ewName == "DebugData " ) {
return Vi e wEngineResult . Found(viewName , new DebugDataView()) ;
el se {
return ViewEngineResult . NotFound(vi ewName ,
new str i ng [ ] { " ( Debug Vi ew Engine - FindView) " } ) ;

М етод GetView () в этом механизме визуали з ации всегда во з вр аща е т ответ


No t Found. Метод FindVi ew () поддерживает только одно представлени е, которое на­
зывается DebugDa ta. Когда м еханизм визуализации получает запрос представления
с таким имен ем. он возвращает новый экземпляр класса DebugDataView:

if (viewName == "DebugData" ) {
retur n ViewEngine Resu l t .Found(vie wName, new Deb ugDa t aView( ));

При реализации более сов е ршенного механизма визуализации эта возможность


использовалась бы для поиска шаблонов. В р ассматрив аемом простом примере требу­
ется только создани е нового экземпляра класса DebugDataView. В случае получения
запроса представлен и я, отличного от DebugData, создается ответ NotFound:
Глава 21 . Представления 651

return ViewEngineRes ul t . NotFound(v i ewName ,


new str i ng [] { " ( Deb ug View Eng ine - FindView) " } ) ;

Метод ViewEngineRes ul t. NotFound () предполагает, что механизм визуализа­


ции располагает местами, в которых необходимо искать представления. Это обос­
нованное предположение, поскольку представления обычно являются шаблонами,
хранящимися как файлы проекта . В данном случае искать негде, потому просто воз­
вращается фm<тивное местоположение, которое укажет, какой метод был вызван для
поиска представления.

Регистрация специального механизма визуализации


Механизмы визуализации регистрируются в классе Startup путем конфигуриро­
вания объекта MvcViewOptions , как показано в листинге 21.6.

Листинг 21.6. Регистрация специального механизма визуализации в файле Startup. cs

using Microsoft . AspNetCore . Builder ;


using Microsoft . Extensions . Dependencyinjection ;
using Microsoft . AspNetCore . Mvc ;
using Views.Infrastructure;
namespace Views {
puЬlic class Startup
puЫic void ConfigureServices(IServiceCollection services)
services . AddMvc() ;
services.Configure<МvcViewOptions>(options => {
options.ViewEngines.Insert(O, new DebugDataViewEngine());
}) ;

puЫic void Configure(IApplicationBuilder арр) {


app . UseStatusCodePages() ;
app . UseDeveloperExceptionPage() ;
app.UseStaticFiles() ;
app .U seMvcWi th DefaultRoute() ;

В классе MvcviewOptions опр едел ено свойство viewEngines , которое представ­


ляет собой коллекцию объектов реализации IViewEngine. Механизм Razor добавля­
ется в коллекцию ViewEng i ne с помощью метода AddMvc () , и стандартный меха­
ни зм визуализации дополняется специальным классом.

Когда инфраструктура MVC получает объект Vi ewResult от метода действия,


она вызывает метод FindView () каждого механизма визуализации, содержащего­
ся в коллекции MvcviewOptions. ViewEngines, до тех пор пока не получит объект
viewEngineResul t. который был создан с применением метода Found () .
Порядок, в котором механизмы добавляются в коллекцию MvcViewOptions .
ViewEngine s, важен в случае, если два или большее число механизмов способны об­
служивать запрос к представлению с одним и тем же именем. Чтобы специальный ме-
652 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC

ханизм визуализации имел приоритет, он должен быть вставлен в начало коллекции


ViewEngine s, как это делалось в листинге 21.6.

Тестирование механизма визуализации


Когда приложение запускается, браузер автоматически переходит на корн евой
URL проекта, который будет отображен на действие I ndex контроллера Home. Этот
метод действия использует метод View () для возвращения объекта Vi ewResu lt, ко­
торый указывает представление DebugDa ta.
Инфраструктура МVС обратится к коллекции механизмов визуализации и н ачнет
вызывать их методы FindView () . Поскольку запрошенное представление является
именно тем, на обработку которого настроен специальный механизм визуализации, он
снабжает МVС представлением, отображающим результаты, как показано на рис . 21 .1.

[j localhost55775

,~~- ~ @>\;~st:.55775
1 ---Ro ut i п g Data---
1 кеу: cont roller, val ue : ноте
Ке у :act ion , val ue : Index
--- Vie1<1 Data---
кey : Message, Value : Hel l o, world
ке у: Time, Val ue : 16 : 09 : 05

[___ _______ !

Рис. 21.1. Использование специального механизма визуализации

Чтобы посмотреть , что произойдет, когда ни один из механизмов визуализации не


может предоставить представление , запросите URL вида / Home / Li st . В таком слу ­
чае будет создан объект Vi ewResul t , указывающий представление по имени List ,
которое не способен предоставить ни Razor, ни специальный механизм визуализации.
Отобразится сообщение об ошибке, продемонстрированное на рис. 21.2.

С\ lntt-rNI Server Erro1 Х

1 :nun:~~~~~~~::~~:cu~~-w~~~~~=~n~~I
request.
lnvalidOperationExceprion: The vie1v 'List' was oor found. The follo1ving
locations wеге searcl1ed:
{Debug Viev.1 Engiпe - GetVievv)
(DebLJ9 Vievv E ngiпe - FiпdViev1)
Nievvs7Home/ List.cs11tml
/Vie1vs/Sl1a1·ed/List.csl1tml
Mkrosoft.AspNl?tCore.Mvc. Vie~v En gi ne s.V ie\ч EngineResu l t.En sureSucce ss1u l (IE11un1erab l e· ·1
~1nallocations)

Рис. 21.2. Запрашивание представления, которое не может быть предоставлено


Глава 21. Представления 653
Здесь видно , что сообщения, выдаваемые специальным механизмом визуали з а­
ции , присут ствуют в списке местоположений, в которых выполнялся поиск представ­
ления List , наряду с местоположениями, проверенными механизмом Razor.
Когда нужно гарантировать , что будет применяться только специальный мех а­
ни зм визуализ ации, понадобится вызвать метод Cl ear () на коллекции механизмов
визуализации , чтобы удалить из нее Razor (листинг 21.7).

Листинг 21.7. Удаление других механизмов визуализации в файле Startup. cs


using Microsoft . As pN e tCore . Bui l der ;
using Microsoft . Extens i ons . Depend encyin j ection ;
using Microsoft . As pNe t Core . Mvc ;
using Views.Infra s t r ucture ;
namespace Vi ews {
puЫic class Sta r t u p
puЫic void Co n f igu reServices (I Servi c e Co ll ect i on se r v i ce s)
services . AddMv c() ;
services . Con f ig u re<MvcViewOption s >(options = > {
options.ViewEngines.Clear();
options . Vi ewEng i nes .I nse r t(O , new DebugDataV i ewEngine() ) ;
}) ;

puЬlic v oid Configure(IApp l ication Bui lde r арр) {


app . UseSta t usCodePages() ;
app . UseDeve l o p erExceptionPage() ;
app . OseStat i c Fi les() ;
app . OseMv cW i t hDe f a u l t Route ( ) ;

Если з а пустить прил ожение и снова перейти на URL вида / Home/L i st , то легко
з ам етить , что используется только специальный механизм визуализации (рис . 21.3).

D lnternat Server Error Х

f- С ГФio:~~l10~;557~5/Ho~e(!::st -··---------~-==--- ~

1-;n·-~nha-;;dled·~·
1 request.
xce~b-on o~curr~d whil~ ~;~~~ssing ~h-;--::-
·
1 lпvaliclOperationException: The vie\.Y 'List' 1.vas поt foL1пd. Tl1e follo~vi11g
1 l ocatioпs v1ere sea rched:
1 (Debug View Engi11e - GetVievv>)
. (Debug V iev{ Е пgiпе - FiпdVie\N)
M icrosolt.AspNetCore.Mvc.Vie1\•Engil\es.Vie1•1EngineResult.EnsureSuccesslul(I En un1eraЬle ' 1
1 originallocations)

1,_. __________ .
Онеrу Cookies
.
Heade1s
----·--------------------
w~"'·' . " - . . ~
J .

Рис. 21.3. Применение только специального механизма визуализации


в примере приложения
654 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

Работа с механизмом Razor


В предыдущем разделе у нас была возможность создать специальный механизм
визуализации за счет реализации всего лишь двух интерфейсов. Правда. итогом ста­
ло простое подобие механизма, который генерировал неуклюжее содержимое, но зато
вы увидели, насколько легко инфраструктура МVС позволяет добавлять или заменять
основную функциональность.
Сложность в механизм визуализации привносится системой шаблонов представ­
лений, которая включает фрагменты кода, поддерживает компоновки и обеспечива­
ет оптимизацию производительности. В своем простом механизме визуализации мы
ничего подобного не делали (и по большому счету не должны), потому что встроен­
ный механизм Razor предоставляет все эти средства и многое другое. На самом деле в
Razor доступна функциональность, которая требуется практически во всех приложе­
ниях МVС. Лишь ничтожно малое количество проектов берут на себя труд создавать
специальный механизм визуализации.
Основы синтаксиса Razor приводились в главе 5, а в настоящем разделе будет по­
казано, как использовать другие средства для создания и визуализации представле­

ний Razor. Вы также научитесь настраивать механизм Razor.

Подготовка проекта для примера


Для подготовки проекта к работе с Razor потребуется внести ряд изменений .
Модифицируйте действие Index контроллера Ноте, чтобы оно выбирало стандартное
представление и предоставляло некоторые данные модели (листинг 21.8).
Листинг 21.8. Изменение действия Index в файле HomeController. cs

using System;
using Microsoft . AspNetCore . Mvc;
namespace Views . Controllers {
puЫic class HomeController : Controller
puЫic ViewResul t Index () =>
View(new string[] { "Apple", "Orange", "Pear" }) ;
puЫic ViewResult List() => View() ;

Для снабжения метода действия Index () представлением создайте папку Views/


Ноте и добавьте в нее файл по имени Index . cshtml с содержимым, приведенным в
листинге 21.9.
Листинг 21.9. Содержимое файла Index. cshtml из папки Views/Horne

@model string []
@{ Layout = null;
< !DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>Razor</title>
<link asp-href- include= 11 liЬ/boots trap/d ist/css/* . min. css 11 rel= 11 stylesheet 11 />
</head>
Глава 21. Представления 655
<body class= "panel - body " >
This is а list of fruit narnes:
@foreach (str i ng narne in Model)
<span><b>@narne</b></span>

</body>
</htrnl>

Пр едставл ени е полагается на СSS-библиотеку Bootstrap. Чтобы доб а вить Bootstrap


в про е кт, создайте в корневой п апке проекта ф айл bower . j son с прим енением ш аб­
лон а элемента Bower Configuration File (Файл конфигурации Bower) и добавьте пакет
Bootstrap в е го р а здел dependencies (листинг 20.10).

Л ист и нг 21.1 О. С одержи м ое файла bower. j son

"name ": "asp . net ",


"private ": true ,
"dependencies ": (
"bootstrap": "3. 3. 6 11

Со здайте в п апке Views файл _ Viewimports . cshtml с выр ажением, показан­


ным в л истинге 21. 11, для включ ения встро е нных дескрипторных вспом огательных
классов.

Листинг 21.11. Содержимое файла_Viewimports. cshtml из па пк и Views

@addTagHelper * , Microsoft . AspNe t Core . Mvc . TagHelpers

Финальный подготовительный шаг связан с переустановкой механизмов визуали­


зации в клас с е Startup с целью удаления специального м еханиз ма и удаления вы з о­

ва метода Clear (), который отключает Razor (листинг 21 .12).

Листинг 21.12. Переустанов ка механизмов визуализации в файле Startup. cs


using Microsoft . Asp NetCo r e . Bu i lde r;
using Microsoft . Extensions . Dependency i njection ;
using Microsoft . AspNetCo r e.Mvc ;
using Views .I nfras t ructu r e ;
narnespace Views (
puЫic class Startup
puЫic void ConfigureServices(IServiceCo l lection services)
services . AddMvc() ;
// services.Configure<МvcViewOptions>(options => {
// options.ViewEngines.Clear();
// options. ViewEngine s. Insert (О, new DebugDa taViewEngine () ) ;
// }) ;

puЫic void Configure(IApplicationBuilder арр) {


656 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC

app . UseStatusCodePages();
app.UseDeveloperExceptionPage() ;
app .UseStaticFiles();
app . UseMvcWithDefaultRoute();

Запустив приложение, вы увидите результат, представленный на рис. 21.4.

Рис. 21.4. Запуск примера приложения

Прояснение представлений Razor


Даже начальное понимание работы механизма Razor может помочь поместить
большой объем функциональности в контекст и приоткрыть тайну обработки файлов
CSHTML.
Так каким же образом Razor берет смесь элементов HTML с операторами С# и вы­
пускает содержимое для НТТР-ответа? Ответ довольно прост и основывается на фун­
кциональности МVС, о которой вы уже узнали в предшествующих главах. Механизм
Razor преобразует файлы CSHTML в классы С#, компилирует их и затем создает но­
вые экземпляры каждый раз, когда представлению требуется сгенерировать резуль­
тат. Вот класс С# , который Razor создает для представления Index. csh trnl из лис­
тинга 21.12:
using System.Thre ad ing.Tasks;
using Microsoft . AspNetCore . Mvc;
using Microsoft.AspNetCore.Mvc . Razor ;
using Microsoft.AspNetCore.Mvc.Razor .In ternal ;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace Asp {
puЫi c class ASPV_Views_Horne Index_cshtml : RazorPage<string(]>
puЫic IUrlHelper Url { get; private set ; }
puЬlic IViewComponentHelper Component { get ; private set; }
puЬlic IJsonHelper Json { get; private set; }
puЬlic IHtmlHelper<string(]> Html { get; private set; }
puЫic override async Task ExecuteAsync() {
Layout = null;
WriteLiteral(@"<!DOCTYPE html> <html ><head>
<meta name=" " viewport" " content= "" width=device - width "" />
Глава 21. Представления 657
<tit le>Razor< /t i t l e >
<li n k a s p -h r e f -in c lude = ""l i Ь/b oo tst ra p/d i st /css/ *. m i n . c ss""
r e l ="" sty l es heet "" / >
</ head><body clas s =""panel-body"" >This i s а li s t of frui t names:" ) ;
f or e ach (s t r i ng name in Model)
Write Li t er al( " <sp a n><b>" ) ;
Wr i te( name) ;
Wr i te Li te ra l(" </ b ></spa n>" ) ;
}
Wr ite Li tera l(" </body>< /h t ml >" ) ;

Код этого класса бьm приведен в порядок, чтобы облегчить его чтение. Кроме того,
были удалены операторы С#, которые Razor добавляет с целью оснащения инстру­
ментами при генерации класса. В последующих разделах этот класс разбивается на
части, и приводятся объяснения работы скомпилированных представлений.

На за м етку! Прежде было легко просматривать классы, созданные более ранними версиями
Razor, т.к. для каждого представления генерировался дисковый файл С#, который затем
компилировался и использовался в приложении. Изучение класса сводилось просто к на­
хождению правильного файла. Текущая версия Razor опирается на усовершенствования
компилятора С# , которые позволяют коду генерироваться и компилироваться в памяти,
что обеспечивает улучшение производительности, но затрудняет выяснение происходя­
щего. Чтобы получить показанный выше класс, пришлось изменить предназначение ряда
модульных тестов, входящих в состав исходного кода ASP.NET Core MVC, которые предо­
ставили фиктивные реализации классов, применяемые Razor для нахождения и обработки
файлов представлений. Это не то, что вам придется предпринимать при повседневной
разработке, но такой процесс позволяет выявить очень многие детали о том, как работа­
ют представления .

Имя класса
Лучше всего начать с имени класса, который создается Razor:

p u Ыi c c l ass ASPV_Views_Home_Index_cshtml : Razo rP age<strin g[ J>

Механизму Razor нужен какой-нибудь способ для трансляции имени и пути к фай­
лу CSHTML в кл асс, который он создает, когда производит разбор файла, и он делает
это за счет кодирования информации в имени к11асса. Механизм Razor снабжает имя
класса префиксом AS PV, за которым следует имя проекта, имя контроллера и, нако­
нец, имя файла представления: такая комбинация облегчает проверку доступности
кл асса при запрашивании инфраструктурой МVС представления через интерфейс
I Vi ewEng in e , описанный ранее в главе.

Базовый класс
Многие основны е средства Razor, такие как ссылка на модель представления в
виде @Model , возможны благодаря базовому классу, производными от которого явля ­
ются сгенерированные классы:
658 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC

puЫic class ASPV_Views_Home Ind ex cshtml : RazorPage<string[]> {

Классы представлений наследуются от масса RazorPage или м асса RazorPage<T>,


если с помощью директивы @model был ука з ан тип м одели . Кл а сс RazorPage пр ед­
лаг ает м етоды и с войст ва, которые могут использ оваться в файл ах CSHT ML для до­
ступа к средствам MVC; наиболее поле з ные методы и свой ства масса RazorPage<T>
описаны в табл. 21 .6.

Таблица 21.6. Методы и свойства класса RazorPage<T>,


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

Имя Описание

Model Это свойство возвращает данные модели, предоставляемые


методом действия

ViewData Это свойство возвращает объект ViewDataDictionary,


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

ViewContext Это свойство возвращает объект ViewContext, описанный в


табл. 21.4
Layout Это свойство применяется для указания компоновки, как
было показано в главе 5и снова упоминается в разделе
"Использование разделов компоновки " далее в настоящей
главе

ViewBag Это свойство предоставляет доступ к объекту ViewBag , кото­


рый был описан в главе 17
TempData Это свойство предоставляет доступ к объекту TempData, кото­
рый рассматривался в главе 17
Context Это свойство возвращает объект HttpContext, который опи ­
сывает текущий запрос и подготавливаемый ответ

User Это свойство возвращает профиль пользователя, ассоцииро­


ванного с текущим запросом. Аутентификация и авторизация
пользователей подробно рассматриваются в главе 28
RenderSection () Этот м етод применяется для вставки раздела содер ж имо -
го из представления в компоновку, как описано в разделе

" Использование разделов компоновки " далее в главе

RenderBody ( ) Этот метод вставляет в компоновку все содержимое представ­


ления , которое не содержится внутри раздела. Подробности
ищите в разделе "Использование разделов к омпоновки" далее
в главе

IsSectionDefined() Этот метод используется для выяснения, определен ли раздел


в представлении

Механи зм Razor такж е предлагает ряд вспо м огательных свойств , которые м ож­
но прим енять в представлениях для генерации содержимого (табл. 21 .7).
Глава 21. Представления 659
Таблица 21. 7. Вспомогательные свойства Razor

Имя Описание

HtmlEncoder Это свойство возвращает объект HtmlEncoder , который может использо­


ваться для безопасного кодирования НТМL-содержимого в представлении

Component Это свойство возвращает вспомогательный объект для компонента пред­


ставления (глава 22)
Json Это свойство возвращает вспомогательный объект JSON, как описано в
разделе "Добавление содержимого JSON в представления" далее в главе

Url Это свойство возвращает вспомогательный объект URL, который может


применяться для генерации URL, используя конфигурацию маршрутизации
(глава 16)
Html Это свойство возвращает вспомогательный объект HTML, который может
применяться для генерации динамического содержимого. Данное средство
было почти полностью замещено дескрипторными вспомогательными клас­
сами, но все еще используется для частичных представлений , как объясня ­
ется в разделе "Использование частичных представлений " далее в главе

Методы и свойства, перечисленные в табл. 21.6 и 21.7, вы будете применять при


ежедневной разработке приложений МVС для доступа к данным модели, конфигури­
рования представлений и решения других важных задач. Они приподнимают занавес
над использованием механизма Razor, возвращая его в хорошо понимаемый мир С#.
Например, когда вы получаете доступ к объекту модели представления либо извлека­
ете значение из TempData посредством @TempData, то ссылаетесь на свойства, кото­
рые определены в классе RazorPage.

Визуализация представлений
В дополнение к свойствам и методам, предоставляющим средства для разработчи ­
ков, в обязанности класса
RazorPage также входит генерирование содержимого от­
ветов через его метод
ExecuteAsync () .Этот метод показывает, каким образом Razor
обрабатывает файл Index. cshtml с помощью набора операторов С#:

puЫic override async Task ExecuteAsync() {


Layout = null ;
Wr iteLiteral(@"<!DOCTYPE html><html><head>
<meta name= "" viewport "" content=""width=device - width "" />
<title>Razor</title>
<link asp - href -i nclude= ""liЬ/b ootstrap/dist/css/* .min. css ""
rel= "" stylesheet"" />
</head><body class= "" panel - body "" >This is а list of fruit names :" );
foreach (string name in Model) {
WriteLiteral( " <span><b>");
Write(name) ;
WriteLiteral( " </b></span> ");

WriteLiteral( " </body></html> " ) ;


660 Часть 11. Подробные сведения об инфраструктуре ASP.NET Соге MVC

Значения данных, та~ше как значения свойства Model, отправляются клиенту с


применением метода Wri te (), который защищает строки так, что они не будут ин­
терпретироваться браузером как НТМL-элементы. Этот аспект важен. поскольку пре­
дотвращает попадание в вывод приложения злонамеренных данных из добавленного
содержимого. Метод Wr i teLi teral () не защищает строки и используется для ста­
тического содержимого в файле Index. cshtml, которое, несомненно, браузер должен
интерпретировать как НТМL-элементы. В результате статическое и динамическое со ­
держимое файла CSHTML содержится в обычном 1шассе С# и выпускается посредс ­
твом простого вызова метода.

Добавление динамического содержимого


к представлению Razor
Основная цель представлений - сделать возможной визуализацию частей модели
предметной области пользователям. Для этого не обходимо уметь добавлять к пред­
ставлениям динамическое содержимое. Динамическое содержимое генерируется во
время выполнения и может быть разным для каждого запроса. Оно является проти­
воположностью статического содер;щuмого , такого как НТМL-разм етка , которое со­
здается при написании кода приложения и одинаково для всех запросов. Добавлять
динамическое содержимое к представлениям можно различными способами, которые
описаны в табл. 21.8.

Таблица 21.8. Добавление динамического содержимого к представлению

Прием Когда применяется

Встраиваемый код Используется для небольших и самодостаточных порций логики


представления, таких как операторы if и foreach. Это фундамен­
тальный инструмент для создания динамического содержимого в
представлениях, на основе которого построены другие подходы .
Данный прием был описан в главе 5 и затем применялся в много­
численных примерах

Дескрипторные вспо- Используются для генерации атрибутов в НТМL-элементах. Дескрип-


могательные классы торные вспомогательные классы рассматриваются в главах 23-25
Разделы Применяются для создания разделов содержимого, которые будут
вставляться в специфические места внутри компоновки, как объяс­
няется далее в главе

Частичные Применяются для совместного использования подразделов ком-


представления поновки представления несколькими представлениями. Частичные
представления могут содержать встраиваемый код, дескрипторные
вспомогательные классы и ссылки на другие частичные представле­

ния. Частичные представления не вызывают метод действия, поэто­


му не могут применяться для выполнения бизнес-логики. Частичные
представления описаны далее в разделе

Компоненты Применяются для создания многократно используемых элементов


представлений управления или виджетов пользовательского интерфейса, которые
должны содержать бизнес-логику. Компоненты представлений рас­
сматриваются в главе 22
Глава 21. Представления 661

Использование разделов компоновки


Механизм визуализации Razor поддерживает концепцию разделов, которые поз­
воляют организовывать области содержимого внутри компоновки. Разделы Razor
обеспечивают больший контроль над тем, какие части представления вставляются в
компоновку и куда они помещаются. Чтобы взглянуть на работу разделов, приведите
содержимое файла /Vi ews /Home / I ndex . cshtml в соответствие с листингом 21.13 .

Листинг 21.1 З. Определение разделов в файле Index. csh tml


@mode l string []
@{ Layout = "_Layout";
@section Header {
<div class="bg-success">
@foreach (stringstrinnew []{"Ноте", "List", "Edit"}) {
<а class="Ьtn Ьtn-srn Ьtn-prirnary" asp-action="str">@str</a>
}
</div>
}

This is а lis t of f ru it names :


@foreac h (st ri ng name i n Mod e l )
<span><b>@name</ b ></ s pan>

@section Footer {
<div class="bg-success">
This is the footer
</div>
}

Из пр едставления были удалены некоторые НТМL-элементы и установлено свойс­


тво Layou t для указания на то, что при визуализации содержимого должен приме­
няться файл компоновки по имени _ La y ou t . csh t ml.
Кроме того, в представление были добавлены разделы. Разделы определяются с
использованием Rаzоr-выражения @section, за которым следует имя раздела. В лис­
тинге 21.13 созданы разделы с именами He ader и Foo te r. Раздел содержит обычную
смесь разметки HTML и выражений Razor, которую можно встретить за рамками раз­
делов в друrих примерах.

Разделы определяются в представлении, но применяются в компоновке с помощью .


выражения @Rende r Se ct i on . Чтобы посмотреть, как это работает, создайте папку
Vi e ws/Shared и добавьте в нее файл компоновки по имени _ Layout . c s h tml, содер­
жимое которого показано в листинге 21.14.
Листинг 21.14. Содержимое файла _Layout. cshtml из папки Views/Shared

< ! DOCT YPE h tml>


<h tml>
<head>
<meta name= " vie wport " conte nt = "width=devi c e- wi d t h " / >
<title>@ViewBag . Tit l e</tit l e>
<li n k asp - href - inc l ude= " liЬ/bootstrap/d i s t /css/* .mi n. css " re l="s tyle sheet " />
</head>
662 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

<body class="panel-body">
@RenderSection("Header")
<d iv class="bg-info">
This is part of the layo ut
</ div>
@RenderBody ()
<d iv class="bg-info">
This is part of the layout
</div>
@RenderSection("Footer")
<div class="bg-info">
This is part of the layout
</div>
</body>
</html>

Когда Razor производит разбор компоновки, выражение @RenderSection заменя­


ется содержимым раздела с указанным именем из представления. Части представле­
ния, которые не содержатся в каком-либо разделе, вставляются в компоновку с ис­
пользованием выражения @RenderBody.
Запустив приложение, можно увидеть эффект от применения разделов (рис. 21.5).
Чтобы было совершенно ясно, какие разделы вывода поступают из представления, а
какие - из компоновки, используются стили Bootstrap. Результат далек от идеала, но
четко демонстрирует, как можно помещать области содержимого из представления в
специфические местоположения внутри компоновки.

This is part о! the layout 1


1 This is а list of fruil names: Apple Orange Pear 1
1 This is part of the fayout

l
This is the footer 1
This is part of the layout

-·-···--- ------------"-----·-------- ------""---J


Рис. 21.5. Использование разделов в представлении для размещения
содержимого в компоновке

На заметку! В представлении можно определять только разделы, на которые имеются ссыл­


ки внутри компоновки. При попытке определить в представлении разделы, для которых
отсутствуют соответствующие выражения @RenderSection в компоновке, инфраструк­
тура MVC сгенерирует исключение.
Глава 21. Представления 663
Смешивание разделов с остальной частью представления обычно не делается. По
соглашению разделы определяются либо в начале, либо в конце представления, что­
бы легче было видеть, какие области содержимого будут трактоваться как разделы, а
какие будут обслуживаться выражением @RenderBody. Еще один подход предусмат­
ривает определение представления полностью в терминах разделов, включая раздел

для тела (листинг 21.15).

Листинг 21.15. Определение представления в терминах разделов Razor


в файле Index. cshtml
@model string[]
@{ Layout = " Layout ";
@section Header {
<div class="bg -s uccess " >
@foreach (string str in new [] {"Ноше", "L ist ", "Edit"}) {
<а class= " Ьtn Ьtn -s.m Ьtn-primary " asp - action= " str " >@str</a>

</div>

@section Body
This is а list of frui t names:
@foreach (string name in Model)
<span><b>@name</Ь></span>

@section Footer {
<div class= " bg-success " >
This is the footer
</div>

При таком подходе получаются более чистые представления и уменьшаются шан­


сы захвата излишнего содержимого выражением @RenderBody. Чтобы задейство­
вать этот подход , выражение @RenderBody понадобится заменить выражением @
RenderSection ( " Body " ), как показано в листинге 21.16.

Листинг 21.16. Визуализация тела в виде раздела в файле_Layou t. csh tml


<!DOCTYPE html>
<html>
<head>
<meta name= " viewport " content= " width=device - width " />
<title>@ViewBag.Title</title>
<link asp-href - include= " liЬ/bootstrap/dist/css/* . min . css "
rel= " stylesheet " />
</head>
<body class= " panel - body " >
@RenderSection( " Header " )
<div class= " bg - info " >
This is part of the layout
</div>
664 Часть 11. Подробные сведения об инфраструктуре ASP.NET Саге MVC

@RenderSection ( "Body")
<div class= " bg -inf o " >
This is part of the layout
</div>
@RenderSection ( " Footer " )
<div class= " bg-info " >
This is part of the layout
</div>
</body>
</html>

Проверка существования разделов


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

доставлять специальное содержимое. В листинге 21.17 приведено модифицированное


содержимое файла _Layout . cshtml , в котором предпринимается проверка, опреде­
лен ли раздел Footer.

Листинг 21.17. Проверка, определен ли раздел в представлении, в файле _Layout. cshtml

<!DOCTYPE html>
<html>
<head>
<meta name= " viewport " content= " width=device - width " />
<title>@ViewBag .Ti tle</title>
<l ink asp - href - include= " liЬ/bootstrap/dist/css/* .min. css "
rel= " stylesheet " />
</head>
<body class= " panel -body " >
@RenderSection( " Header " )
<div class= " bg - info " >
This is part of the layout
</div>
@RenderSection( " Body " )
<div class= " bg-info " >
This is part of the layout
</div>
@if (IsSectionDefined("Footer"))
@RenderSection (" Footer")
} else {
<h4>This is the default footer</h4>
}

<div class =" bg - info " >


This is part of the layout
</div>
</body>
</html>
Глава 21. Представления 665
Вспомогательный метод IsSectionDefined () принимает имя проверяемого
раздела и возвращает true, если он определен в визуализируемом представлении.
В настоящем примере IsSectionDefined () применяется для выяснения, должно
ли визуализироваться стандартное содержимое, когда в представлении не определен

раздел Footer.

Визуализация необязательных разделов


По умолчанию представление должно содержать все разделы, для которых в ком­
поновке имеются выражения @RenderSection. Если разделы отсутствуют, тогда ин­
фраструктура MVC сгенерирует исключение. Чтобы удостовериться в этом, добавьте
в файл_Layout. cshtml новое выражение @RenderSection для раздела по имени
scripts (листинг 21.18).

Листинг 21.18. Визуализация несуществующего раздела в файле_Layout. cshtml

<!DOCTYPE html>
<html>
<head>
<meta name= " viewport" content= " width=device - width " />
<title>@ViewBag.Title</title>
<link asp-href-include=
"liЬ/bootstrap/dist/css/*.min.css" rel="stylesheet " />
</head>
<body class="panel -b ody " >
@RenderSection("Header ")
<div class= " bg -i nfo">
This is part of the layout
</div>
@RenderSection("Body " )
<div class= " bg-info " >
This is part of the layout
</div>
@if (IsSectionDefined("Footer"))
@RenderSection("Footer " )
else {
<h4>This is the default footer</h4>

@RenderSection ( "scripts")
<div class="bg-info " >
This is part of the layout
</div>
</body>
</html>

После запуска приложения механизм Razor попытается визуализировать компо­


новку и представление, в результате чего возникает ошибка (рис. 21.6).
666 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

[.:J lnt~mol S~IV<r Error Х

С ~ locall1o>t:SS775 . '{:: J• 1

Гдn-~~handled
request.
exception occu~~~d \tVhil~ pгocessin; th;
'· 1

lrlvaliclOperatio nExcep iоп : Тfle layoLJt P.age


'/Views/SflarecJ/ _Layout.cshtml' са not fiпa the sectioп 'suipts' in iJ1e
coпtent page 'J\lie\'IS/Home/l пdex.csl1tml'. 1

1 Move№xt

~: __Qll.::_ Cookies __ Headers


------·---- _______:.J
1

Рис. 21.6. Сообщение об ошибке, отображаемое из-за отсутствия раздела

Можно задействовать метод IsSectionDefined ( ) , чтобы избежать выполнения


выражений @RenderSection для разделов, которые не определены в представлении,
но более элегантный подход предусматривает использование необязательных разде­
лов, для которых в @RenderSection указывается дополнительный аргумент false
(листинг 21.19).

Листинг 21.19. Создание необязательного раздела

@RenderSection("scripts ", false )

В итоге создается необязательный раздел, содержимое которого будет вставлено в


результат, если представление определено, а его отсутствие не приведет к генерации

исключения.

Использование частичных представлений


Часто возникает необходимость в применении одних и тех же фрагментов дескрип­
торов Razor и разметки HTML в нескольких местах приложения. Вместо дублирова­
ния содержимого можно использовать частичные представления - отдельные файлы
представлений, содержащие фрагменты дескрипторов и разметку, которые могут быть
включены в другие представления. В настоящем разделе будет показано, как создавать
и применять частичные представления, объяснена их работа и продемонстрированы
приемы, доступные для передачи данных представления в частичное представление.

Создание частичного представления


Частичные представления являются всего лишь обычными файлами CSHTML,
отличаясь от нормальных представлений только способом их использования. Среда
Visual Studio предлагает инструментальную поддержку для создания заранее на­
полненных частичных представлений, но создать частичное представление проще
всего, создав обычное представление с при менением шаблона элемента MVC View
Page (Страница представления МVС). Добавьте в папку Views/Home файл по имени
MyPartial . cshtml и поместите в него содержимое из листинга 21.20.
Глава 21 . Представления 667
Листинг 21.20. Содержимое файла MyPartial. cshtml из папки Views/Home

<div class= "bg -i nfo " >


<div>This is the message from the partial view . </div>
<а asp-action="Index">This is а link to the Index action</a>
</div>

Здесь планируется продемонстрировать возможность смешивания в частичном


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

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


Частичное представление потребляется посредством выражения @Html. Partial
внутри другого представления. Создайте в папке Views/Home файл по имени
List . cshtml и добавьте в него содержимое, показанное в листинге 21.21.

Листинг 21.21. Содержимое файла List. cshtml из папки Views/Home

@( Layout = null ;
< ! DOCTYPE html>
<html>
<head>
<meta name= "viewport " content= " width=device - width " />
<title>Razor</title>
<link asp-href-include= 11 liЬ/boo tstrap/di st/ css/* .min . css 11 rel= 11 stylesheet 11 />
</head>
<body c l ass= " panel - body " >
This is the List View
@Html . Partial( "MyPartial ")
</body>
</html>

Метод Partial () - это расширяющий метод, применяемый к свойству Html , ко­


торое добавл е но в класс, сгенерированный механизмом Razor из файла представле­
ния. Он является примером вспомогательного метода HTML, который использовался
в качестве способа генерирования динамического содержимого внутри представле­
ний в предшествующих версиях MVC, но теперь в значительной степени замещен де­
скрипторными вспомогательными классами . Методу Partial () передается аргумент,
указывающий имя частичного представления, содержимое которого вставляется в
вывод, отправляемый клиенту.

Совет. Механизм Razor ищет частичные представления там же, где и обычные представле­
ния (в папках -/Viеws/<контроллер> и -/Views/Shared). Другими словами, мож­
но создавать специализированные версии частичных представлений, специфичные для
контроллера, и переопределять частичные представления с таким же именем из папки

Shared.

Запустив приложение и перейдя на URL вида /Home/List, можно оценить эффект


от потребления частичного представления (рис. 21. 7).
668 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Саге MVC

С ~~os1:55775/Home/list R- 1 .
----- ----· J :
n 1is is the List View
This is the. message from the partial view.
This is а link to the lndex action

Рис. 21.7. Применение частичного представления

Использование строго типизированных частичных представлений


Можно создавать строго типизированные частичные представл ения и снабжать
их объект ами м одели представления, подлежащим и применению при визуали з ации
частичных представлений . Создайт е в папке Views/ Home ф айл представл ен ия по
имени MyStr o nglyType dPa r tial. c s html и поместите в него содержимое из лис ­
тинга 21 .22.

Листинг 21.22. Содержимое файла MyStronglyTypedPartial. cshtml


из папки Views/Home

@model I EnumeraЫ e <s tr ing>

<di v c l ass= "bg-in f o " >


Th is is t he mes s age from the parti al vi e w.
<ul >
@foreach (s tr i ng st r in Mode l ) {
<l i >@str</li>

</ul>
</div>

С испол ьзованием стандартного выражения @mod e l опр еделяется тип модели пред ­
ставл ения , а с помощью цикла@f o re a c h отображается содержимое объе1па модели
представления в виде элементов списка HTML. Чтобы задействовать новое частично е
представление, модифицируйте содержимое файла /V ie ws/Common/Lis t. cshtml,
как показано в листин ге 21.23.

Листинг 21.23. Применение строго типизированного частичного представления


в файле List. cshtml

@{ Layou t = null ;
<!DOCT YP E html>
<html >
<head>
<meta name ="viewport " cont e nt= "width=de vic e-width " />
<ti t le>Razor</t it le>
<link asp- hre f - include=" liЬ/boots t rap/dis t / css/* .mi n . css " rel= " s tylesheet " / >
</head>
Глава 21. Представления 669
<body class="p a nel-bo dy">
This is the List View
@Html.Partial("MyStronglyTypedPartial",
new string[] { "Apple", "Orange", "Pear" })
</ b o dy >
< / html >

Отличие от предыдущего примера заЮiючается в том, что здесь вспомогательному


методу Par t ial () передается дополнительный аргумент, который поставляет модель
представления. Запустив приложение и перейдя на URL вида /Horne/List, можно пос­
мотреть на работу строго типизированного частичного представления (рис . 21.8).

This is the List View


This is the message from the partial view.
• Apple
• Orange
• Pear

Рис. 21.8. Использование строго типизированного частичного представления

Добавление содержимого JSON в представления


Содержимое JSON часто включается в представления для того, чтобы снабдить
код JavaScript клиентской стороны данными, которые могут применяться при дина­

мической генерации содержимого. Для подготовки к такому примеру добавьте в при­


ложение пакет jQuery, отредактировав файл bower. j son согласно листингу 21.24.
П акет jQuery облегчит обработку данных JSON, когда они получаются браузером как
часть НТМL-документа.

Листинг 21.24. Добавление пакета jQuery в файле bower. j son

"name": "asp.net",
"priva te ": true ,
"dependencies" : {
"bo o tstrap": "З.3.6",
"jquery": 11 2.2.4 11

В листинге 21.25 приведены добавления внутри представления List . cshtrnl,


которые используют Razor для включения данных JSON в ответ, отправляемый
браузеру.
670 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC

Листинг 21.25. Работа с данными JSON в файле List.cshtml

@{ Layout = null;
< ! DOCTYPE html>
<html>
<head>
<meta name="viewport" content= "width=device - width " />
<title>Razor</title>
<link asp-href-include=" liЬ/bootstrap/dist/ css/* .min. css " rel= " stylesheet " />
<script id="jsonData" type="application/json">
@Json.Serialize(new string[] { "Apple", "Orange", "Pear" })
</script>
</head>
<body class= " panel - body " >
This is the List View
<ul id="list"></ul>
</body>
</html>

Выражение @Json . Serialize принимает объект и сериализирует его в формат


JSON. В листинге 21 .25 к представлению добавлен элемент script, содержащий дан­
ные JSON . Когда представление визуализируется и отправляется браузеру, оно вклю­
чает элемент, подобный показанному ниже:

<script id="jsonData" type="application/json">["Apple","Orange", "Pear"J


</script>

Чтобы можно было работать с данными JSON, к представлению List . cshtml до­
бавляется библиотекаjQuеrу и встраиваемый код JavaScript, который прим е няет ее для
разбора данных JSON и динамического создания НТМL-элементов (листинг 21.26).
Листинг 21.26. Использование данных JSON в файле List. cshtml

@{ Layout = null;
<!DOCTYPE html>
<html >
<head>
<meta name="viewport" content= "width=device - width " />
<title>Razor</title>
<link asp-href - include= "liЬ/ bootstrap/dist/css/*.min . css " rel="stylesheet" />
<script id= "j sonData" type= " app l ication/json " >
@Json.Serialize(new string[ ] { "Apple ", "Orange" , "Pear" })
</script>
<script asp-src-include="lib/jquery/dist/*.min.js"></script>
<script type="text/javascript">
$(document) .ready(function () {
var list = $ ("#list")
JSON. parse ( $ ( "# j sonDa ta" ) . text () ) . f orEach ( function (val) {
console. log ( "Val: " + val) ;
list. append ($ ( "<li>") . text (val)) ;
}) ;
}) ;
</script>
</head>
Глава 21 . Представления 671
<body class= " panel - body " >
This is the List View
<ul id="l i st"> </ul >
</body>
</html>

По сле запуска приложения и запроса URL вида /Horne/L ist отобразится содержи­
мое, показанное на рис. 21.9. Это не самая впечатляющая работа с данными JSON, но
она демонстрирует, как их можно включать в представления.

1 This is the List View


1 • Apple
1 • Orange
1

~е_а_г~~--~~·
Рис. 21.9. Использование данных JSON в представлении

Конфигурирование механизма Razor


Механизм Razor можно конфигурировать с применением класса Ra zo rViewEngine
Options , который находится в пространстве имен Microsoft . AspNet Core . Mvc .
Razor . Класс определяет два конфигурационных свойства, описанные в табл. 21.9.

Таблица 21.9. Свойства класса RazorViewEngineOptions

Имя Описание

FileProvider Это свойство используется для установки объекта,


который предоставляет механизму Razor содержи-
мое файлов и каталогов. Функциональность опре­
деляется интерфейсом Microsoft . AspNetCore .
FileProviders. IFi leProvider, а стандартной
реализацией является класс PhysicalFileProvider,
который обеспечивает чтение файлов из диска

ViewLocationExpanders Это свойство применяется при конфигурировании рас­


ширителей местоположений представлений, которые
используются для изменения способа поиска представ ­
ления механизмом Razor
672 Часть 11. Подробные сведения об инфраструктуре ASP.NET Соге мvс

Совет. Если вы действительно заинтересованы в тщательном исследовании, тогда мо­


жете заменить внутренние компоненты Razor, создав классы, к оторые реализуют
интерфейсы из пространства имен Microsoft . AspNetCore . Mvc. Razor, и за­
регистрировав их с помощью поставщика служб в классе Startup. У большинства раз­
работчиков никогда не возникнет необходимость выполнять такую работу, и к ней нельзя
относиться легкомысленно. Первым делом понадобится загрузить исходный код Razor из
http : //g ithuЬ. com/aspnet.

Свойство FileProvider не относится к тем свойствам, которые будут изм е нять


многие приложения , поскольку чтение файлов пр едставлений из диска явля ется
в точности тем, что требуется в большинстве проектов. Механизм Razor исполь­
зует данный поставщик только для загрузки представлений, чтобы их можно было
скомпилировать , когда приложение запускается в первый раз. Однако сво йств о
ViewLocationExpanders более полезно, т.к. оно позволяет применять специальную
логику в отношении способа , которым Razor ищет представления .

Расширители местоположений представлений


Расширители местоположений представлений используются механизмом Razor для
построения списка мест, в которых должен вестись поиск представления. Расширители
местоположений представлений реализуют интерфейс I ViewLocationExpander , оп­
ределенный следующим образом:

u sing Sys t em . Co ll ec ti ons . Generic ;


namespa c e Microsoft . AspNetCore . Mvc . Razor
puЫic interface IViewLocationExpander {
void PopulateValues(View LocationExpanderContext context) ;
IEnumeraЫe<string> ExpandV iewLoca ti ons(
ViewLoc at i on ExpanderCo n text co n text ,
I Enume r aЫe<string> v iewLocations) ;

В приведенных дале е разделах объясняется, как работают расширители м ес­


тоположений представлений , и создается специальная реализация интерфейса
IViewLocationExpander . Для подготовки к созданию расширителей местополо­
жений представлений потребуется изменить метод действия Index {) контроллера
Ноте, чтобы он запрашивал несуществующее представление (листинг 21 .27). В ре­
зультирующем сообщении об ошибке будут присутствовать местоположения, в кото­
рых механизм Razor искал представление , и отражен эффект, оказываемый на них

расширителями местоположений представлений .

Листинг 21.27. Запрашивание несуществующего представления в файле HomeController. cs

using System;
using Microsoft.AspNetCore . Mvc;
namespace Views .Controllers {
puЫic class HomeContr ol l er : Controller
puЬlic ViewResul t Index () =>
View("MyView", newstring[] { "Apple", "Orange", "Pear" });
puЫic ViewResult List() => View() ;
Глава 21. Представления 673
Запустив приложение и запросив стандартный URL, вы увидите, что в сообщении
об ошибке отображаются стандартные местоположения поиска представления:

/Views/Horne/MyView.cshtml
/Views/Shared/MyView.cshtrnl

Создание простого расширителя местоположений представлений


Простейший расширитель местоположений представлений всего лишь изменя­
ет набор мест, где Razor проводит поиск всех представлений. Для этого необходимо
реализовать методExpandViewLocations () и возвратить список местоположений,
которые должны поддерживаться. Добавьте в папку Infrastructure файл клас­
са по имени SirnpleExpander. cs и поместите в него определение, показанное в
листинге 21.28.

Листинг 21.28. Содержимое файла SimpleExpander. cs из папки Infrastructure

using Systern.Collections.Generic;
using Microsoft . AspNetCore.Mvc.Razor ;
namespace Views . Infrastructure
puЫic class SimpleExpander : IViewLocationExpander
puЫic void PopulateValues(ViewLocationExpanderContext context) {
//ничего не делать - метод не требуется

puЬlic IEnumeraЫe<string> ExpandViewLocations(


ViewLocationExpanderContext context,
IEnumeraЫe<string> viewLocations) {
foreach (string location in viewLocations)
yield return location.Replace("Shared", "Common");

yield return " /Views/Legacy/{1}/{0}/View . cshtml";

Механизм Razor вызывает метод ExpandViewLocations ().когда ему нужен спи­


сок местоположений поиска, и предоставляет стандартные местоположения как пос­
ледовательность строк в параметре viewLocations. Местоположения выражаются в
виде шаблонов с заполнителями для имен действия и контроллера. Вот как выглядят
шаблоны местоположений, которые применяются по умолчанию в приложении, не
использующем области маршрутизации:

" /Views/{1}/{0} . cshtml "


" /Views/Shared/{0}.cshtml"
Заполнитель { О} применяется для ссылки на имя метода действия, а заполнитель
{1 } - для ссылки на имя контроллера. Работа расширителя местоположений пред­
ставлений заключается в возвращении набора мест, в которых должен производить­
ся поиск , и в листинге 21.28 используется метод string . Replace () для изменения
Shared на Comrnon в стандартных местоположениях, а также добавляется собствен­
ное местоположение с другой структурой файлов и папок.
674 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC

Применение расширителя местоположений представлений


В листинге 21.29 расширитель местоположений представлений настраи­
вае тся путем конфигурирования м еха низ м а Razor в класс е
Startup. С в о й ство
ViewLocationExpanders во звращает объект Li st <IViewLocationExpander >, на
котором вызывается метод Add ( ) .

Листинг 21.29. Конфигурирование механизма Razor в файле Startup. cs


u s ing Microsoft . AspNe t Core . Bu i lder ;
using Microsoft . Extensions . Dependencyinjection ;
u si ng Microsoft . AspNetCore . Mvc ;
u s ing Views . Infrastr uctur e ;
using Microsoft.AspNetCore.Mvc.Razor;
namespace Views {
pu Ы ic class Startup
puЫic void ConfigureServi ces(ISe r v i ceCo l lection se r vices)
services.AddMvc() ;
services . Confi gure<RazorViewEngineOptions>(options => {
options.ViewLocationExpanders.Add(new SirnpleExpander());
}) ;

puЫic void Conf i gu r e(IApp l icatio n Bui l d e r арр) {


app . UseSta t usCode Pages() ;
app . UseDeveloperExcept i onPage() ;
app . UseStat i cFiles() ;
app . UseMvcWithDefau l tRoute() ;

По сл е запус ка приложения сообщение об ошибке отобразит набор местоположе­


ний, который был предоставлен механизму Razor специальным расширителем место­
положен ий пр едставлений:

/View s/ Home/MyView . cshtml


/V i ew s /Common/MyView . cshtml
/Views/Legacy/Home/MyView/View . cshtml

Выбор специфических представлений для запросов


Расширители местополо жений представлений облегчают изм енение мест поиска
для всех запросов, но могут такж е делать это для индивидуальных запросов. В преды­
дущем приме р е был р еализован только метод ExpandViewLocations () , но р е альную
мощь де монстрирует метод PopulateValues (), являющийся еще одним методо м в
интерф ейсе IV i ewLocationExpan der .
Всякий р аз , когда механи з му Razor требуется представл е ние , он вы зыв ает м е ­
тод Popu l ateValues () своих расширит елей м е стополо же ний пр е дставл е ний.
пер е давая е му объ е кт Vi ewLocationE x panderContext для данны х нонт е кста .
В класс е ViewLo c ationExpanderConte x t определены свойст в а, перечисл енны е в
табл. 21 . 10.
Глава 21. Представления 675
Таблиц а 21. 10. С войства класса ViewLocationExpanderContext
Имя Опис а н и е

ActionContext Это свойство возвращает объе кт ActionContext , который описы­


вает метод действия, запросивший представление, а также включа­
ет детали запроса и ответа

ViewName Это свойство возвращает имя представления , которое запросил


метод действия

ControllerName Это свойство возвращает имя контроллера, который содерж ит ме ­


тод действия

AreaName Это свойство возвращает и м я области, содер ж ащей контроллер ,


если области были определены

IsMainPage Это свойство возвращает false , если ме х анизм Razor ищет час­
тичное представление, и tru e в противном случае
Values Это свойство возвращает объект IDi ctionary<stri ng , string>,
к к оторому расширитель местоположений представлений добавляет
пары "ключ-значение " , уникально идентифицирующие категорию за­
проса, ка к объясняется ни же

Метод PopulateValues () предназначен для к атегор изации запрос а за счет до­


бавлен ия пар "ключ - зн ач е ни е" в сл ов арь , возвращаемый свойством Va l ues объекта
ко нтекста. М еханиз м Razor н е б ес покоит то, как категоризирует ся з апрос , и р еал и ­
з ация метода для з аполн ения словаря возложена полностью на расширитель м е ст о ­

пол оже ний пр ед ставл е н ий . Л ег че вс его это объяснить с помощью прим ера, так что
доб авьте в п апку Infrastructu r e файл класс а по имени ColorExpander . cs с опр е ­
дел ением из листинга 21 .30.

Ли ст и нг 21.ЗО. С одержимое файла ColorExpander. cs из папки Infrastructure


using System.Collections . Gener i c ;
using Microsoft . AspNe t Core . Mvc . Razor ;
namespace Views .I nfrastructu r e {
puЫic class ColorExpander : I View LocationExpander
private static Dictionary<string, string> Co l ors
= new Dictiona r y<string , string> {
[ " red " ] = " Red ", [ " green "] = " Gr een ", [ " Ы u е "] = " Blue "
};

puЫic void PopulateVa l ues(ViewLocationExpanderCon text context)


var routeValues = context . Act i onContext . RouteData . Values ;
string color ;
if (r outeValues . ContainsKey( " id " )
&& Colors . TryGe t Value(routeValues [" id " ] as stri ng , out color)
&& ! str i ng .I s Nu l lO r Empty(colo r )) {
context . Values [ " colo r"] = co l or ;
676 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core мvс

puЬlic IEnumeraЫe<st ri ng> ExpandVi ewLocat i ons(


ViewLoca t ionExpanderContext context ,
IEnumeraЫe<string> viewLocations) {

string color ;
context . Values .T ryGet Va lu e( " co l or", out color) ;
foreach (string location in viewLoca t ions) {
if ( !str i ng . IsNullOrEmpty(color)) {
yield return location . Rep l ace( " {O) ", color) ;
else {
yield r etur n loca t ion ;

Метод Pop ulateVa l ues () исполь зу ет объект Act i onContext для получ е ния дан­
ных маршрутизации и ищет значение сегмента id в URL. Если есть сегмент id и его
значени ем является red, green или Ыuе , тогда расширитель м естополож е ний пр ед­
ставлен ий доб авляет в словарь Va l ues свойство color . Это процесс категори заци и:
з апрос, сегмент id которого соответствует цвету , категоризируется с кл ючо м color,
чье значение выводится из значения сегмента.

Дале е м еханизм Razor вызывает метод ExpandViewLocations () и пр едоставля ­


ет тот же объект контекста, который применялся для м етода PopulateValues () .
Это позволяет расширителю местоположений предс тавлений просматривать про­
веденную ран е е кат егори зацию и г енерировать н а бор м ест, в которых м еханиз м
Razor должен искать пр едст авления. В рассматр иваемо м пример е с помощью м етода
string . Replace () з аполнитель {О J заменялся им енем цв ета.

Совет. Ме х анизм Razor вызывает метод Popu lateValues () для каждого запроса пред­
ставления, но кеширует набор местоположений поиска, возвращаемый методом
ExpandViewLocat i ons () .Таким образом , последующие запросы, для к оторы х метод
PopulateVa l ues () генерирует тот же самый набор ключей и значений категоризации,
не требует вызова метода ExpandVi ewLocat i on s ().

В листинге 21.31 меха низм Razor 1<0нфигурируется для исполь з ов а ния кл а сс а


Co l orExpander.

Листинг 21.31. Добавление расширителя местоположений представлений


в файле Startup. cs

puЫic void Configu r eSe r vices( I Servi ceCol l ec t ion services)


se r vices.AddMvc() ;
services.Configure<RazorVi ewEngineOptions>(opt i ons => {
options . ViewLocationExpande r s.Add( new Simple Expander()) ;
options.ViewLocationExpanders.Add(new ColorExpande r());
}) ;
Гл ава 21. Представл ения 677
Запустив приложение и запросив URL вида / Ho me/Index/red , можно заметить
эффект от нового расширит еля местоположений представлений, который приведет к
тому, что механизм Razor будет выполнять поиск в следующих местах:

/V i ews/Home/Red . cshtml
/Views/Common/Red . csh t ml
/V i ews / Legacy /Home / Red / View . c s h tml
Аналогично запрос URL вида /Home/Index /g r een заставит Razor искать в таких
м е стополож е ниях:

/Views/Home/Green.cshtml
/Views/Common/Green . cshtml
/Views/Legacy/Home/G r een/Vi e w. cs h tm l
Порядо1<, в 1<отором зарегистрированы расширители местоположений пред­
ставл е ний, важен, потому что набор местополож е ний, генерируемый методом
ExpandVie wLocat i on s ( ) одного расширителя, применяется в качестве аргумента
viewLocations для следующего расширителя в списке . Это можно заметить в по­
казанных ран е е местополож ениях, где Vi e ws/ Common и Views/Le g a c y генерирова­
лись классом SimpleExpa nde r, 1<оторый находился перед Co l or Expander в классе
Startup .

Ре з юме
В настоящей главе было продемонстрировано, 1<а1< создавать специальный ме­
ханизм визуализ ации , и объя с нялось, каким образом работает механизм Razor при
трансляции файлов CSHTML в классы С# . Вы узнали, как исполь:Зовать разделы
компоновки и частичные пр едставления, и научились изменять местоположения, в

которых Razor ищет файлы представлений. В следующей главе будут описаны ком­
поне нты пр едставлений, которые прим еняются для обеспечения логи1<и поддержки
частичных представлений .
ГЛАВА 22
Компоненты
v
представлении

в настоящей главе рассматриваются компоненты представлений.


Core MVC
ляются новым добавлением к инфраструктуре ASP.NEТ
которые яв­
собой средство дочерних действий из предшествующих версий . Компоненты пред­
и заменяют

ставлений - это классы, предлагающие логику в стиле действий для поддержки час­
тичных представлений. Это означает возможность встраивания сложного содержимо ­
го в представления и вместе с тем простое сопровождение и модульное т е стировани е

кода С#, который его поддерживает. В табл. 22. l приведена сводка, позволяющая по­
местить компоненты представлений в контекст.

Таблица 22.1. Помещение компонентов представлений в контекст

Вопрос Ответ

Что это такое? Компоненты представлений являются классами , которые обес­


печивают прикладную логику для поддержки частичных пред­

ставлений либо для внедрения небольших фрагментов HTML·


разметки или данных JSON в родительское представление

Чем они полезны? Без компонентов представлений трудно создать встроенную


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

Как они используются? Компоненты представлений обычно являются производными


от класса ViewCompon ent и применяются в дочернем пред­
ставлении с использованием выражения

@awai t Componen t . I n v okeAsync


Существуют ли какие­ Нет, компоненты представлений - это простое и предсказуе­
то скрытые ловушки или мое средство. Основное заблуждение связано с отказом от и х
ограничения? применения и попыткой включить прикладную логику внутрь
представлений, где их трудно тестировать и сопровождать

Существуют ли Можно было бы поместить логику доступа и обработки данных


альтернативы? прямо в частичное представление, но в результате работа с
ней и эффективное тестирование станут значительно труднее

Изменились ли они по срав­ Компоненты представлений являются новым средством в инф­


нению с версией MVC 5? раструктуре ASP.NET Core MVC, пришедшим на замену средс­
тву дочерних действий из предыдущих версий
Глава 22 . Компоненты представлений 679
В табл . 22.2 приведена сводка для настоящей главы.

Таблица 22.2. Сводка по главе

Задача Решение Листинг

Создание частичного представления Используйте компонент представления 22.1-22.13


с собственной логикой и данными

Вызов компонента представления Применяйте выражение 22.14


@await Cornponent . InvokeAsync
в представлении

Упрощение доступа к данным кон­ Унаследуйте класс от класса 22.15, 22.16


текста и результатам ViewCornponent
Выбор частичного представления Используйте метод View () для 22.17-22.19
создания и возвращения объекта
ViewViewCornponentResult
Создание фрагмента НТМL­ Вызовите метод Content () 22.20, 22.21
разметки для создания объекта
ContentViewCornponentResult
или явно создайте объект
HtrnlContentViewCornponentResult,
если не хотите, чтобы фрагмент был
закодирован

Использование деталей запроса Применяйте данные контекста компо­ 22.22


для генерирования результата нента представления

Предоставление данных кон­ Передайте аргументы методу 22.23-22.25


текста при вызове компонента InvokeAsync ()
представления

Создание асинхронного компонен­ Реализуйте метод InvokeAsync () 22.26-22.29


та представления и возвратите объект Task, который
выдает требуемый результат

Создание гибридного компонента Применяйте атрибут ViewCornponent 22.30-22.33


контроллера/представления к классу контроллера

Подготовка проекта для примера


Создайте новый проект типа Empty (Пустой) по имени UsingViewCornponents с ис­
пользованием шаблона Саге Web Application (.NET Саге) (Веб-приложение ASP.
ASP.NET
NЕТ Core (.NET Core)). Добавьте требуемые пакеты NuGet в раздел dependencies файла
proj ect . j son и настройте инструментарий Razor в разделе tools, как показано в лис­
тинге 22.1. Разделы, которые не нужны для данной главы, понадобится удалить.

Листинг 22.1. Добавление пакетов в файле proj ect. j son

" dependencies ": {


"Microsoft.NETCore .App ":
"version ": " 1 . 0 . 0 ",
"type ": "platform "
} '
680 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC
"Microsoft.AspNetCore.Diagnostics": "1.0.0",
"Microsoft . AspNetCore . Server . IISintegration" : " 1.0.О ",
" Microsoft . AspNetCore.Server . Kestrel ": " l . 0 . 0 ",
" Microsoft . Extensions.Logging.Console": "1. 0 . О ",
"Microsoft.AspNetCore . Mvc": 11 1.0.0 11 ,
"Microsoft.AspNetCore.StaticFiles": 11 1.0.0 11 ,
"Microsoft.AspNetCore . Razor.Tools": {
"version": "1.0.0-preview2-final",
"type": "build"

},
" tools ":
"Microsoft . AspNetCore.Server . IISintegration . Tools ": "1. 0 . 0- preview2 - final ",
"Microsoft.AspNetCore .Razor.Tools": "l.0.0-preview2-final"
},

" frameworks ": {


" netcoreappl.0":
" imports ": [ " dotnet5 . 6 ", " portaЫe-net45+win8 "]

},
" buildOptions ":
" emitEntryPoint ": true ,
" preserveCompilationContext": true
},
" runtimeOptions": {
" configProperties " : { " System . GC .Se rver ": true }

Создание моделей и хранилищ


Для демонстрации работы компонентов представлений понадобятся два источни­
ка данных. Часть приложения будет оперировать с набором описаний товаров; чтобы
подготовиться к этому. создайте папку Models и добавьте в нее файл класса по имени
Product . cs с определением из листинга 22.2.

Листинг 22.2. Содержимое файла Product. cs из папки Models

namespace UsingViewComponents . Models


puЫic class Product {
puЫic string Name { get ; set ; }
puЫic decimal Price { get; set ;

Чтобы создать хранилище для объектов Product, добавьте в папку Models файл
по имени ProductReposi tory. cs и определите в нем интерфейс и класс реализа­
ции, как показано в листинге 22.3.
Глава 22 . Комnоненты nредставлений 681
Листинг 22.З. Содержимое файла ProductReposi tory. cs из папки Models
using System.Collections . Generic;
namespace UsingViewComponents . Models
puЬlic interface IProductRepository {
IEnumeraЫe<Product> Products { get;
void AddProduct(Product newProduct);

puЫic class MemoryProductRepository : IProductRepository


private List<Product> products = new List<Product> {
new Product { Name "Kayak", Price = 275 М } ,
new Product { Name "Lifejacket", Price = 48 . 95 М },
new Product { Name "Soccer ball", Price = 19.50 М}
};
puЫic IEnumeraЫe<Product> Products => products;
puЫic void AddProduct(Product newProduct) {
products . Add(newProduct);

В интерфейсе IProductReposi tory определен ограниченный набор средств хра­


нилища, а класс MemoryProductReposi tory реализует данный интерфейс с приме­
нением объекта List, находящегося в памяти. Другая часть приложения будет опе­
рировать с описаниями городов. С этой целью добавьте в папку Models файл класса
по имени Ci ty . cs, содержимое которого приведено в листинге 22.4.

Листинг 22.4. Содержимое файла Ci ty. cs из папки Models


namespace UsingViewComponents.Models
puЬlic class City {
puЫic string Name { get; set ; }
puЫic string Country { get; set;

puЬlic int Population { get ; set;

Для хранилища объектов Ci ty создайте в папке Models файл класса по имени


Ci tyReposi tory . cs и определите в нем интерфейс и класс реализации, как показа­
но в листинге 22.5.

Листинг 22.5. Содержимое файла Ci tyReposi tory. cs из папки Models


using System . Collections . Generic;
namespace UsingViewComponents . Models
puЫic interface ICityRepository {
IEnumeraЫe<City> Cities { get; }
void AddCity(City newCity) ;
682 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC
puЫic class MemoryCityRepository : ICityRepository {
private List<City> cities = new List <City> {
new City { Name = " London ", Country = "UK", Population 8539000} ,
new City { Name = " New York ", Country = " USA ", Population 8406000 },
new City { Name = " San Jose ", Country = " USA ", Population 998537 1 ,
new City { Name = " Paris ", Country = "France", Population = 2244000 )
);
puЫic IEnumeraЫe<City> Cities => cities ;
puЫic void AddCity(City newCity) {
c it ies . Add(newCity);

Интерфейс ICi tyReposi tory предлагает огранич ен ный набор средств храни ­
лища. а класс MemoryCi tyReposi tory реали зует этот интерфейс с использованием
объекта List. находящегося в памяти .

Создание контроллера и представлений


Для н ачала необходим только один контроллер. поэто му создайте папку
Controllers . добавьте в нее файл по имени HomeController. cs и опр еделите в нем
класс, приведенный в листинге 22.6.

Листинг 22.6. Содержимое файла HomeController. cs из папки Controllers


using Microsoft.AspNetCore . Mvc ;
using UsingViewComponents.Models;
namespace UsingViewComponents.Controllers
puЫic c lass HomeController : Controller
private IProductRepository repository;
puЫic HomeController(IProductRepository repo) {
repository = repo;

puЬlic ViewResult Index() => View(repository . Products) ;


puЫic ViewResult Create() => View() ;
(HttpPost]
puЫic IActionResult Create(Product newProduct)
repository .AddProduct(newProduct) ;
return RedirectToAction( " Index " ) ;

Контроллер Home прим еняет свой конструктор для объявления зависимости от ин­
терфейса IProductReposi tory. которая будет распознаваться по ставщиком служб,
когда контроллер станет использоваться при обработке запросов. Действие Index из ­
влекает все объекты Product из хранилища и визуализирует их с применением стан­
дартного представления. Два метода Crea te ( ) задействуют паттерн Post/Redirect/
Get при добавлении новых объектов в хранилище с использованием данных формы.
передаваемых клиентом.
Глава 22. Комnоненты nредставлений 683
Пр едставления в рассматриваемом примере будут разделять общую компоновку.
Создайте папку Views/Shared и добавьте в нее файл по имени La yout . cshtml с
разметкой из листинга 22 .7.

Листинг 22.7. Содержимое файла _Lауоut. cshtml из папки Views/Shared


<!DOCTYPE html>
<html>
<head>
<meta name= " viewport " content= " wi dth=device-width " />
<title>@ViewBag . Tit l e</title>
<link asp-href- incl ude= " liЬ/bootst r ap/dis t / css/* . min . css " rel =" stylesheet" />
</head>
<body class= " panel -body " >
<div class= 11 b g -primary panel -b ody " >
<div class= "row" >
<div class= 11 col- xs-7 " >< hl>Products</hl></div>
<div class= " col - xs-5 " >
<div clas s="bg-info text - primary panel-body">City Placeholder</div>
</div>
</div>
</div>
<div c lass="panel-body">@RenderBody()</div>
</body>
</html>

В компоновке определен заголовок, включающий заполнитель для содержимого,


которое будет создано позже в главе с применением хранилища городов. Затем со­
здайт е папку Views/Home и поместите в нее файл по имени Inde x . cshtml с раз­
меткой, приведенной в листинге 22.8, которая выводит детали объектов Product в
таблице.

Листинг 22.8. Содержимое файла Index. cshtml из папки Views/Home

@model IEnume raЫe<Product>


@{
ViewData[ " Title "] = " Products ";
Layout = 11 Layout 11 ;

<tаЫе class= " taЫe taЬle - condensed taЬle -st riped taЫe -b ordered
11
>
<thead>
<tr><th>Name</ th><th>Price</th></tr>
</thead>
<tbody>
@foreach (var product in Mode l )
<tr>
<td>@product .N ame</td>
<td>@product .P rice</td>
</tr>
}
</tbody>
</tаЫе>

<а asp - action ="Create" class= 11 Ьtn Ьtn - primary " >Crea te</a>
684 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Соге MVC
Финальным элементом в пр едставлении I ndex является элемент а. который сти­
лизован как :кнопк а и нацеле н на действие Create , так что поль з ователь мож ет со­
здавать новый объе 1<Т Product в хранилище. Чтобы со здать форму . которую будет
заполнять пользователь. добавьте в папку Views/Horne файл Create . cshtrnl с раз­
меткой из листинга 22.9.

Листинг 22.9. Содержимое файла Create. cshtml из папки Views/Home


@model Product
@{
Vi ewData[ " Title "] " Create Produc t";
Layout = " Layout ";

<form method= "post " asp - action= "Create " >


<d iv cla ss =" form - group ">
<label asp - for= " Name ">Name : </labe l >
<input c l ass= " fo r m- cont r ol " asp - for ="Name " />
</div>
<div c l ass= " form - group " >
<label asp - for= "P rice " >Pr i ce : </ l abe l >
<input class= " form - contro l" asp - for= "P rice " />
</div>
<b utton type= " subm i t " clas s=" Ьtn Ьtn - primary " >Create</button>
<а c l ass= " Ьtn Ьtn - default " asp - action= " Index " >Cancel</a>
</ f orm>

В представлениях используются дес:крипторные вспомогательные классы. Чтобы


сделать их доступными, создайте в папке Vi ew s файл _ Viewirnports . cshtrnl и до­
бавьте в него выражения, показанные в листинге 22.10, которые также обеспечивают
доступ к классам и з папки Models без указания пространства имен.

Листинг 22.1 О. Содержимое файла _ Viewimports . csh tml из папки Views


@usi ng UsingViewComponents . Models
@a ddTagHelper * , Mic r osoft .AspNetCo r e . Mvc .T a gHelpers

Boots tra p для стилиз ации сво его


Представл ения также пол агаются на СSS-пак ет
содержимого. Создайте в корневой пап1<е проекта файл bower. j son с применение м
шаблона элемента Bower Configuration File (Файл конфигурации Bower) и добавьте па ­
кет Bootstrap в ра здел dependencies (листинг 22.11).

Листинг 22.11. Добавление пакета Bootstrap в файле bower. j son

"name " : "asp . net ",


"private ": true ,
"depende nc i es ": {
"bootstrap": 11
3.3.6 11
Глава 22 . Компоненты представлений 685
Конфигурирование приложения
Последний подготовительный шаг связан с конфитурированием приложения, как де­
монстрируется в листинге 22.12. В дополнение к настройке служб и промежуточного про­
граммного обеспечения МVС создаются службы-одиночки для двух хранилищ данных.

Листинг 22.12. Содержимое файла Startup. cs


using Microsoft.AspNetCore . Bui l der ;
using Microsoft.Extensions . Depe n dency in jection ;
using UsingViewComponents.Models;
namespace UsingViewComponents {
puЫic c l ass Startup {
puЫic void ConfigureServices(IServi ceCollection services) {
services.AddSingleton<IProductRepository, MemoryProductRepository>();
services.AddSingleton<ICityRepository, MemoryCityRepository>();
services.Adc!Мvc();

puЫic void Co nfigure(IApplicationBu ilder арр) {


app.UseStatusCodePages();
app.UseDeveloperExceptionPage();
app.UseStaticFiles();
app.UseMvcWithDefaultRoute();

Запустив приложение , вы увидите список объектов Product из хранилища това­


ров. Можно добавлять новые товары, щелкая на кнопке Create (Со здать) , заполняя
форму и отправляя ее серверу, который затем перенаправит браузер обратно на спи­
сок (рис. 22.1). Поскольку представл ения в приложении разделяют общую компонов­
ку, в течение всего процесса отображается заполнитель для данных по городам.

D Pro<ivcis )(

Products
Name: Name Price

275

Рис. 22.1. Выполнение примера приложения


686 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

Понятие компонентов представлений


Приложениям обычно необходимо встраивать в представления содержимое, ко­
торое не связано с главным назначением приложений. Распространенные примеры
включают инструменты навигации по сайту, облака тегов и панели аутентификации,
которые позволяют пользователю входить, не посещая отдельную страницу.

Общая идея всех упомянутых примеров состоит в том, что данные, требуемые для
отображения встроенного содержимого, не являются частью данных модели, которые
передаются из действия в представление. Именно по этой причине в примере прило­
жения были созданы два хранилища: мы собираемся отображать содержимое, генери­
руемое с использованием хранилища объектов Ci ty, что нелегко делать в представле­
нии, которое получает от своих действий данные из хранилища объектов Product.
В главе 21 было показано , каким образом частичные представления применя­
ются для создания многократно используемой разметки, которая требуется в при­
ложениях. избегая дублирования одного и того же содержимого во множестве мест
приложения. Частичные представления - полезное средство, но они просто содер­
жат фрагменты НТМL-разметки и директивы Razor, а данные, с которыми они имеют
дело , получаются от родительского представления. Если нужно отображать другие
данные, тогда возникает проблема. Можно было бы получать доступ к необходимым
данным прямо из частичного представления, но это нарушит принцип разделения

обязанностей, который является фундаментом паттерна МVС, и приведет к помеще­


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

Здесь на помощь приходят компоненты представлений. Компонент представле­


ния - это класс С#, который предоставляет частичное представление с необходимы­
ми ему данными, независимое от родительского представления и от ви зуализирую ­

щего действия. В таком отношении компонент представления можно трактовать как


специализированное действие, которое используется только для доставки частичного
представления с данными; оно не может получать НТГР-запросы, а выдаваемое им
содержимое будет всегда включаться в родительское представление.

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


Компоненты представлений можно создавать тремя способами: определяя компо­
нент представления РОСО, наследуя от базового класса ViewComponent и применяя
атрибут ViewComponent . Приемы с классом РОСО и базовым классом описаны в пос­
ледующих разделах, а использование атрибута ViewComponent объясняется в разде­
ле "Создание гибридных компонентов контроллеров/представлений" по зже в главе.

Создание компонентов представлений РОСО


Компонент представления РОСО- это класс, который поддерживает функциональ­
ность компонента представления, не полагаясь на какие-то АРI-интерфейсы MVC. Как
и в случае контроллеров РОСО, с таким видом компонента представления неудобно ра­
ботать, но полезно знать особенности его функционирования. Компонентом представ­
ления РОСО является любой класс с именем, заканчивающимся на ViewComponent, в
Глава 22. Компоненты представлений 687
котором определен метод Invoke ().Классы компонентов представлений могут опре­
деляться где угодно в приложении, но по соглашению они собираются вместе в папке
по имени Components, расположенной на корневом уровне проекта. Создайте ука­
занную папку и добавьте в нее файл класса PocoViewComponent. cs с определением,
приведенным в листинге 22.13.

Листинг 22.1 З. Содержимое файла PocoViewComponen t. cs из папки Componen ts


using Systern . Linq;
using UsingViewCornponents.Models;
narnespace UsingViewCornponents . ViewCornponents
puЬlic class PocoViewCornponent {
private ICityRepository repository;
puЬlic PocoViewComponent(ICityRepository repo) {
repository = repo;

puЫic string Invoke()


return $" { reposi tory. Ci ties. Count () ) ci ties, "
+ $"{repository . Cities.Surn(c => c . Population)} people";

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


средство внедрения зависимостей. В рассматриваемом примере компонент представ­
ления РОСО объявляет зависимость от интерфейса ICi tyReposi tory, реализация
которого затем применяется в методе Invoke () для создания объектаstring, опи­
сывающего количество городов и суммарное число жителей.
Для использования компонента представления требуется Rаzоr-выражение @awai t
Component . Invoke. Компонент представления выбирается за счет предоставления в
качестве аргумента имени иласса без окончания ViewComponen t. В листинге 22.14
из разделяемой компоновки удаляется заполнитель, а взамен применяется иомпонент
представления РОСО.

Листинг 22. 14. Применение компонента представления в файле _ Layou t. csh tml

<!DOCTYPE htrnl>
<html>
<head>
<rneta narne="viewport" content="width=device - width " />
<title>@ViewBag . Title</title>
<link asp-href-include="liЬ/bootstrap/dist/css/*.rnin . css" rel="stylesheet" />
</head>
<body class="panel-body">
<div class="bg - prirnary panel -body " >
<div class="row">
<div class="col-xs-7"><hl>Products</hl></div>
<div class="col-xs-S">@await Component. InvokeAsync ( "Росо") </div>
</div>
</div>
<div class="panel-body">@RenderBody()</div>
</body>
</htrnl>
688 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC

Чтобы применить компонент представления, методу Invoke () в качестве аргу­


мента указывается имя Ро с о . Когда представление использует компоновку. оно нахо­

дит класс PocoViewCont r ol l er , вызывает его метод Invoke () и вставляет р е зультат


в вывод родительского представления (рис. 22.2).

Products ><

>С l.so_1ocalt1ost:S7584
----·--------·- -----·-·-------l
_ ~

Name

Рис. 22.2. Применение простого компонента представления

Хотя пример прост, он иллюстрирует ряд важных характеристик компонентов


представлений. Во - первых , класс Po c oVie wCornp o n ent смог получить доступ к тре­
бующимся ему данным без какой-либо зависимости от обработки НТТР-запроса или
от его родительского представления. Во-вторых, определени е лог ики, необходимой
для получения и обработки сводки по городам, в классе С# означает, что он а м ожет
быть легко подвергнута модульному тестированию (за примером обращайтесь к вр е з­
ке "Модульное те стирование компонентов представлений " далее в глав е). В-третьих,
форма приложения не нарушается из-за попытки включить объе кты City в модели
представлений, 1юторые ориентированы на объекты Produ ct . Словом, компонент
представл ения является самодостаточной порцией многократно используемой функ­
циональности , которая может применяться повсюду в приложении, к тому же разра­

батываться и тестироваться в изоляции.

Внимание! При применении компонента представления в представлении должно быть ука­


зано ключевое слово aw ai t . В случае простого вызова
@Cornponent . Invoke ошиб­
ка не возникнет, но отобразится строковое представление объекта Task , подобное
такому: Systern. Thread i ng . Tas k s . Ta s k' 1 [Mi crosoft . AspNe t Core . Htm l.
IHtrnlConten t ].

Наследование от базового класса ViewComponen t


Ком поненты представлений РОСО ограничены в функциональности , если только
н е задействован АРI-интерфейс МVС, что вполне возможно, но тр е бует намного боль­
ших усилий, чем другой распространенный подход, предусматривающи й наследова­
ние от класса ViewComp o n ent. Класс Vi ewCornp o n ent, определенный в пространстве
имен Micro s oft. AspNetCore . Mvc , обеспечивает удобный доступ к данным контекс­
та и облегчает ген ерацию результатов. В листинге 22.15 приведено содержимое фай­
ла Ci t ySurnrna r y . cs , добавленного в папку Cornponents.
Глава 22. Комnоненты nредставлений 689
Листинг 22.15. Содержимое файла Ci tySummary. cs из папки Componen ts
using Systern . Linq;
using Microsoft.AspNetCore.Mvc;
using UsingViewCornponents.Models;
narnespace UsingViewCornponents.Cornponents
puЫic class CitySurnrnary : ViewCornponent {
private ICityRepository repository;
puЫic CitySurnrnary(ICityRepository repo)
repository = repo;

puЫic string Invoke()


return $"{repository.Cities.Count()} cities, "
+ $"{repository.Cities.Surn(c => c . Population)} people";

При наследовании от базового класса ViewComponent указывать суффикс


ViewComponent в имени класса не обязательно. За исключением использования
базового класса этот компонент представления функционально идентичен версии
РОСО. В последующих разделах будет показано, как применять удобные средства,
предлагаемые базовым классом для работы с различными функциями компонентов
представлений.

Совет. Обратите внимание, что в листинге 22.15 метод Invo ke () не переопределялся.


Класс ViewComponent не предоставляет стандартной реализации метода Invoke (),
который должен быть определен явным образом.

В рамках подготовки к демонстрации функций компонентов представлений из­


мените компонент, используемый в разделяемой компоновке (листинг 22.16). Вместо
указания литеральной строки с именем компонента представления можно применять
выражение nameof, описанное в главе 4, что сократит шансы неправильно набора
имени класса.

Листинг 22.16. Изменение компонента представления в файле _ Layou t. csh tml


< ! DOCTYPE htrnl>
<htrnl>
<head>
<meta name="viewport " content="width=device - width" />
<title>@ViewBag.Title</title>
<link asp-href-include=" liЬ/bootstrap/dist/ css/* .min. css" rel=" stylesheet" />
</head>
<body class= "panel-body">
<di v class= "bg-pr irnary panel-body" >
<div class="row">
<div class="col-xs-7"><hl>Products</hl></div>
<div class="col-xs-5">
690 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC
@awa i t Component .I nvokeAsyn c( "CitySummar y " )
</div>
</d i v>
</ di v>
<div class= "panel - bod y " >@RenderBody()</d i v>
</bod y>
</html>

Понятие результатов компонентов представлений


Возможность вставки в родительское представление простых строковых значений
н е особенно полезна, но к счастью компоненты представлений способны на гораздо
большее. Более сложных эффектов можно достичь, заставив метод I nvoke () возвра­
щать объект, который реализует интерфейс I ViewComponen tResu l t . Доступны три
встроенных класса , реализующие интерфейс IV iewComponent Resu l t , которые опи­
саны в табл . 22.3 , наряду с удобными методами для создания их экземпляров, предо­
ставляемыми базовым классом View Componen t. Использование каждого типа резуль­
тата рассматривается в последующих разделах.

На заметку! В случае применения компонентов представлений РОСО создавать экземпля­


ры классов результатов можно напрямую , хотя работать с ними может быть неудобно ,
т.к. они имеют сложные аргументы конструкторов, которые удобные методы из класса
Vi ewComponen t предоставляют самостоятельно.

Таблица 22.З. Встроенные классы реализации IViewCornponentResul t

Имя Описание

ViewViewComponentResult Этот класс используется для указания представ­


ления Razor с дополнительными данными модели
представления . Экземпляры этого класса создаются
с применением метода View ()
ContentVi ewComponentResult Этот класс используется для указания текстового
результата, который будет безопасно закодирован с
целью включения в НТМL-документ. Экземпляры этого
класса создаются с применением метода Content ()
HtmlContentVi ewComponentResult Этот класс используется для указания фрагмента
НТМL-разметки , которая будет включена в НТМL­
документ без добавочного кодирования . Для со­
здания такого типа результата методы в классе

Vi ewComponent не предусмотрены

Два типа результатов обрабатываются специа.тiьным образом . Если компонент


представления возвращает объект string , тогда он исполь зуется для создания объ­
екта Con tentVi ewComponentRe s ul t , на который полагались предшествующие при­
меры. Если компонент представления возвращает объект реализации IHtmlContent,
то он применяется для создания объекта Html Conten tViewComponentResul t.
Глава 22. Компоненты представлений 691
Возвращение частичного представления

Самым полезным ответом является неуклюже именованный объект


ViewViewCornponentResult, который сообщает механизму Razor о необходимости ви­
зуализации частичного представления и включения результата в родительское представ­

ление. Базовый класс


ViewCornponent предлагает метод View () для создания объектов
ViewViewCornponentResul t, который имеет четыре доступных версии (табл. 22.4).

Таблица 22.4. Методы ViewComponent. View ()

Имя Описание

View() В случае использования этого метода выбирается стандартное пред­


ставление для компонента представления, а модель представления не

указывается

View(model) В случае применения этого метода выбирается стандартное представление,


а указанный объект используется в качестве модели представления
View(viewNarne) В случае применения этого метода выбирается указанное представле­
ние, а модель представления не предоставляется

View(viewName , В случае использования этого метода выбирается указанное представле­


rnodel) ние, а заданный объект применяется в качестве модели представления

Описанные в табл. 22.4 методы соответствуют методам, которые предоставляет


базовый класс Controller, и используются аналогичным образом. Добавьте в папку
Models файл 1\ласса по имени Ci tyViewModel. cs и определите в нем модель пред­
ставления, как показано в листинге 22. 17.

Листинг 22.17. Содержимое файла Ci tyViewModel. cs из папки Models

namespace UsingViewComponents.Models
puЫic class CityViewModel {
puЬlic int Cities { get ; set; }
puЫic int Population { get; set ;

В листинге 22.18 приведен модифицированный метод Invoke () компонента пред­


ставления CitySurnmary, который теперь применяет метод View () для выбора час­
тичного пр едставления и передачи ему данных представл ения с использованием объ­
екта Ci tyViewModel.
Листинг 22.18. Выбор частичного представления в файле Ci tySummary. cs
using System . Linq ;
using Microsoft.AspNetCore . Mvc;
using UsingViewComponents . Models ;
namespace UsingViewComponent s.Components
puЫic class CitySurnrnary : ViewCornponent {
private ICityRepository repository;
puЫic CitySurnrnary(ICityRepository repo)
repository = repo ;
692 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC
puЫic IViewComponentResult Invoke() {
return View(new CityViewModel{
Cities = repository.Cities.Count(),
Population = repository.Cities.Sum(c => c.Population)
}) ;

Выбор частичного представления в компоненте представления похож на выбор


представл ения в контроллере, но с двумя важными отличиями: механизм Razor ищет
представления в других местоположениях и применяет другое имя представления ,

если оно не указано .

Поскольку для компонента представления частичное представление н е создава­


лось, после запуска приложения вы увидите сообщение об ошибке, которое пок аз ы­
вает, какие файлы искал механизм Razor:
• /Views/Home/Components/CitySumrnary/Default . cshtml
• /Views/Shared/Components/CitySwтunary/De f ault . cshtml

Если имя не указано, тогда Razor ищет файл Defaul t . cshtml . В поисках частич­
ного пр едставления механизм Razor просматрива ет два местоположения. П е рво е мес­
тоположение учитывает имя контроллера, обрабатывающе го НТТР-з апрос, что позво­
ляет каждому контроллеру иметь собственное пр едставл ение. Второе местоположение
ра зделяется м ежду всеми контроллерами.

Совет. Обратите внимание, что разделяемые частичные представления по-прежнему распозна ­


ются компонентом представления , т.е. компоненты представления не разделяют частичные

представления . Такое поведение можно переопределить, включая путь в имя представления


при вызове метода View (); таким образом, вызов View ( "Views/Shared/Components/
Common/Defaul t . html " ) переопределяет нормальные местоположения для поиска.

Чтобы завершить пример , со здайте папку Views/Home/Components/Ci tySummary


и добавьте в нее новый файл по имени Defaul t . cshtml , поместив в н ег о разметку
и з листинга 22.19.
Листинг 22.19. Содержимое файла Defaul t. cshtml из папки
Views/Home/Components/CitySummary
@mode l CityViewModel
<tаЫе c l ass= " taЫe taЬle - condensed taЬle - bordered " >
<tr>
<td>Cities : </td>
<td class= "text-right " >
@Model . Cities
</td>
</tr>
<tr>
<td>Popu l ation:</td>
<td class= " text -right " >
@Model . Popu l ation . ToString( " # , ### " )
</td>
</tr>
</tаЫе>
Глава 22 . Компоненты представлений 693
Частичные представления для компонентов представлений работают таким же
способом, как и для контроллеров. В данном случае было создано строго типизиро­
ванное представление, которое ожидает объекта Ci tyViewModel и отображает зна­
чения е го свойств Cities и Population в таблице (рис. 22.3).

С) Products Х

!-·-~ - ·.--с-----------
[ @ 1~57584
---·

Рис . 22.3. Визуализация представления с использованием компонента представления

Возвращение фрагментов НТМL·разметки


Класс ContentViewComponentResu l t применяется для включения фрагмен­
тов НТМL-разметки в родительское представление, не используя представл ение.

Экземпляры класса ContentViewComponentResul t создаются с применением ме­


тода Content () ,унаследованного от базового класса
ViewComponent, который при­
нимает значе ние string. Использование метода Content () демонстрируется в лис­
тинге 22.20. В дополнение к методу Content () возвращать string способен также
метод Invoke (),и МVС будет автоматически преобразовывать это значение string
в объект ContentViewComponentResul t .

Листинг 22 .20. Применение метода Content () в файле Ci tySummary. cs


using System . Linq ;
using Microsoft . AspNetCore . Mvc ;
using UsingViewComponents . Mode l s ;
namespace UsingViewComponents . Components
puЫic class CitySumrnary : ViewComponent {
private ICityRepository repository ;
puЫi c CitySumrnary(ICityRepository repo)

repository = repo ;

puЫic IViewCornponentResult Invoke() {


return Content( "This is а <h3>< i >stri ng </ i ></h3> " ) ;
694 Часть 11. Подробные сведения об инфраструктуре ASP.NET Саге MVC
Полученная методом Content () строка кодируется. чтобы стать безопасной для
включения в НТМL-докум ент. Это особенно важно при работе с содержимым, которое
было предоставлено пользователями или внешними системами, т.к. предотвращает
внедрение JavaScript-coдepжимoгo в НТМL-разметку, генерируемую приложением.
В этом примере строка, которая передается методу Content (), содержит базовые
НТМL-дескрипторы, и после запуска приложения вы увидите, что они были безопас­
но закодированы (рис. 22.4).

~ - С ~ localh_ost:S7584 - - - - -
·---...-----~-----"...- -----·--"'-------··----------------··

Name Price
~

Рис. 22.4. Возвращение закодированного фрагмента НТМL-разметки


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

Взглянув на НТМL-разметку, выпускаемую компонентом представления , можно


заметить, что символы угловых скобок бьши заменены так, что браузер не интерпре­
тирует содержимое как НТМL-элементы:

<div class= " col-xs-5 " >


This is а &lt;hЗ&gt;&lt;i&gt;string&lt;/i&gt;&lt;/hЗ&gt; </div>

Кодировать содержимое не понадобится, если вы доверяете источнюtу и хотите


его интерпретировать как НТМL-разметку. Метод Content () всегда кодирует свой ар­
гумент. поэтому вы должны создать объект HtmlContentViewComponentResul t на­
прямую и передать его конструктору объект HtmlString, представляющий строку, о
которой известно, что ее безопасно отображать, либо потому, что она поступила из
надежного источника. либо из-за того, что вы уверены в том, что она уже закодиро­
вана (листинг 22.21).

Листинг 22.21. Возвращение фрагмента надежной НТМL-разметки


в файле Ci tySummary. cs
using System .Linq;
using Microsoft . AspNetCore.Mvc ;
using UsingViewComponents.Models;
using Microsoft.AspNetCore.Mvc.ViewComponents;
using Microsoft.AspNetCore.Html;
namespace UsingViewComponents.Components {
puЬlic class CitySummary : ViewComponent {
Глава 22. Компоненты представлений 695
private ICityRepository repository;
puЫic CitySummary(ICityRepository repo)
repository = repo;

puЫic IViewComponentResult Invoke() {


return new HtmlContentViewComponentResult(
new HtmlString("This is а <hЗ><i>string</i></hЗ>"));

Такой прием должен применяться осторожно и только с источниками содержи­


мого, которые не могут быть подделаны и выполняют собственное кодирование.
Запустив приложение, вы увидите, что угловые скобки были включены в родитель­
ское представление без модификации, что позволяет браузеру интерпретировать вы­
вод компонента представления как НТМL-элементы (рис. 22.5).

Получение данных контекста


Детали о текущем запросе и родительском представлении компонент представле­
ния получает через свойства класса ViewComponentContext, наиболее полезные из
которых описаны в табл. 22.5.

Таблица 22.5. Полезные свойства класса ViewComponentContext

Имя Описание

Arguments Это свойство возвращает словарь аргументов, предостав­


ляемых представлением, который можно также получить
через метод Invoke ()
HtmlEncoder Это свойство возвращает объект HtmlEncoder , который
можно применять для безопасного кодирования фрагмен­
тов НТМL-разметки

ViewComponentDescriptor Это свойство возвращает объект


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

ViewContext Это свойство возвращает объект ViewContext из


родительского представления. Возможности класса
ViewContext обсуждались в главе 21
ViewData Это свойство возвращает объект ViewDataDictionary,
который открывает доступ к данным представления, пред­
назначенным для компонента представления

Базовый класс ViewComponent предлагает набор удобных свойств, которые об­


легчают доступ к специфической информации контекста (табл. 22.6).
696 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

[j Products х

Рис. 22.5.
Price
-....~~/-.•-" .....
Возвращение фрагмента незакодированной НТМL-разметки
-.
с использованием компонента представления

Таблица 22.6. Удобные свойства класса ViewComponent

Имя Описание

ViewComponentContext Это свойство возвращает объект ViewComponentContext


HttpContext Это свойство возвращает объект HttpContext, который описы­
вает текущий запрос и подготавливаемый ответ

Request Это свойство возвращает объект HttpReques t, который описы­


вает текущий НТТР-запрос

User Это свойство возвращает объект реализации IPrincipal, кото­


рый описывает текущего пользователя (глава 28)
RouteData Это свойство возвращает объект RouteData, который описывает
данные маршрутизации для текущего запроса (глава 15)
ViewBag Это свойство возвращает динамичес кий объе кт ViewBag, кото­
рый можно использовать для передачи данных между компонен­
том представления и представлением

ModelState Это свойство возвращает объект ModelStateDictionary, кото­


рый предоставляет детали процесса привязки моделей (глава26)
ViewContext Это свойство возвращает объект ViewContext, который был пе­
редан родительскому представлению (глава 21)
ViewData Это свойство возвращает объект ViewDataDictionary, кото­
рый обеспечивает доступ к данным представления для компонен­
та представления

Url Это свойство возвращает объект реализации IUrlHelper, который


можно применять для генерации URL, как объяснялось в главе 15

Данные контекста могут использоваться любым спо собом, который помогает ком­
поненту представления выполнять свою работу, включая варьирование метода вы­
бора данных или визуализацию другого содержимого либо представлений. В листин­
ге 22.22 данные маршрутизации применяются для суж ения выборки объектов Ci ty.
Глава 22 . Компоненты представлений 697
Листинг 22.22. И спользование данных к онтекста в файле Ci tySummary. cs

using System . Linq ;


using Mic r osoft . AspNetCore . Mvc ;
using UsingViewComponents . Models ;
using Microsoft . AspNetCore . Mvc . Vi ewComponents ;
using Microsoft . AspNetCo re . Mvc . Rendering ;
n amespace UsingViewComponents . Components {
puЫic class CitySummary : ViewComp o n en t {
pr i vate I CityRepos i tory repository ;
puЫic Ci tySummary( I CityRepository repo}
repository = repo ;

puЫic IViewCompon en t Res u lt I nvo ke () {


string target =
RouteData.Values["id"] аз string;
var cities = repository.Cities
. Where (ci ty => target == null 11
string.Compare(city.Country, target, true) О);
return View(new CityViewModel{
Cities =
cities.Count(),
Population = cities.Sum(c => c.Population)
}) ;

Браузе р применя ет сегмент id из м аршрута для указания страны , которая исполь­

зуется LINQ при фильтрации объектов в хранилище. Запустив приложение и запро­


сив стандартный URL, вы увидите, что отображается информация по вс ем городам.
Сузить выборку можно путем запрашивания URL вроде / Home/ I ndex/ USA, который
огр аничит выбор городами в США (рис. 22.6) .

[':) Products х

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


698 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC
получение данных контекста из родительского
представления с использованием аргументов

Родительски е представления могут предоставлять дополнительные данные кон­


текста в виде аргументов для выражения @await Cornponent. Invoke . Это с р едство
можно применять для получения данных из модели родительского представления или

для сообщения инструкции о типе содержимого, которое должен выдавать компонент


пр едставления. Чтобы посмотреть на него в работе, создайте в папке Views/Horne/
Cornponent/Ci tySurnrnary файл представления по имени Ci tyList . cshtml и добавь­
те в него разметку из листинга 22.23.
Листинг 22.23. Содержимое файла Ci tyList. cshtml
из папки Views/Home/Component/CitySummary
@model IEnumeraЫe<City>
<tаЫе class= " ta Ы e taЫe -c ondens ed taЬle-bordered">
@foreach (var city in Model) (
<tr>
<td>@city . Name</td>
<td c l ass= "text- right ">
@city.Population.ToString( " # , ### " )
</td>
</tr>

<tr>
<th>Total : </th>
<td class= "t ext - right " >
@Model . Sum(p => p . Popu l ation) .T oString( " # , ### ")
</td>
</tr>
</tаЫе>

Определение второго представления позволит компоненту представления произ­


водить выбор между ними на основе аргумента , добавленного к м етоду Invoke (), как
показано в листинге 22.24.
Листинг 22.24. Выбор представления в файле Ci tySummary. cs
using System . Linq ;
using Microsoft . AspNetCore.Mvc ;
using Usi ngVi ewComponents .Mode l s ;
using Microsoft .AspNe t Core . Mvc . ViewComponents ;
using Microsoft . AspNetCore.Mvc.Rendering ;
namespace UsingV ie wComponents . Components {
puЫic c l ass CityS urnmary : ViewCornponent {
private ICityRepository repository;
puЫic CitySummary(ICityRepository repo)
repositor y = repo;

puЬlic IViewComponentResul t Invoke (bool showList)


if (showList) {
return View("CityList", repository.Cities);
Глава 22. Компоненты представлений 699
else {
return View(new CityViewModel
Cities = repository.Cities.Count(),
Population = repository.Cities.Sum(c => c.Population)
}) ;

Если аргумент showList метода Invoke () равен true, тогда компонент пред­
ставления выбирает CityList и передает все объекты City из хранилища как мо­
дель представления. Если аргумент showList равен fals e, тогда выбирается стан­
дартное представление с указанием объекта Ci tyViewModel в качестве модели
представления.

Финальный шаг связан с предоставлением данных контекста, когда компо­


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

Invoke () передается анонимный объект (листинг 22.25).

Листинг 22.25. Предоставление данных контекста во время применения компонента


представления в файле _ Layou t . csh tml
< ! DOCTYPE html>
<html>
<head>
<meta name= "viewpor t" content=" width=dev ice - width " />
<title>@ViewBag . Title</title>
<link asp-href-include= " liЬ/bo otst rap/dist/ css/* .min. css " rel=" stylesheet" />
</head>
<body class= "panel - body " >
<div class ="bg - primary panel - body " >
<div class= " row " >
<div class= " col - xs - 7 " ><hl >P r oducts </h l ></div>
<div class= " col -xs-5" >
@await Component .InvokeAsync ("CitySummary ",
new { showList = true } )
</div>
</div>
</div>
<div class= "panel-body " >@RenderBody()</div>
</body>
</html >

После запуска приложения компонент представления получит значение, указываемое


родительским представл ением , и отреагирует соответствующим образом (рис . 22.7).
700 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

D Products Х

~ " С [ ф local~os~~S75~/Hoine/lndex/US~-·---·· -~
- · - - - - - - - · - - - - - - _ _ _4_, __ _
; 1

Рис. 22.7. Предоставление данных контекста компоненту представления

Модульное тестирование компонентов представлений

Компоненты представлений следуют общему подходу MVC, предусматривающему отделе­


ние логики выбора и обработки данных модели от разметки представления , которая их фор­
матирует и отображает, что облегчает проведение модульного тестирования. Вот пример
модульного теста для класса Ci tySummary из рассмотренного приложения:

using System . Collections.Generic ;


using Microsoft . AspNetCore.Mvc.ViewComponents ;
using Moq ;
using UsingViewComponents . Models ;
using UsingViewComponents.Components;
using Xunit;
namespace UsingViewComponents .Tests {
puЫic class SummaryViewComponentTests
[Fact]
puЫic void TestSummary() {

11 Организация
var mockRepository = new Moc k<ICityRepos i tory>();
mockRepository.SetupGet(m => m.Cities) . Returns(new List<City> {
new City { Population 100 } ,
new City { Population 20000 ) ,
new City { Populat i on 1000000 } ,
new City ( Popula t ion 500000 }
) ) ;
var viewComponent
= new CitySummary(mockRepository.Object) ;
Глава 22 . Компоненты представлений 701
11 Действие
ViewViewComponentResult result
= viewComponent . Invoke(false) as ViewViewComponentResult ;
11 Утверждение
Assert . IsType(typeof(CityViewModel) , result . ViewData . Model) ;
Assert .Equal(4 , ((CityViewModel)result . ViewData . Model) . Cities) ;
Assert . Equal(l520100 ,
( (CityViewModel)result . ViewData . Model) . Population) ;

Для организации теста создается имитированное хранилище, которое передается конструк­


тору класса CitySurnmary, чтобы создать экземпляр компонента представления. В разде­
ле действия теста вызывается метод Invoke (),который предоставляет объект результата.
Компонент представления выбирает представление Razor, поэтому результат приводится к
ViewViewComponentResul t и затем через предлагаемое им свойство ViewDa ta .
типу
Model осуществляется доступ к объекту модели представления. В разделе утверждения
теста проверяется тип данных модели представления и содержащиеся в ни х значения .

Создание асинхронных компонентов представлений


Все рассмотренные до сих пор примеры были синхронными 1<омпонентами пред­
ставлений, которые легко опознать по тому, что они определяли метод Invoke ().
Если компонент представления полагается на асинхронные АРI-интерфейсы, то мож­
но создать асинхронный компонент представления, определив метод InvokeAsync (),
который возвращает объект Task. Когда механизм Razor получает объектTask из ме­
тода InvokeAsync () , он будет ожидать его завершения и затем вставит результат в
главное представление. Для подготовки настоящего примера в проект понадобится
добавить пю<ет (листинг 22.26).
Листинг 22.26. Добавление пакета в файле project. json

"dependencies ": {
"Microsoft . NETCo r e . App ":
"version " : "1. 0 . О ",

"type ": "platform "


) 1

"Microsoft . AspNetCore . Diagnostics " : "1. О. О ",


"Microsoft . AspNe t Core . Server . IISintegration ": "1. 0 . 0",
"Microsoft . AspNetCore . Server . Kestrel ": " 1 . О . О ",
"Microsoft.Extensions . Logging . Consol e": " 1 . 0 . 0",
"Microsoft . AspNetCo r e . Mvc" : " 1. О . О ",
"Microsoft. AspNetCore . StaticFiles ": " 1. О. О ",
"Microsoft . AspNetCore . Razor . Tools" :
"version ": "l. O. O- preview2-final ",
" type " : "build "
} 1

"System.Net.Http": "4.1.0"
) 1
702 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

Пакет System. Net . Http предоставляет АРl-интерфейс для выполнения асинхрон ­


ных НТГР-запросов, который будет использоваться для отправки запросов к веб - с айту
Apress.com. Добавьте в папку Components файл класса по имени PageSize . cs, с о­
держимое которого показано в листинге 22.27.

Листинг 22.27. Содержимое файла PageSize. cs из папки Components


using System.Net .H ttp ;
using System. Th r e adin g. Tasks ;
using Mic r osoft . AspNe tCore . Mvc ;
namespace UsingViewComponents . Components
p uЬli c clas s PageS i ze : Vi ewCompone nt {
puЫ i c async Task<IVi e wCompone nt Resu lt> I nvoke Async() {
Http Cl i ent clie nt = new HttpC li ent() ;
Ht t pRe spon s eMe ssage r esp on se
= a wa it cl i e nt.G etAsync( "h t tp: //a pre s s. com" ) ;
return View(response . Co ntent .Headers . Content Length) ;

В методе I n vo keAsync () применяются ключевые слова a syn c и a wa i t , опис ан­


ные в главе 4, для потребления асинхронного АРl-интерфейса, пр едлагаемого 1шас­
сом HttpClient, и получения длины содержимого , возвращаемого в результате от­
правки з апросаGET веб-сайту Apress.com. Длина п ер едается методу View () ,который

выбирает стандартное частично е представление , ассоциированное с компон е нто м


представления.

Чтобы создать это представление, добавьте в проект папку Views / Home / Componen ts /
PageS i ze и поместите в нее файл представления по имени Default . cshtml с содер­
жимым, приведенным в листинге 22.28.

Листинг 22.28. Содержимое файла Defaul t. cshtml


из папки Views/Home/Components/PageSize
@model l ong
<div cla ss=" panel- body bg - i n fo " >Page s i ze : @Mo del </d i v>

Осталось лишь использовать компонент, что и делается в файле _ Layout. cshtml


(листинг 22.29).

Листинг 22.29. Применение асинхронного компонента представления


в файле _Layout. cshtml
< ! DOCTYPE html>
<html >
<head>
<meta name= " viewport " content= "width=device - width " />
<ti t l e >@ViewBag.T i t l e</titl e >
<l i nk a s p - h r ef-include = " l i Ь/bootstra p/ d i st/css/* . m in. css " re l ="st yl esheet" />
</head>
Глава 22. Компоненты представлений 703
<body c l as s = "panel - body " >
<di v c l a ss=" bg - pr i rnary pan e l-body " >
<div c l as s=" row " >
<div c l a ss=" col - xs - 7 " ><hl> Produ c t s </h l ></ di v>
<di v c l ass =" col - x s- 5 " >
@awai t Cornponen t . I nvokeAs ync ( "Ci t ySurnrna ry",
ne w { show Li st = t rue } )
</di v>
</ di v>
</di v>
<div class= "pane l- body " >@Re nd er Body()</ di v>
@await Component. InvokeAsync ("PageSize")
</body>
</htrnl>

Запустив приложение, вы увидите, что браузер отображает новое добавление к со­


держимому (рис. 22.8). Выводимое число может измениться, когда вы запустите при­
мер приложения , т.к . в Apress часто обновляют свой веб-сайт.

t;occer ball 1&.~u

!
1
J
i
:
l.-.. -·-- -----·---·---------------·-------------......!
Рис. 22.8. Создание асинхронного компонента представления

Создание гибридных компонентов


контроллеров/представлений
Компоненты представлений часто предоставляют сводку или краткую характерис­
тику функциональности, которая поддерживается контроллером. Например, компо­
не нты представлений, отображающие сводку по корзине для покупок, нередко содер ­
жат ссылку, нацеленную на контроллер , который выводит подробный список товаров
в корзине и может исполь з оваться для зав е ршения покупок и перехода к оплате .

В такой ситуации можно создать класс, являющийся контроллером и компонентом


представления, который позволяет сгруппировать вместе связанную фующиональ­
ность и сократить дублирование кода. Добавьте в папку Contro ll e r s файл класса
по имени Ci tyController . c s и определите в нем контроллер, как показано в лис­

тинге 22.30.
704 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC

Листинг 22.30. Содержимое файла Ci tyController. cs из папки Controllers


us i ng Sys t em . Col l ec ti o n s . Gene ri c ;
using Mi crosoft . AspNe t Core . Mvc ;
using Mi crosoft . As p NetCore . Mvc . Vi ewComponents ;
u sing Mi crosoft . As p NetCore . Mvc . Vi ew Fe a tures ;
us i ng Us ingViewCompon ents . Mode l s ;
n amespace UsingVi ewCompo n ents . Co n t r o ll ers
[Vi e wComp o nent (Name = " Combo Component")]
puЬl i c class Ci ty Co n t r o ll e r : Co n t r o ll e r
p r i vate I Ci tyRe p ository r e p o sit o r y ;
puЬlic CityContro l le r ( I Ci tyRe po s ito r y repo)
r e p os i tory = r e p o ;

pu Ьli c Vi ewResul t Cr eate ( ) = > View ( ) ;


[ HttpPost ]
puЫic I Actio n Res u l t Create(C it y n ewC it y)
repository.Ad dC ity(newC i ty) ;
return Redirect ToAction ( " Ind e x", " Home " ) ;

p u Ьli c IViewComponentRes u lt Invoke() => n ew Vi ewVi ewCompon e n t Result()


Vi ewData = n ew V i ewData D ic ti o n a r y< IEn ume r a Ьl e<City>>(View D ata ,
r epository . Cities)
};

Атрибут Vi ewCompo nen t применяется к классам , не унаследованным от базово ­


го класса Vi ewCompo ne n t и не им е ющим в своих именах суффикса ViewCompone n t,
т.е. нормальный процесс обнаружения не сможет отнести их к категории компоне н­
тов представлений. Свойство Na me устанавливает имя , по которому на класс м ож ­
но ссылаться во время его исполь з ования в выр ажении @Component . Invoke внут­
ри родительского представления. В данном примере свойство Na me применяется
для установки имени части класса, относящейся к компоненту представл е ния , в
ComboComponent . Это имя будет использоваться для обращения к компоненту пр ед­
ставления и поиска его представл е ний.
Поскольку гибридные классы не наследуются от базового класса ViewCompone n t,
они не имеют доступа к удобным методам для создания объектов реал и за ­
ции I Vi e wCompon e ntR e s ul t , что означает н е обходимость в создании объ е кта
Vi e wv iewComponen tResul t напрямую, как делалось бы в компоненте представления
РОСО.

Создание гибридных представлений


IИбридный класс требует двух наборов представлений: того, который визуализи­
руется в случае применения класса как контроллера. и того , который визуализирует­
ся при использовании класса как компонента представления. Создайте папку Views/
Ci t y и добавьте в нее файл представления по имени Creat e . csht ml с соде ржимым
и з листинга 22.31.
Глава 22 . Компоненты представлений 705
Листинг 22.31. Содержимое файла Create. cshtml из папки Views/Ci ty
@model City
@{
ViewData ["T itle "J "Create City ";
Layout = "_ Layout ";

<form method= "post " asp - action= " Create " >
<div class= " form - gro up " >
<label asp-for= " Name " >Name : </label>
<input class= " form - control " asp - for= " Name " />
</div>
<div class= " form - group " >
<label asp - for= "Country " >Co untry : </label>
<input c l ass= " form-control " asp - for= "Country " />
</div>
<div class= " fo r m- group " >
<label asp - for= " Pop ulation " >Popula ti o n: </ l abel>
<input class= " form - control " asp-for= " Population " />
</div>
<button type= " submit " class= " Ьt n ьt n- p r i m ary " >Create</b u tton>
<а class= " Ьtn Ьtn - default " asp - controlle r = " Home "
asp - action= " Index " >
Cancel
</а>
</form>

Э то пр едставление выводит простую форму для создания новых объектов Ci ty .


Щелчок на кнопке Create (Создать) приводит к отправк е запроса POST к дей ствию
Crea te контролл е ра Ci ty, а на кнопке Cancel (Отме на) - к отпр авке запрос а GET к
де йствию Index контролл е р а Н оте .
Создайте пап ку Vi e ws/S h ared/Compon en ts/ComЬoCo mpone n t и добавьте в не е
ф айл пр едст авления по им е ни Defaul t . cs h trnl, содержим ое которого пок азано в
листинге 22.32. Частичное представл е ние помещено в папку Views/Shared , потому
что она будет применяться для поиска представлений контроллером, в чьем пр ед­
с т авле нии исполь зуется компон е нт пр едставл ения. имя которого включа ется в путь.

Листинг 22.32. Содержимое файла Defaul t. csh tml


из папки Views/Shared/Components/ComЬoComponent

@mode l IEnumeraЫe<City>

<tаЫе class= " taЫe taЬle - condensed taЬle - bordered " >
<tr>
<td>B i ggest City : </td>
<td>
@Model . Orde r ByDescendin g(c => c .Popu latio n ) . First() . Name
</td>
</tr>
</tаЫе>
<а class= " Ьtn Ьtn - sm Ьtn - info " asp - cont r oll er= "City " asp - action= "Create " >
Create City
</а>
706 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Соге MVC

Частичное представление Defaul t. cshtml получает последовательность объек­


товCi ty и сортирует ее с помощью LINQ, чтобы выбрать объект с наибольшим зна­
чением Popu la tion. Кроме того, определен якорный элемент, стилизованный под
кнопку, который нацелен на действие Create контроллера City.

Совет. Обратите внимание на явное указание контроллера Ci ty для элемента а в лис­


тинге 22.32. Дело в том, что URL генерируются с применением данны х контекста, пре­
доставляемых родительским представлением; это значит, что запрос обрабатывается
стандартным контроллером, а не контроллером, который является также компонентом
представления. Если опустить атрибут asp - controller, то ссылка будет нацелена на
действие Create контроллера Home .

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


Финальный шаг заключается в применении гибридного класса 1\ак компонента
представления в разделяемой компоновке с использованием имени, которое было ука­
зано посредством атрибута ViewCornponent (листинг 22.33).

Листинг 22.33. Применение гибридного класса в файле_Layout. cshtml

<!DOCTYPE html>
<html>
<head>
<meta name= "viewport " content= "width=device -w idth " />
<title>@ViewBag . Title</title>
<link asp-href -i nclude="liЬ/bootstrap/dist/css/* . min.css" rel= "stylesheet " />
</head>
<body class= "panel - body " >
<div class= "bg-p rimary panel-body">
<div class ="r ow " >
<div class= " col - xs - 7"><hl>Products</hl></div>
<div class ="c ol - xs - 5 " >
@awai t Component . InvokeAsync ( "ComЬoComponent")
</div>
</ d iv>
</div>
<div class =" panel - body " >@RenderBody()</div>
</body>
</html>

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

рованным контроллером (или, если хотите, контроллер, который имеет собственный


интегрированный компонент представления). Запустив приложение, вы увидите,
что в качестве самого густонаселенного города указан Лондон (London). Щелкните
на кнопке Create City (Со здать город). чтобы отобразить форму, которая позволит до­
бавить новый объект Ci ty в приложение. Заполните и отправьте форму , после чего
контроллер получит данные, обновит хранилище и перенаправит браузер на стандар­
тный URL приложения. Если вы добавили объект Ci ty, население которого превы­
шает показ атели населения других городов, имеющихся в хранилище, тогда вывод из

компонента представления изменится, как пок азано на рис. 22.9.


Глава 22 . Комnоненты nредставлений 707

с [~ ~~~!~!ё~~~~~~--=-~-==~=--;=ю. - ~ -!

№rne:

Bei1lng

Counlry:

Chlna

Population: Name Price

/ Kayak 275

Lile/ackel 48.95

" \ Can~~ 1
, Soccer ba!I 19 .50

--. - -------- ----- -· ---·---{ 111 1

[__ ·------ -- -- - ----- ---- - - - _J


Рис . 22.9. Использование гибридного компонента контроллера/представления

Резюме
В этой главе было дано введение в компоненты представлений, которые являют­
ся новым средством ASP.NET Core MVC и заменяют собой средство дочерних дейс­
твий из предыдущих версий MVC . Вы узнали, каким образом создавать компоненты
представлений РОСО и как применять базовый класс ViewComponent . Были проде­
монстрированы три разных типа результатов, которые могут выпускать компонен­

ты пр едставлений , в том числе выбор частичных представлений для их включения в


родител ьско е представление. В завершение главы было показано , как добавить фун ­
кциональность компонентов представлений в класс контроллера, чтобы сократить
дублировани е кода и упростить приложение. В следующей главе рассматриваются
дескрипторные вспомогательные классы , которые используются для трансформации
НТМL-элементов в представлениях.
ГЛАВА 23
Дескрипторные
вспомогательные классы

д ASP.NET
ескрипторные вспомогательные классы -
Core MVC.
это новое средство, появившееся в
Они являются классами , которые трансформируют НТМL­
элеме нты в пр едставлении. Распространенные случаи использования дескрипторных
вспомогательных классов включают генериров ание URL для форм с применен ием
конфигурации маршрутизации приложения, обеспечение согласованной стилизации
элементов специфического типа и замена специальных СОI{ращающих элементов хо­
довыми фрагментами содержимого. В настоящей главе будет показано, как работают
дескрипторные вспомогательные классы и каким обра зом создавать и использовать
специальные деск рипторные вспомогательные классы. В главе 24 будут описаны
встроенные деск рипторны е вс помогател ьные классы, которые поддерживают НТМL­

формы, а в главе 25 - другие встроенные дескрипторные вспомогательные классы,


предоставляемые инфраструктурой MVC. В табл . 23.1 приведена сводка, позволяю­
щая поместить дескри птор ные вспом огательные классы в контекст.

Таблица 23. 1. Помещение дескрипторных вспомогательных классов в контекст

Вопрос Ответ

Что это такое? Дескрипторные вспомогательные классы - это классы , которые мани­
пулируют НТМL- элементами с целью их изменения каким - либо обра­
зом, дополнения добавочным содержимым или полной замены новым
содер жим ым

Чем они полезны? Дескрип торные вспомогательные классы позволяют генерировать


либо трансформировать содер жимое представления с использовани­
ем логики С#, гарантируя, что отправляемая браузеру НТМL-разметка
отражает состояние приложения

Как они Элементы HTML, к которым применяются дескрипторные вспомогатель­


используются? ные классы, выбираются на основе име ни класса или за счет использова­
ния атрибута HTMLTargetElement . Когда представление визуализиру­
ется , эле м енты трансформ и руются дес к рипторными вспомогательными
классами и включаются в НТМL-разметку, отправляемую клиенту

Существуют ли Довольно легко увлечься и генерировать с применением дескриптор­


какие-то скры - ных вспомогательны х классов сложные разделы НТМL-разметк и, что
тые ловушки или намного проще достигать с помощью компонентов представлений,
ограничения? как объяснялось в главе 22
Существуют ли Вы не обязаны использовать дес к рипторные вспомогательные классы , но
альтернативы? они облегчают генерацию сложной НТМL-разметки в приложе ниях MVC

Изменились ли они Дескрипторные вспомогательные классы являются новым добавлени­


по сравнению ем в ASP.NET Core MVC и заменяют собой функциональность , которую
с версией MVC 5? предлагали вспомогательные методы HTML
Глава 23. Дескрипторные вспомогательные классы 709
В табл. 23.2 прив едена сводка для настоящей главы.

Таблица 23.2. Сводка по главе

Задача Решение Листинг

Трансформация Создайте дескрипторный вспомогательный 23.1-23.13


НТМL-элемента класс и зарегистрируйте его с примене­
нием выражения @addTagHelper в пред­
ставлении или в файле импортирования
представлений

Управление областью дейс­ Используйте атрибут Html TargetElement 23.14-23.18


твия дескрипторного вспомо-

гательного класса

Поддержка сокращающего Применяйте объект TagHe l perOutput для 23.19, 23.20


элемента генерирования заменяющих элементов

Вставка содержимого вок ­ Используйте предоставляемые классом 23.21-23.24


руг или внутрь целевого TagHelpe r Ou t put свойства, имена которых
элемента начинаются на Pre и Pos t

Получение данных контекста Декорируйте свойство атрибутами 23.25, 23.26


в дескрипторном вспомога­ ViewContext и Html Att ributeNo t Bound
тельном классе

Получение доступа к модели Применяйте свойство ModelExpression 23.27, 23.28


представления

Согласование дескрипторных Используйте свойство 23.29 , 23.30


вспомогательных классов TagHelperCon text . Items
Подавление элемента Применяйте метод Supp res s Output () 23.31, 23.32

Подготовка проекта дл я прим ера


Создайте новый проект типа Empty (Пустой) по имени Ci t i e s с использова ­
ни е м шаблона ASP.NET Core Web Application (.NET Core) (Веб-приложение ASP.NET
Core (.NET Core)). Добавьте требуе мы е пакеты NuGet в раздел dependenc i es файла
project . json и н астройте инструментарий Razor в разделе tools , как показано в
листинге 23.1. Разделы , которые н е нужны для данной главы, понадобится удалить .

Листинг 23.1. Добавление пакетов в файле project. j son

"dependencies ": {
"Microsoft . NETCore . Ap p":
" version ": 11
1.0 . 0 11 ,

" type ": "platform "


}'
"Microsoft . AspNetCore . Diagnostics ": " 1 . О . О ",
"Microsoft . AspNetCore . Server . IISin te gratio n": "1 . 0 . 0",
"Microsoft . AspNetCore.Server . Ke st re l ": " 1 . 0 . 0 ",
"Mi crosoft .E xtension s.Logg i ng. Co ns ol e ": " 1 . 0 . 0",
"Microsoft.AspNetCore.Mvc": 11 1.0.0 11 ,
"Microsoft.AspNetCore.StaticFi1es": 11
1.0.0 11 ,
71 О Часть 11 . Подробные сведения об инфраструктуре ASP.NET Соге MVC

"Microsoft.AspNetCore.Razor.Tools":
11
version": 11
1. О. 0-preview2-final. 11 ,

"type": "build"

}/
11
t ools 11 :
"Microsoft . AspNetCore .Server.IISintegration . Tools ": "l. 0.0 - preview2 - final ",
"Microsoft.AspNetCore.Razor.Tools 11 : 11 1.0.0-preview2-final"
}/
11
frameworks 11 : {
11
netcoreappl . 0 11 :
imports 11 : [ " dotnetS.6 11 , " portaЬle - net45+win8"]
11

}'
" buildOptions 11 :
" emi tEntryPoint 11 : true, " preserveCompilationContext 11 : true
}/
11
runtimeOptions 11 : {
11
configProperties 11 : { "S ystem.GC . Server ": true}

Создание модели и хранилища


Создайте папку Models и добавьте в нее файл класса по имени Ci ty. cs с опреде­
лением из листинга 23.2.

Листинг 23.2. Содержимое файла Ci ty. cs из папки Models


namespace Cities . Models {
puЫic class City {
puЫic string Name { get; set; }
puЫic string Country { get; set;
puЬlic int? Population { get; set ; }

Чтобы создать хранилище для объектов Ci ty, добавьте в папку Models файл по
имени Reposi tory . cs и определите в нем интерфейс и класс реализации, как пока­
зано в листинге 23.3.

Листинг 23.З. Содержимое файла Reposi tory. cs из папки Models

using System . Collections.Generic ;


namespace Cities.Models {
puЫic interface IRepository
IEnumeraЫe<City> Cities { get ;
void AddCity(City newCity);
Глава 23. Дескрипторные вспомогательные классы 711
puЫic class MemoryRepository : IRepository {
private List<City> c i tie s = new List<City> {
new City { Name = " London ", Country = " UK ", Population = 8539000} ,
new City { Name = "New York ", Country = " USA ", Population = 8406000 } ,
new City { Name = " San Jose", Country = " USA", Population = 998537 } ,
new City { Name = " Paris ", Country = " France ", Population = 2244000 }
};

puЫic IEnumeraЬle<City> Citi es => cities ;


puЫic void AddCity(City newCity)
ci t ies . Add(newCity) ;

Создание контроллера, компоновки и представлений


В примерах этой главы требуется только один контроллер. Создайте папку
Control l ers. добавьте в нее файл класса по имени HorneController . cs и опреде­
лите контролл ер , код которого приведен в листинге 23.4.

Листинг 23.4. Содержимое файла HomeController. cs из папки Controllers


using Microsoft.AspNetCore . Mvc ;
using Cities . Models ;
namespace Cit i es .Contro ll ers {
puЬlic class HomeController : Controller {
private IRepository repository ;
puЬlic HomeController(IRepository repo) {
repository = repo ;

puЫic ViewRe sult Index() => View(repository.Cities) ;


puЫic ViewResult Create() => View() ;
[HttpPost]
puЫic IActionResult Create(City city)
repository . AddCity(city) ;
return RedirectToAction( " Index " ) ;

Контролл е р поддерживает метод действия Index () , выводящий список объектов


в хранилище, и пару методов Crea te ( ) , которые позволят пользователю применять
форму для создания новых объектов Ci ty , следуя тому же самому подходу, что и при­
меры в предшествующих главах.

Представления в данном приложении будут использовать разделяемую компонов­

ку. Создайте папку Views/ Shared и поместите в нее файл по имени_ La yout. cshtrnl
с разметкой из листинга 23.5.
712 Часть 11. Подробные сведения об инфраструктуре ASP.NET Саге MVC

На заметку! Поскольку цель настоящей главы заключается в демонстрации работы дескрип­


торных вспомогательных классов, компоновка и представления в примере приложения
реализованы с применением только стандартных НТМL-элементов, которые будут заме­
няться по мере рассмотрения различных дескрипторных вспомогательных классов .

Листинг 23.5. Содержимое файла _Layout. cshtml из папки Views/Shared


< ! DOCTYPE html>
<html>
<head>
<meta name= "viewport " content= "width=device - width " />
<title>Cities</title>
<link href= " /l i Ь/bootstrap/dist/css/bootstrap . css " rel= " sty l esheet " />
</head>
<body class= "panel - body " >
<div>@RenderBody()</d i v>
</body>
</html>

Создайте папку Views/Home и добавьте в нее файл по имени Index.cshtml, со­


держимое которого показано в листинге 23.6.

Листинг 23.6. Содержимое файла Index. cshtml из папки Views/Home

@model IEnumeraЫe<City>

@{ Layout = " Layout "; }


<tаЫе class= " taЫe taЬle-condensed taЫe-bordered">
<thead class= "bg-primary " >
<tr>
<th>Name</th>
<th>Country</th>
<th class= " text -ri ght " >Population</th>
</tr>
</thead>
<tbody>
@foreach (var city in Model)
<tr>
<td>@city .Name</td>
<td>@city . Country</td>
<td c l ass= "text-right " >@city. Populat i on? .ToString( " #, ### " )</td>
</tr>

</tbody>
</tаЫе>
<а href=" /Home/Create " class= "Ьtn Ьtn-primary">Create</a>

В представлении используется последовательность объектов Ci ty для заполн ения


табл ицы и включается элемент а , нацеленный на URL вида /Home/Create, который
с помощью Bootstrap стилизован под кнопку . Чтобы создать второ е представл ени е ,
добавьте в папку Views/Home файл по имени Create.cshtml с разм еткой из лис­
тинга 23.7.
Глава 23 . Дескриnторные вспомогательные классы 71 З
Листинг 23.7. Содержимое файла Create. cshtml из папки Views/Home
@rnodel City
@{ Layout = "_Layo ut"; }
<forrn rnethod="post" action= "/Horne/Crea te" >
<div class =" forrn -gr oup " >
<label for= "Narne " >Narne : </ l abel>
<input class ="forrn-control" narne="Narne" />
</div>
<div class= " forrn -gr oup " >
<label for= "Country " >Country : </labe l>
<inpu t class= "forrn- contro l" narne="Country" />
</div>
<d iv class= " forrn -g roup " >
<label for= "Population " >Popu l ation : </label>
<inpu t class= "fo rm-control " name="Population" />
</div>
<button type= " subrnit " class= "Ьt n Ьtn-primary">Add</button>
<а class= " Ьtn Ьtn - primary " href= " /Home/ Index " >Cance l </a>
</forrn>

Со зд айте в папке Vi е ws файл импортирования представлений по имени


_ Viewimports. cshtml и поместите в него выражение, приведенное в листинге 23.8.
Оно позволит ссылаться на классы из папки Models , не указывая пространство
имен.

Листинг 23.8 . Содержимое файла_Viewirnports. cshtml из папки Views


@using Cities . Models

Представления в рассматриваемом примере полагаются на СSS-пакет Bootstrap.


Создайте в корневой папке проекта файл bower. j son с применением шаблона эле­
ме нта Bower Contiguration File (Файл конфигурации Bower) и добавьте пакет Bootstrap
в раздел dependencies (листинг 23.9).

Листинг 23.9. Добавление пакета Bootstrap в файле bower. j son

"name ": "asp . ne t",


"private ": true ,
"dependencies " : {
"bootstrap": 11 3.3.6"

Конфи гурирование приложения


Последний подготовительный шаг связан с конфигурированием приложения. как
пок азано в листинге 23.10. Это та же сам ая базовая конфигурация, используемая во
всех проектах в данной части книги, к которой добавлена реги страции хранилища в
виде службы с применением жизненного цикла одиночки.
714 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC

Листинг 23.1 О. Содержимое файла Startup . cs

using Micro soft . AspNet Co re.Builder ;


using Micro soft . Extensions.Depende ncyinjecti on ;
u s ing Cities . Models;

namespace Citie s {

puЫic cla s s Startup

puЫ i c void Config u re Service s (IServic e Col l ect i on se rvices)


services.AddSingleton<IRepository, MemoryRepository>();
services.AddМvc();

puЫic void Configure(IAppl i ca t ionBuilder арр) {


app.UseStatusCodePages();
app.UseDeveloperExceptionPage() ;
app.UseStaticFiles() ;
app.UseMvcWithDefaultRoute();

Запустив приложение . вы увидите список объектов Ci ty. который хр анилище со­


здает по умол чанию . Щелкните на кнопке Create (Создать), заполните форму и щелк­
ните на кнопке Add (Добавить) ; в хранилище добавится новый объ ект (рис. 23.1) .

D Cities D Citios Х

г;::.:-------
С [ ф l; all1ostS21
с 1~~.1_21
11--------

---- -- L
-- --- ---
--- ~---

:1
·-- ---·- =-=- =-=- ,
Name: ~ ' с jФ~;smJ ----~ 1

Beijing
1 - •
1
, London
1 Ne1v York Country: London UK 8,539,000

1 San Jose 1 China New York USA 8,406,000

'°;,~:"00
San Jose USA 998,537

--1-~:_:_~,:J
Paris France
1: , 1
Beijing China

1----·-----·-------1.
L ___ ._ _
Рис . 23.1. Выполнение примера приложения
Глава 23. Дескрипт ор ные вспомогательные классы 715

Создание дескрипторного
вспомогательного класса
Как и со многими средствами МVС, пониманию дескрипторных вспомогательных
классов лучше всего способствует создание одного такого класса, что позволит вы­
явить особенности их работы и согласование с остальными частями приложения.
В приведенных далее разделах вы ознакомитесь с процессом создания и исполь­
зования дескрипторного вспомогательного класса, который будет применять СSS­

классы Bootstrap к элементу button, так что элемент вида:

<button type= " submit " bs-button-co lor= "danger " >Add</ bu tton>

трансформируется следующим образом:

<button type= " submit " class= "b tn Ьtn-danger">Add</button>

Дескрипторный вспомогательный класс будет распознавать атрибут bs - button -


color и использовать его значение для установки атрибута class элемента , от­
правляемого браузеру. Это не особо впечатляющая или сколько-нибудь полезная
трансформация, но она формирует основу для объяснения работы дескрипторных
вспомогательных классов.

Определение дескрипторного вспомогательного класса


Дескрипторные вспомогательные классы можно определять где угодно в проек­
те, но их удобно держать вместе , потому что в отличие от большинства компонен­
тов МVС перед примен ением они нуждаются в регистрации. Добавьте в проект папку
Infrastructure/TagHelpe rs, в которой будут создаваться дескрипторные вспомо­
гательные классы.

Дескрипторные вспомогательные классы являются производными от класса


TagHelper , который определен в пространстве имен Microsoft. AspNetCore .
Razor . TagHelpers. Чтобы создать дескрипторный вспомогательный класс, добавьте
в папку Infrastructure/TagHelpers файл по имени ButtonTagHelper. cs и опре­
делите в нем н:ласс, как показано в листинге 23. l l.

Листинг 23.11. Содержимое файла ButtonTagHelper. cs


из папки Infrastructure/TagHelpers
using Microsoft . AspNetCore . Razor .TagHe lpers ;
namespace Cities .Infrast ruc ture.Tag Help ers {
puЬlic class ButtonTagHelper : TagHelper {
puЫic string BsButtonColor { get; set; )
puЫic ove rride void Process(TagHelperContext context ,
TagHelperOutput output) {
output.Attributes.SetAttribute( "class ", $ " Ьtn Ьtn-{BsButtonColor)") ;
)
716 Часть 11. Подробные сведения об инфраструктуре ASP. NET Core MVC

В классе TagHelper определен метод Process (),который переопределяется под­


классами для реализации поведения, трансформирующего элементы. Имя дескрип­
торного вспомогательного класса образуется из имени трансформируемого элемента
и суффикса TagHelper . В рассматриваемом примере имя класса ButtonTagHelper
сообщает инфраструктуре МVС о том, что это дескрипторный вспомогатель ный класс,
который оперирует на элементах button. Область действия дескрипторного вспомо­
гательного класса может быть расширена или сужена с использованием атрибутов,
описанных в разделе "Управление областью действия дескрипторного вспомогатель­
ного класса" далее в главе , но таково его стандартное поведение.

Совет. Переопределяя метод ProcessAsync () вместо Process (), можно создавать асин­
хронные дескрипторные вспомогательные классы, но для большинства дескрипторных
вспомогательных классов это не требуется, т.к. обычно они вносят в НТМL-элементы не­
большие и специализированные изменения. Стандартная реализация ProcessAsync ()
в любом случае вызывает метод Process () . Пример асинхронного дескрипторного
вспомогательного класса приведен в главе 24.

Получение данных контекста


Дескрипторные вспомогательные классы получают информацию о трансформируе­
мом элементе через экземпляр класса TagHelperContext, который передается в качес­
тве аргумента методу Process () и определяет свойства, перечисленные в табл. 23.З.

Таблица 23.3. Свойства класса TagHelperContext

Имя Описание

AllAttributes Это свойство возвращает словарь только для чтения с атрибутами,


примененными к элементу, подвергающемуся трансформации , который
индексирован по имени и по числовому индексу

Items Это свойство возвращает словарь, который используется для согласо­


вания дескрипторных вспомогательных классов, как описано в разделе
"Согласование дескрипторных вспомогательных классов" далее в главе

Uniqueid Это свойство возвращает уникальный идентификатор для трансформи­


руемого элемента

Хотя получать доступ к деталям атрибутов элемента можно через словарь


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

puЫic string BsButtonColor { get ; set ; }

В случае применения дескрипторного вспомогательного класса инфраструктура


MVC инспектирует определенные им свойства и устанавливает значения любых из
них, имена которых совпадают с атрибутами, примененными к НТМL-элементу. Как
часть этого процесса MVC будет пытаться преобразовать значение атрибута с цел ью
соответствия типу свойства С# , так что свойства bool могут использоваться для по­
лучения значений булевских атрибутов (true и false ), а свойства int - для получе­
ния з начений числовых атрибутов (наподобие 1 и 2).
Глава 23. Дескрипторные вспомогательные классы 717

Что произошло со вспомогательными методами HTML?


В ранних версияхASP.NET MVC для генерации элементов форм применялись вспомогатель­
ные методы HTML. Они представляли собой методы, обращение к которым осуществлялось
посредством выражений Razor, начинающихся с @Html. Таким образом , элемент inpu t для
свойства Popula t ion создавался бы следующим образом :

@Html . TextBoxFor(m => m.Pop ulat i on )

Проблема выражений со вспомогательными методами HTML в том, что они не соответству­


ют структуре НТМL-элементов; это приводит к неудобным конструкциям вроде показанной
ни же , которая добавляет стили Bootstrap к генерируемому элементу:

@Html . Text BoxFor(m => m. Populat i on , new { @class = "form-control" })

Атрибуты должны выражаться в динамическом объекте и предваряться префиксом @, ког­


да они совпадают по написанию с зарезервированными словами С#, такими как c l ass.
По мере усложнения требуемых НТМL-элементов выражения со вспомогательными мето­
дами HTML становятся все более неуклюжими. Дескрипторные вспомогательные классы
избавляют от такой неуклюжести за счет использования атрибутов HTML:

<inpu t c l a s s= " form - cont r ol " asp - fo r="P opu latio n" / >

Результатом является более естественное соответствие природе HTML и возможность по­


строения представлений, которые легче в чтении и понимании. Инфраструктура MVC по­
прежнему поддер ж ивает вспомогательные методы HTML (на самом деле дескрипторные
вспомогательные классы внутренне применяют вспомогательные методы HTML). Это оз­
начает, что вы можете использовать их для обратной совместимости в представлениях,
которые первоначально разрабатывались для версии MVC 5, однако новые представления
должны задействовать в своих интересах более естественный подход, предлагаемый де­
скрипторными вспомо г ательными классами.

Имя атрибута автоматически преобразуется из стандартного стиля HTML.


bs - button -co l or , в стиль С#, т.е . Bs Butt o nColor . Можно применять любой пре­
фикс атрибута кроме asp - (который использует Micгosoft) и d ata - (зарезервирован
для специальных атрибутов, отправляемых клиенту) . В текущем пример е посредством
атрибута BsBut t onColor получается цветовая схема для применения к элементу
button в методе Process () :

output .Att rib ut es.SetAtt r ibute (" cl ass", $" btn Ьtn- ( BsButtonColor}" ) ;

Свойства, для которых отсутствуют соответствующие атрибуты НТМL-элемента,


не устанавливаются, т.е. вы должны удостоверяться , что не име ет е дело с null или
стандартными значениями. В разделе "Управление областью действия дескриптор­
ного вспомогательного класса'" далее в главе объясняется, как изменять область дейс­
твия дескрипторного вспомогательного класса, чтобы он использовался только в от­
нош ении элементов, определяющих атрибуты , от которых вы зависите.
718 Часть 11 . Подробные сведения об инфраструктуре ASP. NEТ Core MVC

Совет. Применение имен атрибутов HTML для свойств дескрипторного вспомогательного


класса далеко не всегда приводит к получению читабельных или понятных классов . Вы
можете разорвать связь между именем свойства и атрибутом , которое оно представляет,
с использованием атрибута Htm l Att r ibuteName , позволяющего указывать представ­
ляемый свойством атрибут HTML.

Генерирование вывода
Метод Р r о се ss ( ) трансформирует элемент путем конфигурирования объ­
екта Tag He lpe rOu tp u t, который получается в качеств е аргу м ента. Объект
TagHelpe r Ouput начинает сво е существовани е с описания НТМL-элемента в то м
виде, как он появляется в представлении Razor, и модифицирует его посредством
свойств и метода, которые описаны в табл. 23.4.

Таблица 23.4. Свойства и метод класса TagHelperOutput

Имя Описание

TagName Это свойство применяется для получения и установки имени де ­


скриптора в выходном элементе

Attributes Это свойство возвращает словарь, содержащий атрибуты для вы ход­


ного элемента

Content Это свойство возвращает объект TagHelperCont ent, который ис­


пользуется для установки содержимого элемента

PreEl ement Это свойство возвращает объект TagHe lperContext, который при­
меняется для вставки содержимого в представление перед вы ходным

элементом . См . раздел "Вставка содер жимого перед и после элемен­


тов" далее в главе

Pos tEleme nt Это свойство возвращает объект TagHelperContext, который


используется для вставки содержимого в представление после вы­

ходного элемента . См. раздел " Вставка содержимого перед и после


элементов" далее в главе

PreCont ent Это свойство возвращает объект TagHe l perContext, который при­
меняется для вставки содержимого перед содерж имым выходного
элемента. См. раздел "Вставка содержимого перед и после элемен­
тов" далее в главе

Pos t Content Это свойство возвращает объект TagHe l perContext, который ис­
пользуется для вставки содержимого после содерж имого вы ходного

элемента. См . раздел "Вставка содержимого перед и после элемен­


тов" далее в главе

TagMode Это свойство указывает, как будет записываться выходной элемент,


с применением значения из перечисления TagMode. См. раздел
"Создание сокращающих элементов" далее в главе
Supres s Ouput () Вызов этого метода предотвращает включение элемента в представ­
ление . См. раздел " Подавление выходного элемента " далее в главе

В классе ButtonTagHe l p e r с помощью словаря At t ribu tes к НТМL- элементу до ­


бавляется атрибут cla s s, который указывает стили Bootstrap для кнопки, включая
значение свойств а BsBut t onCo l or, что означает возможность указания разных цве­
тов с использованием имен Bootstl'ap, таких как pr i mary, info и danger.
Глава 23. Дескрипторные вспомогательные классы 719

Регистрация дескрипторных вспомогательных классов


Дескрипторные вспомогательные классы можно применять только после того, как
они будут зарегистрированы с использованием Rаzоr-выражения @addTagHelper.
Набор представлений, к которым будет применяться дескрипторных вспомогатель­
ных классов, зависит от того, где используется выражение @addTagHelper.
Для отдельного преставления это выражение присутствует в самом представле­
нии. Для подмножества представлений в приложении выражение @addTagHelper на­
ходится в файле_ Viewimports. cshtml внутри папки, которая содержит представ­
ления, или внутри родительской папки. Таким образом, выражение @addTagHelper
в файле /Views/Home / _ Viewimports . cshtml регистрирует дескрипторные вспо­
могательные классы для всех представлений, ассоциированных с контроллером
Ноте . Нужно , чтобы дескрипторные вспомогательные классы были доступны во всех
представлениях приложения, поэтому добавьте выражение @addTagHelper в файл
Views/ _ Viewimports . cshtml, как показано в листинге 23.12.
Листинг 23. 12. Регистрация дескрипторных вспомогательных классов
в файле _ Viewimports. csh tml
@using Cities . Models
@addTagHelper Cities.Infrastructure.TagHelpers. * , Cities

В первой части аргумента задаются имена дескрипторных вспомогательных клас­


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

В листинге 23.12 регистрируются все дескрипторные вспомогательные классы в


пространстве имен Ci ties . Infrastructure . TagHelpers из сборки Ci ties.

Использование дескрипторного вспомогательного класса


Наконец, дескрипторный вспомогательный класс можно применить для транс­
формации элемента. Удалите атрибут class из элемента button в представлении
Create . cshtml и замените его атрибутом, который ожидает класс ButtonTagHelper
(листинг 23.13).

Листинг 23.1 З. Использование дескрипторного вспомогательного класса


в файле Crea te. csh tml
@model City
@{ Layout = "_Layo ut "; }
<form method= "post" action= " /Home/Cre ate">
<div class= " form - group">
<label for= " Name " >Name : </ label >
<input class= " form-control" name="Name" />
</div>
<div class="form- group " >
<label for= " Country">Country:</label>
<input class= "form- control" name="Country " />
</div>
<div class= "form-group" >
<label for= " Population " >Population : </label>
<input class= " form- control " name="Population " />
</div>
720 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC

<button type="suЬmit" bs-button-color="danger">Add</button>


<а class="Ьtn Ьtn-primary" href=" /Home/Index">Cancel</a>
</form>

Запустив приложение и щелкнув на кнопке Create, вы заметите, что браузер за­


просит URL вида /Home/Crea te, после чего стиль и цвет кнопки Add изменятся
(рис. 23.2).

Population:

l."--·-·--·--· ----"·-·-·-- -·

Рис. 23.2.
·-
Применение дескрипторного вспомогательного класса для стилизации кнопки

Модульное тестирование дескрипторного вспомогательного класса

Модульное тестирование дескрипторного вспомогательного класса - относительно прос­


той процесс, который основан на снабжении метода Process () значащим содержимым,
подлежащим обработке. Вот пример модульного теста для дескрипторного вспомогатель­
ного класса из листинга 23.11 :
using System . Co ll ections . Generic ;
using System . Linq ;
using System . Threading . Tasks ;
using Cities .Infrastructure . TagHelpers ;
using Microsoft.AspNetCore.Razor.TagHelpers;
us i ng Xunit ;
namespace Cities .Tests {
puЫic class TagHelperTests
[ Fact ]
puЫic void TestTagHelper()
11 Организация
var context = new TagHe l perContext(
new TagHelperAttributeList(),
new Dictionary<object , object>() ,
"myuniqueid " ) ;
var output = new TagHelperOutput ( " button ",
new TagHelperAttributeList() , (cache , en coder) = >
Task .F romResult<TagHelperContent>
(new DefaultTagHelpe r Content())) ;
11 Дейс т вие
var tagHelper = new ButtonTagHelper
BsButtonColor = " testValue "
};
tagHelper . Process(context , output) ;
Глава 23 . Дескрипторные вспомогательные классы 721
11 Утверждение
Assert.Equal($ "btn btn-{tagHelper.BsButtonColor}",
output.Attributes[ " class "J . Value);

Большая часть работы этого модульного теста связана с настройкой объектов


TagHelperContext и TagHelperOutput, чтобы их можно было передать методу
Process () дескрипторного вспомогательного класса и удостовериться в том, что НТМL­
элемент был трансформирован корректно. Объем работ, требуемых для подготовки де­
скрипторного вспомогательного класса к тестированию, вполне естественно зависит от

сложности НТМL-разметки, которой он оперирует, и степени ее трансформации. Тем не ме­


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

Управление областью действия дескрипторного


вспомогательного класса

Дескрипторные вспомогательные К1Iассы прим еняются ко всем элементам задан­

ного типа, т.е. метод Process () класса ButtonTagHelper, созданного в предыдущем


разделе, будет вызываться для каждого элемента button в каждом представлении
внутри приложения. Это не всегда полезно. Чтобы более пристально взглянуть на
проблему, добавьте в представление Create . cshtml еще один элемент button (лис­
тинг 23.14).
Листинг 23.14. Добавление элемента Ьutton в файле Create. cshtml
@model City
@{ Layout = "_Layout "; }
<form method="post " action="/Home/Create " >
<div class= " form - group " >
<label for="Name">Name : </label>
<input class= " form - control" name="Name" />
</div>
<div class= " form-group ">
<label for =" Country " >Country : </label>
<input class="form- control " name="Country" />
</div>
<div class= " form-group " >
<label for= " Population " >Population : </label>
<input class= " form-cont r ol " name="Population" />
</div>
<button type= " submit " bs -butt on-color= "danger">Add</button>
<button type="reset" cJ..ass="btn btn-pri.mary">Reset</Ьutton>
<а class= " Ьtn Ьtn-primary" href="/Home/Index">Cancel</a>
</form>

Новый элемент button уже имеет атрибут class и не требует трансформации,


выполняемой классом ButtonTagHelper. Но если вы запустите приложение и запро­
сите URL вида /Home/Create, то заметите, что возникла проблема, как иллюстриру­
ется на рис. 23.3.
722 Часть 11. Подробные сведения об инфраструктуре ASP.NET Соге MVC

Population:

Reset

Рис. 23.3. Эффект от стандартной области действия дескрипторного


вспомогательного класса

Выяснить причину неудовлетворительного форматирования можно. просмотрев


НТМL-разметку. которая отправлена браузеру , где обнаруживается пробл ема с атри­
бутом clas s:
<button type="reset" class="btn btn-" >Reset</button>
Инфраструктура MVC применила класс ButtonTagHelper к новому элементу
button, но не установила значение свойства BsButtonColor из - за отсутствия соот­
ветствующего атрибута bs - button - color в НТМL-элементе. Это привело к тому , что
дескрипторный вспомогательный класс заменил атрибут class атрибутом , в котором
некорректно указаны стили Bootstrap, и в итоге получился неправильно сформатиро­
ванный элемент.

Сужение области действия дескрипторного вспомогательного класса


Для решения проблемы существуют два возможных подхода. П е рвый подход
предусматривает модификацию класса ButtonTagHe l per, чтобы сделать его чувс­
твительным к разным элементам button, с которыми он может столкнуться. В рас­
сматриваемом пример е приложения это потр е бовало бы проверки , есть ли атрибут

bs-button - color , и отказа от замены атрибута class , если он был определен.


Проблема такого подхода в том , что по мере добавления в приложение пр едставле­
ний, содержащих элементы button, дескрипторный вспомогательный класс будет
становиться все более и более сложным, причем вся привносимая сложность связа­
на с описанием условий , при которых класс ButtonTagHelpe r не должен выполнять
свою трансформацию.
Второй подход заключается в том, чтобы позволить определять ограничения н а то,
как используется дескрипторный вспомогательный класс, сужая область действия, в
которой он будет применяться . Ограничения дескрипторного вспомогательного клас­
са указываются с использованием атрибута Html TargetElement (листинг 23.15).

Листинг 23.15. Сужение области действия дескрипторного вспомогательного класса


в файле ButtonTagHelper. cs
using Microsoft . AspNe t Core . Razor .TagHe lpers ;
namespace Cities . Infrastructure .TagHelpers {
[HtmlTargetElement("button", Attributes = "bs-button-color",
ParentTag = "form")]
puЬlic class ButtonTagHelper : TagHelper

puЬlic string BsButtonCo l or { get ; set ;


Глава 23. Дескриnторные всnомогательные классы 723
puЫic override void Process(TagHelperContext context,
TagHelperOutput output) {
output .Attributes .SetAttribute("class", $"Ьtn Ьtn-{BsButtonColor)");

Атрибут Html TargetElement описывает элементы, к которым применяется де­


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

Таблица 23. 5. Свойства атрибута Html TargetElemen t

Имя Описание

Attributes Это свойство используется для указания на то, что дескрипторный вспо­
могательный класс должен применяться только к элементам, которые
имеют заданный набор атрибутов, предоставляемый в виде списка с раз­
делителями-запятыми. Элемент должен располагать всеми указанными
атрибутами. Имя атрибута, которое заканчивается символом звездочки,
будет трактоваться подобно префиксу, так что bs-button-* соответ­
ствует bs-button-color, bs-button-size и т.д.

ParentTag Это свойство используется для указания на то, что дескрипторный вспо­
могательный класс должен применяться только к элементам, которые
содержатся внутри элемента заданного типа

TagStructure Это свойство используется для указания на то, что дескрипторный


вспомогательный класс должен применяться только к элементам, чья
структура дескриптора соответствует заданному значению из пере­

числения TagStructure, которое определено как Unspecified,


NormalOrSelfClosing и WithoutEndTag

В листинге 23.15 класс But tonTagHelper ограничен так, что он применяется


только к элементам button, которые имеют атрибут bs-button-color, а их ро­
дительским элементом является form. Запустив приложение и запросив URL вида
/Home/Create , вы заметите, что кнопка Reset (Сброс) больше не трансформируется,
т.к. в ней отсутствует требуемый атрибут (рис. 23.4).

Population:

l____ ______,___ __!


1

Рис. 23.4. Сужение области действия дескрипторного вспомогательного класса


724 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

Расширение области действия дескрипторного вспомогательного класса


Атрибут Ht mlTa r getElemen t может также использоваться для расширения об­
ласти действия дескрипторного вспомогательного класса , чтобы о н охв атывал боле е
обширный диапазон элементов. Это полезно, когда одну и ту же т р ансформ ацию н е ­
обходимо выполнять для нескольких типов эл е ментов, что идет вразрез с исходны м
условием сопоставления эл е ментов на основ е имени д е скрипторного вспомог ательно­

го класса (листинг 23.16).

Листинг 23.16. Расширение области действия дескрипторного вспомогательного


класса в файле ButtonTagHelper. cs
u s ing Microsoft . AspNetCore . Razor . TagHe l per s;
na mes p ac e Cities . In f ras t r u ct u re . TagHelpers {
[HtmlTargetElement(Attributes = "bs-button-color", ParentTag = "form")]
c l ass ButtonTagHelper : TagHelpe r {
p uЬlic
puЫic string BsButtonCo l or { get ; set ; }
puЫic override void Process(TagHelperContext context ,
Ta gH e l pe r Output out pu t) (
outp u t . Attri b utes . SetAttr i bute( 11 c l ass ", $ " Ьtn Ьtn - (BsButtonCo l o r } 11
) ;

В листинге 23.16 для атрибута Htm l TargetE l emen t тип эл ем ен та не указан, а по­
тому дескрипторный вспомогательный класс будет применяться к любому элементу ,
который имеет атрибут bs - b u tto n- co lo r , независимо от типа эл е мента . В листин­
ге 23.17 элемент а внутри формы модифицирован так , что он использует такой же
набор стилей Bootstrap, как и элементы b u t ton, поэтому он будет трансформирован
дескрипторным вспомогательным классом .

Листинг 23.17. Модификация элемента а в файле Create. cshtml


@model City
@( Layout = 11 Layou t 11 ; }
< f orm met h o d= 11 post " act i o n= 11 / Home/ Cr eate 11 >
<div cla ss = 11 form - group 11 >
<label for= 11 Name 11 >Name : </label>
< i пpu t class= 11 f o rm - con tr ol
11
name= 11 Name" />
</ d i v>
<di v c l as s= 11 form - g r o up 11 >
11 11
<labe l for = Co u пtry >Couпtry : </label>
<input cla s s = 11 f orm- con trol 11 пame = Cou n t ry />
11 11

< / div>
<div class= 11 form - g r oup 11 >
<labe l for= 11 Pop u la t ion 11 >Populat i on : </label>
<input c l ass= 11 form - cont r o l 11 name= 11 Popu l ation 11 />
< /d i v >
<buttoп type= 11 s ubmi t " bs - button - c ol or = da n ger >Add</buttoп>
11 11

<buttoп t ype= rese t class= Ьtп Ьtn - pr i mary


11 11 11 11
>Reset</button>
<а bs-button-color="primary" href="/Home/Index">Cancel</a>
< /f o rm>
Глава 23. Дескриnторные вспомогательные классы 725
Возможность р асшир ения области действия для дескрипторного вспомогательного
кл а сса означает, что вы не обязаны создавать дескрипторные вспомогательные клас­
сы, которые повторяют одну и ту же операцию в отношении разных типов элемен­

тов. Однако тр ебуетс я определенная о сторожность, поскольку довольно л е гко создать


дес крипторный вспомогательный класс, который по мере будущего раз вития содер­
ж имого пр едставл ений в приложении начнет давать совпадения с элементами слиш­
ком широко . Более сбалансированный подход предусматрив ает приме нение атрибута
Html TargetElement несколько раз с указанием полного набора эле ментов, которые
будут тр ансформироваться, как 1юмбинации уз1ш определенных сопоставлений (лис­
тинг 23.18) .

Листинг 23.18. Балансировка области действия дескрипторного вспомогательного


класса в файле ButtonTagHelper. cs

using Mi crosoft . AspNetCore . Razo r. Tag He lp ers ;


namespace Cities.Infrastructure . TagH el pers {
[HtmlTargetEleme nt ("button", Attributes = 11 bs-button-color 11 ,
ParentTag = 11 form 11 ) ]
[HtmlTargetElement("a", Attributes = 11 bs-button-color 11 , l?arentTag = "form")]
puЫic cla s s ButtonTagHe lp er : TagHel per {
puЫic s t ring BsBu t to nC ol or { get ; s et ; }
puЫic ove r ride vo i d Process(Tag He l perContext con t ext ,
TagHelperOutput output) {
output . Attributes . SetAttribute( "class ", $ " Ьtn Ьtn -{ BsButtonColor }" ) ;
}

Та кая конфигур ация обеспечивает тот же самый результат в прилож е нии, но га ­


рантирует, что де скрипторный вспомогат ельный класс не будет вызывать проблем,
есл и позж е в проце сс е р азработки атрибуты bs - b u tton - colo r начнут добавляться к
другим типам элеме нтов по другой причине .

Упорядочи вание выполнения дескрипторных вспомогательных классов

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


могательный класс с любым заданным НТМL-элементом. Причина в том , что совсем не­
сложно попасть в ситуацию, когда один дескрипторный вспомогательный класс не счита­
ется с трансформацией , примененной другим дескрипторным вспомогательным классом,
переопределяя значения атрибутов или содержимое. Если вы нуждаетесь в применении не­
скольких дескрипторны х вспомогательны х классов, тогда можете управлять последователь­

ностью их выполнения, устанавливая свойство Order , которое унаследовано из базового


класса TagHelper . Управление последовательностью выполнения может помочь свести к
минимуму конфликты между дескрипторными вспомогательными классами, хотя столкнуть­
ся с проблемами по-прежнему довольно легко .
726 Часть 11. Подробные сведения об инфраструктуре ASP. NEТ Саге MVC

Усовершенство в анные возможности


дескрипторных вспомогательных классо в
В предыдущем разделе было продемонстрировано, каким образом создав ать и ис­
пользовать базовый дескрипторный вспомогательный класс, но это лишь малая толи­
ка доступных возможностей. В последующих разделах будут показаны бол ее сложные
случаи применения дескрипторных вспомогательных классов и средств, которы е они

пр едоставляют.

Создание сокращающих элементов


Дескрипторные вспомогательные Юiассы не ограничиваются трансформированием
стандартных НТМL-элементов и могут также использоваться для замены специальных
элементов ходовым содержимым. Данная возможность позволяет сделать пр едставле ­
ния более краткими и прояснить их замысел. Замените элементы b utton в представле ­
нии Cr ea t e . c sh trnl специальными элементами, как показано в листинге 23.19.

Листинг 23.19. Добавление специальных элементов в файле Create. cshtml


@model Cit y
@{ Layout = "_Layo u t "; }
<form method= "p ost " act i o n=" / Home/C r eate " >
<div class ="f o r m-g roup " >
<l abe l for ="N a me " >Na me : </ l a bel >
<i n p ut c l as s="fo rm- con tr o l" n a me="Name " / >
</d i v>
<d i v c l as s = " for m- g r o up" >
<label for =" Co u n t r y" >Count r y : </ l ab el >
<inp u t c l ass =" form - con trol " n a me= " Country " />
</ d iv>
<div class =" fo rm- g r oup " >
<l abe l f o r="P o pula tion " >Pop ul at i o n: </ l abel>
<inp ut class= " form - con tro l" name= "P op u lat i on " />
</d i v>
<formЬutton type="suЬmit" bg-color="danger" />
<formЬutton type="reset" />
<а b s -but ton - co l or= " pr i mary " hre f=" / Home/ I nde x " >Ca nce l </a>
</form>

Элемент f o rrnЬut t on не входит в спецификацию HTML и не распознается браузе ­


рами. Взамен мы собираемся применять эти элементы как сокращения для г ен ерации
эл ементов
b ut ton , которые требуются форме. Добавьте в папку Infrastructure/
TagHelper файл класса по имени Fo rrnBu t t onTagHelpe r. cs с определением из лис­
тинта 23 .20.

Совет . Когда приходится иметь дело со специальными элементами, которые не являются


частью спецификации HTML, должен применяться атрибут Htrnl Target Elernent и ука­
зываться имя элемента (листинг 23 .20) . Соглашение о применении дескрипторных вспо­
могательных классов к элементам на основе имени класса работает только для имен
стандартных элементов.
Глава 23 . Дескрипторные вспомогательные классы 727
Листинг 23.20. Содержимое файла ForrnВu ttonTagHelper. cs
из папки Infrastructure/TagHelpers
using Microsoft . AspNetCore . Razor .TagHelpers ;
namespace Cities . Infrastructure .TagHelpers {
[HtmlTargetElement( "formbutton " )J
puЫic class FormButtonTagHelper : TagHelper
puЫic string Туре { get; set ; } = "Submit ";
puЫic string BgColor { get ; set; } = "pr imary ";
puЫic override void Process(TagHelperContext context ,
TagHelperOutput output) {
output . TagName = "button ";
output . TagMode = TagMode . StartTagAndEndTag ;
output . Attributes .SetAttribute( "cl ass ", $ " Ьtn Ьtn - {BgColor} " ) ;
output . Attributes .S etAttribute( "type ", Туре) ;
output.Content . SetContent(Type == " submit " ? "Add ": "Reset " ) ;

Метод Process () использует свойства объекта TagHelperOuput для генерации


совершенно другого элемента. Свойство TagName применяется для указания эле­
мента button, свойство TagMode - для указания на то , что элемент записывается с
испол ьзование м открывающего и закрывающего дескрипторов, метод Attributes .
SetAt tribute () применяется для определения атрибута class со стилями Bootstrap,
а свойство Con ten t используется для установки содержимого элемента.

Совет. Обратите внимание в листинге 23.20 на установку атрибута type выходного элемен­
та . Причина в том , что любой атрибут, для которого имеется свойство , определенное де­
с к рипторным вспомогательным классом, в выходном элементе опускается . Обычно это
хорошая идея, т.к . предотвращает появление атрибутов , применяемых для конфигуриро­
вания дескрипторных вспомогательных классов, в НТМL-разметке, отправляемой брау­
зеру. Тем не менее, в данном случае атрибут type используется для конфигурирования
дескрипторного вспомогательного класса, поэтому необходимо, чтобы он присутствовал
также в выходном элементе.

Установка сво йств а TagName важна из-за того, что выходной элемент по умолча­
нию записывается в таком же стил е, как специальный элеме нт. В листинге 23.19 при­
менялся самозакрывающийся дескриптор:

<formbutton type= " submit " bg - color= "danger " />

Поскольку выходной элемент должен включать с одержимое , нужно явно указать


значение перечисления TagMode . StartTagAndEndTag, чтобы использовались отде­
льные открывающий и закрывающий дескрипторы.
Свойство Content возвращает экземпляр класса TagHelperContent , который
прим е ня ется для установки содержимого элементов. В табл. 23.6 описаны самые важ­
ные методы класса TagHelperContent.
728 Часть 11. Подробные сведения об инфраструктуре ASP.NET Соге MVC

Таблица 23.6. Полезные методы класса TagHelperContent


Имя Описание

SetContent(te xt) Этот метод устанавливает содержимое выходного элемента.


Аргумент st ring кодируется так, чтобы безопасно вставляться
в НТМL-элемент

SetHt mlContent(html) Этот метод устанавливает содержимое выходного элемента.


Предполагается, что аргумент s tr ing закодирован как безопас­
ный . Должен использоваться с осторожностью

Append(te xt) Этот метод кодирует безопасным образом указанный аргумент


s t ri ng и добавляет результат к содержимому выходного элемента
AppendHtml (html ) Этот метод добавляет указанный аргумент s t ring к содержи­
мому выходного элемента, не выполняя кодирование. Должен
применяться с осторожностью

Cle ar () Этот метод удаляет содержимое выходного элемента

В листинге 23.20 дескрипторный вспомогательный класс использует метод


SetConten t () для установки соде ржимого выходного элемента на основе знач ения
атрибута type , которое предоставляется через свойство Тур е . Запустив приложе­
ние и запросив URL вида /Home/Creat e . вы обнаружите, что специальны е эл ементы
fo rrnЬ utton были заменены стандартными НТМL-элементами. В итоге приведе нные
ниже элементы:

<f ormb utto n t ype= "submit " bg- col or= "da nger " />
<f ormbutto n type= " reset " />
трансформировались следующим образом:

<bu t ton clas s = " Ьt n Ьt n -da n ge r" type= " s ubmi t" >Add</bu tton>
<butt on c l as s = " Ьtn Ьtn -prim a r y " t ype= "reset " >Reset</b utton>

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


Класс TagHe lper Ou tpu t предлагает четыре свойства, которые облегчают внед­
рение нового содержимого в представление, так что оно окружает эл е м е нт или е го

содержимое (табл. 23.7). Далее будет показано. как добавлять содержимое вокруг и
внутрь целевого элемента.

Таблица 23.7. Свойства класса TagHelperOutput, предназначенные


для дополнения содержимого и элементов

Имя Описание

PreElement Это свойство применяется для вставки элементов в представ­


ление перед целевым элементом

PostElement Это свойство используется для вставки элементов в представ­


ление после целевого элемента

PreContent Это свойство применяется для вставки содержимого в целевой


элемент перед существующим содержимым

PostContent Это свойство используется для вставки содержимого в целевой


элемент после существующего содержимого
Глава 23. Дескрипторные вспомогательные классы 729
Добавление содержимого вокруг выходного элемента
Первые два свойства класса TagHelperOuput, PreElement и PostElement,
применяются для вставки элементов в представление перед и после выходного эле ­

мента. Добавьте в папку Infras tructure/ TagHelpers файл класса по имени


ContentWrapperTagHelper . cs и определите в нем дескрипторный вспомогатель­
ный класс, как показано в листинге 23.21.

Листинг 23.21 . Содержимое файла ContentWrapperTagHelper . cs


из папки Infrastructure/TagHelpers
using Microsoft . AspN etCore . Mvc . Rendering;
using Microsoft.AspNe t Core . Razor.TagHelpers;
namespace Ci ties .Infrastructure .TagHelpers {
[HtmlTargetElement ( "div", Attributes = "titl e " )]
puЫic class ContentWrapperTagHelper : TagHelper
puЫic bool IncludeHeader { get; set; true;
puЫic bool IncludeFooter { get; set; } = true ;

puЫic string Title { get; set; )


puЫic override void Process(TagHelperContext context,
TagHelperOutput output) {
output .Attributes.SetAttribute("class", "panel -body");
TagBuilder title = new TagBuilder("hl" ) ;
title.InnerHtml.Append(Title);
TagBuilder container = new TagBuilder ( "di v " ) ;
container .Attributes[" class "] = "bg-info panel-body";
container .InnerHtml . AppendHtml(title);
if (Inc lude Header) {
output .PreE l ement .SetHtmlCon tent( cont ainer) ;

if (Inc lude Footer) {


output . PostElement.SetHtmlCon tent(container);

Этот дескрипторный вспомогательный класс трансформирует элементы div. ко­


торые имеют атрибут title, с использованием свойств PreElement и PostElement
для добавления верхнего и нижнего колонтитулов, 01<ружающих выходной элемент.
При генерировании новых НТМL-элементов можно применять стандартное форма­
тирование строк С# для создания требуемого содержимого, но это неудобный и чре­
ватый ошибками процесс за исключением разве что самых простых элементов. Более
надежный подход предусматривает использование класса TagBuilder, который оп­
ределен в пространстве имен Microsoft. AspNetCore. Mvc. Render ing и позволяет
создавать элементы в лучше структурированной манере. Класс TagHelperContent

определяет методы. принимающие объекты TagBuilder, которые облегчают созда­


ние НТМL-содержимого в дескрипторных вспомогательных классах.
730 Часть 11 . Подробные сведения об инфраструктуре ASP. NET Соге MVC

В дескрипторном вспомогательном классе ContentWrapperTagHelper с помощью


класса TagBuilder создается элемент hl, содержащийся внугри элемента
di v, который
был стилизован посредством классов Bootstrap. Имеются также необязательные атрибу­
ты i nclude - header и inc l ude - foote r типа boo l, применя емые для указания , куда
внедрять содержимое; по умолчанию оно добавляется перед и после выходного элемента .
В листинге 23.22 приведена обновленная компоновка, содержащая элемент, который бу­
дет трансформироваться дескрипторным вспомогательным классом .

Листинг 23.22. Включение дескрипторного вспомогательного класса


в фaйлe _Layout.cshtml
< ! DOCT YPE html >
<h tml >
<head>
<me t a name= "viewport " con t e nt ="wid t h=devi ce - wi dth " />
<tit le >Cities</tit l e >
<l ink hre f = " /liЬ/bootst r a p /d is t/css/ b oot s tra p. c s s " re l =" s tylesheet " />
</ head>
<body c l ass =" pane l-body " >
<di v ti tle=" Ci ties ">@RenderBody () </ di v>
</body>
</html>

Запустив приложение, вы увидите, что дескрипторный вспомогательный класс


применя ется повсюду в приложении, добавляя верхний и нижний колонтитулы к
каждой странице (рис. 23.5).

Cities
Nвmв

London

New York

San Jose
. . ·

-
UK

USA

USA
. Populвtion

8,539,000

8,406,000

998,537

-
Paris France 2,244,000

1 Cities
L. _ ---·-·. ------
Рис. 23.5. Вставка НТМL-элементов с помощью дескрипторного вспомогательного класса
Глава 23. Дескрипторные вспомогательные классы 731

Вставка содержимого внутрь выходного элемента

Свойства PreContent и PostContent используются для вставки содержимого


внутрь выходного элемента, окружая первоначальное содержимое. Добавьте в папку
Infrastructure/TagHelpe rs файл иласса по имени TaЬleCell TagHe lper. cs с оп­
ределением из листинга 23.23.

Листинг 23.23. Содержимое файла TaЫeCell TagHelper. cs


из папки Infrastructure/TagHelpers

using Microsoft .AspN etCore .Razor .TagHelpers ;


namespace Cities . Infrast ructure.T agHe lper s {
[H tmlTargetElement( "td", Attributes = "wrap ")J
puЫic class TaЬleCellTagHelper : TagHelper {
puЫic override void Process(TagHelperContext context,
TagHe lperOutput output)
output .PreCont ent .SetHtmlContent(" <b><i> ");
output.PostContent .SetHtml Conte nt ( " </i></b> " ) ;

Этот дескрипторный вспомогательный класс оперирует на элементах td с атрибу­


том wrap и вставляет элементы Ь и i вокруг содержимого выходного элемента. В лис­
тинге 23.24 демонстрируется добавление атрибута wrap к одной из ячеек таблицы
внутри файла представления Index. cshtrnl.

Листинг 23.24. Добавление атрибута HTML в файле Index. cshtml


@model IEnumeraЬle<City>

@{ Layout = " _Layout "; }


<tаЫе class= " taЫe taЫe-condensed taЫe-bordered">
<thead class= " bg -primary" >
<tr>
<th>Name</th>
<th>Country</th>
<th class="text-right">Population</th>
</tr>
</thead>
<tbody>
@foreach (var city in Model)
<tr>
<td wrap>@city.Name</td>
<td>@city.Country</td>
<td class="text-right">@city.Population?.ToString("#,###")</td>
</tr>

</tbody>
</tаЫе>
<а href="/Home/Create" class="Ьtn Ьtn-primary">Create</a>
732 Часть 11. Подробные сведения об инфраструктуре ASP.NET Соге MVC

После запуска приложения вы заметите, что первая колонка ячеек в таблице, в


которой перечислены объекты Ci ty, отображается полужирным курсивом. Заглянув
в НТМL-разметку, отправленную браузеру, вы увидите, что содержимое, добавленно е
через свойства PreContent и PostContent , находится с обеих сторон исходного со ­
держимого элемента:

<tr>
<td wrap><b><i>London</i></b></td>
<td>UK</td>
<td class= " text - r i ght " >B , 539 , 000</td>
</tr>

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

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


и использование внедрения зависимостей
Одним из наиболее распространенных применений дескрипторных вспомогатель­
ных классов, вr<лючая встроенные классы такого рода, которые рассматриваются в

главах 24 и 25, является трансформация элементов так , что они содержат детали теку­
щего запроса или текущей модели представления. Добавьте в папку Infrastructure/
TagHelpers файл класса по имени FormTagHe lper. cs , содержимое которого приве­
дено в листинге 23.25.
Листинг 23.25. Содержимое файла FormTagHelper. cs
из папки Infrastructure/TagHe lpers
using Microsoft . AspNetCore . Mvc ;
using Microsoft . AspNetCore . Mvc . Rende ring ;
using Microsoft . AspNetCore . Mvc . Routing ;
using Microsoft .AspNetCore . Mvc . ViewFea tures ;
using Microsoft . AspNetCore.Razor .T agHelpers ;
namespace Cities.Infrastructure . TagHe lpe rs {
puЫic class FormTagHelper : TagHe l per {
private IUrlHelperFactory urlHelperFact ory ;
puЫic FormTagHelper(IUrlHelperFactory factory)
urlHelperFac t ory = factory ;

[ViewContext]
[HtmlAttributeNotBound]
puЬlic ViewContext ViewContextData get; set ; }
puЫic str ing Controller { get ; set;
puЫic string Action { get ; set ; }
puЬl i c override void Process(TagHelperContext context ,
TagHelperOu t put output) {
Глава 23. Дескрипторные вспомогательные классы 733
I UrlHe lpe r ur l Helper = u rl Helpe rFactory.Ge t Ur lHe l per(ViewContext Data) ;
ou tput.A tt rib ut es.SetAt t ribu t e("action", urlHelp e r.Act ion(
Ac t ion ??
Vi e wC ontextData.Ro u teData . Val ues[" ac ti on "] .T oS t ring() ,
Cont r oller ??
Vi e wContextDat a.Ro u t eDa ta . Va lue s [ " co n t r o ll e r"] .ToSt ri ng ())) ;

Как и можно было предположить по имени, класс FormTag Helper оперирует на


элементах fo r m, уст анавливая их атрибуты action для указания, куда будут отправ­
ляться данные формы . Если элемент f orm имеет атрибуты c o n t r ol le r и a ct i o n, то
их значения будут использоваться для генерации целевого URL; в противном случае
будут применяться значения c o n t r o ller и acti o n из данных маршрутизации для
текущего запроса.

Чтобы получить данные контекста, понадобится добавить свойство по имени


ViewContex t Data и декорировать его двумя атрибутами:

[Vi.ewContext]
[HtmlAttributeNotBound]
puЫ i c Vie wCo n text ViewCon te xtDa ta get; s e t ; }

АтрибутViewCon t ex t указыва ет на то, что значение этого свойства должно


быть присвоено объекту Vie wCo n te x t , когда создается новый экземпляр класса
FormTagHelp e r, как объяснялось в главе 18. Класс ViewCon te xt предоставляет де­
тали о визуализируемом представлении, данных маршрутизации и текущем НТТР­
запросе, как было описано в главе 21.
Атрибут HtmlAt t rib uteNo t Bo und предотвращает присваивание инфраструк­
турой MVC значения данному свойству, если НТМL-элемент input имеет атрибут
view - cont e xt . Это рекомендуемый прием, особенно если вы создаете дескрипторные
вспомогательные классы для использования другими разработчиками.

Совет. Существует встроенный дескрипторный вспомогательный класс для элемента form,


который может применяться при нацеливании на методы действий и должен использо­
ваться в реальных проектах. Дескрипторный вспомогательный класс, рассматриваемый в
настоящем разделе, предназначен просто для демонстрации, как можно применять дан­

ные контекста. За деталями об этом встроенном дескрипторном вспомогательном классе


обращайтесь в главу 24.

Дес1iрипторные вспомогательные классы могут объявлять в своих конструк­


торах зависимости от служб, которые распознаются с использованием средства
внедрения зависимостей . В настоящем примере объявлена зависимость от службы
IU rl He lp e rFactory , которая позволяет создавать исходящие URL из данных мар­
шрутизации (и явля ется службой , лежащей в основе свойства Url, предоставляемо­
го классом Co ntroller . :который был описан в главе 16). Внутри метода Pr o c e ss ( )
дескрипторный вспомогательный класс применяет метод IU rlHelperF act or y.
GetU rlHe lper ( ) для получения объекта реализации IU r lHe l p e r, который конфи­
гурируется с помощью объекта Vi e wCo n t ext и затем используется при создании URL
734 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC

для атрибута action выходного элемента. В листинге 23.26 демонстрируется подго­


товленное пр едставле ние , из которого удален атрибутaction, так что его можно ус ­
танавливать посредством дескрипторного вспомогательного класса.

Листинг 23.26. Удаление атрибута action из элемента form


в файле Create. cshtml
@model City
@{ Layout= " Layout "; }
<form method="post">
<div class= "f orm- group " >
<label for= " Name " >Name : </label>
<input c l ass= " form - control " name="Name " />
</div>
<div class="form-group " >
<label for="Country " >Country : </label>
<input class= " form-control " n ame= " Country" />
</div>
<div class= " form-group " >
<label for= " Population " >Populat i on : </label>
<input class= " form - control " name= " Population " />
</div>
<formbutton type= " submit " bg-color= " danger " />
<formbutton type= "reset " />
<а bs - button - color= " primary " href="/Home/Index">Cancel</a>
</form>

Запустив приложение , запросив URL вида /Home/Create и просмот р ев НТМL­


разметку. отправл е нную браузеру, вы заметите, что элемент form имеет атрибут
action, значение которого получено с примен ением данных контекста:

<form method="post" action="/Home/Create" >

Работа с моделью представления


Дескрипторные вспомогательные классы могут оперировать на модели пред­
ставл е ния, подгоняя выполняемые ими трансформации или создаваемый вы­
вод. Добавьт е в папку
Infrastructure/TagHelpers файл класса по имени
LabelAndinputTagHelper . cs с содержимым из листинга 23.27.

Листинг 23. 27. Содержимое файла LabelAndinpu tTagHelper. cs


из папки Infrastructure/TagHelpers
using Microsoft . AspNetCore . Mvc.ViewFeatures ;
using Microsoft . AspNetCore . Razor .T agHelpers ;
namespace Cities . Infrastructure . TagHelpers {
[HtmlTargetElement("label", Attributes = " helper-for " )]
[HtmlTargetElement("input", Attributes = " helper - for " )]
puЫic class LabelAndinputTagHelper : TagHelper {

puЫic ModelExpression HelperFor { get; set; }


Глава 23. Де с к рипторные вспомогательные классы 735
puЫic override void Process(TagHelperContext context ,
TagHelperOutput output) {
if (output.TagName == " label " ) {
output . TagMode = TagMode.StartTagAndEndTag ;
output . Content . Append(HelperFor . Name) ;
output . Attributes.SetAttribute( " for ", HelperFor . Name) ;
else if (out p ut . TagName == " input " ) {
output . TagMode = TagMode . SelfClosi n g ;
output . Attributes . SetAttribute( "n ame ", HelperFo r.N ame) ;
output . Att ribu t e s. Se t Attribute( " class ", " form - cont r o l" ) ;
if (Helpe r Fo r. Metadata . Mode l Type == typeof(in t ?)) {
output . Attributes . SetAttribute( " type ", " number " ) ;

Де скрипторный вспомогательный класс LabelAndinputTagHelpe r трансформи­


рует элем е нтыl abel и input . которые имеют атрибут h elper - for . Важной частью
этого дескрипторного вспомогательного класса является тип свойства He lper Fo r , ко­
торое используется для получ ения значения атрибута he l per - for :

puЫic ModelExpression HelperFor { get ; set ; }

Класс ModelExpression применяется, когда нужно оперировать с частью модели


представления , что легче всего объяснить , продвинувшись дальше и показав , как де ­
скрипторный в с помогательный .кл ас с используется в представлении (листинг 23.28) .

Листинг 23.28. Применение дескрипторного вспомогательного класса,


который оперирует на модели, в файле Crea te . csh tml

@model Cities . Models . City


@( Layout = " Layout "; }
<form method= " post " >
<div class= " form - group " >
<laЬel helper-for="Name" />
<input helper-for="Name" />
</div>
<div class= " form - group " >
<label helper-for="Country" />
<input helper-for="Country" />
</div>
<div class= " form - group " >
<laЬel helper-for="Population"/>
<input helper-for="Population" />
</div>
<formbutton type =" subm i t " bg-color =" danger " />
<formbutton type= " reset " />
<а bs - button - color= " primary " href= " /Home/Index " >Cancel</a>
</form>
736 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

Значение атрибута helper-fo r - это свойство из IOiacca Model, которое обнару­


живается инфраструктурой MVC и предоставляется дескрипторному вспомогательно­
му классу как объект ModelExpr es si on.
Класс Mod el Expr e ssi on здесь подробно не рассматривается, потому что любой
анализ типов приводит к бесконечным спискам классов и свойств. Кроме того , в
MVC имеется удобный набор встроенных дескрипторных вспомогательных классов.
используемых моделью представления для трансформации элементов, как описано в
главе 24, т.е. создавать собственные классы такого рода вам не придется.
В примере дескрипторного вспомогательного класса задействованы две базовых
возможности, которые полезно рассмотреть. Первая из них - получение имени свойс­
тва модели. так что его можно включить в выходной элемент:

outp ut. Conte nt . App end (HelperFor.Name ) ;


outpu t . Attr i b ut es . SetAtt ri b ut e( " for ", He1perFor.Name ) ;

Свойство Name возвращает имя свойства модели. Вторая возможность свя з ана с
получением типа свойства модели, что позволяет изменить значение атрибута type
элементов i nput :

if (HelperFor.Metadata.Mode1Type == typeof (int?) ) {


out p ut. At t r ibutes.Se tAtt r i b ute("type", " number" ) ;

Запустив приложение, запросив URL вида / Home/ Cre a te и просмотрев НТМL­


разметку, которая была отправлена браузеру. вы обнаружите следующие элементы:

<div cla s s= " f orm- g r oup " >


<l abe l for= " Narne " >Narne</ l a b e l >
<input name= "Narne " c l ass =" fo rm-con t r ol " />
</di v>
<di v c l ass =" fo r rn- gr oup ">
<l a be l for= "Coun t r y " >Coun tr y</ l a b e l >
<inpu t name =" Countr y " c l ass =" for rn- cont r o l" />
</d i v>
<div c l a s s= "form- g r oup" >
<labe l f or="Popul a ti on ">Popula t ion </ l abe l >
<input name= "Populati on" cl a ss=" fo rm- co nt rol" type =" number" / >
</div >
Атрибут type элемента i nput по имени Popula ti o n был установлен в n umber,
подчеркивая тот факт, что свойство Ci ty. Popu lat ion в классе С# имеет тип int; это
демонстрирует возможность отражения различных характеристик модели в НТМL­
разметке, генерируемой дескрипторным вспомогательным классом. В зависимости от
применяемого браузера элемент i nput будет разрешать ввод только чисел .

Согласование дескрипторных вспомогательных классов


Свойство TagHe lperCont ext . It e ms предоставляет словарь , который использу­
ется для согласования дескрипторных вспомог ат ельных классов , оперирующих на

элементах и их дочерних элементах. Чтобы ознакомиться с применением коллек­


ции Item s , добавьте в папку I nf r a s tructur e /TagHelpers файл класса по имени
Coo r d inati n g TagHelper s . cs и определите в нем пару дескрипторных вспомога­
тельных классов, как показано в листинге 23.29.
Глава 23. Дескрипторные вспомогательные классы 737
Листинг 23.29. Содержимое файла CoordinatingTagHelpers. cs
из папки Infrastructure/TagHelpers
using Microsoft .AspNetCore .Razor . TagHelpers;
narnespace Cities . Infrastructure.TagHelpers {
[H trnlTarget Elernent ( "div", Attributes = "theme")]
puЫic class ButtonGroupThemeTag Helper : TagHelper
puЬlic string Theme { get; set ; )
puЫic override void Process(TagHelperContext context ,
TagHelperOutput output) {
context . Items[ " theme "] = Theme;

[HtmlTargetElement("button", ParentTag = "div")]


[Htm1 'l'argetElement ("а", ParentTag = "di v") ]
puЫic class ButtonThemeTagHelper : TagHelper {

puЫic override void Process(TagHelperContext context ,


TagHelperOutput output) {
if (context . Items. ContainsKey ( "theme")) {
output . Attr ibu tes .S etAttribute( "clas s ",
$ "btn btn-{context.Items["theme"] }");

Первый дескрипторный вспомогательный класс, ButtonGroupThemeTagHelper,


оперирует на элементах div, которые имеют атрибут theme. Согласование дескрип­
торных вспомогательных ш~ассов может трансформировать их собственные элемен­
ты, но в рассматриваемом примере оно просто добавляет значение атрибута theme
в словарь Items, так что это значение становится доступным дескрипторным вспо­

могательным классам, которые оперируют на элементах, содержащихся внутри эле­

мента div .
Второй дескрипторный вспомогательный :класс, ButtonThemeTagHelper, опери­
рует на элементах but ton и а, находящихся внутри элемента di v. Он использует зна­
чение атрибута theme из словаря Items, чтобы установить стиль Bootstгap для своего
выходного элемента. В листинге 23.30 приведен набор элементов, к которым будут
применены эти дескрипторные вспомогательные классы.

Листинг 23.30. Применение согласованных дескрипторных вспомогательных


классов в файле Crea te. csh tml
@rnodel Cities . Models . City
@{ Layout=" Layout";}
<form method="post">
<div class= " form -gr oup " >
<label helper-for= " Name" />
<input helper - for="Name" />
</div>
738 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Соге MVC

<div cla s s= " fo r m- g r oup " >


<label helper - for =" Cou nt r y " / >
<input he l pe r-f or=" Cou ntr y" />
</d i v>
<div class =" form - group " >
<label he l per - for =" Population " />
<i nput helper-for= " Populat;ion " />
</d i v>
<div theme="primary">
<button type="suЬmit">Add</button>
<button type="reset">Reset</button>
<а href="/Home/Index">Cancel</a>
</div>
</ f orm>

Запустив приложение и запросив URL вида / Home/C rea te , вы заметите, что груп­
па кнопок стилизована одинаково. Если вы замените значение атрибута theme эле­
м ента di v друго й настройкой темы Bootstrap. такой как info , danger или primary,
и затем перезагрузите страницу , то увидите , что изменение отразилось в стилях кно­

пок (рис . 23.6) .

•Е+Н'
~.. -. - J
1

:";
t J.P"·

Рис. 23.6. Согласование дескрипторных вспомогательных классов

Подавление выходного элемента


Дескрипторные вспомогательные классы могут использоваться для того, чтобы
предотвратить включение элемента в НТМL-разметку, отправляемую браузеру, за счет
вызова метода Suppre ss Ouput () на объекте TagHelpe r Output, который получается
в качестве аргум е нта метода Pr o c es s () .В листинге 23.31 к раздел яемой компоновке
добавляется элемент, который отображает хорошо видимое сообщение , но только для
запросов, направленных на заданное действи е.

Листинг 23.31. Добавление хорошо видимого сообщения в фaйлe _Layout.cshtml

<!DOCTY PE html>
<html >
<he a d >
<meta name= " viewpo r t " content= " wi dt h=device - wi d t h " />
<t i tle>C i t i es</t i t le >
<l i nk hr ef= " /li Ь/bootstrap/dist/ c ss /bootstrap . css " rel= " st yl es he et " />
</ head>
Глава 23. Дескрипторные вспомогательные классы 739
<body class="panel-body">
<div show-for-action="Index" class="panel-Ьody Ьg-danger">
<h2>Important Message</h2>
</div>
<div title="Cities">@RenderBody()</div>
</body>
</html>

Атрибут show - for-action указывает имя действия, для которого необходимо


отобразить сообщение. Такой подход к управлению включением содержимого нельзя
считать приемлемым в реальном приложении, но его вполне достаточно для приме­

ра приложения с единственным контроллером и двумя действиями. Добавьте в папку


Infrastructure/TagHelpers файл класса по имени Selecti veTagHelper. cs с со­
держимым из листинга
23.32.

Листинг 23.32. Содержимое файлаSelecti veTagHelper. cs из папки


Infrastructure/TagHelpers
using System;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft . AspNetCore . Mvc.ViewFeatures;
using Microsoft . AspNetCore . Razor.TagHelpers;
namespace Cities . Infrastructure.TagHelpers {
[HtmlTargetElement(Attributes = " show-for-action")J
puЫic class SelectiveTagHelper : TagHelper {
puЫic string ShowForAction { get; set; }
[ViewContext]
[HtmlAttributeNotBound]
puЫic ViewContext ViewContext { get ; set; }

puЫic override void Process(TagHelperContext context ,


TagHelperOutput output) {
if (!ViewContext.RouteData . Values["action"] .ToString()
.Equals{ShowForAction, StringComparison.OrdinalignoreCase))
output.SuppressOutput() ;

С помощью объекта ViewContext дескрипторный вспомогательный класс получа­


ет значениеaction из данных маршрутизации и сравнивает его со значением атри­
бута show-for-action в НТМL-элементе. Если они не совпадают, тогда вызывается
метод SuppressOutput (). Чтобы взглянуть на результат, запустите приложение и
запросите URL вида /Home/Index и /Home/Create . Как показано на рис. 23.7 , сооб­
щение отображается только в случае нацеливания на действие Index .
740 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

. - --·· х

1
··~ -~ -~ lo_.:all ~st _:~::::.'~~~~':cle~ -·-\ ~- ----' С' 1.J focall1ost:SS652/Home/Cr~at"

Рис. 23. 7. Подавление элементов с применением дескрипторного


вспомогательного класса

Резюме
В этой главе рассматривалось использование дескрипторнъrх вспомогательных
классов, которые являются новым дополнением инфраструктуры ASP.NET Core MVC.
Была объяснена роль, которую они играют в представлении Razor, а также продемонс­
трировано создание, регистрация и применение специальньrх дескрипторньrх вспо­

могательных классов . Вы узнали , как управлять областью действия дескрипторного


вспомогательного класса, и ознакомились с разнообразными способами, которыми
дескрипторные вспомогательные классы могут трансформировать НТМL-элементы .
В следующей главе будут описаны дескрипторные вспомогательные классы , которые
используются для работы с элементами НТМL-форм.
ГЛАВА 24
Использование
дескрипторных

вспомогательных

классов для форм

и
нфраструктура MVC предлагает набор встроенных дескрипторных вспомога­
тельных кла ссов, кот орые используют ся для выполнения часто требующих ­
ся тр а нсфор ма ц ий в отнош ении НТМL- элементов . В этой глав е рас сматриваются
де скрипторны е в сп о м огат ельны е клас сы, оперирующие на НТМL- фор м ах, которые
пр едн аз н ач е ны дл я эл ементов form , i nput, label , select , option и textarea.
В гл а ве 25 будут опи са ны други е в с тро е нные де скрипторные вспо м огательные клас­
с ы , н е относящие ся к форм ам . В табл . 24. l приведе на сводка, позволяющая пом е с­
тить де скриптор ны е в спомогательные классы для форм в конт е кст.

Таблица 24. 1. Помеще ние дескрипторных вспомогательных классов для форм в контекст

Вопрос Ответ

Что это такое? Дескрипторные вспомогательные классы для форм применяются


при трансформации элементов НТМL-форм, поэтому вам не при­
дется писать собственные классы такого рода, чтобы решать наибо­
лее распространенные задачи

Чем они полезны? Дескрипторные вспомогательные классы для форм обеспечивают


согласованную генерацию элементов внутри НТМL-форм , таких как
label и inpu t . Большей частью дескрипторные вспомогательные
классы гарантируют, что важные атрибуты наподобие .id, name и
for устанавливаются напрямую с использованием классов моде­
лей представлений , но некоторые дескрипторные вспомогательные
классы также могут генерировать содержимое, заполняя, например,
эле м енты select элементами option
Ка к они Встроенные дескрипторные вспомогательные классы ищут атрибу­
используются? ты с префиксом asp-, такие как asp - for
742 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

Окончание табл. 24. 1


В опрос Ответ

Существуют ли какие ­ Единственное ограничение связано со способом, которым данные


то скрытые ловушки модели должны предоставляться дескрипторному вспомогатель­

или ограничения? ному классу, генерирующему элементы opt i on внутри элементов


s elect. Описание проблемы и специального дескрипторного
вспомогательного класса, который ее решает, приведено в разделе
"Работа с элементами select и option" далее в главе

Существуют ли Создавать НТМL- формы в представлениях возможно вообще без при­


альтернативы? менения атрибутов дескрипторных вспомогательных классов. Можно
также разработать собственные дескрипторные вспомогательные
классы , используя приемы , которые были описаны в главе 23
Изменились ли они по Дескрипторные вспомогательные классы для форм являются новым
сравнению с версией средством в ASP.NET Core MVC и предоставляют более элегантный
MVC 5? подход, чем вспомогательные методы HTML, которые предлагали
похо жую функциональность в предшествующи х версия х MVC. За
дополнительными деталями обращайтесь во врезку " Что произошло
со вспомогательными методами HTML ?" в главе 23

В табл. 24.2 прив едена сводка для настоящей главы.

Таблица 24.2. Сводка по главе

Задача Решение Листинг

Установка атрибута act i on Используйте дескрипторный вспомо­ 24.1-24.5


в элементе form гательный класс для элемента form
Препятствование подделке Примените атрибут 24.6, 24.7
ме жсайтовых запросов ValidateAntiForger yToken к ме­
тоду действия и дополнительно уста­
новите атрибут asp- antiforgery
в true для элементаform
Установка атрибутов id, name Применяйте атрибут asp - for 24.8
и val ue в элементе input
Форматирование значения , отоб­ Примените атрибутasp- format 24.9-24.12
ражаемого элементом inpu t к элементуinput или атрибут
Displ ayFormat в классе модели
Установка атрибута for и содержи­ Применяйте атрибут asp - for 24. 13
мого элемента label
Изменение содержимого элемента Примените атрибут Display к свойс- 24.14
labe l, к которому был применен тву класса модели и с помощью
атрибут asp- for свойства Name ука ж ите содержимое

Установка атрибутов id и name Пр и меняйте атрибут asp - for 24.15


в эле м енте select
Генерация элементов opt i on Применяйте атрибут asp - i tems 24.16-24.21
Установка атрибутов id и name Применяйте атрибут asp- for 24.22, 24.23
в эле м енте textarea
Глава 24. Использование дескрипторных вспомогательных классов для форм 743

Подготовка проекта для примера


Мы продолжим работать с проектом Ci ties, созданным в предыдущей главе.
Сначала потребуется провести небольшую подготовку, отключив специальные де­
скрипторные вспомогательные классы, возвратив представления к использованию

стандартной НТМL- разметки и добавив пакет NuGet, который будет задействован


позж е в главе.

Изменение регистрации дескрипторных вспомогательных классов


Для целей настоящей главы необходимо включить встроенные дескрипторные
вспомогательные классы. поступающие с инфраструктурой MVC, и отключить специ­
альные дескрипторные вспомогательные классы, которые были созданы в главе 23.
В листинге 24. l показаны изменения. которые понадобится внести в файл импорти­
рования представлений, где выражение @addTagHelper для вспомогательных клас­
сов из сборки Ci ties заменено выражением. настраивающим вместо них дескрип­
торные вспомогательные классы MVC.

Листинг 24. 1. Изменение дескрипторных вспомогательных классов


в файле _ Viewimpo rts . csh tml

@using Cities . Models


@addTagHelper *, Microsoft.AspNetCo re.Mvc.TagHelpers

Встроенные дескрипторные вспомогательные классы определены в сборке по

имени Microsoft . AspNetCo re . Mvc. Ta gHelpe rs, которая добавляется в проект


как зависимость пакета Micr osoft . AspNetCore. Mvc , перечисленного в файле
proj ect . j son .

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


В листинге 24.2 приведено содержимое представления Index . cshtml , из которого
удалены атрибуты . применяемые специальными дескрипторными вспомогательными

классами.

Листинг 24.2. Содержимое файла Index . cshtml

@model IEnumeraЬle<Cit y>

@{ Layout = " Layout "; }


<tаЫе class= "t aЫe taЬle -c onden s ed taЬle - bordered">
<thead c l ass= "bg - primary " >
<tr>
<th>Name< / th>
<th>Country</th>
<th class= "text-right " >Popu l ation</th>
</tr>
</thead>
<tbody>
@foreach (var city in Model)
<tr>
<td>@c ity . Name</td>
<td>@city . Country</td>
744 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC

<td class="text-right">@city.Population?.ToString("#,###")</td>
</tr>

</tbody>
</tаЫе>
<а href= " /Home/Create " c l ass= "Ьt n Ьtn-primary">Create</a>

В листинге 24.3 показаны соответствующие изменения файла Create . cshtml,


который возвращен 1< использованию стандартных НТМL-элементов без атрибутов,
применяемых в главе 23.

Листинг 24.З. Содержимое файла Create. cshtml


@model City
@{ Layout = " Layout "; }
<form method="post" act i on= " /Home/Create " >
<div class= " form-group " >
<l abel for= " Name " >Name:</labe l >
<input c l ass= " form -control " name="Name" />
</div>
<div class= " form - gro up" >
<l abel for="Country">Country:</label>
<input class= " form -con trol " name="Country" />
</d i v>
<div class= " form-group " >
<labe l for="Population">Population:</label>
<input class= " form - control" name="Population" />
</div>
<button type="submit " c l ass= "Ьtn Ьtn-primary">Add</button>
<а class= " Ьtn Ьtn-primary " href= " /Home/Index">Cancel</a>
</form>

Последнее изменение касается разделяемой компоновки (листинг 24.4) .

Листинг 24.4. Содержимое фaйлa _Layout.cshtml


<!DOCTYPE html>
<html>
<head>
<meta name= " viewport " content="width=device-width" />
<title>Cities</title>
<link href="/liЬ/bootstrap/dist/css/bootstrap . css" rel="stylesheet" />
</head>
<body class="panel-body">
<div>@RenderBody()</div>
</body>
</htm l >

Запустив приложение, вы увидите список городов; щелкнув на кнопке Create


(Создать), можно заполнить форму и отправить новые данные серверу (рис. 24.1).
Глава 24 . Использование дескрипторных вспомогательных классов для форм 745

D Cit", х D Citios Х

~- [p_1~~-~~49l7~--- - --==,-:-- :r1 ~-1--~--~~~ocalhost~~7.!!Ho1,';;/Create ___ ii]_~

Namв:
Nam~ ·" ". · ' ' · еоuПtТУ ,,.·..,:'' .· -'.
'
·J\Jpulation
London UK 8,539,000 ,,

New York USA 8,406,ООО !I Country:


San Jose USA 998,537 1

Par1s Francв 2,244.000 1


111
Population:
1

: •
L__ ____________________ J, 111!'*!1!_________ ---
1

Рис. 24.1. Выполнение примера приложения

Работа с элементами f orm.


Класс Fo rmTagHelper - это встроенный дескрипторный вспомогательный класс
для элементов form, который используется для управления конфигураци ей НТМL­
форм, так что их можно нацеливать на правильный метод действия, основываясь на
конфигурации маршрутизации приложения . Дескр ипто рный вспомогательный класс
FormTagHelper поддерживает атрибуты, описанные в табл. 24.3.

Таблица 24.3. Атрибуты встроенного дескрипторного вспомогательного класса


для элементов form
Имя Описание

asp- control l er Этот атрибут применяется, чтобы указать системе маршрутизации


значение controller для URL в атрибуте action. Если он опу­
щен, тогда будет использоваться контроллер, визуализирующий
представление

asp- action Этот атрибут применяется, чтобы указать системе маршрутизации ме­
тод действия для URL в атрибуте action. Если он опущен, тогда будет
использоваться действие, визуализирующее представление

asp- r oute - * Атрибуты с именами, начинающимися с asp- route -, применяются


для указания дополнительных значений URL в атрибуте action , так
что атрибут asp- route-id используется для предоставления систе­
ме маршрутизации значения сегмента id
asp- route Этот атрибут применяется для указания имени маршрута, который бу­
дет использоваться при генерации URL в атрибуте action
asp- area Этот атрибут применяется для указания имени области, которая будет
использоваться при генерации URL в атрибуте action
asp- antiforgery Этот атрибут управляет добавлением информации, противодейству­
ющей поддел ке , как объясняется в разделе " Использование средства
противодействия подделке" далее в главе
746 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

Установка цели формы


Основным назначение класса Fo rmTa gH elpe r является установка атрибута
action элемента form с применением конфигурации маршрутизации приложения ,
гарантируя тем самым , что данные формы всегда посылаются корректному URL,
д аже когда схема маршрутизации изменяется. В листинге 24.5 используются атри­
буты asp - a c tion и as p- controller для нацеливания на метод действия Create ()
контроллера Home .

На заметку! Дескрипторный вспомогательный класс не устанавливает атрибут me t hod, и


если он отсутствует в элементе form , тогда браузер будет применять запрос GET для
отправки данных формы клиенту. Как объяснялось в главе 17, это может вызвать про ­
блемы, если данные формы используются для модификации данных в прилож ении.
Рекомендуется устанавливать атрибут method , даже если нужны запросы GET , чтобы
стал очевидным тот факт, что вы не забыли выбрать тип метода.

Листинг 24.5. Установка цели формы в файле Create. cshtml


@model City
@{ La yout = " Layout "; }
<formmethod="post" asp-controller="Home" asp-action="Create">
<di v class =" fo r m-group" >
<label for =" Name " >Name : </label>
<in put clas s =" form - control " name=" Name" />
</div>
<div c l ass= " fo rm- group ">
<l abe l for= "Coun tr y " >Country : </ l abel>
<inp ut c l as s=" fo r m- co ntro l" name=" Co unt ry " />
</ di v >
<div cla s s= " form - g r oup " >
<label for= " Populatio n" >Popu lat i on : </label>
<inp ut c l ass= " form - control " name="Populat i on " />
</div>
<button type= " subm i t " c l as s = " Ьt n Ьt n - pri m ary " >Add</button>
<а cl as s ="b tn b tn -pr imary " href=" / Home/ Index " >Cancel</a>
</f o rm>

Запустив приложение , запросив URL вида /Home/C r eate и просмотр е в НТМL­


разметку, которая была отправлена клиенту, вы заметите , что дескрипторный вспо­
могательный класс добавил к элементу f orm атрибут a c ti on и установил его значе­
ние с применением системы маршрутизации:

<f o r m me t hod="pos t " action="/Home/Create" >

Использование средства противодействия подделке


Подделка межсайтовых запросов (cross-site request forgery - CSRF) - это спо соб
злоумышл енной эксплуатации веб-приложения, направленный на то, чтобы задейс­
твовать в своих интересах метод аутентификации пользовательских запросов . В боль ­
шинстве веб-приложений, включая создаваемые с использованием ASP.NET Core,
идентификация запросов, относящихся к специфическому сеансу, с которым обычно
ассоциирован пользователь , осуществляется с применением сооkiе-наборов.
Глава 24. Исnользование дескриnторных всnомогательны х классов для форм 747
Атака CSRF (также называемая мистификацией сеанса) опирается на ситуацию,
когда пользователь посещает злоумышленный веб-сайт после работы со своим веб­
приложе нием, явно не завершив свои сеансы щелчком на кнопке выхода из прило­

ж ения. Приложение по - прежнему с читает пользователя активным, а сооkiе-набор, ко ­


торый браузер с охранил, пока еще не истек . Злоумышленный веб-сайт содержит код
JavaScript, отправляющий приложению запрос формы, который выполняет операцию
бе з согл асия поль з ователя; при этом природа операции будет зависеть от приложе­
ния, подвергаемого атаке . Поскольку код JavaScript выполняется браузером пользо ­
вателя, з апрос к приложению включает сооkiе-набор сеанса и приложение выполняет
операцию безо всякого уведомления пользователя или согласия с его стороны.
Если элемент form не содержит атрибут ac ti on (из-за того, что он генерирует­
ся системой маршрутизации с помощью атрибутов asp - con troller и a s p- acto n),
то класс FormTag Helpe r автоматически включает средство противостояния ата ­
кам CSRF. Ука з анное средство добавляет к форме скрытый элемент inpu t с марке ­
ром безопасности внутри НТМL-разметки, которая отправляется клиенту наряду с
сооki е- набором. Приложение будет обрабатывать запрос, только если он содержит и
сооkiе - набор , и скрытое значение формы, к которому злоумышленный веб-сайт не
име ет доступа. Каждый запрос к форме генерирует новый уникальный набор марке­
ров б е зопасности.
Запустите приложение , запросите URL вида /Home/Crea t e и просмотрите НТМL­
разметку, отправленную браузеру: в ней обнаружится СI<рытый элемент i n put вроде
показ анного ниже:

<input name= " RequestVer i ficationToken "


type= " hidden " va lue =" CfDJ8Ku VkH8h FlRApe
FBxTrhCFTKZe0B9BKw n W DJ q L R U Dk~Pr E waeCJm i BbG k wW1 Z I816c
TrM5XQkJBeqNI5IL8 Fhu ORv jZuYIL-G Zv nWZ620ThsZYT02 HNX
Lu5LW DNWDdVoS505h Zt z aoH LeY5 1N to " /> -
Воспользовавшись инструментами браузера, доступными по нажатию клавиши
< F12 >. можно такж е увидеть соответствующий сооkiе-набор , который добавляется
к отв ету. Добавление маркеров безопасности к НТМL-ответам - лишь часть про­
цесса; они должны также пров еряться контроллером, как продемонстрировано в

ли стинг е 24.6.
Листинг 24.6. Проверка маркеров противодействия подделке в файле HomeController. cs

using Microsoft . AspNe t Co re . Mvc ;


using Cities . Models ;
namespace Ci ties . Contro ll ers {
puЫic class HomeController : Co n t r ol l er {
privat e I Reposito ry rep os i tor y;
puЫic HomeController( IRepos it ory r epo) {
repository = repo ;

puЫ i c ViewRes u lt I ndex() => View(re p os i tory . Cities) ;


puЬlic ViewRes u lt Cr eate() = > Vi e w () ;
[HttpPost ]
[ValidateAntiForgeryToken]
748 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

puЫic IActionRe s ul t Create(C ity c ity)


r e pository . AddCit y( city) ;
retu r n RedirectToAct i on( "I ndex " ) ;

Атрибут Va li date AntoFo r ge r yToken удостоверяется, что запрос содержит до­


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

Класс FormTagHelper предлагает атрибут asp - antiforgery для переопредел е ­


ния стандартного пов едения противодействия ат акам CSRF. При установке этого ат­
рибута в true маркеры безопасности будут включаться в запросы , даж е если элемент
form имеет атрибут action. Когда значением атрибута a s p-antiforgery явля ется
fa l se , мар1< е ры безопасности будут отключены . В листинге 24.7 ср едство противо­
действия атакам CSRF включается явно, хотя маркеры безопасности были бы добав­
лены в любом случае, потому что для элемента form не определен атрибут act i on .

Листи нг 24.7. Включение средства противостояния атакам CSRF в файле Create. cshtml
@rnodel City
@{ Layout = " Layout "; }
<formmethod="post" asp-controller="Home" asp-action="Create"
asp-antiforgery="true">
<di v class= " forrn - group " >
<label for= "Narne " >Narne : </ l a bel>
<input clas s=" forrn - con t rol " narne=" Narne " />
</div>
<div class= " forrn - g r oup " >
<l abel for= "Country " >Coun t ry : </labe l >
<input class= " forrn - control " narne=" Country " />
</div>
<div class= " forrn- group " >
<labe l for= " Population " >Populat i on: </ l abe l >
<i np ut class= " forrn - control " narne=" Popu l a t ion " />
</div>
<button type= " subrnit " c l ass= "btn btn - pr i rnary " >Add</button>
<а class= "btn btn - prirnary " href=" / Horne/Index " >Cancel</a>
</forrn>

Совет. Тестирование средства противодействия атакам CSRF связано с некоторыми ух ищ­


рениями. Это делается путем запрашивания URL, который содержит форму ( / Home /
Create в рассматриваемом примере), и последующего применения инструментов <F12>
браузера для нахождения и удаления из формы скрытого элемента input (либо измене­
ния его значения) . После заполнения формы и ее отправки приложению браузер не будет
иметь одну часть требующихся данных, в результат чего запрос должен завершиться не­
удачей с отобра жением страницы ошибки.
Глава 24. Использование дескрипторных вспомогательных классов для форм 749

Работа с элементами input


Элемент input является основой НТМL-форм и предоставляет главные средства, с
помощью которых пользователь может снабжать приложение неструктурированными
данными. Класс I npu t TagHelper используется для трансформирования элементов
i np u t , чтобы они отражали тип данных и формат свойства модели представления ,
применяемого для сбора информации, с использованием атрибутов из табл. 24.4.
Таблица 24.4. Атрибуты встроенного дескрипторного вспомогательного класса
для элементов input
Имя Описание

asp - for Этот атрибут применяется для указания свойства модели представления,
которое олицетворяет элемент i nput
asp-format Этот атрибут используется для указания формата, применяемого при отоб­
ражении значения свойства модели представления, которое олицетворяет
элемент inpu t

Конфигурирование элементов i.nput


Атрибут asp - fo r устанавливается в имя свойства модели представления, 1юторое
затем используется для установки атрибутов n arne , id, type и value элемента input .
В листинге 24.8 демонстрируется применение атрибута asp -for к элементам i nput
внутри представления Cre a te . cshtrnl .
Листинг 24.8. Конфигурирование элементов input в файле Create. cshtml
@model City
@{ Layout = " Layou t"; }
<form method="po s t " asp - cont r ol ler= " Horne " asp - action= "Create"
asp - ant i forgery =" t r ue" >
<div class= " for m- group " >
<label for= " Name " >Name : </la bel>
<input class="form-control" asp-for="Name" />
</div>
<d i v class== " for m-g ro up" >
<label for= " Cou nt r y " >Cou nt ry: </l a bel>
<input class="form-control" asp-for="Country" />
</div>
<div class= " form - gr oup " >
<labe l for= " Pop ulat i on " >Popu l atio n: </ l abe l >
<input class="form-control" asp-for="Population" />
</div>
<button type= " subm i t " class= " Ьtn Ьtn -pri mar y" >Add</button>
<а c l a s s= " Ьt n Ьt n-pr i m ary " hr ef=" / Home/Index " >Cance l </a >
</f orm>

Запустив приложение и запросив URL вида / Home/Creat e, вы обнаружите, что


дескрипторный вспомогательный класс использовал свойство, указанное в атрибуте
asp- for , для подстройки каждого элемента inpu t, как показано в следующем фраг­
менте (марке р б е зопасности, защищающий от атак CSRF, опущен) :
750 Часть 11. Подробные сведения об инфрастру ктуре ASP.NET Core MVC

<fo r m method= "post " action=" /Home/C r eate ">


<d i v class= " form-group " >
<label for= " Name " >Name : </label>
<input c1.ass="form-contro1." type="text" id="Name" name="Name" va1.ue="" />
</div>
<di v class= "form- group " >
<label for= "Country " >Country : </label>
<input class="form-control" type="text" id="Country"
name="Country" value="" />
</div>
<d iv class= " form - group ">
<label for= " Population " >Popu l ation : </label>
<input class="form-control" type="numЬer" id="Population"
name="Population" value="" />
</div>
<bu t ton type =" submit " c l ass= " Ьtn Ьtn- primary " >Add</bu tt on>
<а class= " Ьtn Ьtn - primary " href= " /Home/Index " >Cancel</a>
</form>
Атрибут type элемента input сообщает браузеру о то м , к ак отобр аж ать эл е м ент
в форме. Вы м ожете просмотреть результат этого процесса в эл емент е inpu t для
свойства Populat i on, атрибут
type которого был установлен в numЬer . Дел о в том ,
что типом С # свойства Population является int ?; таким образом , дес криптор н ы й
вспомогат ельный класс применя ет атрибут type, чтобы указать брауз еру о приняти и
только чи сл овых знач е ний.

На заметку! Способ интерпретации атрибута type возлагается целиком на браузер. Не все


браузеры реагируют на все значения атрибута type, которые определены в специфи ка­
ции HTML5, и даже когда они реагируют, существуют отличия в том, к а к им образом они
это делают. Атрибут t уре может быть удобной подсказкой о разновидности данных , кото­
рые ожидаются внутри формы, но чтобы гарантировать предоставление пользователями
пригодных данны х, обязательно дол ж но использоваться средство проверки достовернос­
ти модели, ка к объясняется в главе 27 .

В табл . 24.5 опис ан способ примен ения различных типов св ойств С# для уст ано в­
ки атрибута type элем е нтов input .

Таблица 24.5. Типы свойств С# и атрибуты type эле м ентов input,


которые они генерируют

Тип С# Атрибут type элемента input


byte,sbyte, int,uint,sho r t, numЬer
ushort , long, ulong
floa t , douЬle , decima l text с дополнительными атрибутами для проверки до­
стоверности модели, как будет показано далее в главе

bool checkЬox

str ing text


Da t e Time datet ime
Глава 24 . Использование дескрипторны х вспомогательных классов для форм 751
типы float, douЫe и decirnal выпускают элементы input с атрибутом type ,
установле нны м в text, т.к . н е все браузеры разрешают использов ать полный диапа­
зон символов для выражения допустимых значений указанных типов. Чтобы оказ ать
по м ощь пол ьзов ател ю , дес крипторный вспомогательный класс добавля ет к эл ем енту
input атрибуты , которые прим е няются со средством проверки достоверности м одели
(глава 27).
Стандартные отображе ния из табл. 24.5 можно пер е определ ить, определив атри­
бут type элеме нта
i nput . Дескрипторный вспомогательный класс не будет переопре ­
делять указанно е в type значение, что позволит задействовать разнообразны е до­
ступные типы элементов input , таки е как pass wo r d или h idden , либо новые типы,
появившиеся в HTML5, наподобие numЬer.
Недостаток этого подхода в том, что приходится помнить о н е обходимости уста­
новки атрибута type во всех представлениях , где элем енты input генерируются для
заданного сво й ств а модели . Если нужно переопределить стандартное отображение во
множестве пр едставл ений , тогда можно применить атрибут UI Hin t к свойству в клас­
с е модели С # и уиа зать в иач е ств е его аргумента одно из значений, п е р ечисленных в

табл. 24.6.

Совет. Дескрипторный вспомогательный класс будет устанавливать атрибут type элементов


input в text, когда тип свойства модели не является одним из описанных в табл. 24.5
и не де корирован атрибутом UI Hin t.

Таблица 24.б. Аргументы атрибута UIHint и генерируемые ими значения


атрибута type элементов input

Значение Атрибут type элемента input


Hiddeninput hidden
Password passwo r d
Text tex t
P h one N wnЬer t el
Url url
ErnailAddress ernail
Time time (это значение используется для отображения компонента
времени объекта DateT i me)
Date date (это значение применяется для отображения компонента
даты объекта DateTirne )
DateTime- local datetirne - local (это значение используется для отображения
объекта DateTime без информации о временном поясе)

Форматирование значений данных


Когда метод действия снабжает представление объ ектом модели представления ,
десирипторный в с помогательный ил асс применяет значение свойства , указанное в ат ­
рибуте asp - for , для установки атрибута va l ue элемента i nput . Атрибут asp - format
используется для указания, Iiаким образом форматировать это значение данных.
752 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

Чтобы продемонстрировать сказанное, в контроллер Ноте добавлен новый метод


действия (листинг 24.9), который выбирает из хранилища первый объект Ci ty и при­
меняет его в качестве модели представления для Create. cshtml.

Листинг 24.9. Добавление метода действия в файле HomeController. cs

using Microsoft.AspNetCore . Mvc;


using Cities . Models;
using System.Linq;
namespace Cities . Controllers
puЫic class HomeController : Controller {
private IRepository repository;
puЫic HomeController(IRepository repo) {
repository = repo;

puЬlic ViewResult Index() => View(repository . Cities);


puЫic ViewResu1 t Edi t () => View ( "Crea te" , reposi tory. Ci ties. First () ) ;
puЫic ViewResult Create() => View();
[HttpPost]
[ValidateAn tiForgeryToken]
puЫic IActionResult Create(City city)
repository . AddCity(city);
return RedirectToAction("Index");

Запустив приложение, запросив URL вида /Horne/Edi t и просмотрев НТМL­


разметку, которая была отправлена браузеру, вы заметите, что атрибуты value за­
полнились с использованием объекта модели представления, например:

<input class= " form-control" type="number" id= "P opulation"


name="Population" va1ue="8539000" />
Атрибут asp - format принимает значение, которое будет передано стандартной
системе форматирования строк С# (листинг 24.10).
Листинг 24.1 О. Форматирование значения данных в файле crea te. csh tm1

@model City
@{ Layout = " Layout"; }
<form method="post" asp-controller="Home" asp - action="Create"
asp-antiforgery="true">
<div class="form-g roup">
<label for="Name " >Name : </label>
<input class="form - control " asp-for="Name" />
</div>
<div class="form- group " >
<label for="Country " >Country : </label>
<input class="form - control " asp-for="Country" />
</div>
Глава 24. Использование дескрипторны х вспомогательных классов для форм 753
<div class= " form - group">
<label for="Population " >Population : </label>
<input class="form-control" asp-fo="Population" asp-format="{O:#,###}" />
</div>
<button type= " submit " class= " btn btn - primary " >Add</button>
<а class= " btn btn - primary" href="/Home/Index">Cancel</a>
</form>

Значение атрибута применяется дословно, т.е. вы обязаны включить фигурные


скобки и ссылку О :, а также требуемый формат. В результате запуска приложения и
запроса URL вида /Home/Edit , вы заметите, что значение Population было сфор­
матировано следующим образом:

<input class="form- control" type="number" id= " Population "


name= " Population " value="8,539,000" />
Данное средство должно использоваться с осторожностью, т.к. вам нужно гаран­
тировать. что остальная часть приложения сконфигурирована для поддержки подоб­
ного формата. В рассматриваемом случае возникает проблема из-за форматирования
значения Population. Дескрипторный вспомогательный 1ишсс установил атрибут
type элементаinput для свойства Population в nwnЬer с применением стандарт­
ных отображений, описанных в табл. 24.5, но указанная строка формата сгенериро­
вала атрибут value, который содержит нецифровые символы. В результат е браузеры,
поддерживающие тип элемента nwnЬer (вспомните, что не все браузеры это делают).
могут вообще не отобразить какое-либо значение в элементе.
В приложении необходимо также обеспечить возможность разбора значений в ис­
пользуемом формате. Пример приложения ожидает получать значение Population,
которое может быть разобрано в int, так что значения, содержащие нецифровые
символы, приведут к ошибкам проверки достоверности (глава 27).
Применение форматирования через класс модели
Если вы хотите всегда использовать для свойства модели одно и то же форма­
тирование, тогда можете декорировать класс С# атрибутом DisplayFormat, кото­
рый определен в пространстве имен System . ComponentModel .DataAnnotations.
Для форматирования значения данных атрибут DisplayFormat требует двух ар­
гументов: аргумент DataFormatString указывает строку формата, а аргумент
Appl yForma t InEdi tMode - что форматирование должно применяться при редак­
тировании значений. В листинге 24.11 атрибут Population декорирован атрибутом
DisplayFormat с использованием формата, который может быть обработан прило­
жением и браузером как числовой.

Листинг 24.11. Применение атрибута форматирования к классу модели


в файле Ci ty. cs

using System.Componentмodel.DataAnnotations;
namespace Cities.Models {
puЫic class City {
puЫic string Name { get; set; }
puЫic string Country { get ; set;
[DisplayFormat(DataFormatString = "{0:F2}", ApplyFormatinEditмode = true)]
puЫic int? Population { get; set; }
754 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Соге MVC

Атрибут asp - format имеет преимущество перед атрибуто м Disp l ayForma t, поэ­
тому он удален из пр едставл ения (листинг 24.12).

Листинг 24.12. Удаление атрибута форматирования в файле Crea te. csh trnl
@mod el City
@{ La yout = " Layout "; }
<fo r m method= " post " asp - control l er= " Home " asp - action= " Create "
asp - ant i forger y = " true " >
<di v class= " fo r m- group " >
<l abe l for= " Name " >Name : </label>
<input class= " form - control " asp - for= " Name " />
</ di v >
<div c lass= " form - group " >
<l a bel for= "C ountry " >Country : </ l ab e l>
<i nput clas s = " form - control " asp - for = " Coun try" />
</di v >
<div c lass= " form - group " >
<label for= " Popu l ation " >Popu l ation : </ l abel>
<input class="form-control" asp-for="Population" />
</div>
<butt o n type= " s ubmit " class= " Ьtn Ьtn - primary " >Add < /button>
<а cl a ss = " Ьtn Ьtn - primary " href= " /Home/Index " >Cancel</a>
< /form >

Запустив приложение и запросив URL вида / Home/Edi t , вы обнаружите, что з на­


чение Po pulation было сформатировано с двумя знаками после десяти ч ной точки :

<input class= " form - contro l" type =" number " i d =" Population "
name =" Populatio n" value =" 8539000 . 00 " />

Ра бота с эл ементами laЬel


Элем ент label трансформируется классом Label TagHelper , которы й использу ­
ет класс модели представления, чтобы удостовериться в том, что метки н е содержат
опечаток и являются согл асованными . Существует только один подд е рживаемы й ат­
рибут, который описан в табл. 24. 7.

Таблица 24. 7. Атрибут встроенного дескрипторного вспомогательного класса


для элементов label
Имя Описание

asp- for Этот атрибут применяется для указания свойства модели представления,
которое олицетворяет элемент label

Дескрипторный вспомогат ельный класс будет использовать имя сво йства модели
представления для установки значения атрибута for и содержимого элемента label .
В листинге 24. 13 атрибут asp - for применяется к элементам label формы, которы е
будут трансформированы дескрипторным вспомогательным классом.
Глава 24. Использование дескрипторных вспо мо гательных классов для форм 755
Листинг 24. 13. Использование дескрипторного вспомогательного класса
в файле Create.cshtml

@model City
@{ Layout = " Layout "; }
<form method= " post " asp - controller="Home " asp - action= " Create "
asp - antiforgery= "t rue">
<div class= " form - group">
<laЬel asp-for="Name"></laЬel>
<input class= " form-control " asp-for="Name" />
</div>
<div class = " form-group " >
<laЬel asp-for="Country"></laЬel>
<input class= "f orm- contro l" asp - for= " Country " />
</div>
<div class= " form-group " >
<label asp-for="Population"></laЬel>
<input class= " form - control " asp-for="Population" />
</div>
<button type= " submit " class= " btn btn - p rimary " >Add</button>
<а class= " Ьtn Ьtn - primary " h ref= " /Home/ In dex " >Cancel</a>
</form>

Поскольку элементы label пусты , дескрипторный вспомогательный класс будет


применять им ена свойств модели в качестве содержимого этих элементов и установит
атрибут for, который сообщает браузеру о том, с каким элементом input ассоции­
рован каждый элемент label. Если вы запустите приложение, запросите URL вида
/Ноте/ Crea te или / Home / Edi t и просмотрите НТМL-разметку, отправленную брау­
зеру. то заметите следующие выходные элементы:

<form method= " post " action ="/H ome/ Create" >
<div class= " form-group " >
<label for="Name">Name</laЬel>
<i nput class= "f orm- control " typ e= "text" id="Name"
name= " Name " val u e= "London " />
</div>
<div class= " form - group " >
<laЬel for="Country">Country</laЬel>
<input class= " form - control " type= " text" id= " Country "
name= " Country " va lu e ="U K" />
</div>
<div class= "form - group " >
<laЬel for="Population">Population</laЬel>
<input class= " form - control " type= "number " id="Population"
name= " Population" va l ue= " 8539000 . 00 " />
</div>
<button type= " submit " class= "Ьtn Ьtn - pr im ary " >Add</button>
<а class= " Ьtn Ьtn -pr ima ry" hr ef= " /Home/ Inde x " >Cancel</a>
</form>
Переопределить значени е, используемое как содержимое элемента label, можно

путем применения атрибута Display к свойству класса модели (листинг 24.14) .


756 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC

Листинг 24.14. Изменение описания для свойства модели в файле Ci ty. cs


using System.ComponentModel . DataAnnotations ;
namespace Cit ie s . Models
puЫ i c class Ci ty {
[Display(Name = "City")]
puЫic st r ing Name { get ; set ; }
puЫic string Country { get ; set ;
[ DisplayFormat(DataFo r matString = " {O : F2} ",
App l yFormatinEditMode = t rue)]
p uЬli c int? Population { ge t; se t;

Аргум ент Name указыв ает знач е ние для испол ь з ования вм е сто им е ни сво й с­
тв а . Запустив приложение , запросив URL вида /Home/Create и заглянув в НТМL­
разметку , которая отправлена браузеру , вы обнаружите, что содерж и м ое элемент а
label и з менилось:

<div class= " form - gro up " >


<laЬel for="Narne">City</laЬel>
<input class =" form - contro l" type=" text " i d= "Name "
name= "Name " va lu e =" London " />
</div>
Обратите внимание, что значение атрибута for не изменилось , та к что браузеру
известно об ассоциировании эл е м е нта label со специфическим эл е ментом input, на
который атрибут Display не оказыва ет воздействия .

Совет. Вы можете воспрепятствовать установке дескрипторным вспомогательным классом


содержимого элемента label, определив его самостоятельно . Это полезно, если вы хо­
тите , чтобы элементы labe l содержали нечто большее, чем просто имя свойства, кото­
рое способен обеспечить встроенный дескрипторный вспомогательный класс .

Ра бота с элементами select и option


Элем енты select и option прим еняются для пр едо ставл ен ия поль з о вателю фик­
сированного набора вари антов в м есто открытой з апис и данных, которая во зм о жн а
посредством элемента inpu t . Класс SelectTagHelper отвечает з а трансформирова­
ние эл емент ов select и поддерживает атрибуты , опис ан ные в табл . 24.8.

Таблица 24.8. Атрибуты встроенного дескрипторного вспомогател ь ного класса


для элементов select

Имя Описание

asp- for Этот атрибут используется для указания свойства модели представления,
которое олицетворяет элемент select
a s p - iterns Этот атрибут применяется, чтобы указать источни к значений для элемен­
тов option, содержащихся внутри элемента se l ect
Глава 24. Использование дескрипторных вспомогательных классов для форм 757
Атрибут asp-for устанавливает значения атрибутов for и id, чтобы отразить
свойство модели, которое он получает. В листинге 24.15 элемент inpu t для свойства
Country заменен элементом select , определяющим атрибут asp -fo r .
Листинг 24.15. Использование элемента select в файле Crea te. csh tml
@model City
@{ Layout = "_Layout "; }
<form method= "post " asp-controller= "Home" asp-action="Create"
asp - antiforgery= " true " >
<div class= " form - group " >
<label asp - for= " Name " ></label>
<input class= "form- control " asp -fo r="Name" />
</div>
<div class= " form -g roup ">
<label asp-for="Country"></label>
<select class="form-control" asp-for="Country">
<opti.on di.saЬl.ed sel.ected val. ue=" ">Sel.ect а Coun try</ opti.on>
<option>UK</option>
<opti.on>USA</opti.on>
<option>France</option>
<option>China</option>
</select>
</div>
<div class= " form-group " >
<label asp - for= " Population " ></label>
<input class= " form-control" asp - for =" Population" />
</div>
<button type =" submit " class= "Ьtn Ьtn-primary">Add</button>
<а class= "btn Ьtn - primary " href=" /Home/Index">Cancel</a>
</form>

Здесь элемент select вручную заполняется элементами option , которые пред­


лагают пользователю для выбора диапазон стран. Запустив приложение и запросив
URL вида /Home/Create , вы заметите, что НТМL-размет1щ, отправленная клиенту,
содержит следующий элемент select:
<sel.ect cl.ass="f'orm-control." i.d="Country" name="Country">
<option disaЫed selected value="">Select а Count ry</option>
<option>UK</option>
<option>USA</option>
<option>France</option>
<option>China</option>
</select>
Если вы запросите URL вида /Home/Edit и просмотрите НТМL-разметку, послан­
Country объекта модели
ную браузеру. то обнаружите, что значение свойства было
применено для изменения выбранного элементаoption:
<select class= "form-control " id="Country" name="Country">
<option disaЫed selected value="">Select а Country</option>
<opti.on selected="selected">UК</option>
<option>USA</option>
<option>France</option>
<option>China</option>
</s e lect>
758 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC

Задача выбора элемента option решается Юiассом OptionTagHelper, ноторый


получает инструкции от SelectTagHelper через коллекцию TagHelperContext .
Items . Как объяснялось в главе 23, эта коллекция используется дескрипторными
вспомогательными классами, которым нужно работать вместе. Данные, добавляемые
в коллекцию Items классом SelectTagHelper, будут задействованы в следующем
разделе при создании специального дескрипторного вспомогательного класса, пред­

назначенного для обхода ограничения встроенного класса.

Использование источника данных для заполнения элемента select


Явное определение элементов option для элемента select - удобный подход для
вариантов выбора, которые всегда имеют одни и те же значения. Однако он ничем не
поможет в ситуации, когда необходимо предоставить варианты, берущиеся из данных
модели, или когда нужен одинаковый набор вариантов в нескольких представлениях,
но нежелательно вручную поддерживать дублированное содержимое.

Генерирование элементов option из перечисления


Если есть фиксированный набор вариантов для предоставления пользователю,
но вы не хотите дублировать его в представлениях повсюду в приложении, тогда
можете применить перечисление. Добавьте в папку Mode l s файл н:ласса по имени
CountryNames. cs и определите в нем перечисление, как показано в листинге 24.16.
Листинг 24.16. Содержимое файла CountryNames. cs из папки Models
narnespace Cities.Models {
puЬlic enurn CountryNarnes
UK,
USA ,
France ,
China

Использовать перечисление в атрибуте asp - i tems напрямую нельзя, т.н. дескрип ­


торный вспомогательный класс ожидает получить последовательность объектов
SelectListltem. Тем не менее, доступен удобный вспомогательный метод, который
выполняет требуемое преобразование (листинг 24. 17).
Листинг 24.17. Применение перечисления для генерации элементов option
в файле Crea te . csh tml
@model City
@{ Layout = 11
Layout 11 ; }

<form method= post asp - controller= 11 Home 11 asp - action= 11 Create 11


11 11

asp-antiforgery= 11 true 11 >


<div class= " form-group 11 >
<label asp-for="Name 11 ></label>
<input class= 11 forrn - control " asp - for= 11 Name" />
</div>
<div class= " form-group " >
<label asp-for= 11 Country 11 ></label>
Глава 24. Использование дескрипторных вспомогательных классов для форм 759
<select class="form-control" asp-for="Country"
asp-items="@new SelectList(Enum.GetNames(typeof(CountryNames)))">
<option disaЫed selected value=" ">Select а Country</option>
</select>
</div>
<div class="form-group">
<label asp-for= " Population"></label>
<input class= " form - control" asp - for="Population" />
</div>
<button type="submit" class="Ьtn Ьtn-primary">Add</button>
<а class="Ьtn Ьtn-primary " href= " /Home/Index">Cancel</a>
</form>

Когда используется перечисление, лучший способ генерации элементов option


предусматривает снабжение атрибута asp-items объектом SelectList, который за­
полнен именами значений перечисления. "За кулисами" класс SelectTagHelper ге­
нерирует элементы option из последовательности IEnumeraЫe<SelectListitem>,
а ЮJасс SelectList реаЛизует этот интерфейс.
Запустив приложение и запросив URL вида /Home/Create или /Home/Edit, вы
заметите, что НТМL-разметка, отправленная браузеру, содержит набор элементов
option, которые соответствуют значениям в перечислении:

<select class="form-control " id="Country" name= " Country">


<option disaЫed selected value= "" >Select а Country</option>
<option>UK</option>
<option>USA</option>
<option>France</option>
<option>China</option>
</select>
Обратите внимание, что дескрипторный вспомогательный класс оставил один эле­
мент-заполнитель option. Любые элементы option, которые определяются явным
образом, остаются на месте, т.е. вам не придется смешивать заполнители и значения
данных.

Генерирование элементов option из модели


Если необходимо генерировать элементы option для отражения данных в модели,
то самый простой подход заключается в предоставлении данных, требуемых при ге­
нерации элементов, посредством объекта ViewBag (листинг 24.18).

Листинг 24.18. Предоставление данных для выбора через объект ViewBag


в файле HomeController. cs

using Microsoft.AspNetCore.Mvc ;
using Cities . Models;
using System . Linq ;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace Cities.Controllers (
puЬlic class HorneController : Controller
private IRepository repository;
puЬlic HomeController ( IReposi tory repo) {
repository = repo;
760 Часть 11. Подробные сведения об и нфраструктуре ASP.NET Core MVC

puЫic ViewResu l t Index() => View(repository . Ci t i e s) ;


puЫ i c ViewResu l t Edit() (
ViewBag.Countries = new SelectList(repository.Cities
.Select(c => c.Country) .Distinct());
retu r n View( " Crea t e ", repository . Cities . First( ) ) ;

puЬlic ViewResu l t Create() {


ViewBag.Countries = new SelectList(repository.Cities
. Select(c => c . Country) .Distinct());
retu r n View () ;

[HttpPost ]
[ValidateAntiForgeryToken]
puЫic IActionResult Create(City city)
reposi t ory . AddCity(city) ;
return RedirectToAction( "Index " ) ;

М е тоды д е йствий Edi t () и Crea te () устанавливают с войство Vi ewBag .


Countries в объект SelectList, который з аполня ется уникал ьными знач е ниями
для сво й ства Ci ty . Country в хранилище . В листинге 24 . 19 посредство м атри бута
a s p - items деск рипторному в с помогательному классу сообщается о необходимости
получ е ния данных дл я элементов option из свойства View Bag . Cou ntries .

Листинг 24. 19. Применение объекта ViewBag для заполнения элементов option
в файле Crea te . csh tml

@model Ci ty
@{ Layou t = " Layout "; }
< f orm method= "post " asp - control l er= " Home " a s p - action= " Create "
asp - antiforgery= " true " >
<div class= " form-gro u p " >
<labe l asp - for= " Name " ></ l abel>
<i nput c l ass= " form - control " a s p - for= " Name " />
</div>
<div class= " form - group" >
<labe l asp - for= " Country " ></ l abel>
<select class="form-control" asp-for="Country"
asp-items="ViewBag.Countries">
<option disaЬled selected value= "" >Se l ect а Co un try</opt i on>
</se l ect>
</div>
<div class= " form - group " >
<label asp - for= " Population " ></ l abe l >
<input class= " form - contro l" asp -for= " Popu l ation " />
</div>
<button type= " submit " class= " Ьtn Ьtn - pr im ary " >Add</button>
<а c l ass= " Ьtn Ьtn - pri mary " href= " /Home/ Index " >Ca n cel</a>
</form>
Глава 24. Исnользование дескриnторных всnомогательных классов для форм 761
Запустив приложение и запросив URL вида/Home/Createor /Home/Edi t, вы об­
наружите, что были созданы такие элементы option:
<select class="form- control" id="Country " name="Country">
<op t ion disaЫed selected value="">Select а Country</option>
<option selected>UK</option>
<option>USA</option>
<option>France</option>
</s e l e c t >

Использование специального дескрипторного вспомогательного


класса для генерации элементов option из модели
Проблема с передачей данных для элементов option через объект ViewBag связа­
на с тем, что нужно не забыть генерировать данные в каждом методе действия, кото­
рый визуализирует представление, где применяется дескрипторный вспомогательный
класс. Это приводит к дублированию кода, что видно в листинге 24.18, и затрудняет
тестирование и сопровождение контроллера.

Более эффективный подход заключается в создании специального де­


скрипторного вспомогательного класса, который дополняет встроенный класс
SelectTagHelper . Добавьте в папку Infrastructure/TagHelper файл класса по
имени SelectOptionTagHelper. cs с определением из листинга 24.20.

Листинг 24.20. Содержимое файла SelectOptionTagHelper. cs


из папки Infrastructure/TagHelper

using System;
using System . Collections.Generic;
using System .L inq;
u sing System.Reflection ;
using System.Threading . Tasks;
using Cities . Models;
u sing Micro s oft . AspNetCore.Mvc . Rendering;
u sing Microsoft.AspNetCore.Mvc.TagHelpers;
u sing Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore . Razor.TagHelpers;
namespace Cities . Infrastructure.TagHelpers {
(HtmlTargetElement("select", Attributes = " model -for")]
puЫic class SelectOptionTagHelper : TagHelper {
private I Repository repository;
puЬlic SelectOptionTagHelper(IRepository repo)
repository = repo;

puЬlic ModelExpression ModelFor { get; set; }


puЫic override async Task ProcessAsync(TagHelperContext context ,
TagHelperOutput output)
output.Content.Append(
(await output.GetChildContentAsync(false)) .GetContent() );
object selected;
context.Items.TryGetValue(typeof(SelectTagHelper), out selected) ;
IEnumeraЫe<string> selectedValues = (selected as IEnumeraЫe<string>)
?? EnumeraЫe . Empty<string>();
762 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Соге MVC

Prop er t yinfo p r ope r ty = typeo f (C i ty)


. GetTypeinfo() . GetDeclaredProp erty(ModelFor . Name) ;
foreach (str i ng country i n r ep os i tory . Ci ties
. Select(c => prope r ty . Ge t Va l ue(c)) . Di stinct())
if (selectedValues . Any(s = > s . Equals(country ,
St ringComparison . Ordina l ignoreCase))) {
outpu t. Content
. AppendHtml($ " <option s elected>{country}</option> " ) ;
else {
output . Content . AppendHtm l ($ " <option>{country}</option> " ) ;

Специальный д е скрипторный вспомогательный класс оп е риру ет на элеме нтах


select с атрибутом model - for и использует внедрени е зависимост е й , чтобы полу­
чить объект хранилища, который можно применять для доступа к данным модели
независимо от контролл ера , визуализирующего представ ле ние. В дескрипторно м
в спом огательном классе определен асинхронный метод ProcessAsync () , т.к. он уп­
роща ет проце сс получ е ния и сохранения любого существующего соде ржимого эле ­
мента select , что делается посредством метода GetChildContentAsync () .
Кл асс
SelectTagHe l per указывает имена элементов option , которые должны
быть выбраны, через запись в коллекции Items, используя собст в е нный тип в ка­
честв е нл юч а. Де скрипторный вспомогательный клас с получает с писок выбр анных
элем ентов и применяет его в сочетании с результатами з апрос а LINQ при ге не рации
эле м ентов option для каждого уникального значения в хранилище .
В листи н ге 24.21 элемент sele c t был обновл е н с ц е лью зам е ны атрибута
asp -i tems ат рибутом model - for , а также добавл е но выр аж ение @addTagHelper,
которо е внлюча ет специальный дескрипторный вспомогательный класс только для
этого пр ед ставл е ния.

Листинг 24.21. Включение специального дескрипторного вспомогательного класса


в файле Create. cshtml

@model City
@addTagHelper Cities.Infrastructure.TagHelpers.SelectOptionTagHelper, Cities
@{ Layou t = " Layout "; }
<form method= " post " asp - contro l ler=" Home " asp - action= " Create "
asp - antiforgery= " true " >
<div class= " form - group " >
<label asp - for =" Name " ></ l abel>
<input class =" form - control " asp - for =" Name " />
</div>
<div class= " form - group " >
<label asp - for= " Country " ></label>
<select class="form-control" asp-for="Country"
asp-items="ViewBag.Countries">
<option disaЫed selected valu e = "" >Select а Country</option>
</select>
</div>
Глава 24. Использование дескрипторных вспомогательных классов для форм 763
<div class= " form - group " >
<label asp-for="Population"></label>
<input class= " form-cont rol " asp-for="Population" />
</div>
<button type= " submit " c l ass= " Ьtn Ьtn -p rimary " >Add</button>
<а class= "btn btn-primary " href= " /Home/Index ">Cancel</a>
</form>

Новый дескрипторный вспомогательный класс генерирует тот же самый вывод.


но делает это без использования данных ViewBag, которые требуются встроенному
вспомогательному классу. Такой подход предпочтителен тем, что он сохраняет мето­
ды действий сфокусированными на свои специфические задачи и сберегает общую
форму приложения.

Работа с элементами textarea


Элемент textarea применяется для запроса у пользователя крупного объема текс­
та и обычно используется для неструктурированных данных, таких как комментарии

или замечания . Класс TextAreaTagHelper отвечает за трансформирование элемен­


тов textarea и поддерживает единственный атрибут, описанный в табл. 24.9.

Таблица 24.9. Атрибут встроенного дескрипторного вспомогательного класса


для элементов textarea
Имя Описание

asp- fo r Этот атрибут применяется для указания свойства модели представления ,


которое олицетворяет элемент textarea

КлассTextAreaTagHelper относительно прост; значение, предоставляемое в ат­


рибуте asp-for, используется для установки атрибутов id и name элемента textarea.
Чтобы взглянуть на работу этого дескрипторного вспомогательного класса, добавьте в
класс модели Ci ty новое свойство (листинг 24.22).

Листинг 24.22. Добавление свойства в файле Ci ty. cs

using System . ComponentModel. DataAnno t a ti ons;


namespace Cities . Models
puЬlic c l ass City {
[Display(Name = "City " ) ]
puЫic string Name { ge t ; set; )
puЬlic string Count ry { get ; set ;
[DisplayFormat(Data FormatString = " {O:F2)", ApplyFormatinEditMode = true)]
puЫic int? Popu lation { get ; set; }
puЫic string Notes { get; set; }

Добавьте элемент textarea к представлению Create . cs html с применением ат­


рибута asp - for для ассоциирования элемента со свойством Notes класса Ci ty (лис ­
тинг 24.23).
764 Часть 11. Подробные сведения об инфраструктуре ASP.NET Соге MVC

Листинг 24.23. Добавление элемента textarea в файле Create. cshtrnl


@model City
@addTagHelper Cities . Infrastructure . TagHelpers .Sel ectOptionTagHelper ,
Ci ties
11
@{ Layout = Layout 11 ; }

<fo rm method= 11 post 11 asp -c ontroller= 11 Home 11 asp-action= 11 Create 11


asp-antiforgery="true 11 >
<div class= form-g roup 11 >
11

<label asp-for= " Name " ></label>


<i np ut class= 11 form - control " asp - for= 11 Name " />
</div>
<div class= 11 form - group " >
<label asp - for= "C ountry 11 ></ l abe l>
<sel ect class= 11 f orm-control 11 asp - for= " Country "
asp -items="V iewBag .C ountries " >
<o ptio n disaЬled selected value='"'>Select а Countr y</option>
</select>
</div>
<div class= 11 form-group 11 >
<label asp -f or= " Population " ></lab el >
<input class = 11 form- control 11 asp - for= "Pop ulation 11 />
</div>
<div class= 11 forrn-group 11 >
<laЬel asp-for= 11 Notes"></laЬel>
<textarea class= 11 forrn-control 11 asp-for="Notes 11 ></textarea>
</div>
<button type= 11 submit " class ="Ьt n Ьtn-primary >Add</button>
11

<а class= Ьtn Ьtn-primary


11 11
href= 11 /Home/Index 11 >Cancel</a>
</form>

Запустив приложение и запросив URL вида /Ноте/ Crea te или / Н оте/ Edi t . вы
заметите, что НТМL-разметка, отправленная браузеру, включает следующий элемент
textarea:

<di v cl ass= 11 form - group " >


<label for= " Note s">Not es</labe l>
<textarea id= 11 Notes 11 narne= 11 Notes 11 ></textarea>
</div>

Несмотря на простоту, IOiacc TextAreaTagHelper обеспечивает согласованность


с остальными дескрипторными вспомогательными классами для форм , которые рас­

сматривались в настоящей главе.

Дескрипторные вспомогательные классы


для проверки достоверности форм
Есть еще два дескрипторных вспомогательных класса, которые имеют отношение
к НТМL-формам; ради полноты они описаны в табл. 24.10, но объясняются в главе 27.
Эти вспомогательные классы используются для предоставления пользователю откли ­
ка. когда введенные им данные не удовлетворяют ожиданиям приложения.
Глава 24 . Использование дескрипторных вспомогательных классов для форм 765
Таблица 24.1 О. Дескрипторные вспомогательные классы для проверки достоверности

Имя Описание

Valida t ionMessage Этот дескрипторный вспомогательный класс применяется для


предоставления пользователю отклика проверки достоверности,
касающегося одиночного элемента формы

ValidationSшrnna ry Этот дескрипторный вспомогательный класс используется для


предоставления пользователю отклика проверки достоверности ,
касающегося всех элементов в форме

Рез ю м е
В данной главе были описаны встро енные дескрипторные вспомогательные клас­
сы, которые применяются для трансформации элементов НТМL-форм . Эти дескрип­
торные вспомогательные классы гарантируют, что формы генерируются напрямую из
класса модели, сокращая возможности возникновения ошибок и предлагая согласо­
ванный подход к написанию представлений Razor. В следующей главе рассматрива­
ются оставшиеся встроенные дескрипторные вспомогательные классы , которы е име­

ют дело с набором разных НТМL-эл ементов.


ГЛАВА 25
Использование
других встроенных

десв:рипторных

вспомогательных Rлассов

д ескрипторные вспомогательные массы, которые были описаны в главе


средоточены на выпуск НТМL-форм, но они не являются единственными встро­
24, со­
енными дескрипторными вспомогательными массами, пр едлагаемыми инфраструкту­
рой ASP.NEТ Core MVC. В этой главе рассматриваются дескрипторные вспомогательные
массы, которые управляют файлами JavaScript и таблицами стилей CSS. создают URL
для якорных элементов, пр едоставляют возможность аннулирования кеша для элемен ­

тов изображений и подде рживают ке ширование данных . Вдобавок будет описан де­
скрипторный вспомогательный класс, обеспечивающий доступ для URL, относитель­
ных к приложению, которые помогают гарантировать, что браузеры могут обращаться
к статическому соде ржимо му, когда приложение развернуто в среде, разделяемой с
другим и приложениями. В табл. 25.1 приведена сводка для настоящей главы.

Таблица 25.1. Сводка по главе

Задача Решение Листинг

Включение содер ж имого на Используйте элемент environment 25.1, 25.2,


основе среды размещения 25.6
Выбор файлов JavaScript Примените атрибуты asp - src- include и 25.3-25.5
asp-src- exclude к элементу script
Использование сети достав­ Примените атрибуты asp - fallback к 25.7, 25.8
ки содержимого для файлов элементу script
JavaScript
Выбор файлов CSS Примените атрибуты asp- href - include 25.9
и asp- href- exc l ude к элементу link
Использование сети доставки Примените атрибуты asp-fallback к 25.10
содержимого для файлов CSS элементу link
Глава 25. Использование други х встроенных дескрипторных вспомогательных классов 767
Окончание табл . 25. 1

Задача Решение Листинг

Генерация URL для якорного Используйте вспомогательный класс 25. 11


элемента AnchorTagHelper
Обеспечение обнаружения Примените атрибут asp- append- version 25.12
изменений в изображениях к элементу img
Кеширование данных Используйте элемент cache 25. 13-25.21
Соэдание URL, относительных Снабдите URL префиксом - 25.22-25 .24
к приложению

Подготовка проекта для примера


Мы продолжим работать с проектом Ci ties, созданным в главе 24 . Подготовка
для настоящей главы предусматривает создание пап ки wwwroot/ images и добавле­
ние в нее файла изображения по имени ci ty . png. В нем находится общедоступная
фотография панорамы Нью-Йорка (рис . 25.1).

Рис. 25. 1. Добавление изображения в проект

Файл изображения ci ty . png входит в состав кода примеров для книги, доступ­
ного для загрузки на веб-сайте издательства. При желании м ож ете воспользоваться
собственным изображением .
Еще одно изменение, которое потребуется сделать, связано с добавлением в проект
пакетаjQuегу (листинг 25.1). Щелкните на кнопке Show All Files (Показать все файлы)
в верхней части окна Solution Explorer, чтобы отобразился файл bower . j son.

Листинг 25.1. Добавление пакета jQuery в файле bower. j son

"name ": "asp . net ",


"private ": true ,
" dependencies ": {
"bootstrap ": " 3 . 3 . 6",
"jquery": "2. 2. 4"

Запустив приложение, вы увидите список объектов в хранилище и сможете созда­

вать новы е объекты (рис. 25.2) .


768 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC

~ J:!
· С LФ -i~~-;;~~7711н;;~;~~~.-------·--Q
·- - -- ·- -·--"'==·'"='·"'==--::-:===-с=~"==-·-----·-·11,
· POpulalion City
:1
l ondon uк 8 ,539.000 11

№1vYork USA
8,406.ООО :.·.•!

San Jose USA 998,537 Select а Cou11try • i


i i

-
Paris France 2 ,244,000
Population

!\ Notes

1 ~ А

L-~-------- ;1 111•
"____,L_______________________J!
Рис. 25.2. Выполнение примера приложения

Использование вспомогательного
класса для среды размещения
Класс EnvironmentTagHelper применяется к специальному элементу environment
и определяет, включается ли область содержимого в НТМL-разметку, отправляемую
браузеру, на основе среды размещения, как было показано в главе 14. Это может не
казаться самым захватывающим местом, чтобы начинать с него, но данный вспомога­
тельный класс необходим для эффективного использования ряда связанных средств,
которые будут описаны позже. Элемент environment полагается на атрибут names,
который описан в табл. 25.2 для ссьшки в будущем.

Таблица 25.2. Атрибут встроенного дескрипторного вспомогательного класса


для элементов environment

Имя Описание

names Этот атрибут применяется для указания списка разделенных запятыми имен
сред размещения, для которых содержимое, находящееся внутри элемента

environment, будет включаться в НТМL-разметку, отправляемую клиенту

Добавьте в разделяемую компоновку элементы environment, включающие разное


содержимое в представление для среды разработки и производственной среды (лис­
тинг 25.2).

Листинг 25. 2. Использование элементов environmen t в файле _ Layou t. csh tml


< !DOCTY PE html>
<html >
<head>
<meta name=" viewpo r t " content=" width=device - width " />
Глава 25. Использование других встроенных дескрипторных вспомогательных классов 769
<title>Cities</tit le >
<link href="/liЬ/bootstrap/dist/css/bootstrap.css" rel= " stylesheet" />
</head>
<body class= "pane l- body " >
<environment names="development">
<div class="panel-body bg-info"><h2>This is Development</h2></div>
</environment>
<environment names="production">
<div class="panel-body bg-danger"><h2>This is Production</h2></div>
</environment>
<d i v>@Re n derBody()</div>
</body>
</html>

На рис. 25.3 показаны результаты запуска приложения в среде разработки и про­


изводственной среде. Элемент environment проверяет имя теriущей среды размеще­
ния и либо включает имеющееся в нем содержимое, либо пропускает его (сам элемент
environment всегда опускается в НТМL-разметке, отправляемой клиенту).

1
! +- ·)> С . [J locallюs t:62495/Home/
,---··------·--------

!
1 Loodoo UK Loodon UK
New ,УШ/(
1
~ --·r
New York
"~
USA USA
r-#,__. ~·- -.#r...,.....,.~ .~

Рис. 25.3. Управление содержимым с применением среды размещения

Использование вспомогательных
классов для JavaScript и CSS
Следующая категория встроенных вспомогательных классов применяется для уп­
равления файлами JavaScrlpt и таблицами стилей CSS посредством элементов script
и link, которые обычно вюrючаются в разделяемую компоновку. Как будет показано
в последующих разделах, эти дескрипторные вспомогательные классы являются мощ­

ными и гибкими , но требуют пристального внимания во время использования, чтобы


избежать создания непредсказуемых результатов.

Управление файлами JavaScript


Класс ScriptTagHelper - это встроенный дескрипторный вспомогательный
класс для элементов script . Он предназначен для управления включением файлов
JavaScrlpt в представления с применением атрибутов, которые кратко описаны в
табл. 25.3 и подробно рассматриваются далее в главе.
770 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

Таблица 25.З. Атрибуты встроенного дескрипторного вспомогательного класса


для элементов script

Имя Описание

asp-s rc -include Этот атрибут используется для указания файлов JavaScript,


которые будут включены в представление

asp- src- exclude Этот атрибут применяется для указания файлов JavaScript,
которые будут исключены из представления

asp- append- version Этот атрибут используется для аннулирования кеша, как
описано во врезке " Понятие аннулирования кеша" далее
в главе

asp-fallback- src Этот атрибут применяется для указания запасного файла


JavaScript, подлежащего использованию в случае возникно­
вения проблемы с сетью доставки содержимого
asp- fallback- src- include Этот атрибут применяется для выбора файлов JavaScript,
которые будут использоваться при наличии проблемы
с сетью доставки содержимого

asp- fallback-s rc- exclude Этот атрибут применяется для исключения файлов
JavaScript, чтобы предотвратить их использование в случае
возникновения проблемы с сетью доставки содержимого

asp-fallback-test Этот атрибут применяется для указания фрагмента


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

Выбор файлов JavaScript


Атрибут asp-src - include применяется для включения файлов JavaScript в пред­
ставление с использованием шаблонов универсализации имен, которые поддержива­
ют набор групповых символов, применяемых для сопоставления с именами файлов.
В табл. 25.4 о пи саны наиболее распростран енные шаблоны универсализации имен.

Таблица 25.4. Распространенные шаблоны универсализации имен

Шаблон Пример Описание

? js/src? . js Этот шаблон соответствует любому одиночному символу кроме /.


Пример шаблона соответствует любому файлу из каталога j sс име­
нем, состоящим из src, за которым следует любой символ, а после
него расширение . j s, j s / srcl . j s
такому как и j s/ srcX . j s, но
не j s/ src123. j s или j s/mydir / srcl . j s
* js/* . js Этот шаблон соответствует любому количеству символов кроме /.
Пример шаблона соответствует любому файлу из каталога j s с
именем , которое заканчивается расширением . j s, такому как
js/srcl . js и js/srcl23 . js, но не js/mydir/srcl. js
** j s1**1 * . j s Этот шаблон соответствует любому количеству символов, включая /.
Пример шаблона соответствует любому файлу с расширением
. j s, который содержится внутри каталога j s или любого его под­
каталога, та кому как/j s/ srcl . j s и /j s/mydir / srcl . j s
Глава 25. Исnользование других встроенных дескриnторных всnомогательных классов 771
Использование шаблона универсализации имен в атрибуте asp-src-include оз­
начает, что представление будет всегда вЮiючать файлы JavaScript приложения, даже
если имя или путь к файлам изменяются либо файлы добавляются или удаляются.
В листинге 25.3 демонстрируется выбор файлов JavaScript для пакета jQuery, кото­
рый Воwеrустанавливает в папку wwwroot/liЬ/jquery/dist.

Листинг 25.З. Выбор файлов JavaScript в файле_Layout. cshtml

<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width " />
<title>Cities</title>
<script asp-src-include="/lib/jquery/dist/**/*.js"></script>
<link href = " /liЬ/bootstrap/dist/css/bootstrap.css" rel="stylesheet" />
</head>
<body clas s ="panel-body">
<div>@RenderBody()</div>
</body>
</html>

В примере применяется распространенный шаблон. Шаблоны оцениваются внут­


ри папки wwwroot, а библиотекаjQuеrу доставляется как одиночный файл JavaScript
по имени j query . j s.
Этот шаблон универсализации имен пытается выбрать файл jQuery, одновре­
менно приспосабливаясь к любым будущим изменениям в способе распространения
jQuery, таким как изменение имени файла JavaScript. Запустив приложение и про­
смотрев НТМL-разметку, отправленную Юiиенту, вы увидите, что в ней присутствует
проблема:

<h ead>
<meta name="viewport" content= "width=device - width" />
<title>Cities</title>
<script src="/lib/jquery/dist/jquery.js"></script>
<script src="/lib/jquery/dist/jquery.min.js"></script>
<link href="/liЬ/bootstrap/dist/css/bootstrap.css" rel="stylesheet" />
</head>
Класс ScriptTagHelper генерирует элемент
script для каждого файла, имя кото­
рого соответствует шаблону, указанному в атрибуте
asp - src-include. Вместо выбора
только файла jquery . js имеется также элемент script для файла jquery.min . j s.
который является минифицированной версией файла j query . j s и будет использо­
ваться большинством приложений, поскольку он содержит тот же самый код, выра­
женный в более компактной, но менее читабельной манере. Вы можете даже не подоз­
ревать, что дистрибутив jQuery вЮiючает минифицированный файл, т.к. Visual Studio
его по умолчанию скрывает. Чтобы отобразить полное содержимое папки wwwroot/
liЬ/jquery/dist, понадобится раскрыть элемент jquery. j s в окне Solution Explorer
и затем сделать то же самое с элементом j query. min. j s (рис. 25.4).
772 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Саге MVC

Solution Explorer " ,..:~


=-.;.Х.;_,__ _ _ _ _ __ _ _--1 Solution Explorer " ~ Х

(1) 1 •0 • ~ @ !iiJ) 1~ Solution Explorer ~ . ;:,} _GJ 1 •0 " ~- ~ ~@ _J_~ ~-1


-
Search Solulion Explor" (Ctrl+ ;)
.......... . - ~ -~ 1 ·~~ ~=~"'~ 1® I ~ : P-
$ wwwroot Search Solution!_><P!_orer (Ctrl+;)
Qll ' •-юw root
1> in1ages &· ww.чroot 1> til imag es
,,, ~ lib 1> images " . lib
1> llili bootstrap ... ;,;.· lib 1> • bootst.rap
" ! jquery 1> ~ boot$trap
" '°"' jquery
"
.
'*1 dist
[]
" ""'' j query
... fio,.1 dist
" ". dist
" !J jquery.js
1> U extern al " .!J jquery.js " tJ jquery.niin.js
1> src . ti . ! ,,-,. tJ •
lJ .bower.json 1> ~ external 1> ~ external
~ AUTHORS.txt 1> DJi src 1> src
!J bower.json !J .bower.json !J .bower.json
[ii'! LICENSE.txt li!J AUTHORS.txt l!i') AUT H ORS.Ь<t
~ README.md !J bower.json 6J' bower.json
[Н1 LICENSE.txt (йЬ LICENSE.txt
11iJ README.md 1EJ README.md

Рис. 25.4. Отображение полного содержимого папки в окне Solutioп Explorer

Шаблон , примененный в листинге 25.3, вызвал двукратную отправку браузеру


кода JQuery, как в исходной, так и в минифицированно й форме, ч т о приводит к из­
лишней трате полосы пропускания и замедлению работы приложения. В случае не­
которых библиотек результатом могут стать ошибки или непредсказуемое поведение.
Решить проблему можно тремя способами, которые рассматриваются в последующих
разделах.

Использование отображений на исходный код

Файлы JavaScript минифицируются с целью уменьшения размеров, что означает возмож­


ность их доставки клиенту быстрее и с меньшим расходом полосы пропускания . Процесс
минификации удаляет из файла все пробельные символы, а также переименовывает фун­
кции и переменные так, что значащие имена вроде myHe l pfullyNamedFun ction будут
представлены меньшим количеством символов, скажем, xl . В случае применения отлад­
чика JavaScript браузера для поиска источника проблем в минифицированном коде имена ,
подобные xl , сделают почти невозможным прохождение по коду.

Файл j q u ery . min . map является отображением на исходный код , которое рядом браузе­
ров используется для того, чтобы помочь в отладке минифицированного кода , предоставляя
соответствие между минифицированным кодом и читабельным полным исходным кодом.

На момент написания книги отображения на исходный код не были универсально поддержи­


ваемым средством, но их можно было применять в большинстве последних версий брау­
зеров Chrome и Edge. Например, браузер Chrome будет автоматически запрашивать файл
отображений на исходный код, если открыто окно инструментов разработчика, а это значит,
что браузеру мож но отправлять минифицированные версии файлов JavaScript и по-прежне­
му иметь возможность их легкой отладки.
Глава 25. Использование других встроенных дес кри пторны х вспомогательных классов 773
Сужение шаблона универсализации имен
Многие пакеты предоставляют обычные и минифицированные версии своих фай­
лов JavaScript, и если вы собираетесь использовать только минифицированные вер­
сии, тогда можете ограничить набор файлов, которые буlJУт соответствовать шаблону
универсализации имен (листинг 25.4). Это хороший подход, когда вы не планируете
заниматься отладкой библиотеки jQuery, которая реализована эффективно и не вы­
зывает особых проблем, или же знаете, что целевые браузеры поддерживают отобра­
жения на исходный код.

Листинг 25.4. Выбор только минифицированных файлов JavaScript


в файле _Layout. cshtml
<! DOCTYPE html>
<html>
<head>
<meta name= "viewport" content= "width=device-width" />
<title>Cities</title>
<script asp-src-include="/lib/jquery/dist/**/* . min.js"></script>
<link href= " /liЬ/bootstrap/dist/css/bootstrap. css " rel= " stylesheet " />
</head>
<body class= "panel - body " >
<div>@RenderBody()</div>
</body>
</html>

Запустив приложение и просмотрев НТМL-размет~tу, отправленную браузеру, вы


заметите, что был включен только минифицированный файл библиотеки jQuery:
<head>
<meta name ="viewport " content= "width=device-width " />
<t itle>Cities </title >
<script src= "/lib/jquery/dist/jquery.min.js"></script>
<link href= " /liЬ/bootstrap/dist/css/bootstrap . css" rel="stylesheet" />
</head>
Исключение файлов
Применять обычные версии файлов JavaScript без их минифицированных версий
труднее , потому что шаблоны универсализации имен усложняют исключение файлов.
К счастью, с помощью атрибута asp - src - exc lude можно удалять файлы из списка,
построенного атрибутом asp - src-include, как показано в листинге 25.5.

Листинг 25.5. Исключение файлов JavaScript в файле_Layout. cshtml


< ! DOCTYPE html>
<html>
<head>
<meta name= "viewport " content= "width=device-width" />
<title>Cities</tit l e>
<script asp-src-include="/lib/jquery/dist/**/*.js"
asp-src-exclude="**.min.js">
</script>
<link href= " /liЬ/bootstrap/dist/ css/bootstrap . css" rel= " stylesheet" />
</head>
774 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Со ге MVC

<body class =" panel - body " >


<d i v>@Re nderBody()</div>
</body>
</h tml >

Если вы запустите приложение и заглянете в НТМL-разметку. посл анную браузеру,


то увидите, что была включена только обычная версия файла JavaScript:
<head>
<meta name= "viewport " content= "width=device-width /> 11

<title >Cities</ title>


<script src="/lib/jquery/dist/jquery.js"></script>
<link href= /liЬ/bootstrap/dist/css/boo t strap . css rel="s tyl esheet
11 11 11
/>
</ head >

Использование среды размещения для выбора файлов

Распространенный подход предусматривает применение обычных файлов


JavaScript на этапе разработки, что облегчает отладку, и использование минифициро­
в анных файлов в производственной среде для сокращения расхода полосы пропуска­
ния . В таком случае можно задействовать элемент environment, чтобы избирательно
включать элементы script , основываясь на среде размеще ния (листинг 25.6).

Листинг 25.6. Применение среды размещения для выбора файлов


в файле _ Layou t. csh tml
< ! DOC TYPE html >
<html>
<head>
<meta name= "viewport " content= "width=devi ce - width " />
<titl e>Cit ies </title>
<environment names="development">
<script asp-src-include="/lib/jquery/dist/**/ *. js"
asp-src-exclude="**.min.js">
</script>
</environment>
<environment names="staging, production">
<script asp-src-include="/lib/jquery/dist/**/*.min . js"></script>
</environment>
<link href= " /liЬ/bootstrap/dist/ css /bootstrap . css " rel= " stylesheet 11
/>
</head>
<body class= panel -body " >
11

<div>@RenderBody()</div>
</body>
</html >

Пр еимуществ о такого подхода в том, что он адаптирует приложени е к среде р аз­


мещения. но при этом придется писать и сопровождать многочисленные наборы эле­
ментов script.
Глава 25. Использование других встроенных дескрипторных вспо м огательных классов 775

Понятие аннулирования кеша

Статическое содержимое, такое как изображения, таблицы стилей CSS и файлы JavaScript,
часто кешируется, чтобы предотвратить попадание на серверы приложений запросов к
редко изменяющемуся содержимому. Кеширование делается разными способами: браузер
может сообщить серверу о необходимости кеширования содержимого, приложение может
использовать серверы кешей как дополнение к серверам приложений или содержимое мо­
жет распространяться с использованием сети доставки содержимого. Не все кеширование
будет находиться под вашим контролем. Например, в крупных корпорациях кеши часто ус­
танавливаются для снижения требований к полосе пропускания, поскольку значительный
процент запросов направляется к одним и тем же сайтам или приложениям.

С кешированием связана одна проблема: клиенты не получают новые версии статических


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

жимое , но остается промежуток, когда динамическое содержимое, генерируемое контрол­

лерами приложения, не согласовано со статическим содержимым, доставляемым к ешами.

В зависимости от содержимого, которое было обновлено, могут возникнуть проблемы с


компоновкой или непредсказуемое поведение приложения.

Упомянутая проблема решается с помощью аннулирования кеша. Идея состоит в том, чтобы
позволить кешам обрабатывать статическое содержимое, но незамедлительно отражать лю­
бые изменения, которые произошли на сервере. Дескрипторные вспомогательные классы
поддерживают аннулирование кеша за счет добавления к URL для статического содержимо­
го строки запроса , которая включает контрольную сумму, действующую в качестве номера
версии. Скажем , для файлов JavaScript класс ScriptTagHelper поддерживает аннулиро­
вание кеша через атрибут asp - append- version :

<script asp - src - include= " /liЬ/jquery/d is t/**/*.min . js "


asp-append-version="true" >
</script>

Включение средства аннулирования кеша приводит к появлению в НТМL-разметке, отправ­


ляемой браузеру, элемента следующего вида :

<script src= " / li b/jquery/dist/jquery . min .js?v=ЗzRSQlHF-ocUiVcdv9yKTXqM" >


</script>

Тот же самый номер версии будет применяться дескрипторным вспомогательным клас­


сом до тех пор , пока вы не измените содержимое файла, например, обновив библиотеку
JavaScript, из - за чего будет вычислена другая контрольная сумма . Добавление номера вер­
сии означает, что всякий раз, когда изменяется файл , клиент будет запрашивать отлича­
ющийся URL, трактуемый кешами как запрос для нового содержимого, который не может
быть удовлетворен с использованием ранее кешированного содержимого, поэтому он пе­
редается серверу приложений. Затем содержимое кешируется как обычно до следующего
обновления, которое приведет к генерации еще одного URL с другим номером версии.
776 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

Работа с сетями доставки содержимого


Сеть доставки содержимого (content delivery network - CDN) применя ется для пе­
редачи запросов к соде ржим ому приложения се рверам , которые нах одятся поблизос­
ти от пользовател я. Вм е сто запрашивания файла JavaScript у ваших с ерверов браузер
запрашивает е го у имени хоста, которое преобразуется в географически м естный сер­
вер. что уменьшает время загрузки файлов и сокращает ширину полосы пропускания,
необходимую приложению. При наличии крупной географически распределенной
базы пользователей может оказаться целесообразной коммерческая подписка на CDN,
но даже небольшое и простое приложение способно извлечь выгоду от использования
бесплатных CDN, приводимых в действие ведущими технологическими компаниями
для доставки общих пакетов JavaScript, таких как jQuery.
В настоящей главе будет применяться сеть Microsoft CDN, которая предлагает
бесплатный доступ I< популярным пакетам, со списком 1<0торых можно о з накомиться
по адресу www . asp . ne t /а j ах/ cdn . Здесь используется пакет jQuery 2.2.4, и вот три
URL в сет и Microsoft CDN, у~шзывающие на этот выпуск:
• http : //ajax.aspnetcdn . com/ajax/jQuery/jquery- 2 . 2 . 4 . js
• h ttp : //ajax . aspnetcdn . com/a j ax/ j Qu ery/ jquery- 2 . 2 . 4 . min .j s
• http : //ajax . aspnetcdn . com/a j ax/jQu ery/jquery- 2 . 2 . 4 . min .map
Ук аза нные URL пр едоставляют обычный Jav aScript, минифицированный файл
JavaScript и отображение на исходный код для минифицированного файла. В лис­
тинг е 25. 7 локальные файлы заменены минифицированным файлом, получаемым из
CDN.

Листинг 25.7. Применение CDN в файле _Layout.cshtml

< ! DOCTYPE html>


<html>
<head>
<meta name= " viewport " content = "width=d evi ce - wi d t h " />
<ti tle>C i ties< / title>
<script src="http://ajax.aspnetcdn.com/ajax/jQuery/jquery- 2.2.4.min.js">
</script>
<li nk h r ef=" / liЬ/ b ootstrap/ dist / c ss /b ootstr ap . css " rel= " styl e sh eet " />
</head>
<body class= " panel - body " >
<div>@RenderBody()</div>
</body>
</ h tm l >

Указание CDN означает, что никакие запросы для jQuery не будут попадать на сер­
веры приложения . Пробл ема с с етями CDN связана с тем, что они не находятся под
контролем вашей организации , т.е. они могут отказывать, ост авляя приложение фун­
кционирующим, но неспособным работать ожидаемым образом из- за недоступности
содержимого CDN. Чтобы решить проблему, JUiacc Sc ri pt TagHelper предлагает воз­
можность обратиться за помощью к локальным файлам, когда клиент не может загру­
зить содержимое из CDN (листинг 25.8).
Глава 25. Использование други х встроенных дескрипторны х вспомогательных классов 777
Листинг 25.8. Использование обхода загрузки из CDN в файле _Layout. cshtml

<!DOCTYPE html>
<html>
<head>
<meta name="view.port " content= " width=device - width" />
<tit le >Cit ies< /tit le>
<script src="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-2.2.4.min.js"
asp-fallback-src-include="/lib/jquery/dist/**/*.min.js"
asp-fallback-test="window.jQuery">
</script>
<link href= " / liЬ/bootstrap/dist/css/bootstrap. css" rel="stylesheet " />
</head>
<body class ="panel-body">
<div>@RenderBody()</div>
</body>
</html>

Атрибуты asp - fallback - src-include и asp - fallback - src - exclude приме­


няются для выбор а и исключения локальных файлов . которые будут использоваться.
если сеть CDN не способна доставить файл, указанный посредством обычного атрибу­
та src. Чтобы выяснить, работает ли CDN, с помощью атрибута asp - fallback - test
определяется фрагмент JavaScript, который будет оцениваться в браузере. Если фраг­
мент оценивается как false, тогда будут запрашиваться запасные файлы.
Запустите приложение и просмотрите НТМL-разметку, которая была отправле­
на клиенту. Вы увидите, что класс ScriptTagHelper взял фрагмент из атрибута
asp-fallback-test и применил его для создания е ще одного элемента script :
<head>
<meta name="viewport " content="width=device-width" />
<title>C ities</title>
<script src="http://ajax . aspnetcdn.com/ajax/jQuery/jquery-2.2.4.min .js" >
</script>
<scr ipt>
(window . jQuery 1 1document . wri te ( " \u003Cscript
src=\u0022\/lib\/jquery\/dist\/jquery .min .j s
\u0022\u003E\u003C\/script\u003E "));
</sc ript>
<link href= " /liЬ/bootstrap/dist/css/bootstrap . css " rel="stylesheet" />
</head>
Фрагмент JavaScript, указываемый в атрибуте asp - fallback - test . должен
возвращать true , если файл из CDN был загружен. и false в противном случае.
Простейший подход обычно предусматривает проверку точки входа в функциональ­
ность. предлагаемую кодом JavaScript. Библиотека jQuery создает функцию по имени
j Query на глобальном объекте window, которая и проверяется в листинге 25.8. Вам
понадобится найти эквивалентные тесты для всех файлов, загружаемых из CDN.
Важно проверять настройки обхода , т.к. вы не сможете обнаружить их отказ до тех
пор, пока сеть CDN не прекратит работу, а пользователи утратят доступ к вашему при­
ложению. Проверить обход проще всего путем изменения имени файла, указанного в
атрибуте src, на такое, о котором доподлинно известно, что оно не существует (здесь
I< имени файла присоединено слово FAIL). и затем с помощью инструментов <F12>
778 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

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


для файла из сети CDN, за которой следует запрос запасного файла.

Внимание! Средство обхода CDN полагается на то, что браузеры загружают и выполняют
содержимое элементов script синхронным образом в порядке, в котором они опре­
делены. Существует несколько приемов ускорения загрузки и выполнения сценариев
JavaScript за счет того, что они делают процесс асинхронным. Однако такой подход мо­
жет привести к тому, что проверка обхода выполнится перед тем, как браузер извлечет
файл из CDN и выполнит его содержимое, в результате порождая запросы для запасных
файлов даже при нормальной работе CDN и ставя под сомнение вообще использование
CDN . Не смешивайте асинхронную загрузку сценариев со средством обхода CDN.

Управление таблицами стилей CSS


Класс LinkTagHelper - это встроенный вспомогательный класс для элементов
link, который применяется при управлении включением таблиц стилей CSS в пред­
ставление. Класс LinkTagHelper поддерживает атрибуты, описанные в табл. 25.5,
использование которых демонстрируется в последующих разделах.

Таблица 25.5. Атрибуты встроенного дескрипторного вспомогательного класса


для элементов link
Имя Описание

asp- href-include Этот атрибут применяется для выбора файлов, пред­


назначенных атрибуту href вы ходного элемента

asp - href- exclude Этот атрибут используется для исключения файлов


из атрибута href выходного элемента

asp-append-version Этот атрибут применяется для включения средс­


тва аннулирования кеша, как описано во врезке

"Понятие аннулирования кеша" ранее в главе

asp-fallback-href Этот атрибут используется для указания запасного


файла на случай возникновения проблемы с сетью
CDN
asp-fallback-href-include Этот атрибут применяется для выбора файлов, ко­
торые будут использоваться при наличии проблемы
с CDN
asp-fallback-href-exclude Этот атрибут применяется для исключения файлов
из набора, который будет использоваться при нали­
чии проблемы с CDN
asp-fallback-href-test-class Этот атрибут применяется для указания класса CSS,
который будет использоваться при тестировании
работоспособности сети CDN
asp-fallback-href- test - property Этот атрибут применяется для указания свойства
CSS, которое будет использоваться при тестирова­
нии работоспособности сети CDN
asp-fallback-href-test-value Этот атрибут применяется для указания значения
CSS, которое будет использоваться при тестирова­
нии работоспособности сети CDN
Глава 25. Использование других встроенных дескрипторных вспомогательны х классов 779

Выбор таблиц стилей


Класс LinkTagHelper разделяет с классом ScriptTagHelper многие средства, в
том числе поддержку шаблонов универсализации имен для выбора либо исключения
файлов CSS, так что их не нужно указывать по отдельности. Наличие возможности
точного выбора файлов CSS имеет такую же важность, как и для файлов JavaScript,
поскольку таблицы стилей тоже поступают в виде обычных и минифицированных
версий, дополнительно поддерживая отображения на исходный код. Популярный па­
кет Bootstrap. который применялся для стилизации НТМL-элементов повсеместно в
данной книге. включает свои таблицы стилей CSS в папке wwwroot/liЬ/bootstrap/
dist/ css. и если вы раскроете все элементы в окне Solution Explorer, то заметите, что
доступны четыре файла (рис. 25.5).
Файл boostrap.css является обычной таблицей стилей, файл
boostrap.min.
css - ее минифицированной версией, а файл bootstrap. css .map - отображением
на исходный код. В других файлах находятся стандартная тема Bootstrap, ее обычная
и минифицированная версии, а также отображение на исходный код. В листинге 25.9
атрибут asp-href - include используется для выбора минифицированной таблицы
стилей. (Кроме того, удален элемент script для загрузки библиотекиjQuеrу, которая
больше не требуется. )

Листинг 25.9. Выбор таблицы стилей в фaйлe _Layout.cshtml


<!DOCTYPE html>
<html>
<head>
<meta name= "viewport " content= "width=devi ce - width " />
<title>Cities</tit le>
<link asp-href-include="/liЬ/Ьootstrap/dist/**/.min.css" rel="stylesheet"/>
</head>
<body class="panel - body">
<div>@RenderBody()</div>
</body>
</html>

Здесь должно уделяться аналогичное внимание деталям, как и при выборе файлов
JavaScript, потому что довольно легко сгенерировать элементы link для нескольких
версий того же самого файла или файлов, которые не нужны. Чтобы управлять выби­
раемыми файлами, можно следовать тем же трем подходам , которые были описаны
для файлов JavaScript в предыдущем разделе: сужать шаблон универсализации имен,
исключать файлы с применением атрибута asp - href - exclude и использовать эле­
мент env ironment для выбора между дублирующимися наборами файлов.

Работа с сетями доставки содержимого


Вспомогательный класс Link Tag предоставляет набор атрибутов для возвраще­
ния к локальному содержимому. когда сеть CDN не доступна, хотя процесс проверки,

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


файла JavaScript. В листинге 25.10 применяется URL сети MaxCDN для библиоте­
ки Bootstrap. просто чтобы продемонстрировать альтернативу платформе Microsoft
(M axCDN - это сеть CDN. рекомендуемая проектом Bootstrap).
780 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Саге MVC

Листинг 25.10. Использование CDN для загрузки таблицы стилей CSS


в файле _Layout. cshtml

<!DOCTYPE htrnl>
<htrnl>
<head>
<rneta narne= "viewport " content= "wid th=device-width " />
<t i t le >C i tie s</title >
<link href=
"https://maxcdn.bootstrapcdn . com/bootstrap/3.3.6/css/bootstrap . min.css"
asp-fallback-href-include="/lib/bootstrap/dist/**/*.min.css"
asp-fallback-test-class="btn"
asp-fallback-test-property="display"
asp-fallback-test-value="inline-Ыock"
rel= "s tylesheet " />
</head>
<body class= "panel-body " >
<di v>@RenderBody() </div>
</body>
</htrnl>

В ат риб уте href указывается URL сети CDN, а с помощью атрибута


a sp- fa llback - href-include выбирается файл, который будет прим еняться, если
сеть CDN окажется н едоступной. Тем не менее, проверка. работает ли сеть CDN. тре­
бует использования трех разных атрибутов и понимания классов CSS, которые опре ­
деляются необходимой таблицей стилей CSS.
Ср едство перехода на запасные таблицы стилей CSS инициируется путем добавле­
ния к отправляемому документу элемента meta для класса, определенного в атрибуте
asp - fallback - test - class . В листинге 25.10 был указан класс Ьtn , т.е. к НТМL­
разм етке. отправляемой браузеру. будет добавлен элемент rneta следующего вида:

<rneta narne= "x-s ty lesheet- fallba ck-test" clas s="btn" />


Указываемый класс CSS должен быть определен в таблице стилей, которая подле ­
жит загрузке из CDN. Заданный здесь класс Ьtn обеспечивает базовое форматирова ­
ние для кнопочных элементов Bootstrap.
Атрибут asp -fallback- test - property применяется для указания свойства CSS,
которое устанавливается классом CSS, а атрибут asp - fallback - test - value исполь­
зуется для указания устанавливаемого им значения. Дескрипторный вспомогатель­
ный класс добавляет к представлению код JavaScript, который проверяет значение
свойства CSS из элемента meta с целью выяснения, загрузилась ли таблица стилей,
и на случай, если не загрузилась. пр едусматривает элементы link для запасных фай­
лов. Класс Ьtn из Bootstrap устанавливает свойство display в inline - Ыock , что
является тестом, который проверяет, удалось ли браузеру заг рузить таблицу стилей
Bootstrap из сети CDN.

Совет. Простейший способ понять, каким образом проводить проверку для сторонних паке­
тов вроде Bootstrap, предполагает применение инструментов <F12> браузера. Чтобы оп­
ределить тест в листинге 25. 1О, элементу назначается класс Ьtn , после чего в браузере
инспектируются индивидуальные свойства CSS, которые этот класс изменил. Такой прием
проще попытки просмотра длинных и сложных таблиц стилей.
Глава 25. Использование других встроенных дескрипторных вспомогательных классов 781

Ра бота с якорными эл еме нтами


Элемент а является базовым инструментом для навигации в рамках приложения и
отправки запросов GET приложению с целью получения разнообразного содержимого.
Класс AnchorTagH elper используется для трансформации атрибута h re f элементов
а , чтобы целевые URL генерировались при участии системы маршрутизации, с при­
м е нением атрибутов из табл. 25.6.
Таблица 25.6. Атрибуты встроенного дескрипторного вспомогательного класса
для якорных элементов

Имя Описание

asp- action Этот атрибут указывает метод действия, на который будет нацелен URL
asp - con tro lle r Этот атрибут указывает контроллер , на который будет нацелен URL
a sp - area Этот атрибут указывает область, на которую будет нацелен URL
a sp - fragme n t Этот атрибут используется для указания фрагмента URL
(который находится после символа # )

asp - host Этот атрибут указывает имя хоста, на который будет нацелен URL
asp- protocol Этот атрибут указывает протокол, который будет применять URL
asp- route Этот атрибут указывает имя маршрута, который будет использоваться
для генерации URL
asp - route - * Атрибуты с именами, начинающимися на asp- rou t e -, применяются
для указания дополнительных значений, относящихся к URL, так что
атрибут a sp- rout e-id используется для предоставления системе
маршрутизации значения сегмента id

Класс AnchorTagHelper прост и предсказуем: он облегчает генерацию URL в эле­


ментах а , которы е задействуют конфигурацию маршрутизации приложения . В лис ­
тинге 25.11 эл е мент а внутри представления Index . cshtml обновлен, чтобы е го ат­
рибут href выпускался дескрипторным вспомогательным классом.

Листинг 25.11. Трансформация якорного элемента в файле Index. cshtml


@model I E n umeraЫe<City>
@{ La yout = " Layo ut"; )
<tаЫе clas s = " ta Ы e t a Ьle - condensed taЬl e - bo r de r ed " >
<thead class= "bg - p r ima r y " >
<tr>
<th>Name</th>
<th>Country</th>
<th class ="t ext - right ">Populati on</th>
</tr>
</thead>
<tbody>
@foreach (va r ci t y i n Mode l )
<t r >
<td>@city .Name </td>
<td>@cit y. Country</td>
<td class= " text - right " >@city .Populat i on ? .ToStr i ng( " # , ### " )</td>
</tr>
782 Часть 11. Подробные сведения об инфраструктуре ASP.NET Соге MVC

</tbody>
</tаЫе>
<а asp-action="Create" class="Ьtn Ьtn-primary">Create</a>

Запустив прилож ение и запросив URL вида /Horne/ I ndex , вы з а м етите , что де ­
скрипторный вспомогат ельный класс трансформировал эл еме нт а следующим
образом:

<а c l a s s =" Ьt n Ьtn-p rim ary " href="/Home/Create" >Create</a>

Работа с эле м ентами i mq


Класс Irnage TagHe lp e r позволя ет предоставить ср едство аннулирования кеш а

для изображений через атрибут src элементов img, давая приложению возможность
применять в своих интересах к е ширование и гар а нтируя н е м е дл е нное отр аж е ние

л юбых изменений в изображениях. Класс Image TagHelpe r оперирует с элементами


i mg, которые определяют атрибут asp - a pp endversion, опи санн ый в та бл . 25. 7.

Таблица 25.7. Атрибут встроенного дескрипторного вспомогательного класса


для элементов img
Имя Описание

asp- append - vers i on Этот атрибут используется для включения аннулирования кеша,
как объяснялось во врезке "Понятие аннулирования кеша"
ранее в главе

В листинге 25.12 к разделяемой компоновке добавлен элемент irng с изображени е м


панорамы Нью-Йорка, которое было включено в проект в начале главы. (Кроме того,
ради простоты обеспечено применение локальных файлов с таблицами стилей.)

Листинг 25.12. Добавление изображения в файле _ Layou t. csh tml


< ! DOCTY PE h tml>
<html>
<head>
<meta na me= " viewpor t" conten t =" wi dth=device - widt h" />
<t it l e >Cities</t i tle>
<link asp - href - incl ude =" / l iЬ/bootst r a p /dist/**/* . m in. c s s " rel= "stylesheet " />
</head>
<b ody class =" panel - body " >
<img src="/images/city.png" asp-append-version="true" />
<div>@RenderBody()</d i v>
</body>
</ html>

Запустив прил ожение , вы увидите , что изображение появляется в верхней части


каждо й страницы . Если вы прос м отрите НТМL-разм етку, которая была отправле на
браузеру, то заметите, что URL, используемый для запроса файла и з ображ ения , с о­
держит контрольную сумму версии:

<img src=
" /images/city . png ?v=KaМNDSZFЬzNpE8PkЬЗOEXcAJufRcRDpKhOK_IIPNc7E" />
Глава 25. Использование других встроенных дескрипторных вспомогательных классов 783
Как и со средствами аннулирования кеша для файлов JavaScript и таблиц стилей
CSS, контрольная сумма, включенная в URL, остается неизменной до тех пор, пока
файл не будет модифицирован .

И с п ользо в ание кеша данных


Инфраструктура MVC поддерживает кеш в памяти, который может применяться
для кеширования фрагментов содержимого с целью ускорения визуализации пред­
ставлений. Содержимое, подлежащее кешированию, обозначается в файле пред­
ставления с использованием элемента c ac he, который обрабатывается классом
Cach e Ta gH e lpe r через атрибуты, описанные в табл. 25.8.

На заметку! Кеширование - удобный инструмент для многократного использования раз­


дела содержимого, чтобы его не приходилось генерировать при каждом запросе. Но эф­
фективное применение кеширования требует тщательного обдумывания и планирования.
Хотя кеширование способно улучшить производительность приложения, оно также созда­
ет необычные эффекты, такие как получение пользователями устаревшего содержимого,
наличие множества кешей с разными версиями содержимого и нарушение работы раз­
вертываемых обновлений из-за того , что содержимое, кешированное из предыдущей вер­
сии приложения, смешалось с содержимым из новой версии. Не включайте кеширование,
если только не имеете дело с четко определенной проблемой низкой производительности,
и удостоверьтесь в том, что хорошо понимаете влияние, которое оно будет оказывать .

Таблица 25.8. Атрибуты встроенного дескрипторного вспомогательного класса


для элементов cache

Имя Описание

enaЫ ed Этот атрибут типа bool используется для управления тем, будет
ли кешироваться содержимое элемента c ache . Отсутствие дан­
ного атрибута означает включение кеширования

expires - on Этот атрибут применяется для указания абсолютного времени,


когда срок существования кешированного содержимого истечет,

в виде значения DateTime


expi r es - af ter Этот атрибут используется для указания относительного вре­
мени , когда срок существования кешированного содержимого

истечет, в виде значения TimeSpan


expires - sliding Этот атрибут применяется для указания промежутка времени
с момента последнего использования, когда срок существо­

вания кешированного содержимого истечет, в виде значения

Ti meSpan
vary- by- heade r Этот атрибут применяется для указания имени заголовка за­
проса, который будет использоваться при управлении разными
версиями кешированного содержимого

vary-by- query Этот атрибут применяется для указания имени ключа строки за­
проса, который будет использоваться при управлении разными
версиями кешированного содержимого
784 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

Окончание табл. 25.8


Имя Описание

vary- by-route Этот атрибут применяется для указания имени переменной


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

vary- by- cookie Этот атрибут применяется для указания имени сооkiе-набора ,
который будет использоваться при управлении разными версия­
ми кешированного содержимого

vary-by-user Этот атрибут типа bool применяется для указания, будет ли имя
аутентифицированного пользователя использоваться при управ­
лении разными версиями кешированного содержимого

vary- by Этот атрибут оценивается для предоставления ключа, приме­


няемого при управлении разными версиями кешированного

содержимого

priority Этот атрибут используется для указания относительного приори­


тета, учитываемого в случае исчерпания пространства кеша в

памяти и очистки кешированного содержимого, срок существо­

вания которого еще не истек

Чтобы посмотреть, как работают атрибуты кеша, создайте папку Cornponents, до­
бавьте в нее файл класса по имени TirneViewCornponent. cs и определите компонент
представления, приведенный в листинге 25. 13.
Листинг 25.13. Содержимое файла TimeViewComponen t. cs из папки Componen ts

using Systern;
using Microsoft.AspNetCore.Mvc ;
namespace Cities.Components {
puЬlic class TirneViewComponent : ViewCornponent
puЫic IViewComponentResult Invoke()
return View(DateTime.Now) ;

Метод Invoke () выбирает стандартное представление и передает ему объект


DateTirne в качестве модели представления. Для того чтобы обеспечить компонент
пр едставления необходимым ему представлением, создайте папку Views/Horne/
Components/Time и добавьте в нее файл представления по имени Defaul t . cshtml
с разметкой из листинга 25.14.

Листинг 25.14. Содержимое файла Default.cshtml из папки


Views/Home/Components/Time

@rnodel DateTime
<div class= "panel -b ody bg-info">
Rendered at @Model . ToString ( "НН: rnm: ss")
</div>
Глава 25. Использование других встроенных дескрипторны х вспомогательных классов 785
Объект модели DateTime применяется для отображения текущего времени с точ­
ностью до секунды. В листинге 25.1 5 элемент img, добавленный в предыдущем разде­
ле, заменяется выражением @await Component . InvokeAsync, которое обращается к
компоненту представления.

Листинг 25.15. Использование компонента представления в файле _ Layou t. csh tml


<!DOCTYPE html>
<html>
<head>
<meta name= "v iewport" content= "width=device-width " />
<title>Cit i es</t itle>
<link asp- href- include=" /liЬ/bootstrap/dist/** /* .min. css " rel= " stylesheet " />
</head>
<body class= "panel-body " >
@await Component. InvokeAsync ("Time")
<div>@RenderBody()</div>
</body>
</html>

Запустив приложение, вы увидите баннер, отображающий время, когда содержи­


мое бьvю визуализировано. Подождите несколько секунд и перезагрузите страницу;
вы заметите, что отображаемое время изменилось (рис. 25.6).

London UK 8.539.000
New York USA
New York USA 8,406.000
Sa11 Jose USA
Sал Jose USA 998,537
Paris France
. ! Paris France 2,244.000
1 !
l lВI
1
!
1
L--·----------·-··---··--t
Рис. 25.6. Отображение времени в примере приложения

Элемент cache применяется для окружения содержимого, которое должно быть


добавлено в кеш. В листинге 25.16 элемент cache используется для добавления в кеш
вывода из компонента представления.
786 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

Листинг 25.16. Кеширование содержимого в файле _ Layou t. csh tml


<!DOCTYPE htrnl>
<html>
<head>
<rneta name="viewport" content="width=device-width" />
<title>Cities</title>
<link asp-href-include="/liЬ/bootstrap/dist/**/*.rnin.css" rel="stylesheet" />
</head>
<body c l ass="panel-body">
<cache>
@await Component. InvokeAsync ( "Time")
</cache>
<div> @RenderBody()</div>
</body>
</htrnl>

Применение элемента cache без каких-либо атрибутов сообщает инфраструктуре


MVC о том, что для удовлетворения всех будущих запросов необходимо повторно ис­
пользовать отмеченное содержимое. Теперь после запуска приложения содержимое,
сгенерированное компонентом представления, кешируется, поэтому даже при пере­

загрузке страницы будет отображаться то же самое показание времени.

Совет. Применяемый классом CacheTagHelper кеш основан на памяти, т.е. его емкость
ограничивается доступным объемом ОЗУ. При нехватке пространства содержимое из
кеша начинает удаляться, а в случае останова или перезапуска приложения все содер­

жимое кеша утрачивается.

Установка времени истечения для кеша


Атрибуты expires-* позволяют указывать время, когда истекает срок существова­
ния кешированного содержимого, выражаемое как абсолютное время, время относи­
тельно текущего времени или промежуток времени, в течение которого кешированное

содержимое не запрашивается. В листинге 25.17 с помощью атрибута expires-after


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

Листинг 25.17. Установка времени истечения для кеша в фaйлe _Layout.cshtml


<!DOCTYPE html>
<html>
<head>
<meta name = "v iewport" content="width=device-width" />
<title>Cities</title>
<link asp-href-include="/liЬ/bootstrap/dist/**/*.min.css" rel="stylesheet "/>
</head>
<body class="panel-body">
<cache expires-after="@TimeSpan.FromSeconds(15)">
@await Componen t .InvokeAsync("Tirne")
</cache>
<d i v>@RenderBody()</div>
</body>
</html>
Глава 25. Использование других встроенны х дескрипторных вспомогательных классов 787
Запустив приложение, вы заметите, что срок существования кешированных дан­
ных истекает спустя 15 секунд, после чего перезагрузка страницы приводит к вызову
компонента предс т авления и кешированию нового содержимого на протяжении сле­

дующих 15 секунд.

Установка фиксированного момента истечения


Посредством атрибута e xp i r es - on, принимающего значение Da t e Tirne , можно
указывать фиксированный момент времени, при наступлении которого истечет срок
существования кешированного содержимого (листинг 25.18).

Листинг 25.18. Указание фиксированного момента истечения в файле_Layout. cshtml


< ! DOCTYPE html>
<html>
<head>
<meta name ="vi ewpo r t " c onte nt =" width=devi c e- widt h" />
<t i t l e>Cities</title>
<l i nk asp - href - i n c l ude= " / li Ь/boots tr a p /d i s t /**/* .mi n . c ss" re l ="s tyl esheet" />
</head>
<body class= "p a ne l-body" >
<cache expires-on="@DateTime.Parse("2100-0l-01")">
@awa i t Cornponen t.InvokeAs ync ( "Tirne " )
</cache>
<div>@RenderBody() </di v>
</body>
</html >

Здесь указано , что данные должны кешироваться до 2100 года. Это не особенно
полезная стратегия кеширования, поскольку приложение наверняка будет перезапу ­
щено до того, как начнется следующее столетие, но иллюстрирует способ указания
фиксированного момента в будущем вместо выражения времени истечения относи­
тельно момента, когда содержимое попадает в кеш.

Установка периода истечения с момента последнего использования


Атрибут expires - sl i din g применяется для указания промежутка времени, по
прош е ствии которого срок существования содержимого истекает, если оно не извле­

калось из кеша. В листинге 25.19 задан скользящий период истечения, составляю­


щий 1О се1<унд .

Листинг 25. 19. Указание периода истечения с момента последнего использования


кешированного содержимого в файле _ Layou t. csh tml
< ! DOCTYPE html >
<html>
<head>
<me ta name= "viewpo r t " c ont ent=" width=de vi c e-widt h" / >
<tit l e>Cities</t itl e>
<link asp -hr ef -in c lud e= " / liЬ/b oo t s trap/dist/* */* .min. css " re l="stylesheet" />
</h e ad>
<body cla s s= "pane l-body" >
<cache expires-sliding="@TimeSpan.FromSeconds(lO)">
788 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

@await Component .I nvokeAsync("Time")


</cache>
<div>@RenderBody()</div>
</body>
</html>

Запустив приложение и периодически перезагружая страницу, вы сможете уви ­


деть эффект от наличия атрибута express - sliding . До тех пор, пока вы перезагру­
жаете страницу в рамках 1О-секундного интервала, будет применяться кешированное
содержимое. В случае ожидания свыше 1О секунд перед перезагруз1юй страницы ке­

шированное содержимое отбрасывается, с помощью компонента представления гене­


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

Использование вариаций кеша


По умолчанию все запросы получают одно и то же 1<ешированное содержимое.
Класс Cache TagHelpe r способен поддерживать разные версии кешированного со­
держимого и применять их для удовлетворения различных типов НТГР-запросов,
указываемых с использованием одного из атрибутов, имена которых начинаются с
vary - by. В листинге 25.20 демонстрируется применение атрибута vary - by - route
для создания вариаций кеша на основе значения action, предоставляемого системой
маршрутизации.

Листинг 25.20. Создание вариации кеша в файле_Layout. cshtml


<!DOCTYPE html>
<html>
<head>
<meta name= "viewport" content="width=devi ce - width " />
<title>Cities</title>
<l ink asp-href-include="/lib/bootstrap/dist/**/*.rnin.css" rel="stylesheet"/>
</head>
<body c lass="panel-b ody " >
<cache expires-sliding="@TimeSpan.FromSeconds(lO)" vary-by-route="action">
@await Component . InvokeAsync("Time")
</cache>
<d iv>@RenderBody()</div>
</body>
</html>

Если вы запустите приложение и воспользуетесь двумя вкладками или окнами


браузера для запроса URL вида /Horne/ Index и /Horne/Create, то заметите, что
каждая вкладка или окно получает собственное кешированное содержимое со своим
сроком истечения, т.к. каждый запрос генерирует разное значение маршрутизации
action. Класс CacheTagHelper поддерживает ряд атрибутов, которые определяют
разнообразные вариации, включая кеширование содержимого для индивидуальных
пользователей.
Существует также атрибут vary-by, который позволяет определять произвольные
вариации кеша с применением любого значения данных. В листинге 25.21 воссозда­
ется эффект от атрибута vary - by - route за счет указания значения, получаемого
прямо из данных маршрута.
Глава 25. Использование других встроенных дес к рипторны х вспо мо гат е льны х классов 789
Листин г 25 .21 . Указание специальной вариации кеша в файле_Layout. cshtml
<!DOC TYPE html>
<html>
<head>
<meta name= " viewpor t " c onte nt = " wi dt h=de vi ce - widt h " />
<title>C i ties</title >
<li nk asp- href-inc l ude= " /l i b/boot s trap/dist / ** / * .min . css" r e l="s t yl esheet " / >
</head>
<body class =" panel -b ody " >
<cache expires-sliding="@TimeSpan.FromSeconds(lO)"
vary-by= 11 @ViewContext.RouteData.Values[ 11 action 11 ] 11 >
@await Component . I nvokeAsync( " Time " )
</cac he>
<d i v>@ Re nde r Body( )< /div>
</body>
</html>

Атрибут vary- by может использоваться для создания более сложных вариаций


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

И спользо ва ние URL, относительных к приложению


Последний встроенный дескрипторный вспомогательный класс, Ur lReso l ut ion
TagHelper , прим е ня е тся для обеспечения поддержки URL, относительных к при­
ложению, которы е предст авляют собой URL, снабженные префиксом в виде тильды
(символа - ). В листинге 25.22 показано изменение элемента li n k внутри разделяе­
мой компоновки, чтобы он использовал явно определенный URL вместо URL, сгенери­
ров анного из систе м ы маршрути з ации с помощью дескрипторных вспомогательных

классов .

Листинг 25 .22 . Применение явного URL в фaйлe _Layout.cshtml


<!DOCTYPE ht ml>
<html>
<head>
<meta name= "viewport " c on te nt =" widt h=devi ce - width " />
<title> Ci t i es< / t it l e >
<link href="/lib/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet" />
</head>
<body c l ass= "panel - body " >
<cache expires - slidi ng=" @TimeSpa n.FromSe cond s( l O) "
va r y- by= " @ViewContext . Ro uteDa t a . Va lu es [ "ac t ion " ] " >
@await Component . InvokeAs ync( "Time " )
</cache>
<div>@Ren derBody( ) </d i v>
</body>
</htm l >
790 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

Явные URL прекрасно подходят при условии, что вы осознаете необходимость их


обновления при изменении схемы URL приложения. И для многих прил ож е ний это
единственный ф актор, который след;ует учитыв ать.
Однако есть приложения, которые буд;ут предназначены для разделя е мой среды,
где единственный сервер поддерживает множество приложений, р азлич а емых путем
доб авл е ния префикса к URL. В листинге 25.23 приведе на измен е нная конфигурация
приложения, так что конвейер запросов настраивается для обработки запросов с пре­
фиксом mvcpp , эмулируя разделяемую среду.

Листинг 25.23. Добавление префикса URL в файле Startup. cs


us i ng Mi c r o s oft . Asp NetCore .Builde r;
using Mic r osoft. Ex t ensions . Depend ency inj ect i on ;
using Cities . Mode l s ;
namespa c e Cit i es {
puЫic cla s s St a rtup
puЬlic void Con f i gureS ervice s(I ServiceCol l ection services)
servi ce s. Ad d Sing l e t on <IReposi t o r y , Me mo r yRepo s i t ory>( );
services . AddMvc() ;

puЫic void Conf i gure(I App l icat i onBuilde r арр) {


( 11 /mvcapp 11 , appBuilder => {
арр. Мар
appBuilder.UseStatusCodePages();
appBui1der.UseDeve1operExceptionPage();
appBuilder.UseStaticFiles();
appBuilder.UseМvcWithDefau1tRoute();
}) ;

Метод М а р () позволя ет настраивать несколько конвейеров запросов с р аз ны ми


префиксами. Это не особенно полезная возможность при повседневной р азработке
приложений MVC, потому что создавать префиксы URL внутри приложения MVC мож­
но с использованием системы маршрутизации . Но для настоящей главы это удобное
средство, т.к. оно означает. что каждый URL з апрашивается клиентами , включая за­
просы к статическому содержимому .

Возникшую проблему можно заметить, запустив приложение и запросив URL вида


/ mvcapp, который теперь является стандартным URL для прилож ения и нацел ен
на действие Index контроллера Ноте . Теперь , когда все URL должны начинаться с
/ mvcapp , явный URL для таблицы стилей в элем енте link не работ ает, по этому со­
держимое в приложении не может быть стил и зовано (рис . 25.7).
Проблему можно бьuю бы устранить, обновив явный URL для включения префик­
са, но это не вс егда удается, т. к . префикс мож ет измениться при р аз вертывании ил и
не быть и з вестным на этапе разработки. Более эффективное решение пред;усматрива­
ет применение URL, относительных к приложению, когда путь к статиче с кому соде р­
жим о му выражается относительно любого префикса , который мож ет быть с конфигу­
рирован (листинг 25.24) .
Глава 25. Использование други х встроенны х дескрипторны х вспомогательны х классов 791

+- -> С ,-[] localhost:S1036/ mvcapp "{;{ : -


Re11derecl at 16:59:05
:'llaшe Couut1·y Popt1latio11
London UК 8.539.000
Ne" ' York USA 8.406.000
Sall Jo$e USA 998.53 7
Paris Fra11ce 2.244.000
Create

Рис. 25. 7. Эффект от явно определенного URL

Листинг 25.24. И спользование URL, относительного к приложению,


в файле _Layout.cshtml
<!DOCTYPE html>
<html>
<head>
<meta name= "viewpo r t " conte nt= "wi dth=devi ce - width " />
<title>Cities</t i tle>
<link href="~/lib/Ьootstrap/dist/css/Ьootstrap.min . css" rel="stylesheet" />
</head>
<body c l ass= "panel - body " >
<cache expires - sliding=" @T imeSpan .FromSeconds(lO) "
vary- by=" @ViewContext . Rou t eDa t a . Val ues [" act i on " J " >
@await Component . Inv o keAsync( "T ime " )
</cache>
<div>@RenderBody()</div>
</body>
</html>

Символ тильды обн аруживается классом Ur l ResolutionTag He l per , который


зам еняет его путем , требующимся для достижения содержимого папки wwwroo t.
Запустив приложение , вы увидите, что содержимое стилизовано , а в результате про­
с мотра НТМL-разметки, отправляемой браузеру , выяснится, что элемент l i nk содер­
жит URL. который включает префикс mvcapp:
<link href=
"/mvcapp /liЬ/bootstrap/dist/c ss /bootstrap . m i n . cs s" rel=" s t yle s he e t " />
Вспомогательный класс UrlRe sol u t i on Tag ищет в URL широкий диапазон эле ­
ментов, как описано в табл. 25.9.

Со в ет. Если вы применяете другой встроенный дескрипторный вспомогательный класс для


генерации URL из систе м ы маршрутизации, тогда генерируемая им НТМL-размет ка будет
автоматически включать любой необходимый префикс, который получается из свойства
контекста HttpRequest . PathBase , а его значение предоставляется сервером, разме­
щающим прило ж ение.
792 Ч асть 11 . П одробные сведения о б инфраструкту р е ASP.NET Core MVC

Таблица 25.9. Элементы и атрибуты, трансформируемые классом UrlResolutionTagHelper


Элемент Атрибуты

а href
applet archive
area href
audio src
base href
Ьlockquote cite
button formaction
del cite
emЬed src
form action
html manif est
iframe src
img src, srcset
input src, formaction
ins cite
link href
menuitem icon
object archive,data
q cite
script src
source src, srcset
track src
video src, poster

Резюме
В настоящей главе рассматривались встроенные дескрипторные вспомогательные
классы, которые не связаны с НТМL-формами . Эти дескрипторные вспомогательные
классы помогают управлять доступом к файлам JavaScript и таблицам стилей CSS,
создавая URL для якорных элементов, выполняя аннулирование кеша для изображе ­
ний, кешируя данные и трансформируя URL, относительные к приложению . В сле ­
дующей главе будет представлена система привязки моделей, которая используется
для обработки данных в НТТР-запросах, так что их можно легко потреблять внутри
приложения МVС .
ГЛАВА 26
Привязка моделей

п
ривязка моделей - это процесс создания объектов .NET с использованием дан­
ных из НТГР-запроса для снабжения методов действий аргументами, в которых
они нуждаются. В настоящей главе будет описана работа системы привя зки моделей,
показано , как она привязывает простые типы, сложные типы и коллекции. а также

продемонстрировано, каким образом взять на себя контроль над проце ссом, чтобы
указывать часть запроса, которая предоставляет значения данных, требующиеся ме­
тодам действий. В табл. 26.1 прив едена свод1\а, позволяющая пом естить привязку
моделей в контекст.

Таблица 26. 1. Помещение привязки моделей в контекст

Вопрос Ответ

Что это такое? Привязка моделей представляет собой процесс создания объектов ,
которые требуются методам действий в качестве аргументов, с приме­
нением значени й данных, получаемых из НТТР-запроса

Чем она полезна? Привязка моделей позволяет методам действий объявлять параметры,
используя типы С#, и автоматически получать данные из запроса без не­
обходимости в инспектировании, разборе и обработке данных напрямую

Как она В своей простейшей форме методы действий объявляют параметры, имена
используются? которы х применяются дпя извлечения значений данных из НТТР-запроса.
Часть запроса, используемая для получения данных, может быть сконфигу­
рирована за счет применения атрибутов к параметрам методов действий

Существуют ли Основная ловушка - получение данных из неправильной части за­


какие- то скрытые проса . Способ поиска данны х внутри запросов объясняется в разделе
ловушки или " Понятие привязки моделей" далее в главе, а места для по и ска м огут
ограничения? быть указаны явно с использованием атрибутов, которые описаны поз­
же в разделе " Указание источника данных привязки моделей"

Существуют ли Методы де й ствий вообще не обязаны объявлять параметры , и могут


альтернативы? получать данные прямо из НТТР-запроса с применением объектов кон­
текста, которые были описаны в главе 17. Однако результатом будет
более слож ный код, который труднее читать и сопровождать

Изменилась ли она В инфраструктуре ASP.NET Core MVC средство привязки моделей


по сравнению с было переписано, но работает так же, как в предшествующи х версиях.
версией MVC 5? Самое заметное изменение состоит в том, что атрибут Bind больше
не может ис пользоваться для исключения свойств модели из процесса
привязки, что теперь делается с помощью атрибута BindNever. За
подробными сведениями обращайтесь в раздел "И збирательная при­
вяз ка свойств" далее в главе
794 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC

В табл. 26.2 приведена сводка для этой главы.

Таблица 26.2. Сводка по главе

Задача Решение Листин г

Привязка простого типа Добавьте параметр к методу действия 26.1-26.11 ,


или коллекции 26.24-26.30
Привязка сложного типа Удостоверьтесь , что НТМL-разметка, 26.12-26.20
генерируемая представлением, хорошо

структурирована

Избирательная привяз ка Укаж ите имена значений данных с применени- 26.21-26.23


свойств ем атрибута Bind либо используйте атрибут
BindNever для исключения свойств модели из
процесса привязки

Указание источника Примените к аргументу метода действия или к 26.31-26.39


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

Подготовка проекта для примера


Созда йте новый про ект ти па Empty (Пустой) по имени MvcModels с использова­
ни ем шаблона ASP.NET Core Web Application (.NET Core) (Веб-приложе ни е ASP.NET
Core (.NET Core)). Добавьте требуемые пакеты NuGet в раздел dependencies файла
project . j son и настройте инструментарий Razor в раздел е tools, как показано в
листинrе 26. 1. Разделы , которые не нужны для данной главы, понадобится удалить.

Листинг 26.1. Добавление пакетов в файле project. j son

" dependencies ": {


" Microsoft . NETCore . App ":
" version ": " 1 . 0 . О ",
" type" : "p latform "
} 1

" Microsoft . AspNetCore . Diagnostics ": " 1 . 0 . 0 ",


" Microsoft . AspNetCore . Server.IISintegration ": " 1 . 0 . 0 " ,
" Microsoft.AspNetCore . Se rver.Kest re l": " 1 . 0 . О ",
" Microsoft.Extensions . Logging . Console ": " 1 . 0 . 0 ",
"Microsoft.AspNetCore .Mvc": 11 1.0.0 11 ,
"Microsoft.AspNetCore.StaticFiles": 11 1.0.0 11 ,
"Microsoft.AspNetCore.Razor.Tools":
"vers ion": "1. О. O-preview2-final",
"type": "build"

} 1

" tools ":


"Microsoft.AspNetCore.Razor . Tools": "1.0.0-preview2-final",
" Microsoft . AspNetCore . Server.IISintegration.Tools ": " 1 . 0 . 0- preview2 - final "
} 1
Глава 26 . Привяз ка моделей 795
" frameworks ": {
" netcoreappl.0 ":
" imports ": [ " dotnet5 . 6 ", "portaЬle-net4 5+win8 "]

}'
"buildOptions":
" emitEntryPoint ": true, " preserveCompilationContext ": true
}'
"runtimeOptions": {
" configProperties ": { " System . GC.Server ": true}

Создание модели и хранилища


Создайте папку Models и добавьте в нее файл класса по имени Person . cs с опре­
делением классов и п е речисления из листинга 26.2.

Листинг 26.2. Содержимое файла Person. cs из папки Models

using System ;
namespace MvcMode l s . Models
puЬlic class Person {
puЫic int Personld { get ; set ; }
puЫic string FirstName { get ; set;
puЫic string LastName { get ; set; }
puЫic DateTime BirthDate { get ; set ; }

puЫic Address HomeAddress { get ; set ; }


puЬlic bool IsApproved { get ; set ; }
puЫic Role Role { get ; set ; }

puЫic class Address {


puЬlic string Linel { get ; set; }
puЫic string Line2 { get ; set ; }
puЫic string Ci ty { get; set ; }
puЬlic string PostalCode { get ; set ;
puЫic string Country { get; set ; }

puЫic enum Role {


Admin ,
User ,
Guest

Добавьте в папку Models файл класса по имени Reposi tory . cs и поместите в


него определения интерфейса и класса реализации, приведенные в листинге 26.3.
796 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

Листинг 26.3. Содержимое файла Reposi tory. cs из папки Models


u s ing System . Collec t ions . Generic ;
namespace MvcModels . Mode l s {
puЬlic i n t erface IRepository {
IEnumeraЫe<Person> People { get ;
Person this [int id] { ge t ; set ; )

puЫic c l ass MemoryRepository : IRepos it ory


private Dictionary<int , Person> people
new Di ct i ona r y<int , Pe rs on> {
[1] = new Person {Perso ni d = 1, Fi r s tN ame " ВоЬ ",
LastName = " Sm i th ", Role = Role . Admin) ,
[2 ] = new Pe rson {Pe rson id = 2 , Firs t Name "Anne ",
LastName = " Do uglas ", Role = Role . User) ,
[ 3] = new Per s on {Person i d = З , Fi rstName " Joe ",
LastName = " АЫе ", Ro l e = Ro l e . User} ,
[4 ] = new Person {Pe rson i d = 4 , Fir s t Name "Mary ",
LastName = " Peters ", Ro l e = Role . Guest}
};

puЬlic IEnu m eraЫ e <Person> Pe opl e => pe opl e .Valu es ;


puЬlic Person t his [int i d] {
get {
return peop l e .Contai nsKey(i d) ? p e op l e[ id] nul l;

set {
pe ople[i d ] val ue ;

В интерф ейсе IReposi tory определено свойство Peop l e для извлечения всех объ­
ектов модели и индексатор, который позволяет извлекать или сохранять отдельные
объекты Person. Кл асс MemoryReposi t ory реализует этот интерфейс и прим еняет
словарь со стандартным содержимым . Реализация хранилища не поддерживает пос­
тоянство, так что после останова или перезапуска состояние приложения во з вр аща­

ется к стандартному содержимому .

Создание контроллера и представления


Создайте папку Con t ro 11 е r s , добавьте в нее файл кл ас с а по имени
HomeCon troller . cs и опр еделите контроллер, код которого показан в листинге 26.4.
Контроллер полагается на внедрение зависимостей при получении хр анилища, 1юто­
рое он использует в методе Index () для выбор а одиночного объекта Person с приме ­
нени е м знач е ния его свойства Person i d .

Листинг 26.4. Содержимое файла HomeController. cs из папки Controllers


using Microsoft . AspNetCo r e . Mvc ;
using MvcModels . Mode l s ;
Глава 26 . Привязка моделей 797
namespace MvcModels.Controllers {
puЫic class HomeController : Cont r ol ler
private IReposito r y reposito ry;
puЫic HomeController( I Repository repo) {
repository = repo ;

puЫic ViewRes ult Index( int id) => View(repository[id]);

Чтобы снабдить метод действия представлением, создайте папку Vi ews/Home и


добавьте в нее файл представления Razor по имени Index. cshtml с разметкой из
листинга 26.5, которая отображает ряд свойств объекта модели в таблице.

Листинг 26.5. Содержимое файла Index. cshtml из папки Views/Home

@model Person
@{ Layout = " Layout"; )
<div class= " bg - primary panel -body "><h 2>Person</h2></div>
<tаЫе class= " taЫe taЬle - condensed taЬle-bordered taЫe - striped " >
<tr><th>Personid : </th><td>@Model . Personi d</td></tr>
<tr><th>First Name : </th><td>@Model . FirstName</td></tr>
<tr><th>Last Name : </th><td>@Model . LastName</td></tr>
<tr><th>Role : </th><td>@Mode l.Role</td></tr>
</tаЫе>

Представление Index. cshtm l рассчитывает на разделяемую компоновку.


Создайте папку Views/Shared и поместите в нее файл по имени Layout . cshtml,
содержимое которого приведено в листинге 26.6.

Листинг 26.6. Содержимое файла _Layout.cshtml из папки Views/Shared

< ! DOCTYPE html >


<html>
<head>
<meta name="viewport" content= " width =dev ice-w idth " />
<title>@ViewBag . Title</title>
<link asp - href -i nclu de= " /liЬ/bootstr ap /dis t /**/* . min . css " rel= "stylesheet " />
@Rende r Section( "scr ipts ", fa ls e)
</head>
<body class= " panel -b ody" >
@RenderBody ()
</body>
</html>

Компоновка включает элемент link для таблицы стилей Bootstrap и визуализи­

рует содержимое представления. Имеется также дополнительный раздел scripts,


который будет использоваться позже в главе. Чтобы упростить представления , при­
меняемые в этой главе, добавьте в файл_ Viewimports. cshtml из папки Views про­
странство имен, содержащее классы модели (листинг 26.7).
798 Часть 11. Подробные сведения об инфраструктуре ASP.NET Саге MVC

Листинг 26. 7. Импортирование пространства имен в файле _View!mports . csh tml


@using MvcModels . Models
@addTagHelper * , Microsoft . AspNetCore . Mvc .TagHelpers

Представления в примере опираются на СSS-пакет Bootstrap. Создайте в корневой


папке проекта файл bower . j son с применением шаблона элемента Bower Configuration
File (Файл конфигурации Bower) и добавьте пакет Bootstrap в раздел dependencies
(листинг 26.8).

Листинг 26.8. Добавление пакета Bootstrap в файле bower. j son

" name ": "asp . net ",


" private ": true ,
"dependencies ": {
"bootstrap": 11 3.3.6 11

Конфигурирование приложения
В листинге 26.9 показан код класса Startup, который конфигурирует средства,
предоставляе мые пакетами NuGet, и настраивает службу хранилища.

Листинг 26.9. Содержимое файла Startup. cs


using Microsoft.AspNetCore.Builder ;
using Microsoft.Extensions.Dependencyinjection;
using MvcМodels . Models;
namespace MvcModels {
puЫic class Startup
puЬlic void ConfigureServices(IServiceCollection services)
services.AddSingleton<IRepository, MemoryRepository>();
services.AddМvc();

puЫic void Configure(IApplicationBui l der арр) {


app.UseStatusCodePages();
app . UseDeveloperExceptionPage();
app.UseStaticFiles();
app.UseMvc(routes => {
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
}) ;

Запустите прилож ение и запросите URLвида /Horne/Index/1; результат пр едстав­


лен на рис. 26.1. (В данный момент запрос стандартного URL вызывает ошибку . )
Глава 26. Привязка моделей 799

J lcx:alhost:S1861/Home/lr Х

~ -+ С []
----'===:::
localhost:51861/Home/lndex/1 _______ '{::s , -__,

Personld:

First Name: В оЬ

Last Name: Smith


Role: Admin

Рис. 26.1. Выполнение примера приложения

Понятие привязки моделей


Привязка моделей - элегантный мост между НТГР-запросом и методами действий
С#. Большинство приложений МVС Framework в той или иной степени полагаются на
привязку моделей, в том числе простой пример приложения, созданный в предыду­
щем разделе.

Привязка моделей использовалась при тестировании примера приложения из пре­


дыдущего раздела. Запрошенный URL содержал значение свойства Pe rsonid объекта
Pers on, подлежащего отображению:

/ Home / Index / 1
Инфраструктура MVC транслировала эту часть URL и применила ее в качест­
ве аргумента, когда вызывала метод Index () контроллера Home, чтобы обслужить
запрос:

puЫic ViewResult I ndex(int id ) => View( repo sitory[id ] );

Чтобы иметь возможность вызьmать метод Inde x () , инфраструктуре МVС необхо­


димо значение для аргумента id. Снабжение этим значением входит в число обязан­
ностей системы привязки моделей, отвечающей за предоставление значений данных,
которые могут использоваться при вызове методов действий.
Система привязки моделей опирается на связыватели моделей, которые являются
компонентами, ответственными за предоставление значений данных из одной части
запроса или приложения. Стандартные связьmатели моделей ищут значения данных
в следующих трех местах:

• отправленные данные формы;

• переменные маршрутизации;

• строки запросов.
800 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

Каждый источник данных инспектируется по порядку до тех пор , пока не будет


обнаружено значение для аргумента. В рассматриваемом примере данные формы
отсутствуют, поэтому значение в них не может быть найдено. Но в конфигурации
приложения, приведенной в листинге 26.9, предусмотрен сегме нт маршрутизации
по имени id, который позволит системе привязки моделей снабдить инфраструктуру
МVС значением, подлежащим применению при вызове метода I ndex () . После того,
как найдено подходящее значение , поиск останавливается, т.е. строка запроса небу­
дет просматриваться на предмет наличия в ней значения данных.

Совет. В разделе "Указание источника данных привязки моделей" далее в главе объясняет­
ся, как можно указать источник данных привязки моделей с использованием атрибутов .
Это позволяет установить, что значение данных должно получаться, скажем, из строки
запроса, даже если подходящее значение также есть в данных формы или переменных
маршрутизации.

Знание порядка поиска значений данных важно из-за того, что запрос может со­
держать несколько значений , например:

/ H om e/ Ind e x / З?id = l

Система маршрутизации обработает запрос и сопоставит сегмент id в шаблоне


URL со значением 3, но строка запроса содержит значение i d, равное 1. Поскольку
данные маршрутизации просматриваются раньше строки запроса, метод действия
Index () получит значение 3, а значение из строки запроса будет проигнорировано.
С другой стороны , в случае запроса URL, который не имеет сегмента id, будет ис­
следоваться строка запроса , т.е. URL вроде приведенного ниже также позволит сис ­
теме привязки моделей предоставить инфраструктуре МVС значение аргумента id ,
чтобы она смогла вызывать метод Index ( ) :
/ Home /Index ?id=l

Результаты запросов обоих URL показаны на рис . 26.2 .

Personld: 3
Personld:
First Name: Joe
First Name: ВоЬ
LastName: АЫе
Last Name: Smith

Role: Admin

Рис. 26.2. Эффект от упорядочивания источников данных привязки моделей


Глава 26 . П ривязка моделей 801

Стандартные значения привязки


Привязка моделей является негарантированным средством, т.е. МVС будет при­
менять его в попытке получить значения, необходимые для вызова метода действия,
но все равно вызовет метод, даже если значения данных предоставить не удалось.

Это может приводить к непредсказуемому поведению. Например, запрос URL вида


/Home/Index генерирует исключение (рис. 26.3) .

D lnterna! Server Error Х


г~·-·---·-------·--·--·--·----·-----·--~·----

1-·~-----~-l-~,:~~-'~~;~~~~~~:1:;;~~,::_,.:,===·==:=.:==-=-
IAn
1
Linhaпdl ed exception оссштеd \1Vhile processing the
request. ·
1
*l -.:_

1 NL1 llRefe1·enceExceptioп: Obj ect reference not set to an insta1се of ап o!Jject. ~!


Asp._Vie,vs_Home_111dex_csl1tml.< ExecuteAsyno d_ 17.Move№xtO 1

OL1ery Cookies Headers 1

1
~~~e~9l>~e~~~bl~~~:~~~1. a~~
Рис. 26.3. Ошибка обраб отки св о йства м одел и

Исключение было инициировано не системой привязки моделей. Взамен оно про­


изошло при обработке представления Index, выбранного методом действия Index () .
Чтобы вызвать метод Index (), инфраструктура MVC должна обеспечить значение
для аргумента id, а потому она предлагает каждому связывателю модели проинспек­

тировать свою часть запроса и предоставить значение.

В примере отсутствуют данные формы, не указано значение для сегмента мар­


шрутизации id и нет строки запроса в URL, так что система привязки моделей не
в состоянии предоставить значение данных. Чтобы вызвать метод Index () , инф­
раструктура MVC обязана предоставить какое-нибудь значение для аргумента id,
поэтому она использует стандартное значение и надеется на лучшее. Стандартным
значением для аргументов int является О, что и приводит к генерации исключения.
В методе Index () значение аргумента id применяется при извлечении объекта мо­
дели из хранилища:

puЫic ViewResult Index(int id) => View(repository[id ] );

Когда MVC использует стандартное значение, метод действия пытается извлечь


объект модели сid, равным О. Такой объект не существует, из-за чего хранилище воз­
вращает значение null, которое затем передается методу View () контроллера, что­
бы указать данные модели представления для представления Index. cshtml. Когда
выражение Razor в файле Index. cshtml пытается обратиться к свойствам объекта
модели представления, возникает исключение NullReferenceException, как было
проиллюстрировано на рис . 26.3 .
802 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

Это означает, что м етоды действий должны быть написаны так, чтобы справиться
со стандартными знач ениями , предоставляемыми систе мой привя з ки м одел ей, что
может быть сделано несколькими способами. Можно добавить стандартные значе­
ния в шаблоны URL маршрутов (как было описано в главе 15), присвоить стандар­
тные значения параметрам методов действий или обесп еч ить , чтобы методы де й с­
твий не передавали недопустимые значения данных в качестве части своих ответов .
Предпочтительный подход будет зависеть от того , что предприним ает метод действия;
в листинге 26.10 избран последний подход , который предусматривает изм е н е ние м е­
тода действия так, что он всегда передает методу View ( ) объект Person, даже если
аргумент id не соответствует какому-то объекту в модели данных.

Листинг 26.1 О. Защита от стандартных значений привязки модели


в файле HomeController. cs

using Mic r os o f t.AspNetCore. Mvc ;


us i ng MvcMode l s . Models ;
using System.Linq;
names pace MvcModels . Contro l lers
puЫ ic class HomeControlle r : Cont r o l ler
private I Repos it ory repository ;
puЫic HomeControl l er( I Reposi t ory repo) {
repository = repo ;

puЫicViewResult Index(int id) =>


View(repository[id] ?? repository.People.First());

В методе действия с при м енением LINQ и операции объединения с null возвра­


щается первый объект из хранилища, если значение параметра id не прив ело к из­
влечению объекта.

Привязка простых типов


Когда доступно подходящее значение, оно преобразуется в значение типа С#, ко­
торое может использоваться при вызове метода действия. Просты е типы - это зна ­
чения, порожденны е от одного элемента данных в запросе, который может быть ра­
з обран из строки. В их состав входят числовые значения , булевские знач е ния , даты
и, разумеется, значения st ri ng .
Аргум ент id метода действия Index ( ) имеет тип int, так что процесс привязки
модел ей снабжает инфраструктуру MVC значением за счет разбора пер еменной сег­
мента id в знач ение int .
Если значение из запроса не может быть преобразовано (например, для пара­
метра, требующего знач е ние i n t, указано значение a p p le), тогда процесс привязки
моделей окажется неспособным предоставить значение приложению, и будет приме­
няться стандартное з начение.

В итоге возникает проблема, т. к. метод действия будет получать стандартное зна­


чение О в двух ситуациях. Первая из них - когда запрос содержит значение, которое
не может быть преобразовано в тип аргумента , как в URL вида /Home/Index/Apple.
Вторая ситуация - когда з апрос содержит значение, поддающее ся преобразованию ,
но равное О, как в URL вида /Home/Index / 0.
Глава 26. Привязка моделей 803
Большинству приложений нужна возможность различения таких ситуаций, и лег­
че всего это делается с использованием для аргумента метода действия типа, допус­
кающего null (листинг 26.11).

Листинг 26.11. Применение типа, допускающего null, в файле HomeController. cs


using Microsoft.AspNetCore.Mvc;
using MvcModels.Models;
namespace MvcModels.Controllers
puЫic class HomeController : Controller
private IRepository repository;
puЫic HomeController(IRepository repo) {
repository = repo;

puЫic IActionResult Index(int? id) {


Person person;
if (id.HasValue && (person = repository[id.Value]) != null) {
return View(person);
else {
return NotFound();

Стандартным значением для типов, допускающих null, является null. Оно поз­
воляет проводить различие между запросами, не содержащими значение, которое

может быть преобразовано в int, и запросами со значением, поддающимся преобра­


зованию в int, но равным О. Реализация метода Index () в этом примере использует
метод NotFound () для возвращения ошибки 404, если аргумент типа, допускающего
null, не имеет значения или если его значение не соответствует какому-то объекту в
модели. Такой подход более надежен, чем просто полагаться на то, что первый объект
в модели окажется подходящим, как делалось в предыдущем разделе.

ПривS1зка сложных типов


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

Чтобы посмотреть, как это работает, добавьте в контроллер Horne два метода действия
(листинг 26.12).
Листинг 26.12. Добавление методов действий в файле HomeController. cs
using Microsoft.AspNetCore.Mvc;
using MvcModels.Models;
namespace MvcModels.Controllers
puЫic class HorneController : Controller
private IRepository repository;
804 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

puЫic HomeController(IRepository repo ) {


repository = rep o ;

puЫic IAc tionResult Index(int? id) {


Person person ;
if (id . HasValue && (person = repository[id . Value]) ! = null) {
return View(person) ;
else {
return NotFound() ;

puЬlic ViewResul t Create () => View (new Person ()) ;


[HttpPost]
puЫic ViewResult Create (Person model) => View("Index" , model);

В ерсия метода
Create () без пар аметров создает новый объект Person и п ере­
дает его методуView (), который выбирает стандартное представление, ассо цииро­
ванное с действием. Добавьте в папку Views/Home файл пр едставления по имени
Create . cshtml и пом естите в н его разметку, прив еденную в листинге 26.13.

Листинг 26.13. Содержимое файла Create.cshtml из п апки Views/Horne


@model Person
@{
ViewBag . Title = "Create Person ";
Layout = " Layout ";

<form asp - action="Create " method= " post " >


<div class= " form - group " >
<labe l asp - for="Person i d " ></label>
<input asp - for= " Personid " class= " form - contro l" />
</div>
<div class= " form - group " >
<label asp - for= " FirstName " ></ label >
<input asp-for= " FirstName " class= " form-control" />
</div>
<div c l ass= " form - group " >
<label asp - for= "LastName " ></ label >
<input asp-for= "LastName " class= " form - control " />
</div>
<div class= " form-group " >
<label asp - for= " Role " ></label>
<select asp - for= " Role " class= " form - control "
asp - items= " @new SelectList(Enum . GetNames(typeof(Role)))">
</select>
</div>
<button type="submit" c lass="Ьtn Ьtn - primary " >S ubmi t</button>
</form>
Глава 26. Привязка моделей 805
Представление содержит форму, позволяющую предоставлять значения для ряда
свойств объекта Person, элемент form которой отправляет данные обратно версии
метода Create () контроллера Ноте, декорированной атрибутом HttpPost.
Метод действия, который получает данные формы, использует для их отображе­
ния представление /Views/Home/Index . cshtml. Чтобы посмотреть на все это в ра­
боте, запустите приложение, перейдите к URL вида /Home/Create, заполните форму
и щелкните на кнопке Submit (Отправить); результат показан на рис. 26.4.

1~ Cr•at• Pe~on Х
1 r-·-------------
t-+- -+ _ С . : [J~call1ost:~~~:.~~:~~~~-::r-=-
Personld
l ocal host: S186 1 /Нomo/C Х

100 С С:! tocalho st :51861/H o me/Cгeate <{;:s _

FirstName

Peter

LastName
Personld: 100
James First Name: Peter
1

~~'~-·--·----.. ~ ~:::№me
James

Guest
l--~~-~~----·
'--------
---_]
Рис . 26.4. Применение методов действий Crea te ()

После отправки данных формы серверу процесс привязки моделей обнаруживает,


что метод действия требует сложного типа - объекта Person. Класс Person исследу­
ется на предмет открытых свойств. Для каждого свойства простого типа связыватель
модели пытается найти значение в запросе, как делалось в предыдущем примере.
Таким образом, например, связыватель модели встречает свойство Personid и
ищет значение Personid в тех же самых местах. где производился поиск значения
id в предыдущем разделе. Поскольку данные формы содержат подходящее значение,
настроенное с использованием дескрипторного вспомогательного класса asp - for в
элементе i nput, именно это значение и будет применено .
Если свойство требует другого сложного типа, тогда процесс повторяется для ново­
го типа. Сначала получается набор открытых свойств и связыватель пытается найти
значения для всех этих свойств. Разница в том, что имена свойств являются вложен­
ными. Например, свойство HomeAddress класса Person имеет тип Address , как вы­
делено ниже:
806 Часть 11. Подробные сведения об инфраструктуре ASP.NET Саге MVC

using System ;
namespace MvcModels . Models
puЫic class Person {
puЫic int Personid { get ; set ; }
puЫic string FirstName { get; set;
puЬlic string LastName { get ; set; }
puЫic DateTime BirthDate { get; set ; )
puЫic Address HomeAddress { get; set; }

puЫic bool IsApproved { get; set; )


puЫic Role Role { get ; set ; )

puЫic class Address {


puЫic string Linel { get; set; }
puЫic string Line2 { get; set; }
puЫic string City { get; set; }
puЫic string PostalCode { get; set;
puЬlic string Country { get; set; }

puЫic enum Role {


Admin,
User,
Guest

При поиске значения для свойства Linel связыватель модели ищет значение для
HomeAddress. Linel, т.к. имя свойства в объекте модели комбинируется с именем
свойства во вложенном типе модели.

Создание легко привязываемой НТМL-разметки


Использование префиксов означает, что представления должны включать инфор­
мацию. интересующую связыватель модели. Это легко делается с применением де­
скрипторных вспомогательных классов, которые автоматически добавляют требуе­
мые префиксы к трансформируемым элементам. В листинге 26.14 форма расширена,
чтобы принимать данные адреса.

Листинг 26. 14. Обновление формы в файле Crea te . csh tml

@model Person
@{
ViewBag.Title = "Create Person";
Layout = " _Layout";

<form asp - action= " Create " method="post">


<div class= " form - group">
<label asp - for= " Personid"></label>
<input asp -f or= " Personid " class= "form- control " />
</div>
<div class= " form - group " >
<label asp - for= " FirstName " ></label>
<input asp-for= " FirstName " class= " form - control " />
</div>
Глава 26. Привязка моделей 807
<div class= " form - group " >
<label asp - for= " LastName " ></label>
<input asp - for= " LastName " c l a s s =" form - cont r o l" />
</div>
<div c l ass= " form - group " >
<label asp - for= " Role " ></labe l >
<select asp - fo r=" Role " c l ass ="f o r m- co n t r ol "
asp - items= " @new SelectLis t (Enum . GetNames(typeof(Role) )) " >
</select>
</div>
<div class="form-group">
<label asp-for="HomeAddress.City"></label>
<input asp-for="HomeAddress.City" class="form-control" />
</div>
<div class="form-group">
<laЬel asp-for="HomeAddress.Country"></laЬel>
<input asp-for="HomeAddress.Country" class="form-control" />
</div>
<button type= " submit " class= "bt n Ьtn - primary " >Submit</button>
</form>

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


тва указывается с применением соглашений С# , так что внешние и вложенные имена
свойств отделяются точками : Home Addre s s. Coun t r y . Запустив приложени е , запро­
сив URL вида /Home/Create и просмотрев НТМL-разметку. отправл е нную брауз еру .
вы заметите , что для некоторых атрибутов используется другое соглашение:

<div class= " form - group " >


<laЬel for="HomeAddress_City">City</label>
<input class="form-control" type="text" id="HomeAddress City"
name="HomeAddress. City" value="" /> -
</div>
<div c l ass =" form - group " >
<label for="HomeAddress_Country">Country</laЬel>
<input class="form-control" type="text" id="HomeAddress Country"
name="HomeAddress. Country" value="" /> -
</div>
Атрибуты name элементов i nput следуют стилю С#, но в атрибутах fo r эл ементов
labe l и атрибутах id эле ментов input имена свойств отделяются с помощью сим во­
лов подч еркивания . Если вы предпочитаете определять НТМL- элем енты без дескрип ­
торных в с по м огательных кл ассов, то должны обеспечить прим е нение одной и той же
схем ы именования.

Как следствие , предпринимать какие-либо специ альные действия, чтобы гаранти­


ровать создани е связывателем модели объ екта Address для свой ства HomeAddress
не понад обится. Это мо ж но прод е монстрировать, и з м е нив представление
Index . cshtml для отображения свойств HomeAddress . когда они отправляются из
фор м ы (листинг 26. 15).
Листинг 26.15. Отображение свойств HomeAddress в файле Index. cshtml

@model Person
@{ Layout = " Layout ";
<div class = " bg - primary panel - bo d y " ><h2>Person</h2></div>
808 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC

<tаЫе class= " taЫe taЫ e - co ndensed taЫe-bordered taЫe-striped">


<t r ><th>Personid : </t h ><td>@Model.Person id</ td ></tr>
<t r> <th>First Name:</th >< t d>@Mo d e l.Firs tName</td></tr>
<tr><th>Last Name : </th ><td>@ Model.LastNa me</td> </t r>
<t r ><th>Role : </ th><td>@Model . Role</td> </ tr>
<tr><th>City:</th><td>@Model.HomeAddress?.City</td></tr>
<tr><th>Country:</th><td>@Model.HomeAddress?.Country</td></tr>
</tаЫе>

Запустив приложение и перейдя на URL вида / Home /Create , можно ввести значе­
ния для свойств Ci ty и Country , после чего отправить форму и убедиться, что зна­
чения были привязаны к объекту модели (рис . 26.5).

1 Creatt P~rson Х

Е- ~ СD localhost:51861 /Home/Create
---- - ·---- ----~ --~-------_,__::.__ ____
Personld

100 f loc.Jiho:; t:5 1 861/Нomt-/C х

FirstName
+- ~ с iёi"i~;~ih;;-s~s18.611н;;~;ё;;;;t;-".---·-···-········ --:& · =1
John

LastName

Peters Personld: 100

Flrst Name: John


Role
Last Name: Peters
Guest
Role: Guest

_ _ _ _ _____
City City: Paris
Paris Country: France

Country г __J
Frar1ce

iiN 1
·---------··-____J
Рис. 26.5. Привязка свойств в сложных объектах

Указание специальных префиксов


Встречаются ситуации, когда генерируемая НТМL-разметка относится к одному
типу объекта, но ее необходимо привязать к другому типу. Это означает, что префик­
сы, содержащиеся в представлении, не будут соответствовать структуре, которую
ожидает связыватель модели, и данные не смогут быть правил ьно обработаны. Чтобы
взглянуть на проблему, добавьте в папку Models файл по имени AddressSumma r y . cs
с определением класса, показанным в листинге 26. 16.
Глава 26. Привяз ка моделей 809
Листинг 26.16. Содержимое файла AddressSummary. cs из папки Models
namespace MvcModels.Models {
puЫic class AddressSurnmary {
puЫic string City { get; set;
puЫic string Country { get; set;

Добавьте в контроллер Ноте новый метод действия, который использует класс


AddressSummary (листинг 26.17).

Листинг 26.17. Добавление метода действия в файле HomeController. cs


@model AddressSummary
@{
ViewBag . Ti t l e = " DisplaySummary ";
Layout = "_Layout";

<div class= "bg-primary panel - body"><h2>Address</h2></div>


<tаЫе class= "taЫe taЫe-condensed taЫe-bordered taЫe - striped " >
<tr><th>City : </th><td>@Model . City</td></tr>
<tr><th>Country : </th><td>@Model . Country</td></tr>
</tаЫе>

Новый метод действия называетсяDisplaySummary (). Он принимает параметр


AddressSummary , который передается методу
View (),так что параметр может быть
отображен в стандартном представлении. Создайте в папке /Views/Home файл
DisplaySшmnary . cshtml и поместите в него разметку, приведенную в листинге 26.18.

Листинг 26.18. Содержимое файла DisplaySummary. cshtml из папки /Views/Home


@model AddressSummary
@{
ViewBag . Ti tle = "DisplaySummary ";
Layout =" Layout";

<div class ="bg-primary panel-body"><h2>Address</h2></div>


< t аЫе class= " taЫe taЫe-condensed taЬle-bordered taЫe-striped">
<tr><th>City : </th><td>@Model .City</td></tr>
<tr><th>Country : </th><td>@Model .Country</td></tr>
</tаЫе>

Представление отображает значения двух свойств, определенных в классе


AddressSummary. Для иллюстрации проблемы с префиксами во время привязки мо­
делей разных типов модифицируйте элемент form в представлении Create. cshtml,
чтобы он отправлял данные действию DisplaySummary (листинг 26. 19).
810 Часть 11. Подробные сведения об инфраструктуре ASP.NET Саге MVC

Листинг 26.19. Изменение целевого действия формы в файле Create.cshtml


@model Person
@{
ViewBag . Title = " Create Person ";
Layout = " Layout ";

<form asp-action="DisplaySummary" method="post">


< ! -- Для краткости НТМL-элементы не показаны -->
</form>

Запустив прилож ение и перейдя на URL вида /Home/CreatePerson, можно взгля­


нуть на происходящее. После отправки формы значения, которые были введены для
свойств City и Country, не отображаются в НТМL-разм етке , сгенерированной пред­
ставлением DisplaySummary.
Проблема в том, что атрибуты name в форме имеют префикс HomeAddress, не
являю щийся тем, который ищет связыватель модели при попытк е привяз ат ь тип

AddressSummary .
Чтобы исправить проблему, к параметру метода действия можно прим е нить ат­
рибут Bind, в котором указывается префикс , подлежащий испол ьзованию во время
привязки модели (листинг 26.20).

Листинг 26.20. Изменение префикса привязки модели в файле HomeController. cs


using Microsoft . AspNetCore .Mvc;
using MvcModels . Models ;
namespace MvcModels . Controllers
puЫic class HomeController : Controller
private IRepository repository ;
puЫic HomeController(IRepository repo) {
repository = repo ;

puЬlic IActionResult Index(int? id) {


Person person;
if (id .HasValue && (person = repository [i d . Value]) != null) {
return View(person) ;
else {
return NotFound() ;

puЫic ViewResult Create(} => View(new Person()) ;


[HttpPost ]
puЫic ViewResult Create(Person model) => View( "Inde x ", model) ;

puЫic ViewResult DisplaySumma ry(


[Bind(Prefix = nameof(Person.HomeAddress))] AddressSummary summary)
=> View ( summary) ;
Глава 26. Привязка моделей 811
Синтаксис неудобен, но результат приемлем. При заполнении свойств объек­
таAddressSurnmary связыватель модели будет искать в запросе значения данных
HomeAddress. Ci ty и HomeAddress. Country. Запустив приложение и отправив
форму еще раз, вы увидите, что значения, введенные в поля City и Country, теперь
отображаются корректно (рис. 26.6). Решение может выглядеть слишком длинным
для такой простой проблемы, но потребность в привязке разных видов объектов воз­
никает на удивление часто, и об этом приеме полезно знать.

r Cte.:ite Pt ( SOn х

1 ~ -+ С ·с;- locaih-;;~·-t-
:5-'18=6=1/=H=onie/Cr;;\;-
1--·---~----·----·-
· ··-·-·~-· ·-·--·-----·--'-"-------~---·
Personld

100

"l
t Dtspl&ySummг.ry
FirstName

John
" ~•lh~"s""/Homo/D~pl•,Som~ <:,
LastName

Pelers

City: Paris
Role

С~у
User
Country: France

_J
Paris

Рис. 26.б. Привяз ка свойств другого объектного типа

Избирательная привязка свойств


Предположим, что свойство Country класса AddressSummary является критичес­
ки важным, и пользователь не должен иметь возможность указывать для него зна­

чения. Первое, что мы можем предпринять - не допустить, чтобы пользователь мог


видеть или редактировать свойство , удостоверившись в отсутствии внутри представ­
лений приложения любых НТМL-элементов, которые ссылаются на данное свойство.
Тем не менее, злоумышленник может просто отредактировать данные формы,
посьJлаемые серверу, во время отправки формы и указать нужное ему значение для
свойства Country. На самом деле связывателю модели необходимо сообщить о том,
чтобы он не привязывал свойство Country к значению из запроса. Для этого можно
сконфигурировать атрибут Bind на параметре метода действия, указав имена только
тех свойств, которые должны привязываться (листинг 26.21) .
812 Часть 11. Подробные сведения об инфраструктуре ASP.NET Соге MVC

Листинг 26.21. Указание свойств, подлежащих привязке, в файле HomeController. cs


us i n g Mi crosoft . AspNe tCore. Mvc ;
u sing MvcModels . Models ;
n ame s pace MvcModel s. Cont r ol l er s
pu Ы ic c l ass HomeCon t ro l le r : Co nt roller
p r ivate IRepos i to r y r epositor y;
puЫ ic HomeCon t r ol l er(IRep o si to r y re p o) {
re pository = r e po ;

puЫic I ActionRes ult I ndex(i nt ? i d ) {


Pe r s on per s on ;
if (id . Ha s Valu e && (pe rson = repository [i d . Val u e ] ) ! = nu ll )
r etur n View( p e r son) ;
e l se {
return No t Fou n d () ;

p uЬlic ViewRes u lt Create() => View(new Person()) ;


[ HttpPost]
puЫic ViewResu lt Cr e ate(Pe r son mod el) => View( " Inde x ", model) ;
pu Ыi c ViewResu lt Displ a y Summary (
[Bind(nameof(AddressSununary.City),
Prefix = nameof(Person.HomeAddress))]
AddressS umma r y summary) => Vie w(s ummary) ;

Первый аргумент атрибута Bi nd представляет собой разделяемый запятыми спи­


сок имен свойств, которые должны быть включены в процесс привязки моделей.
В листинге 26.21 указано, что свойство Ci ty должно быть включено в процесс, а
поскольку свойство Count r y в спис ке отсутству ет, оно будет исключено из процесса
привязки м оделей.
Запустив приложение. запросив URL вида/Home/ Creat e, заполнив форму и от­
правив ее, вы заметите, что для свойства Country никакого значения не отобр аж ает­
ся, хотя оно было отправлено браузером как часть НТТР-запроса POS T (рис . 26.7).

1 D»pl•ySumm•<y Х

._ -> С 'Dlocall10st:S1861/нome/DisplaySummary 'tl -


--'----~------·------·-------

City: Paris

Country:

Рис. 26. 7. Исключение свойства из процесса привязки моделей


Глава 26. Привязка моделей 813
Когда атрибут Bind применяется к параметру метода действия, он влияет только
на экземпляры данного класса, которые привязываются для этого метода действия; все
остальные методы действий продолжат попытки привязать все свойства, определенные
типом параметра. Если нужно получить более обширный эффект, тогда атрибут Bind
можно применить к самому классу модели, как демонстрируется в листинге 26.22.
Листинг 26.22. Применение атрибута Bind в файле AddressSummary. cs

using Microsoft.AspNetCore.Mvc;
namespace MvcModels . Models {
[Bind(nameof(City))]
puЫic class AddressSummary
puЫic string City { get ; set;
puЫic string Country { get; set;

Можно также исключать свойства явным образом, декорируя их атрибутом


BindNever, как показано в листинге 26.23, но это означает, что новые свойства, до­
бавляемые в класс модели, будут включены в процесс привязки моделей, если только
вы не забудете применить к ним данный атрибут.

Листинг 26.23. Применение атрибута NeverBind в файле AddressSummary. cs

using Microsoft . AspNetCore . Mvc;


using Microsoft.AspNetCore.Mvc.ModelBinding;
namespace MvcModels . Models {
puЫic class AddressSummary {
puЫic string City { get; set;
[BindNever]
puЫic string Country { get; set;

Совет. Имеется также атрибут BindRequired, который сообщает процессу привязки моде­
лей о том, что запрос должен включать значение для свойства. Ели запрос не содержит
требуемого значения, тогда возникает ошибка проверки достоверности модели, как будет
объясняться в главе 27.

Привязка массивов и коллекций


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

Привязка массивов

Одной из элегантных возможностей стандартного связывателя модели является

его поддержка параметров методов действий, которые являются массивами. В целях


демонстрации добавьте в контроллер Home новый метод по имени Names (),код кото­
рого показан в листинге 26.24.
814 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Саге MVC

Листинг 26.24. Добавление метода действия в файле Homecontroller. cs


using Microsoft . AspNetCore . Mvc ;
using MvcModels.Models;
namespace MvcModels . Controllers
puЫic class HomeController : Contro ll er
private I Repository repository;
puЫic HomeController(IRepository repo) {
repository = repo ;

11 .. . для кра~'кости другие методы действий не показаны ...

puЬlic ViewResult Names(string[] names) => View(names ?? new string[O]);

Метод действия Names () имеет параметр типа строкового массива по имени


names . Связыватель модели будет искать любые элементы данных под названи ем
names и создавать массив, содержащий эти значения. Чтобы снабдить метод дейс­
твия представлением, создайте в папке Views/Home файл Razor по имени Names .
csh tml и поместите в него разметку из листинга 26.25.

Листинг 26.25. Содержимое файла Names. cshtml из папки Views/Home


@model string []
@{
ViewBag . Title = "N ames ";
Layout = "_La yout ";

@if (Model.Length == 0)
<form asp - action= " Names" method="post " >
@for (int i = О ; i < 3 ; i++ ) {
<div class= " form - group " >
<label>Name @(i + 1) :</ l abe l >
<input id= " names " name="names" class= " form - control " />
</div>

<bu t ton type =" submit" class= "b tn btn - primary " >Submit</button>
</form>
else {
<tаЫе c lass="t aЫe taЬl e- conden sed taЬle - bordered taЬle - striped " >
@foreach (strin g name i n Model) {
<tr><th>Name : </th><td>@name</ td ></t r >

</tаЫе>
<а a sp-a ctio n="Names " class= " btn btn-primary">Back</a>

Это представление отображает разное содержимое на основе количества элемен­


тов в модели представления. Если элементы отсутствуют, то представление выводит
форму, которая содержит три идентичных элемента input:
Глава 26. Привязка моделей 815

<form method="post" action="/Home/Names">


<div class="form- group">
<label>Name 1:</label>
<input id="names" name="names" class="form-control" />
</div>
<div class="form-group">
<label>Name 2 : </label>
<input id="names" name="names" class="form-control" />
</div>
<div class="form-group">
<label>Name 3:</label>
<input id="names" name="names" class= " form-control" />
</div>
<button type="submit" class="Ьtn Ьtn-primary">Submit</button>
</form>

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


метод действия принимает массив, и производит поиск элементов данных, которые

имеют такое же имя, как у параметра метода действия. В настоящем примере это
означает, что все значения из элементов input с атрибутом name, установленным в
names, будут собраны вместе для создания массива и его использования в качестве
аргумента при вызове метода действия. Чтобы взглянуть на результат, запустите при­
ложение, перейдите на URL вида /Home/Names и заполните форму. Отправив форму,
вы заметите, что отображаются все введенные значения (рис. 26.8).

1 +- ~ С D loca lhost:51861/Home/Names
1--:-m~-1:--·------IBNalo,l"llllBBll

·,1 N
::',. 1 <- ~ С ~6'oco1h;;;,;-i0i/Нo;;of";~::°' -=~ -
Joe Name. Joe
i 1 Nam~ Реtе г
NameЗ: ~
l -;;e ler_________ ------·J •· ·
L______ .
i
ifi' L____ ___·~~~---------~--~---------~

Рис. 26.8. Привязка моделей для массивов

Привязка коллекций
Процесс привязки моделей способен создавать не только массивы . Он также
поддерживает классы коллекций. В листинге 26.26 тип параметра метода действия
Narnes () изменен на строго типизированный список.
816 Часть 11. Подробные сведения об инфраструктуре ASP.NET Саге MVC

Листинг 26.26. Применение строго типизированной коллекции в файле


HomeController.cs
using Microsoft . AspNetCore.Mvc;
using MvcModels . Models ;
using System.Collections.Generic;
namespace MvcModels . Contro ll ers {
puЫic class HomeController : Controller
private I Repository repository ;
puЫic HomeController(IRepository repo) {
repository = rep o ;

// .. . для краткости другие методы действий не показаны ...


puЫic ViewResul t Names (IList<string> names) =>
View(names ?? new List<string>());

Здесь используется интерфейс IList<T>. Указывать конкретный класс реализа­


ции не нужно, хотя при желании это можно было бы сделать. В листинге 26.27 приве­
дено моди фицированное содержимое файла представления Names. cshtml, в 1<0тором
задействован новый тип модели.

Листинг 26. 27. Применение коллекции в качестве типа модели в файле Names . csh tml
@model IList<string>
@{
ViewBag.Title = "Names ";
Layout = "_Layout ";

@if (Model . Coun t == О)


<form asp - action= " Names " method="p ost " >
@for (int i = О ; i < З ; i++) {
<div class= "form- group ">
<label>Name @(i + 1) :</label>
<input id= " names " name= "names " class= " form - control " />
</div>

<button type= "submit " class= "Ьtn Ьtn-pr ima ry " >Submit</button>
</form>
else {
<tаЫе class= " taЫe taЫe - condensed taЫe - bordered t aЫe - striped">
@foreach (string name in Model) {
<tr><t h>Name : </th><td>@name</td></t r>

</tаЫе>
<а asp-action= "Names " class= "btn btn- primary " >Back</a>

Функциональность действия Names не изменилась , но теперь оно способно рабо­


тать с классом коллекции вместо массива .
Глава 26 . Привязка моделей 817

Привязка коллекций сложных типов


Индивидуальные знач ения данных можно также привязьmать к массиву сложных
типов, что позволя ет собирать из единственного запроса множество объектов (таких
как класс модел и AddressSurnmary в примере). В листинг е 26.28 контроллер Home
дополня ется новым методом действия по им ени Address () ,парам етр которого пр ед­
ставляет собо й списо1' объектов AddressSurnmary.

Листинг 26.28. Определение метода действия в файле HomeController. cs


using Microsoft.AspNetCore . Mv c ;
us i ng MvcMode l s . Model s;
using System . Col l ections . Gene ri c ;
namespace MvcModels . Contro ll ers {
puЬlic class HomeController : Co nt r o l ler
private IRepository repository ;
puЫic HomeController(IRepos i to r y r epo ) {
repository = repo ;

// .. . для краткости другие методы действий не показаны ...


puЬlic ViewResult Address(IList<AddressSummary> addresses) =>
View(addresses ?? new List<AddressSummary>());

Чтобы снабдить новый м етод действия представлени е м, добавьте в папку Views/


Home файл по и м ени Address . cshtml с разметкой из листинга 26.29.

Листинг 26.29. Содержимое файла Address. cshtml из папки Views/Home


@model IList<AddressSumma r y>
@{
Vie wBag . Title = "Address ";
Layout = " Layou t";

@if (Model. Count () == О) {


<form asp - action= "Address " me t hod=" pos t" >
@for (int i = О ; i < 3 ; i++) {
<fieldset class= " form - gro up " >
<legend>Address @(i + l)</legend >
<div c l ass= " form - group " >
<l abel>City:</labe l >
<input name =" [@i] . City " clas s = " form- co ntrol " />
</div>
<div class= " form - group " >
<label>Country : </ l abe l >
<inp ut name =" [@i ] . Country " c l a s s= " for m- contro l" />
</div>
</fie l dset>

<button type= " submit " clas s ="b tn bt n-p rima r y " >Submi t</button>
</form>
818 Часть 11 . Подробные с ведения об и нфрастру ктуре ASP.NET Core MVC

else (
<tаЫе class= " taЫe taЫe - condensed taЫe - bordered taЫe - striped">
<tr><th>City</th><th>Country</th></tr>
@foreach (var address in Model) {
<tr><td>@address . City</td><td>@address . Country</td></tr>

</tаЫе>
<а asp - action="Address" c l ass= " btn btn - primary " >Back</a>

Пр едста вле ни е визуализи рует эле м ент form , е сли элеме нты в коллекци и м одели
отсут ствуют. Эл еме нт form с о стоит из пары элеме нтов input, атри бут ы name кото ­
рых имеют префи ксы в виде индексов м ассива :

<form method= " post " action= " /Home/Address " >
<fieldset class= " form - group " >
<legend>Address l</legend>
<div class= " form-group " >
<label>City : </label>
<input name="[O] .City" class="form-control" />
</div>
<div class= " form - group " >
<label>Country : </label>
<input name="[O] .Country" class="form-control" />
</div>
</fieldset>
<fieldset class= " form - group " >
<legend>Address 2</legend>
<div class= " form - group " >
<label>City:</label>
<input name=" [ 1] . Ci ty" class=" form-control" />
</div>
<div class= " form - group " >
<label>Country : </labe l >
<input name=" [1] .Country" class="form-control" />
</div>
</fieldset>
<fieldset class= " form - group " >
<legend>Address 3</legend>
<div class= " form - group " >
<label>City : </label>
<input name=" [2] .City" class="form-control" />
</div>
<div class= " form - group">
<label>Country:</label>
<input name=" [2] .Country" class="form-contr ol" />
</div>
</fieldset>
<button type= " submit " class= " Ьtn Ьtn - primary " >Submit</button>
</form>
Глава 26. Привязка моделей 819
Когда форма отправляется, связыватель модели выясняет, что необходимо создать
коллекцию объектов AddressSummary, и использует префиксы индексов массива в
атрибутах name, чтобы получить значения для свойств этих объектов. Свойства с
префиксом [О ] применяются для первого объекта AddressSumary, свойства с пре­
фиксом [ 1] - для второго объекта AddressSumary и т.д.
Представление Address . cshtml определяет элементы input для трех таких
индексированных объектов и отображает их, если коллекция модели содержит эле­
менты. Перед тем, как можно будет продемонстрировать это в работе, понадобится
удалить атрибут BindNever из класса модели AddressSummary, иначе связыватель
модели проигнорирует свойство Country (листинг 26.30).
Листинг 26.30. Удаление атрибута BindNever в файле AddressSurnmary. cs

using Microsoft . AspNetCore.Mvc;


using Microsoft . AspNetCore . Mvc . ModelBinding ;
namespace MvcModels . Models {
puЫic class AddressSummary {
puЫic string City { get; set;
// [BindNever]
puЫic string Country { get; set ;

Запустив приложение и перейдя на URL вида /Home/Address, можно понаблю­


дать за работой процесса привязки для коллекций специальных объектов. Введите
значения в полях для города и страны, после чего щелкните на кнопке Submit, чтобы
отправить форму серверу.
Процесс привязки моделей найдет и обработает индексируемые значения данных
и использует их для создания коллекции объектов AddressSummary, предоставляе­
мой методу действия, который затем применяет удобный метод View (),чтобы пере­
дать их обратно представлению с целью отображения (рис. 26.9).

1 .AddttSS Х

Address 1
City:

London
~-~ ::____с _D !.0 :·~~~~:~1:~у~~'::~/д!~'.':'~ ·- _ -~ "-- ='
1
1
Country: City Country

United Кingd om 1 London Unlted Kingdo111

Paris Fraпce

Berlin Germany

1
Country: 1111
Germeny 1
L----------------~--------~

_ __________J
Рис. 26.9. Привязка коллекций специальных объектов
820 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

Указание источника данных привязки моделей


Как объяснялось в начале главы , стандартный процесс привяз1<и моделей ищет
значения в трех местах : отправленные данные формы, переменные маршрутизации
и строки запросов .

Стандартная последовательность поиска не всегда полезна , либо потому. что дан­


ные должны всегда поступать из специфической части запроса, либо потому, что
нужно использовать источник данных, в котором поиск по умолчанию не произво­

дится . Средство привязки моделей включает набор атрибутов. которые применяются


для переопределения стандартного поведения поиска и описаны в табл. 26.3.

Таблица 26.3. Атрибуты для источников данных привязки

Имя Описание

FrornForrn Этот атрибут используется для выбора данных формы в качестве источника
данных привязки. По умолчанию для нахождения значения данных формы
применяется имя параметра, но такое поведение можно изменить с исполь­

зованием свойства Narne, которое позволяет указывать другое имя

FrornRout e Этот атрибут применяется для выбора системы маршрутизации в качестве


источника данных привязки. По умолчанию для нахождения значения данных
маршрута используется имя параметра, но такое поведение можно изменить
с применением свойства Narne, которое позволяет указывать другое имя

FrornQue r y Этот атрибут используется для выбора строки запроса в качестве источника
данных привязки. По умолчанию для нахождения значения строки запроса при­
меняется имя параметра, но такое поведение можно изменить с использовани­

ем свойства Narne, которое позволяет указывать другой ключ строки запроса

FrornHeader Этот атрибут применяется для выбора заголовка запроса в качестве источ­
ника данных привязки. По умолчанию имя параметра используется как имя
заголовка, но поведение можно изменить с применением свойства Narne,
которое позволяет указывать другое имя заголовка

FrornBody Этот атрибут используется для указания, что в качестве источника данных
привязки должно применяться тело запроса. Требуется в ситуациях , когда
из запросов необходимо получать данные, не закодированные как данные
формы, что происходит в контроллерах API

Выбор стандартного источника данных привязки


Атрибуты FrornFo r rn, Fr ornRout e и FrornQuery позволяют указывать , что данные
привязки модели будут получаться от одного из стандартных местоположений, но
без нормальной последовательности поиска. Ранее в главе использовался следующий
URL:
/ Ho me/ In d ex /З?i d =l

Приведенный URL содержит два возможных значения , которые могут применяться


для параметра i d метода действия Index () в контроллере Н оте. Система маршрути­
зации назначит последний сегмент URL переменной по имени id, которая определена
в шаблоне URL внутри класса St a r tup. Строка запроса также содержит значение id.
Стандартный шаблон поиска означает, что значения привязки модели будут извле­
каться из данных маршрутизации, а строка запроса игнорируется.
Глава 26. Привязка моделей 821
Чтобы изменить тако е поведе ние, в листинге 26.31 к м етоду действия применя­
ется атрибут FromQuery . Ради простоты также удалены вс е остал ьные методы дейс­
твий, которые были определены в предшествующих примерах.

Листинг 26.31. Выбор строки запроса для привязки модели в файле HomeController. cs
using Microsoft . AspNetCore . Mvc ;
using MvcMode l s . Models ;
namespace MvcModels . Contro l lers
puЫic class HomeController : Contro l ler
private IRepos i tory repository ;
puЫic HomeController(IReposito ry r epo) {
repository = repo ;

puЫic IActionResul t Index ( [FromQuery] int? id) {


Person person ;
if (id . HasVal ue && (person = r e po s i t ory[id . Value]) != null) {
ret urn View(person) ;
else {
return NotFound() ;

Здесь атрибут FromQuery пр именяется к параметру id, т.е . во время поиска про­
це ссо м привязки моделей з начения данных для id будет использоваться только стро­
ка з апрос а .

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

И спользование заголовко в в качеств е источников данных привязки


Атр и бут FromHeader позволя ет задействовать заголовки НТГР-запроса как источ ­
н ики данных привязки. В листинге 26.32 в контроллер Home добавлен простой метод
дей ствия, который получает при вязку параметра с прим е не нием данных из стандар­
тного з аголовк а НТГР-запро с а.

Листинг
26.32. Привязка модели из заголовка НТТР-запроса в файле
HomeController.cs
using Microsoft . AspNetCore . Mvc ;
using MvcModels . Models ;
namespace MvcModels . Control l ers
puЫic class HomeController : Controller
private IRepository repository ;
822 Часть 11 . Подробные сведения об инфраструктуре ASP.NET С аге MVC

puЫic HomeController(IRepository repo) {


repository = repo;

puЫic IActionResult Index( [FromQuery] int? id) {


Person person ;
if (id .H asValue && (person = repository[id.Value]) ! = null) {
return View(person) ;
else {
return NotFound();

puЬlic string Header ( [FromНeader] string accept) => $ 11 Header: {accept} 11 ;

Метод действия Header () опр еделяет параметр accept , знач ение которого будет
бр аться из заголовка Accept в текущем запросе и возвращаться в качестве р езульта ­
та метода. Запустив приложение и запросив URL вида /Home/Header, вы получите
прим е рно такой результат (точный результат может отличаться в зависимости от ис­
пользуем ого бр аузе ра):

Header : text/html , application/xhtml+xml , application/xml;


q=0 . 9 , image/webp , */* ; q=0 . 8
Не все имена НТГР-заголовков могут легко выбираться. полагаясь на имя пар амет­
ра метода действия, потому что система привязки моделей не пр еоб разует соглаше­
ния по именованию С# в соглашения. принятые в НТГР-заголовках . В таких ситуаци­
ях вы должны конфигурировать атрибут FromHeader с применением сво йств а Name
для указания имени заголовка (листинг 26.33).

Листинг 26.33. Указание имени заголовка в файле HomeController. cs

using Microsoft . AspNetCore . Mvc;


using MvcModels . Models ;
namespace MvcModels . Contro l lers
puЫic class HomeController : Controller
private IRepository repository ;
puЫic HomeController(IRepos i tory repo) {
repository = repo ;

puЫic IActionResult Index([FromQuery] int? id) {


Person person ;
if (id . HasValue && (person = repository[id .Val ue]) ! = null) {
return View(person) ;
else {
return NotFound() ;

puЬlicstring Header( [FromНeader(Name = 11


Accept-Language") ] string accept)
=> $ Header: {accept} 11 ;
11
Глава 26. Привязка моделей 823
Использовать Accept -Language для имени параметра С# нельзя, и связыва­
тель модели не будет автоматически преобразовывать имя вроде AcceptLanguage в
Accept - Language, чтобы оно соответствовало заголовку. Взамен с помощью свойс­
тва Name атрибут :конфигурируется та:к, что он соответству ет правильному заголов­
ку . Запустив приложение и запросив URL вида /Home/Header, вы получите ответ,
подобный показанному ниже, :который будет варьироваться на основе региональных
настроек :

Header: en - US , en ; q=0 . 8

Привязка сложных типов из заголовков


Несмотря на то что это редко требуется , привязку сложных типов можно выпол­
нять. используя значения заголовков, за счет применения атрибута FromHeader :к
свойствам :класса модели. Добавьте в папку Models файл по имени HeaderModel. cs
и определите в нем :класс, представленный в листинге 26.34.

Листинг 26 .34. Содержимое файла HeaderModel. cs из папки Models

using Microsoft . AspNetCore . Mvc;


namespace MvcModels . Mode ls {
puЫic class HeaderModel {
[FromHeader]
puЫic st r ing Accept { get; set ; }

[FromHeader(Name = "Accept - Language " )]


puЫic string Accep tLanguage { get ; set ;
[FromHeader(Name = "Accept-Encoding " )]
puЫic string AcceptEncoding { get; set ;

В :классе определены три свойства, каждое из которых декорировано атрибутом


FromHeader . В двух атрибутах с помощью свойства Name указываются имена заго­
ловков , которые невозможно выразить как имена параметров С# . В листинге 26.35
приведен обновленный метод действия Header () контроллера Ноте, теперь получа­
ющий объект HeaderModel.

Листинг 26 .35. Использование класса HeaderModel в файле HomeController. cs

using Microsoft . AspNetCore . Mvc ;


using MvcModels . Models ;
namespace MvcModels . Controllers
puЫic class HomeController : Controller

private IRepository repository ;


puЫic HomeController(IRepository repo) {
repository = repo ;

puЫic IActionResult Index( [FromQuery] int? id) {


Person person;
if (id . HasValue && (person = repository[id . Value]) != null) {
return View(person) ;
824 Часть 11 . Подробные сведения об инфрастру ктуре ASP.NET Core MVC

e l se {
return NotFound() ;

puЬlic ViewResult Header(HeaderModel model) => View(model);

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


имени Header . cs h tm l с разметкой и з листинга 26.36.

Листинг 26.36. Содержимое файла Header. cshtml из папки Views/Home

@model Heade r Model


@{
Vi ewBag . Ti tl e = "H eade r s ";
La y o ut = " Layout ";

<tаЫе class= "t aЫe taЬle - condensed taЬle -b ordered taЫe - striped " >
<tr><th>Accept : </th><td>@Mode l. Acce pt</ t d></ t r>
<tr><t h >Accept - Encoding : </t h >< t d>@Mode l.Accep t Encoding</td></tr>
<tr><t h >Accept -La n guage : </th ><td>@ Model . Acce p t Lang u age</td></tr>
</tаЫе>

Процесс привязки моделей будет исследов ать свойства сло жных типов в поис ­
ке атрибутов , опи с анных в табл . 26.3. Это дает возможность посредством атрибута
FromHeader определить сложный тип, свойства которого являются моделью , привя­
зываемой и з заголов1юв, в ч ем л егко удостов е риться, запустив приложение и запро ­
сив URL вида / Home/Header (рис . 26.10).

1 Head•rs Х

+- . . ;., С [j locall1ost:s1861/н-;~;;н-;ade;:------------------·--~ Е 1
·---·--

Ассер!: text/html.application/xhtml+xml,application/xml;q=0.9,image/webp,•1•;q=0.8
Accept-Encoding: gzip, deflate. sdch
Accept-Language: en-US,en;q=0.8

Рис. 26.10. Привязка слож ного типа из заголовков запроса

Использование тел запросов в качеств е


источников данных привяз к и

Не все данные, отправляемые клиентами , посылаются в виде данных форм ы , как


происходит в ситуации, когда кли ент JavaScript отправля ет данные JSON контролле­
ру API . Атрибут F r omBody указыв ает, что тело з апроса должно быть де кодировано и
применяться в качеств е источник а данных привязки модели. В ли стинге 26.37 пока ­
з аны новы е методы действий Body ( ) ,которые демонстрируют, как это работает.
Глава 26. Привязка моделей 825
Листинг 26.37. Добавление методов действий в файле HorneController. cs
using Microsoft . AspNetCore.Mvc ;
using MvcModels . Models;
namespace MvcModels . Controllers
puЫic class HomeController : Controller
private IRepository repository ;
puЫic HomeController(IRepository repo) {
repository = repo;

puЬlic IActionResult Index([FromQuery] int? id) {


Person person ;
if (id.HasValue && (person = repository[id.Value]) != null) {
return View(person);
else {
return NotFound () ;

puЫic ViewResult Header(HeaderModel model) => View(model) ;


puЫic ViewResul t Body () => View () ;
[HttpPost]
puЫic Person Body ( [FrornВody] Person rnodel) => rnodel;

Параметр метода Body (), принимающего запросы POST, декорирован атрибутом


FromBody, т.е. содержимое тела запроса будет декодировано и использовано для при­
вязки модели. Как объяснялось в главе 20, инфраструктура MVC имеет расширяемую
систему для работы с разными форматами данных, но по умолчанию она настроена
на обработку только данных JSON.
Добавьте в прилож ение пакет jQuery, отредактировав файл bower . j son согласно
листингу 26.38.

Листинг 26.38. Добавление пакета jQuery в файле bower. j son

" name ": " asp . net ",


" private ": true,
" dependencies ": {
" bootstrap ": " 3 . 3 . 6 ",
"jquery": "2. 2. 4"

Чтобы снабдить метод действия требуемыми данными, добавьте в папку Views/


Horne ф айл по имени Body. cshtrnl и поместите в него содержимое, приведенное в

листинге 26.39.
826 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

Листинг 26.39. Содержимое файла Body. cshtml из папки Views/Horne


@(
ViewBag . Title = " Address ";
Layout =" Layout " ;

@section scripts {
<script src="/liЬ/jquery/dist/jquery.min.js"></script>
<sc r ipt type= " text/javascript " >
$(document) . ready(function () {
$( " button " ) . click(function (е)
$ . ajax( " /Home/Body ", {
method : " post ",
contentType : " app l ication/ j son ",
data: JSON . stringify({
firstName: " ВоЬ ",
lastName : " Smith "
) ) ,
success: function (data)
$("#firstName") .tex t(data .f irstName) ;
$( " #lastName " ) . text(data . lastName) ;
)
) ) ;
) ) ;
) ) ;
</script>

<tаЫе class= " taЬle taЫe-condensed taЫe - bordered taЬle-striped">


<tr><th>First Name:</th><td id="firstName"></td></tr>
<tr><th>Last Name:</th><td id="lastName"></td></tr>
</tаЫе>
<button class="Ьtn Ьtn-primary " >Submit</button>

Для простоты представление содержит встраиваемый код JavaScript, который


с помощью jQuery посылает НТГР -запрос POST, соде ржащий данные JSON , на URL
вида /Home/Body, когда производится щелчок на элементе button . Сервер кодирует
объект, созданный с применением привязки модели, и отправляет его обратно кли­
енту как закодированные данные JSON. Запустив приложение, запросив URL вида
/Home/Body и щелкнув на кнопке Submit, можно просмотреть результат (рис . 26 . 11).

Совет. Не весь клиентский код JavaScript требует использования атрибута FromBody. В рас­
смотренном примере намеренно не применялся удобный метод jQuery для отправки Аjах­
запросов POST, потому что он кодирует информацию как данные формы. Взамен исполь­
зовался другой метод, который позволил послать данные JSON.

Атрибут FromBody можно прим енять для привязки только одного пар аметра ме­
тода действия, и в случае использования этого атрибута более одного раза в отдельно
взятом методе генерируется исключение. Если нужно получить несколько объектов
модели из тела запроса, тогда придется создать простой класс п е редачи данных, ко­
торый имеет все необходимые свойства и применя ет содержащиеся в нем данные для
создания объ ект ов, требуемых внутри метода действия.
Глава 26 . Привязка моделей 827

1 Addrш х

1 ~ -"!- ,~ · CJ localhost:S1861 / Home/Bo


!-·---·-------- - - - - - - - - - ·~> С . CJ localhost:51861 / H()me/Body

First Name:
First Name: В оЬ
Last Name:
Smilh

1
[•+1 ________,
'-------·- -----------------
Рис. 26.11. Использование тела запроса для привязки моделей

Резюме
В настоящей глав е рассматривался процесс привязки моделей, который позволя ет
предоставлять методам действий необходимые аргументы, используя значения дан ­
ных из обрабатываемого НТГР-запроса. Было показано, как привязывать простые и
сло жные типы, каким образом иметь дело с массивами и коллекциями, а также как
управлять процессом привязки моделей за счет применения атрибутов к параметрам
методов действий или свойствам классов моделей. В следующей главе будет описано
средство проверки достоверности моделей.
ГЛАВА 27
Проверка достоверности
v
моделеи

в предыдущей главе было показано, как инфраструктура MVC создает объекты


моделей из НТТР-запросов с помощью процесса привязки моделей . В ней пов­
семестно предполагалось, что предоставляемые пользователем данные были допус­
тимыми. Реальность же такова, что пользователи часто вводят данные, которые не
являются допустимыми и не могут использоваться, поэтому темой настоящей главы
будет проверка достоверности моделей.
Проверка достоверности моделей представляет собой процесс выяснения, что дан­
ные, полученные приложением, подходят для привязки моделей, а если это не так, то
пользователям предоставляется полезная информация. которая помогает устранить
проблему.
Первая часть этого процесса - проверка полученных данных - является од­
ним из основных способов предохранить целостность модели предметной области.
Отклонение данных , которые не имеют смысла в контексте предметной области , мо­
жет предотвратить возникновение странных и нежелательных состояний в прило­
жении. Вторая часть - помощь пользователям в корректировке проблемы - важна
в равной степени. Без такой информации и обратной связи, необходимой для вза­
имодействия с приложением, пользователи будут недовольны и озадачены. В обще­
доступных приложениях это означает, что пользователи просто прекратят ими поль­

зоваться. В корпоративных приложениях это означает нарушение рабочего потока


пользователей. Ни тот, ни другой исход не является желательным, но к счастью инф­
раструктура MVC предоставляет широкую поддержку проверки достоверности моде­
лей. В табл . 27. l прив едена сводка, позволяющая поместить проверку достоверности
моделей в контекст.

Таблица 27 .1. Помещение проверки достоверности моделей в контекст

Вопрос Ответ

Что это такое? Проверка достоверности моделей - это процесс выяснения, что
данные, предоставленные в запросе, пригодны для применения

в приложении

Чем она полезна? Пользователи не всегда вводят допустимые данные.


Использование таких данных в приложении может приводить к
неожиданным и нежелательным ошибкам
Глава 27. Провер ка достоверности моделей 829
Окончание табл . 27. 1

Вопрос Ответ

Как она используются? Контроллеры исследуют результат процесса проверки достовер­


ности, а посредством дескрипторных вспомогательных классов в

представления, отображаемые пользователям, включаются отклик


проверки . Проверка достоверности автоматически выполняется
во время процесса привязки моделей. Она обычно дополняется
специальной проверкой в классе контроллера или за счет приме ­
нения атрибутов проверки достоверности
Существуют ли какие-то Важно протестировать эффективность кода проверки достовер ­
с к рытые ловушки или ности, удостоверившись в том , что он способен справляться с пол­
ограничения? ным диапазоном значений , которые может получать приложение

Существуют ли Нет, проверка достоверности моделей тесно интегрирована в


альтернативы? ASP.NET Саге MVC
Изменилась ли она по Базовый подход к выполнению проверки достоверности остался
сравнению с версией таким же , как в предшествующи х версиях MVC, но изменились
MVC5? некоторые лежащие в основе классы и интерфейсы

В табл. 27.2 приведена сводка для настоящей главы.

Таблица 27.2. Сводка по главе

Задача Решение Листинг

Явная проверка достовер­ Используйте объект ModelState для регист­ 27.1-27.11


ности модели рации ошибок проверки достоверности

Генерация сводки по ошиб­ Примените атрибут asp- validation- swnmary 27.12


кам проверки достоверности к элементу di v
Изменение стандартных со­ Переопределите функции сообщений в постав­ 27.13
общений привязки моделей щике сообщений привязки моделей

Генерация ошибок проверки Примените атрибут asp - val i dation - fo r 27.14


достоверности на уровне к элементу span
СВОЙСТВ

Генерация ошибок проверки Используйте объект ModelState для регист­ 27.15, 27.16
достоверности на уровне рации ошибок проверки достоверности , которые
модели не ассоциированы со специфическими свойства­
ми , и укажите значениеModelOnly для атрибу­
та asp- validation - summary в элементе div
Определение самопроверя­ Применяйте атрибуты проверки достоверности 27.17, 27.18
емой модели данных к свойствам модели

Создание специально- Реализуйте интерфейс I Mode l Validato r 27.19, 27.20


го атрибута проверки
достоверности

Выполнение проверки досто­ Используйте пакеты ненавязчивой проверки 27.21 , 27.22


верности на стороне клиента достоверности jQuery
Выполнение удаленной про­ Определите метод действия для выполнения 27.23, 27.24
верки достоверности проверки достоверности и примените атрибут
Remote к свойству модели
830 Часть 11. Подробные сведения об инфраструктуре ASP.NET Соге MVC

Подготовка проекта для примера


Создайте новый проект типа Empty (Пустой) по имени Model Validati o n с исполь­
зованием шаблона ASP.NET Core Web Application (.NЕТ Core) (Веб-приложение ASP.NET
Core (.NET Core)). Добавьте требуемые пакеты NuGet в раздел dependencies ф айла
proj ect . j son и настройте инструментарий Razor в разделе tools, как показано в
листинге 27.1. Разделы, которые не нужны для данной главы , понадобится удалить.

Листинг 27 .1. Добавление пакетов в файле proj ect. j son

"dependencies ": {
"Microsoft.NETCore.App":
"version ": " 1.0.0 ",
"type": "pl atform "
},
"Microsoft . AspNetCore . Diagnostics ": "1.0.0",
"Microsoft.AspNetCore.Server.IISintegration": "1.0. 0 ",
" Microsoft .AspNetCore . Server . Kestrel ": " 1 . 0 . 0 ",
" Microsoft .Ex tensions . Logging .Conso l e ": " 1 .0. О ",
"Microsoft.AspNetCore.Mvc": "1.0.0",
"Microsoft.AspNetCore.StaticFiles": 11 1.0.0 11 ,
"Microsoft.AspNetCore.Razor.Tools": {
"version": "1.0.0-preview2-final",
"type": "build"

},

" tools":
"Microsoft.AspNetCore.Razor.Tools": "1.0.0-preview2-final",
"Microsoft . AspNetCore .Se rver .IISintegration.Tools ": "1 .0 .0-preview2-final "
},

"frameworks": {
"net coreappl.0 ":
"imports ": [ "do tnet5 . б ", "p ortaЫe -n et45+win8"]

},
"b uildOptions ":
"emi tEntryPoint ": tr ue, "preserveCompilationContext ": true
},

" runtimeOptions ": {


" configProperties ": { "System .GC.S e rve r ": true}

В листинге 27.2 показан код класса Startup, который конфигурирует средства,


предоставляемые пакетами NuGet.
Глава 27. Проверка достоверности моделей 831
Листинг 27.2. Содержимое файла Startup. cs

using Microsoft . AspNetCore . Builder ;


using Microsoft . Extens i ons . Dependencyinjection ;
namespace ModelValidation
puЫic class Startup {
puЫic void ConfigureServices(IServiceCollection services) {
services.AddМvc();

puЫic void Configure(IApplicationBuilder арр) {


app.UseStatusCodePages();
app.UseDeveloperExceptionPage();
app.UseStaticFiles();
app.UseMvcWithDefaultRoute();

Создание модели
Создайте папку Models и добавьте в нее файл класса по имени Appointment. cs
с определением, приведенным в листинге 27.3.

Листинг 27.3. Содержимое файла Appointment. cs из папки Models

using System;
using System . ComponentModel . DataAnno t ations ;
namespace ModelVa lida tion . Models {
puЫic class Appointment {

puЫic string ClientName { get; set;


[UIHint (" Date " )]
puЫic DateTime Date { get; set ;

puЫic bool TermsAccep te d { get ; set;

В классе Appoi ntment определены три свойства, и с помощью атрибута UIHint


указано, что свойство Da te должно выражаться как дата без компонента времени.

Создание контроллера
Создайте папку Controllers, добавьте файл класса по имени HomeController. cs
и поместите в него определение контроллера из листинга 27.4, который оперирует с
классом модели Appointment .

Листинг 27.4. Содержимое файла HomeController. cs из папки Controllers


using System ;
using Microsoft.AspNetCore . Mvc;
using ModelValidation.Mode l s ;
832 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

namespace ModelValidation . Controllers {


puЫic class HomeController : Controller
puЫic IAc tio nResult Index() =>
View ( " MakeBooking ", new Appointment { Date = Date Time . Now ) ) ;
[ HttpPost]
puЫic ViewResult MakeBoo king(App oin tmen t appt) =>
View ( " Completed" , appt) ;

Действие Index ви зуализи рует представление MakeBooking с новым объектом


Appointment в к ачестве модели представления. Метод действия MakeBooking () бо­
лее интересен , т.к . в н ем будет выполняться провер ка достоверности модели.

На заметку! Пример приложения настолько прост, что в нем да же не определено х ранилище


и не добавлен код для сохранения объектов Appointment, которые выпускаются про ­
цессом привязки моделей. Тем не менее , важ но иметь в виду, что главной причиной про­
верки достоверности модели является предотвращение попадания в х ранилище непра­

вильных или бессмысленных данных с последующим возникновением проблем (либо при


попытке сохранения данных·, либо при попытке их обработки в более позднее время).

Создание компоновки и представлений


Для ряда примеров в этой главе требуется простая компоновка. Со здайте пап ку
Views/Shared и добавьте в нее файл _Layout. cshtml, с одержим о е которого пока­
зано в листинге 27.5.

Листинг 27.5. Содержимое файла _Layout. cshtml из папки Views/Shared

< ! DOCTYPE html>


<html>
<h ead>
<meta charset= " utf - 8 " />
<meta name= " viewport " conten t="width=device-wi dth " />
<title>Model Validation</title>
<link asp - href - inc l ude= " /l i Ь/bootstrap/dist/**/*.min.css" rel= " stylesheet " />
@RenderSection( " scripts ", false)
</head>
<body class= "panel-body " >
@RenderBody ()
</body>
</html>

Чтобы снабдить методы действий представлениями , создайте папку Views/Home и


добавьте в нее файл по имени MakeBooking. cshtml с разметкой из листинга 27.6.

Листинг 27.6. Содержимое файла МakeBooking. cshtml из папки Views/Home

@mode l Appointment
@{ Layout = " _Layo ut";
<div class= " bg-primary panel - body " ><h2>Book an Appointment</h2></div>
Глава 27 . Проверка достоверности моделей 833
<form class= "panel - body " asp-action="MakeBooking " method=" post " >
<div class= "form-g roup ">
<label asp - for= "ClientName " >Your name : </labe l >
<input asp - for= " Client Name " clas s=" for m-con trol " />
</div>
<div class= " form-group " >
<label asp -f o r = "Date " >Appointme nt Date : </label>
<input asp- for="Date " type="text " asp- format= "{ O:d)" class="form- control " />
</div>
<div class= " ra dio form-group " >
<input asp - fo r="T ermsAccepted " />
<labe l asp - for="TermsAccepted"> I accept the terms & cond iti ons</label>
</div>
<but t on type= " submit " class ="Ьtn Ьtn-primary " >Make Booking</button>
</fo rm>

Когда форма, содержащаяся в файле Index . cs html, отправляется обратно при­


ложению , метод действия MakeBoo k ing () отображает детали созданной пользовате­
лем встречи с применением представления Completed. cshtml из папки Views/Home
(листинг 27.7) .
Листинг 27.7. Содержимое файла Completed.cshtml из папки Views/Home

@model Appointment
@{ Layout = " Layout ";
<div class= "bg -su ccess panel-body " >< h2 >Your Appo intment</ h2 ></div>
<tаЫе class= "taЫe taЬle - bordered">
<tr>
<th>Your name is : </th>
<td>@Mode l. Cli entName</t d>
</tr>
<tr>
<th>Your appontment date i s : </t h >
<td>@Mode l. Date . ToString("d")</td>
</tr>
</tаЫе>
<а class= "btn btn-success " asp-action= "I ndex " >Make Anothe r Appointment</a>

При стилизации НТМL-элементов представ ления полагаются на СSS-пак ет


Bootstrap. Создайте в корневой папке проекта файл bower. j son с использованием
шаблона элемента Bower Configuration File (Файл конфигурации Bower) и добавьте па­
кет Bootstrap в раздел dependenci es (л истинг 27.8). Кроме того, добавьте также и
пакет jQuery, который понадобится позже в глав е.
Листинг 27.8. Добавление пакета Bootstrap в файле bower. j son

" name" : "asp.net ",


"private ": true ,
"dependencies ": {
"bootstrap": 11
3.3.6 11 ,

"jquery": "2. 2 . 4"


834 Часть 11. Подробные сведения об инфраструктуре ASP.NET Соге MVC

Последний подготовительный шаг связан с созданием файла Viewirnports.


cshtml в папке Views, в котором настраиваются встроенные дескрипторные вспо­
могательные классы для применения в представлениях Razor и импортируется про­
странство имен модели (листинг 27.9).

Листинг 27.9. Содержимое файла_Viewimports. cshtml из папки Views


@using ModelValidation.Models
@addTagHelper * , Micr o so ft .AspNetCore.Mvc.TagHelpers

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


на него в работе можно, запустив приложение и запросив стандартный URL. Ввод в
элементах формы информации о встрече и щелчок на кнопке Make Booking (Создать
встречу) приведет к отправке данных серверу. который выполнит процесс привязки
моделей для создания объекта Appo int ment, детали которого затем визуализируются

с использованием представления Completed. cshtml (рис . 27.1).

Cj Mod•I Volidotion

С 10 localhost:S5750

Your name:

ВоЬ

Appointment Date:
12/12/2020
12/12/2020

----·- _ _J
]
Make.Booking
' ~ ~ ~

----------------·-·------
Рис. 27 .1. Запуск примера приложения

Необходимость в проверке достоверности модели


Проверка достоверности модели - это процесс принудительного применения
требований, которые приложение налагает на данные, получаемые от клиентов. Без
проверки достоверности приложение будет пытаться оперировать с любыми получен­
ными данными, что может привести к генерации исключений и непредвиденному по­
ведению, которое отразит немедленные или долговременные проблемы, постепенно
накопившиеся по мере того, 1<ак хранилище наполняется неправильными, незавер­

шенными или злонамеренными данными.


Глава 27. Проверка достоверности моделей 835
В настоящее время пример прилож ения будет принимать любые данные, которые
отпр авляет пользоват ель . Чтобы предохранить целостность приложения и м одели
пр едм етно й обл асти, должны быть удовлетворены три перечисленных ниже условия ,
прежде чем станет известно , что поль з ователь предоставляет приемл е мый объект
Appointment:
• пользователь должен предоставить имя:

• пользоват ель должен пр едоставить дату, относящуюся к будуще му;

• пользователь должен отм етить фл ажок для принятия условий.

В последующих разделах демонстрируется использование проверки достоверности


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

Slвная проверка до стов е р ности модели


Самый прямой способ проверки достоверности модели предусматривает е е вы­
полнение в м етоде действия. В листинге 27.10 показан новый метод действия
MakeBooking () с добавленными явными проверками для каждого свойства, опреде­
ляемого классом App o i ntmen t .

Листинг 27.10. Явная проверка достоверности модели в файле HorneController. cs


us ing System ;
using Mi crosoft . AspNetCo r e .Mvc ;
using ModelValidat i on. Mode ls;
using Мicrosoft.AspNetCore.Mvc . ModelBinding;
namespac e ModelVal i dat i on . Contr o l ler s {
puЬlic class HomeCont r o ll er : Con tro l l e r
puЫic IAc ti onRes ult Inde x ( ) =>
View( "MakeBooking ", new Appo i ntment { Date = Da t e Ti me . Now )) ;
[HttpPost]
puЫic ViewResult MakeBooking(Appointment appt) {
if (string . IsNu110rEmpty(appt.C1ientName)) {
ModelState.AddМodelError(nameof (appt.ClientName),
"Please enter your name");
//Введите свое имя

if (ModelState. GetValidationState ( "Date")


== ModelValidationState.Valid && DateTime.Now > appt.Date)
ModelState.AddМodelError(nameof(appt.Date),
"Please enter а date in the future") ;
// Введите дату, относящуюся к будущему

if (!appt.TermsAccepted) {
ModelState.AddМodelError(nameof (appt.TermsAccepted),
"You rnust accept the terms");
11 Вы должны принять условия
836 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

if (ModelState.IsValid) {
return V.iew ( 11 Completed 11 , appt) ;
else {
return View();

В коде проверяются значения, которые связыватель модели присвоил свойств ам


объекта параметра. Любые обнаруженные ошибки регистрируются с применение м
объекта ModelStateDictionary, который возвращается свойством Model Sta te,
унаследованным из базового класса Cont r ol ler.
Как подсказывает его имя, класс ModelStateDictionary - это словарь, кото­

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


на ошибках проверки достоверности . В табл. 27.3 описаны наиболее важны е члены
класса ModelStateDictiona r y.

Таблица 27.З. Избранные члены класса ModelStateDictionary

Имя Описание

AddМode lE rror(property , message) Этот метод применяется для регистрации ошиб ки


проверки достоверности модели, связанной сука­
занным свойством

GetValidationState(property) Этот метод используется для выяснения, связаны


ли со специфическим свойством ошибки проверки
достоверности модели; результат выражается как

значение перечисления Model ValidationState


IsValid Это свойство возвращает true, если все свойства
модели допустимы, и false в противном случае

В качестве примера применения ModelStateDictionary в з гляните, как выпол­


нялась проверка достоверности свойства ClientName:

if (string . IsNullOrEmpty( app t . ClientName)) {


ModelState . AddModelError(narneof(appt.ClientNarne) ,
Pl ease enter your name
11 11
) ;

Одна из целей приведенного примера связана с гарантированием того, что пользо­


ватель предоставляет значение для данного свойства, поэтому здес ь используется ста­
тический метод string . IsNullOrEmpty () для проверки значения свойства , которое
процесс привязки моделей извлек из запроса . Если свойство ClientName равно null
или пустой строке, тогда известно , что цель проверки достоверности не была достиг­
нута. В таком случае посредством метода Mode l State . AddModelError () регистри­
руется ошибка проверки достоверности с указанием имени свойства (Cl i entName) и
сообщения, которое будет отображаться пользователю для объяснения природы про­
блемы (Please en ter your name (Введите свое имя)).
Глава 27. Проверка достоверности моделей 837
Система привяз ки м оделей также применя ет объект ModelStateDi ctionary для
регистрации любых проблем с нахождением и присваивание м значений свойств ам
модели. М етод GetVa l idatio n State () используется с цел ью выяснения , были ли
з арегистриров аны к аки е -то ошибки для свойства модели, либо из процесс а привязки
моделей, л ибо по причине вызова м етода AddModelError () во время явной проверки
достоверности в методе действия. Метод GetVa l idationState () возвращает значе­
ни е пер е ч и сл ения ModelVa l idationState , которое описано в табл. 27.4.
Таблица 27.4. Значения перечисления ModelValidationState
Имя Описание

Unvalidated Это значение указывает, что в отношении свойства модели проверка


достоверности не выполнялась, обычно из-за отсутствия в запросе
значения, которое бы соответствовало имени свойства
Valid Это значение указывает, что значение в запросе, ассоциированное со
свойством, является допустимым

Invalid Это значение указывает, что значение в запросе, ассоциированное со


свойством, является недопустимым и применяться не должно

Ski pped Это значение указывает, что свойство модели не было обработано.
Обычно это говорит о наличии настолько большого количества ошибок
проверки достоверности , что продолжать проверку не имеет смысла

Для свойств а Da te выполняется проверка. сообщил ли процесс привязки моделей


о пробле м е во время пр е образования отправле нного браузером значения в объ ект
DateTi me :

if (ModelState.GetValidationState("Date") == ModelValidationState .
Valid
&& DateTime . Now > appt . Da t e) {
ModelState . AddMode l Error(nameof(appt . Date) ,
" Please enter а date in the fut ure " ) ;

Цель пров е рки д остов е рности для свойства Da t e - обеспечить предоставле­


ни е пользов ат ел е м допу стимой даты в будущем. Метод GetValidationState ()
используется для выяснения, смог ли процесс привяз ки моделей преобразовать
значение из з а проса в объ е ктDateTime, за счет проверки на предм ет значения
Model Valida tionSta te . Vali d. В случае допустимой даты выполняется проверка,
относится ли он а к будуще му, и е сли н ет, то поср едством м етода AddМode l Er r o r ()
регистрируется пробл е ма проверки достоверности.
После того, как все свойства объ е кта модели проверены , с помощью свойства
ModelState . IsValid выясняется, возникали ли ошибки. Данное свойство возвраща­
ет true, если во время проверок вызывался метод Mo del. State . AddModelE r r or ( )
и ли у связывателя модели возникали проблемы с созданием объекта Appointment :

if (ModelState. IsValid) {
return View ( "Complete d", appt) ;
e l se (
return View () ;
838 Часть 11. Подробные сведения об инфраструктуре ASP.NET Саге MVC

Объект Appointment является допустимым, если свойство IsValid возвраща­


ет true, в случае чего метод действия визуализирует представление Completed.
cshtml. Если же свойство IsValue возвращает false, тогда есть проблема проверки
достоверности. которая обрабатывается путем вызова метода View () для визуализа­
ции стандартного представления.

Отображение пользователю ошибок проверки достоверности


Обработка ошибки проверки достоверности за счет вызова метода View () может
показаться странным подходом. но данные контекста, которыми инфраструктура MVC
снабжает представление, содержат детали ошибок проверки достоверности модели.
Эти детали автоматически обнаруживаются и применяются дескрипторным вспомо­
гательным классом, который используется для трансформации элементов input.
Чтобы посмотреть , как все работает, запустите приложение и щелкните на кнопке
Make Bookiпg, не заполняя форму данными о встрече. Визуальных изменений в окне
браузера не произойдет, но если просмотреть НТМL-разметку, которую МVС возвра­
щает из запроса POST. то вы увидите. что изменился атрибут class элемента формы.
Вот как выглядел элемент ClientName до отправки формы:

<input class= " form-control " type="text" id="ClientName"


name="ClientName" value=" ">
А после отправки пустой формы этот элемент input принимает следующий вид:

<input class= " form-control input-validation-error" type= "text "


id="ClientName" name="ClientName" value=" ">
Дескрипторный вспомогательный класс добавляет элементы, чьи значения не
прошли проверку достоверности. в класс input-validation-error, который затем
можн о стилизовать с целью выделения проблемы.
Это можно сделать за счет определения специальных стилей CSS в таблице сти­
лей. но если нужно задействовать встроенные стили для проверки, предоставляемые
СSS-библиотеками вроде Bootstrap, придется выполнить небольшую дополнительную
работу. Поскольку имя класса. добавляемое к элементам формы. изменять нельзя.
потребуется код JavaScript для сопоставления имени. применяемого инфраструкту­
рой МVС, и классами ошибок CSS, предлагаемыми библиотекой Bootstrap.

Совет. Использовать код JavaScript подобного рода может быть неудобно, из-за чего возни­
кает соблазн применять специальные стили CSS даже при работе с СSS-библиотеками
типа Bootstrap. Однако цвета, используемые классами проверки достоверности в
Bootstrap, можно переопределять посредством тем или путем настройки пакета и опре­
деления собственных стилей. Это означает, что вам придется обеспечить соответствие
между любыми изменениями в теме и изменениями в любых специальных стилях, которые
вы определили. В идеальном случае разработчики из Microsoft сделают имена классов
проверки достоверности конфигурируемыми в будущем выпуске ASP.NET Соге MVC , но
до тех пор написание кода JavaScript для применения стилей Bootstrap является более
надежным подходом, чем создание специальных таблиц стилей.

В листинге 27. 11 к представлению Ма keBoo king добавляется код


jQuery для по­
ис1ш элементов в классе input-validation-error, нахождения ближайшего роди­
тельского элемента, который был назначен классу form - group, и добавления этого
элемента в класс has-error (используемый библиотекой Bootstrap для установки
цвета ошибки в элементах формы).
Глава 27 . Проверка достоверности моделей 839
Листинг 27 .11. Назначение элементов классам проверки достоверности
в файле MakeBooking. cshtml
@model Appointment
@{ Layout = "_Layou t"; }
@section scripts {
<script asp-src-include="/lib/jquery/dist/*.min.js"></script>
<script type="text/javascript">
$(document) .ready(function () {
$("input.input-validation-error")
. closest( ".form-group") .addClass("has-error");
}) ;
</script>

<div class= " bg -p rimary panel - body " ><h2>Book an Appointment</h2></div>


<form class= "panel - body " asp - action="MakeBooking " method=" post " >
<div class= " form - group " >
<label asp - for= " Cl i entName " >Your name : </labe l>
<input asp - for ="ClientName " clas s="form-con tro l" />
</div>
<div cl ass= " form -gro up " >
<label asp - for= "Date " >Appointmen t Dat e : </label>
<inpu t asp -for= " Date " type= " text " asp-format =" {О : d} "
class= " form - control " />
</div>
<d i v class= " radio form - g r oup " >
<input asp - for= " TermsAc cepted " />
<label asp - for= "Terms Accepted " >I accept the terms & conditions</ label>
</div>
<button type= " submit " class= " Ьtn Ьtn - primary">Make Booking</button>
</form>

Добавленный код jQuery выполняется, когда браузер завершает разбор всех эле­
ментов в НТМL-документе, и результатом будет выделение элементов input , которые
назначены классу input -val idaton - error. Запустив приложение и отправив форму
без з аполнения всех полей, можно получить результат, представленный на рис. 27.2.
Когда форм а о тп равляется без ввода каких-либо данных, все три свойства
выделяются как содержащие ошибки. Пользов атель не увидит представление
Completed . cshtml до тех пор, пока форма не будет отправлена с данными, которые
могут быть разобраны связывателем модели и успешно проходят явные проверки до­
стоверности в методе MakeBooking (). Пока это н е произойдет, отправка формы будет
приводить к визуал изации представления MakeBooking . cshtml с текущими ошиб­
ками проверки достоверности.

Отображение сообщений об ошибках проверки достоверности


Классы CSS, которые дескрипторные вспомогательные классы применяют 1~
элементам input, указывают на наличие проблемы с полем формы , но не сообща ­
ют пользователю, в чем конкретно состоит проблем а. Предо с тавл ени е пользовате­
лю дополнительной информации требует использования другого дескрипторного
вспомогательного класса, который добавляет в представление сводку по проблемам
(ли стинг 27.12).
840 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Саге MVC

б Modtl Volidotion Х

~- _:_ ? L~-105·;~~~-~~/н~~~:~4~~;8~=~~--~=-°':~:::~=~~:=~--=~=--==-:~=-С -С ~) 1
-1
1

1
1
Yourname: i
г
1
х

·-
!1
1

i Appointment Date:
i
1
E oos xj
1 О 1accept the terms & coпditions
i
1
'1'if1Мid
1L_____ _ 1
·---·-·-----------·--"-----·---·-----~-·-"--------------·------·-------.!

Рис. 27.2. Подсвечивание ошибок проверки достоверности

Листинг 27. 12. Отображение сводки по проверке достоверности


в файле мakeBooking. cshtml

@model Appointment
@{ Layout = " Layout ";
@section scripts {
<script asp - src - include= "/l iЬ/jq uer y/dist/* .m in . js " ></script>
<script type= " text/ j avascript " >
$(document) . ready(funct i on () {
$( "i nput . input - validation -e rror " )
. closest (". form - group ") . addClass ( "has - error " ) ;
}) ;
</script>

<div class= " bg-primary panel-body"><h2>Book an Appointment</h2></div>


<form class="panel-body" asp-action= "MakeBooking " method=" post " >
<div asp-validation-summary="All" class="text-danger"></div>
<div class= " form-group " >
<label asp - for= "ClientName " >Your name : </ label>
<input asp - for= "ClientName " class= " form - control " />
</div>
<div class="form-group">
<labe l asp - for= " Date " >Appoin tmen t Date:</label>
<i nput asp-for= " Date " type= "text " asp- format= " {O : d }" class="form-control " />
</div>
<div c l ass= " radio form-group" >
<input asp - for= "TermsAccep t ed " />
<label asp - for= "TermsAccepted " >I accept the terms & conditions</label >
</div>
<button type= " submit " class= " Ьtn Ьtn - primary " >Make Booking </button >
</form>
Глава 27 . Проверка достоверности моделей 841
Класс Validat ionSurnmaryТagHelper обнаруживает атрибуг asp- validation-surnmary
в элементах di v и реагирует добавлением сообщений, которые описывают любые ошибки
проверки достоверности, выявленные методом действия. В атрибутеasp-validation-
sumrnary указывается одно из значений перечисл ения ValidationSumrnary, которые
описаны в табл . 27.5 и вскоре будуг продемонстрированы.

Таблица 27.5. Значения перечисления ValidationSummary

Имя Описание

All Это значение применяется для отображения всех ошибок проверки до­
стоверности, которые были зарегистрированы
ModelOnly Это значение используется для отображения только ошибок проверки
достоверности, относящихся ко всей модели, за исключением тех, кото ­
рые были зарегистрированы для индивидуальных свойств, как описано
в разделе " Отображение сообщений об ошибках проверки достовер­
ности на уровне модели" далее в главе

None Это значение применяется для отключения дескрипторного вспомога­


тельного класса, так что он не будет трансформировать НТМL-элемент

Если вы запустите приложение и отправите форму, не внося какие-либо изме­


нения, то увидите сводку, которую сгенерировал дескрипторный вспомогательный
класс. Цвет текста в рассматриваемом примере определяется классом text-danger
из Bootstrap, который гарантирует, что цвет текста соответствует цвету, используемо­
му для выделения текстовых полей (рис. 27.3).

[j Model V•liclation Х

1 ~ с [~i;;i1.~~~~}soi~~~~~~~~-~;~~~~--=~=-~~~~~~~~:~~:~------=-~~:~==~--=-~~l __ i

1 1

j Please enler your name 1

lj • Please en!er а date in !he futuгe


• You must accept the terms
1

J Your name: 1
1 c-----------------------XJ 1

1 1
• Appointmenl Date: !

1
i
~"'"°'___ ~ 1

1
О 1accept the terms & conditlons

, diiiilr'·I
1
1

[_____ " ________________________ .____ __ --"---·-·---·---·-"--- --"--------·---·----·-·------"-·-----"-)


"

Рис. 27.З. Отображение пользователю сводки по проверке достоверности


842 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

Заглянув в НТМL-разметку, которая бьmа получена браузером , вы заметите , что


сообщения проверки достоверности отправлялись в виде списка:

<div c l ass= " text - danger validation - summary- errors "


data-valmsg - summary= "true " >
<ul >
<l i >Please enter your name</li>
<li> Pleas e enter а date in the future</li>
<li>You must accept the te rms </li>
</ul>
</div>

Конфигурирование стандартных сообщений


об ошибках проверки достоверности
Процесс привязки моделей, описанный в главе 26, выполняет собств енную провер­
ку достоверности, когда он пытается предоставить значения данных , требующиеся
для вызова метода действия. Чтобы увидеть, как это работает, запустите приложение ,
очистите содержимое поля Appoiпtmeпt Date (Дата встречи) и отправьте форму . Бы
обнаружите, что одно из отображаемых сообщений проверки достоверности измени­
лось, и его нет среди строк, передаваемых методу AddModelError () внутри метода
действия:

The value ' ' is invali'd


Показанное сообщение добавляется в ModelStateDictionary процессом привяз­
ки моделей, когда он не может найти значение для свойства или находит его, но не
может преобразовать. В данном случае ошибка возникла из -за отправки в данных
формы пустой строки , которая не может быть преобразована в объект Da teT ime дл я
свойства Date 1<.ласса Appointment .
Связыватель модели располагает набором предопределенных сообщений , которые
он применяет для обозначения ошибок пров ерки достоверности. Их можно заменить

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


ным в интерфейсе IModelBindingMes sage Provider (табл. 27.6).

Таблица 27.б. Свойства интерфейса IModelBindingMessageProvider

Имя Описание

ValueMustNotBeNullAccessor Функция , присвоенная этому свойству, исполь­


зуется для генерации сообщения об ошибке
проверки достоверности, когда свойству моде­
ли, не допускающему null , поступает значе­
ние nul l
MissingBindRequiredVa l ueAccessor Функция , присвоенная этому свойству, приме ­
няется для генерации сообщения об ошибке
проверки достоверности, когда запрос не со­

держит значение для обязательного свойства

MissingKeyOrValueAccessor Функция, присвоенная этому свойству, исполь­


зуется для генерации сообщения об ошиб ке
проверки достоверности, к огда данные , требу ­
ющиеся объекту словаря, содер ж ат ключи или
значения, равные null
Глава 27. Провер к а достоверности моделей 843
Окончание табл. 27.6

Имя Описание

AttemptedValueisinvalidAccessor Фун к ция, присвоенная этому свойству, приме­


няется для генерации сообщения об ошибке
проверки достоверности, когда системе при­

вязки моделей не удается преобразовать зна­


чение данны х в требуемый тип С#
UnknownValue i sinval i dAccessor Функция, присвоенная этому свойству, исполь­
зуется для генерации сообщения об ошибке
проверки достоверности , к огда системе при­

вязки моделей не удается преобразовать зна­


чение данны х в требуемый тип С#

ValueMustBeANumЬerAccesso r Функция, присвоенная этому свойству, применя­


ется для генерации сообщения об ошиб ке про­
верки достоверности, когда значение данных не

может быть преобразовано в числовой тип С#

ValueisinvalidAccessor Функция, присвоенная этому свойству, исполь­


зуется для генераци и запасного сообщения об
ошибке провер к и достоверности, которое при ­
меняется в качестве последнего средства

В се функции. пр исв аиваемые с в о йств ам из табл . 27.6 , получают стр01~у. с оде ржа­
щую з начени е дан ных из запрос а , и во звраща ют стро ку с сообще нием об ошибке .
В кла с се Startup специ ал ьные функции можно сконфигуриров ать как п а р аметры,
27 .1 3 ,
что пр о илл юстр и ровано в л исти н г е где прои зв одится замена ст андартно й

фун кции ValueMustNotBeNullAccessor.

Листинг 27 .1 З. З амена функци и генерации сообщений при привязке моделей


в файле Startup. cs

using Microsoft . AspNetCore . Builder ;


using Microsoft . Extensions. Depende ncylnject i on ;
namespace ModelVa l idation
puЫic class Startup {
puЫic void ConfigureServices(IServiceCollection services) {
services .Adc!Мvc () .AddМvcOptions (opts => {
opts . ModelBindingMessageProvider . ValueMustNotBeNullAccessor =
value => "Please enter а value";
}) ;

puЫic void Configure ( IApplicat i onBuilder арр) {


app . UseStatusCodePages() ;
app.UseDeveloperExceptionPage() ;
app . UseStaticFiles() ;
app.UseMvcWithDefaultRoute() ;
844 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

Свойство MvcOptions. ModelBindingMessageProvider возвращает объект


ModelBindingMessageProvider, который реализует интерфейс IModelBinding
MessageProvider и может использоваться для замены стандартных функций гене­
рации сообщений. В листинге 27. 13 определена функция, возвращающая сообщение
Please enter а value (Введите значение); чтобы увидеть результат, запустите прило­
жение и отправьте форму. предварительно очистив поле Appointment Date (рис. 27.4).

~ M<>del Volidotion

~ С Г(i)l~s750/Home/МakeBoo;;;;;-;;-------------···--

• Please enter your name


1 • Please enter а value
• You must accept the terms

1 Yourname:

j J:::;;~
Рис. 27.4. Эффект от изменения функции генерации сообщений при привязке моделей

Отображение сообщений об ошибках проверки


достоверности на уровне свойств
Несмотря на то что специальное сообщение об ошибке более выразительно, чем
стандартное сообщение, польза от него по-прежнему невелика, т.к. оно не указыва­
ет ясно пользователю на проблему. Для ошибок такого рода более практично отоб­
ражать сообщения об ошибках проверки достоверности рядом с НТМL-элементами.
которые содержат проблемные данные. Это можно делать с применением дескрип­
торного вспомогательного класса ValidationMessageTag. Он ищет элементы span
с атрибутом asp-validation-for, используемым для указания свойства модели, к
которому должны относиться отображаемые сообщения об ошибках.
В листинге 27 .14 для каждого элемента inpu t внутри формы добавляются элементы
сообщений об ошибках проверки достоверности на уровне свойств. Индивидуальные
сообщения об ошибках проверки достоверности обеспечат достаточное выделение для
того, чтобы можно было понять, какие элементы содержат ошибки.

Листинг 27 .14. Добавление сообщений об ошибках проверки достоверности


на уровне свойств в файле MakeBooking. cshtml
@model Appointment
@{ Layout = " Layout";
@section scripts {
<scri pt asp-src-include="/liЬ/jquery/dist/*.min.js"></script>
<script type="text/javascript">
Глава 27 . Проверка достоверности моделей 845
$(document) . ready(function () {
$( "i nput . input -validation -e rror " )
. closest ( ". form - group " ) . addClass ( "has-error");
}) ;
</script>

<div class= " bg - primary panel-body"><h2>Book an Appointment</h2></div>


<form class= " panel - body " asp - action= "M akeBooking " method= " post ">
<div asp - validation - summary=" All " class =" text - danger " ></div>
<div class= " form - group " >
<label asp-for= " ClientName " >Your name: </labe l>
<divXspan asp-validation-for="ClientName" class="text-danger"X/span>
</div>
<inp ut asp-for= " ClientName " class= " form - control " />
</div>
<div class= " form - group " >
<label asp - for= " Date " >Appointment Date : </labe l>
<divXspan asp-validation-for="Date" class="text-danger"></span>
</div>
<input asp - for= "Date" type= " text" asp-foпnat=" {О: d} "
class= " form - cont rol" />
</div>
<span asp-validation-for="TermsAccepted" class="text-danger"></span>
<div class= " radio form - group " >
<inp ut asp - for= " TermsAccepted " />
<label asp-for= "T ermsAccepted " >I accept t he terms & conditions</label>
</div>
<butto n type= " submit " class= " Ьtn Ьtn-primary">Make Booking</button>
</form>

Поскольку элементы span отображаются внутри , нужно позаботиться о том, что­


бы было вполне очевидно, к каким элементам относятся сообщения об ошибках про­
вер1ш достов е рности. Запустив приложение и отправив форму без предварительного
ввода данных, можно увидеть новые сообщения об ошибках проверки достоверности
(рис. 27.5).

Отображение сообщений об ошибках проверки


достоверности на уровне модели

Может п01щзаться, что сводка по проверке достоверности в приложении избыточ­


на, т. к. она просто .цублирует сообще ния уровня свойств, которые в целом более полез­
ны пользователю , поскольку они находятся рядом с элементами формы, где должны
быть устранены проблемы. Но сводка позволяет предпринять удобный трюк, который
заключается в возможности отображать сообщения, применимые ко в сей модели, а не
только к отдельным свойствам. Другими словами, можно сообщать об ошибках, ко­
торые возникают в комбинации индивидуальных свойств, например, когда заданная
дата допустима только в сочетании со специфическим именем .
В листинге 27.15 добавлена проверка достоверности, которая предотвращает на­
значение встр е чи пользователем по имени Джо (Joe) по понедельникам (Monday).
846 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

• Please enter your name


• Please ente r а value
• You must accept the terms

Youгname:

Please енtег уоuг name

Appointment Date:
Please enter а value

1 ~ust accept tt1e terms

~ ·=~ '""'~' ""~~--------··-------------------__]


&

Рис. 27.5. Применение сообщений об ошибках проверки достоверности на уровне свойств

Листинг 27.15. Выполнение проверки на уровне модели в файле HomeController. cs

using System;
using Microsoft . AspNetCore.Mvc ;
using ModelValidation.Models ;
using Microsoft.AspNetCore.Mvc . ModelBinding ;
namespace ModelValidation . Controllers {
puЫic class HomeController : Controller
puЫic IActionResult Index() =>
View ( "MakeBooking ", new Appointment () Date = DateTime.Now )) ;
[HttpPostJ
puЫic ViewResult MakeBooking(Appointment appt)
if (string.IsNullOrEmpty(appt . Cl ientName)) {
ModelState . AddMode l Error(nameof(appt.C li entName),
" Please enter your name " ) ;

if (ModelState . GetValidationState( " Date " )


== ModelValidationState . Valid && DateTime.Now > appt .Date) {
ModelState.AddModelError(nameof(appt.Date) ,
"Please enter а date in the future") ;
Глава 27. Пров е р ка достоверности моделей 847
if (!appt.TermsAccepted) {
ModelState . AddModelError(nameof(appt . TermsAccepted),
" You mus t accept the terms " ) ;

if (ModelState.GetValidationState(nameof(appt.Date))
== ModelValidationState.Valid
&& ModelState.GetValidationState(nameof(appt.ClientName))
== ModelValidationState.Valid
&& appt . ClientName. Equals (" Joe" , StringComparison . OrdinalignoreCase)
&& appt . Date . DayOfWeek == DayOfWeek .Monday) {
ModelState.AddМodelError("",
"Joe cannot book appointments on Mondays");

i f (ModelState . IsValid) {
return View ( " Completed ", appt) ;
else {
return View () ;

Код выглядит более запутанным. чем есть на самом деле , что отража ет саму при­
роду проверки достоверности данных. Допустимо сть значений ClientName и Date
проверяется путем инспектирования состояния модели перед выяснением, выпада­

ет ли указанная дата на понедельник и содержит ли свойство ClientName строку


Joe. Если Джо пытается назначить встречу на понедельник, тогда вызывается метод
AddModelError () с передачей в первом аргументе пустой строки(""), которая ука­
зывает на то, что ошибка касается всей модели, а не отдельного свойства.
В листинге 27.16 значение атрибута asp - validation -s ummary изменено на
Mode l Onl y , что приводит к исключению ошибок уровня свойств, поэтому сводка будет
отображать только сообщения об ошибках , которые применимы к модели целиком.

Листинг 27 .16. Отображение сообщений об ошибках проверки достоверности


на уровне модели в файле MakeBooking. csh tml

@mode l Appo in tment


@{ Layout = " _ Layout ";
@section scripts {
<script asp - src - inc lude = " /liЬ/jquer y/di st/* . min . js " ></script>
<script type= " text/ja vasc ript " >
$(doc ume nt) . ready(funct i on () {
$( " input . input - va li dation - error " )
. closest(" .form-g roup " ) . addC l ass( " has - error " ) ;
}) ;
</script >

<div class= " bg - primar y panel - body " ><h2>Book ап Appointment</h2></ d iv>
<form class= " panel - body " asp - action= " MakeBooking " method= " post " >
<div asp-validation-sununary="ModelOnly" class="text-danger"></div>
848 Часть 11. Подробные сведения об инфраструктуре ASP.NET Соге MVC

<div class= "form- group " >


<label asp -f or= "ClientName " >Your name : </ label>
<div><span asp-validation-for="ClientName" class= " text - danger " ></span>
</div>
<inp ut asp-for= "ClientName " c l ass= " form - control" />
</div>
<d iv c l ass= " form-group " >
<label asp - for= " Date">Appointment Date : </label>
<div> <span asp-validation - for= " Date " class="text- danger " ></span></div>
<input asp- for= 11 Date 11 type= 11 text 11 asp- format= 11 {О : d} 11 class= 11 form- control 11 />
</div>
<s pan asp - va lidation-for= 11 TermsAcce pted" c l ass= " text -danger " ></span>
<d iv class= " radio form - group " >
<input asp -f or= "TermsAccepted " />
<label asp - for= "TermsAccepted " >I accept the terms & conditions</label>
</div>
<but ton type= " submit 11 class= " Ьtn Ьtn-primary">Make Booking</button>
</fo rm>

Запустите приложение , введите Joe в поле ClientNarne и выберите дату, выпада­


ющую на понедельник , такую как 18 января 2027 года (01/18/2027 ). Отправив фор­
му, вы получите ответ, показанный на рис. 27.6.

• Joe cannot book appoir1tments on Mo11days

Yourname:

' Joe

Appointment Date:

01 /18/2027

You must accept the terms


О 1ассер11!1е terms & conditions
< ' ~ ~ '

_,.· M~ke_~~ki~g -~
1
L..•·---·------·------·----·-··-·--·-··- ·---·····-·--·-····--·"-·--···-·---·-···--·-··---·-··----·-···-·------·-"-·- -·J

Рис. 27.6. Использование сообщений об ошибках проверки достоверности


на уровнях модели и свойств
Глава 27 . Проверка достоверности моделей 849

Указание правил проверки достоверности


с помощью метаданных
Одна из проблем с помещением логики проверки достоверности внутрь мето­
да действия связана с тем, что в итоге она дублируется в каждом методе действия,
который получает данные от пользователя. Чтобы помочь сократить дублирование,
процесс проверки достоверности поддерживает использование атрибутов. Атрибуты
позволяют выражать правила проверки достоверности моделей прямо в классе моде­
ли, гарантируя применение одного и того же набора правил независимо от метода,
который используется для обработки запроса.
В листинге 27. 17 к классу Арро in tmen t применяются атрибуты для навязывания
того же набора правил проверки достоверности на уровне свойств, который исполь­
зовался в предыдущем разделе.

Листинг 27 .17. Применение атрибутов проверки достоверности


в файле Appointment. cs

using System ;
using System . ComponentModel .DataAnnotat i ons ;
namespace ModelValidation . Models
puЫic class Appointment {
[Required]
[Display (Name = "name")]
puЬlic string Clien t Name { get; set; }
[UIHint ( "Da te" ) ]
[Required (ErrorMessage = "Please enter а date")]
puЬlic DateTime Date { get ; set ; }
[Range(typeof(Ьool), "true", "true",
ErrorMessage = "You must accept the terms")]
puЫic bool TermsAccepted { get ; set ; }

Здесь используются два атрибута проверки достоверности - Required и Range .


Атрибут Required указывает, что ошиб1ш проверки достоверности возникает, если
пользователь не отправил значение для свойства . Атрибут Range задает подмножес­
тво приемлемых значений. В табл. 27.7 приведен набор встро енных атрибутов про­
верки достоверности, доступных в приложении МVС.
Все атрибуты проверки достоверности позволяют указывать специальное сообще­
ние об ошибке за счет установки значения для свойства ErrorMessage. например:

[UIHint("Date " ) ]
[Required(ErrorMessage = "Please enter а date")]
puЬlic DateTime Date { get ; set ; }
850 Часть 11. Подробные сведения об инфраструктуре ASP.NET Саге MVC

Таблица 27. 7. Встроенные атрибуты проверки достоверности

Атрибут Пример Описание

Compare [Compare Этот атрибут гарантирует, что свойства


( " ДругоеСвойство " ) ] имеют одно и то же значение . Это по­
лезно, когда вы предлагаете пользова­

телю два раза предоставить ту же самую

информацию, такую как адрес эле ктрон­


ной почты или пароль

Range [Range ( 1 О , 2 О) ] Этот атрибут гарантирует, что число­


вое значение (или значение свойства
любого типа, реализующего интер­
фейс IComparaЫe ) не на ходится за
рамками заданны х минимального и

максимального значений. Чтобы ука­


зать границу только с одной стороны ,
применяйте константу MinValue или
MaxValue (например, [Range ( int.
MinValue , 50)] )
RegularExpression [RegularExpression Этот атрибут гарантирует, что строко­
( " шаблон " )] вое значение соответствует указанно­

му шаблону регулярного выражения.


Обратите внимание , что шаблон должен
соответствовать всему предостав­
ленному пользователем значению, а

не только подстроке внутри него. По


умолчанию при сопоставлении учитыва­

ется регистр символов, но с помощью

модификатора (? i ) его можно сделать


нечувствительным к регистру, напри­

мер, [RegularExpression( " (?i)


шаблон")]

Required [Required ] Этот атрибут гарантирует, что значение


не является пустой строкой или строкой ,
состоящей только из пробелов. Чтобы
трактовать пробельные символы как
допустимые, необх одимо использовать
[Required(AllowEmptyStrings =
true)]
StringLength [StringLength(lO)] Этот атрибут гарантирует, что
строковое значение не превыша­
ет указанную максимальную длину.

Можно также задавать минималь­


ную длину : [ StringLength ( 10 ,
MinimumLength=2)]
Глава 27. Проверка достоверности моделей 851
Если специальное сообщение об ошибке не указано, тогда будут применяться стан­
дартные сообщения, но они имеют тенденцию раскрывать детали класса модели, ко­
торые не имеют смысла для пользователя, если только также не используется атрибут
Display, как делалось в отношении свойства ClientNarne:

[Required]
[Display (Narne = "narne")]
puЫic string ClientName { get; set ; }

Стандартное сообщение, генерируемое атрибутом Required, отражает имя, ука­


занное с помощью атрибута Display, поэтому оно не раскрывает пользователю имя
самого свойства .
Для обеспечения согласованной работы проверки достоверности такого рода тре­
буется определенное внимание. В качестве примера взгляните на атрибут проверки
достоверности, примененный к свойству TermsAccepted:

[Range(typeof(bool), "true", "true",


ErrorMessage="You must accept the terms")]
puЫic bool TermsAccep ted { get ; set; }

Необходимо удостовериться, что пользователь отметил флажок для принятия ус­


ловий. Использовать атрибут Required нельзя, т.к. брауз ер отправит для данного
свойства значение false, если пользователь не отметил флажок. Проблема решается
за счет возможности атрибута Range предоставлять объект Туре и указывать верх­
нюю и нижнюю границы в виде строковых значений. Установка обеих границ в true
создает эквивалент атрибута Required для свойств типа bool, которые редактиру­
ются с применением флажков. Обеспеч ение успешной совместной работы атрибутов
проверки достоверности и данных, отправляемых браузером , может потребовать не­
которого экспериментирования.

Использование атрибутов проверки достоверности в классе модели означает, что


метод действия в контроллере можно упростить, как пока зано в листинге 27 .18.

Листинг 27 .18. Удаление проверки достоверности на уровне свойств


в файле HomeController. cs

using System;
using Microsoft . AspNetCo re.Mvc ;
using ModelValidation.Models;
using Microsoft . AspNetCore . Mvc . ModelBinding ;
namespace ModelValidation . Controllers {
puЫic class HomeControl ler : Controller

puЫic IActionResult Inde x() =>


View( "MakeBooking ", new Appointment() Date = DateTime .Now }) ;
[HttpPost]
puЫic ViewResult MakeBooking(Appointment appt)
if (ModelState.GetValidationState(nameof(appt.Date))
== ModelValidationState.Valid
&& ModelState.GetValidationState(nameof(appt.ClientName))
== ModelValidationState.Valid
852 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

&& appt.ClientName.Equals("Joe", StringCornparison.OrdinalignoreCase)


&& appt.Date . DayOfWeek == DayOfWeek.Monday) {
ModelState.AddМodelError("",
"Joe cannot book appointrnents on Mondays 11
);

if (ModelState.IsValid) {
return View ( Completed appt) ;
11 11
,

else {
return View();

Атрибуты проверки достоверности применяются перед вызовом метода действия,


т.е. при выполнении проверки достоверности на уровне модели для выяснения, до­

пустимы ли индивидуальные свойства , можно по-прежнему полагаться на состояние


модели. Чтобы взглянуть на атрибуты проверки достоверности в действии, запустите
приложение и отправьте форму, не вводя какие-либо данные (рис. 27.7).

D MO<i•I Validation х

г ;;--·-·-·- - - - - - - - - - · - - - - - - - - - - -- ---·---··- ----- ---· i


С Lф
--~~.
locall1ost:55750/liome/M.'!ke8ook111g
-· - ·-- .~-- ·--·-·· ~ -
-i:tj- -···
-·--::.-~.--==-::.._--:-· ~-::-::.:::::-·:-_.-;;-:-~
; 1

Your name: 1

The name field ls required.


1

Appoinlmenl Date:
i
1
Please enter а value

1
YoL1 musl accept lhe lerms !
О 1accepl tl1e terms & conditions '
1
1

*Иi=iid 1
-·----------·-·---- --- --------------- -·--- -- -·- ___ J
Рис. 27. 7. Использование атрибутов проверки достоверности

Создание специального атрибута проверки


достоверности для свойства
Процесс проверки достоверности может быть расширен за счет создания атрибута ,
который реализует интерфейс IModel Validator. Создайте папку Infrastructure
и добавьте в нее файл класса по имени MustBeTrueAt tribute. cs с определением,
приведенным в листинге 27.19.
Глава 27. Проверка достоверности моделей 853
Листинг 27.19. Содержи м ое файла MustBeTrueAttribute. cs из папки
Infrastructure
using System ;
using System . Collections . Gener i c ;
using System . Linq ;
using Microsoft . AspNetCore.Mvc . ModelBinding . Val i dation ;
namespace ModelValidation . Infrastructure {
puЫic class MustBeTrueAttribute : Attr ib ute , I Mode l Validator {

puЫic bool IsRequired => true ;


puЬlic string ErrorMessage { get ; se t; ) = "This value must Ье true ";
puЫic I EnumeraЫe<ModelValidationRes u lt> Val i date(
ModelValidationContext context) {
bool? value = context . Model as bool? ;
if ( !va l ue . HasValue 11 va l ue. Va l ue == false)
return new List<ModelVal i dationRes ult> {
new ModelValidat i onRe s ult( "", Er r orMessage)
);
else {
r eturn Enume r aЫe . Empty< M ode l Va l id a tionResu l t>() ;

Б инте рф е йсе IModelValidator определено свойство I sRequired, которое приме­


няется для ук азания, тр е буется ли проверка достоверности с помощью этого класса
(что не много вводит в заблуждение , т.к . значение, возвращаемое данным свойством ,
просто испол ьзуется для упорядочения атрибутов проверки достоверности. чтобы
обяз ат ельн ы е атрибуты выполнялись первыми). Метод Validate () применяется
для выполн ен ия проверки достоверности и получает информацию чер ез экзе мпляр
клас са Model ValidationContext . наиболее полезные сво й ства ко торого описаны в
табл . 27.8.

Таблица 27 .8. Полезные свойства класса Model Valida tionCon text


Имя Описание

Mode l Это свойство возвращает значение свойства, подлежащего проверке


достоверности, которым в рассматриваемом примере будет значение
свойства Terms Accepted
Conta i ner Это свойство возвращает содер ж ащий свойство объект, которым в
рассматриваемом примере будет объект Appointment
ActionContext Это свойство возвращает объект Act i onContext , предоставляющий
данные контекста и описывающий метод действия, который будет об­
рабатывать запрос
ModelMetadata Это свойство возвращает объект Mode lMetadata , который детально
описывает класс модели, подвергающийся проверке достоверности
854 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

Метод Validate () возвращает последовательность объектов Model ValidationResul t,


каждый из которых описывает одиночную ошибку проверки достоверности. В примере
атрибута объект Model ValidationResul t создается, если значение context. Model
не равно true. В первом аргументе конструктору ModelValidationResult переда­
ется имя свойства, с которым ассоциирована ошибка, что указывается как пустая
строка при проверке достоверности индивидуальных свойств . Во втором аргументе
задается сообщении об ошибке, которое будет отображаться пользователю. В листин­
ге 27.20 атрибут Range заменен специальным атрибутом .

Листинг 27.20. Применение специального атрибута в файле Appointment. cs

using System;
using System .ComponentModel .DataAnnotati ons ;
using ModelValidation.Infrastructure;
namespace ModelValidation.Models
puЬlic class Appointment {
[Required]
[Display(Name = "name")]
puЫic string ClientName { get; set; }
[UIHint("Date")]
[Required(ErrorMessage = "Please enter а date")]
puЫic DateTime Date { get; set; }
[MustBeTrue (Erro rMessage = "You must accept the terms")]
puЫic bool TermsAccepted { get; set; }

Результат использования специального атрибута проверки достоверности будет


точно таким же, как и атрибута Range, но при чтении кода цель специального атри ­
бута более очевидна.

Выполнение проверки достоверности


на стороне клиента
Все продемонстрированные до сих пор приемы проверки были примерами провер­
ки достоверности на стороне сервера. Это означает, что пользователь посылает свои
данные серверу, сервер проверяет данные и отправляет обратно результаты проверки
(либо признак успешности, либо список ошибок, подлежащих исправлению) .
В веб-приложениях пользователи обычно ожидают немедленного отнлика провер­
ки достоверности, без необходимости отправки чего-либо серверу. Такая возможность
известна нан проверка достоверности на стороне клиента и реализуется с примене­
нием JavaScript. Вводимые пользователем данные проверяются на предмет достовер­
ности перед их отправкой серверу, снабжая пользователя немедленным откликом и
шансом скорректировать любые проблемы .
Инфраструктура МVС поддерживает ненавязчивую проверку достоверности на сто­
роне клиента. Термин ''ненавязчивая" означает, что правила проверки достоверности
выражаются с использованием атрибутов, которые добавляются к НТМL-элементам, ге­
нерируемым представлениями . Атрибуты интерпретируются библиотекой JavaScript,
входящей в состав инфраструктуры МVС, которая, в свою очередь, конфигурирует биб-
Глава 27. Проверка достоверности моделей 855
лиоте ку jQuery Validation, выполняющую действительную работу по проверке достовер­
ности . В последующих р азделах будет показ ано, как работает встроенная поддержка
проверки достоверности, и продемонстрированы способы расширения ее функцио­
нал ьности для об еспечения проверки достоверности на стороне клиента.

Совет. Проверка достоверности на стороне клиента сосредоточена на проверке достовер­


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

Прежде всего , понадобится добавить в приложение новые пакеты JavaScript, ис­


пользуя Bower (листинг 27.21).

Листинг 27. 21 . Добавление пакетов в файле bower . j son

" name ": " asp . net ",


"private ": true ,
"dependencies ": {
"bootstrap ": " 3. 3 . 6",
" jquery": " 2 . 2 . 4 ",
"j query-valida tion" : "1. 15. О" ,
"jquery-validation-unoЬtrusive": 11
3. 2. 5"

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


представлению трех файлов JavaScript: библиотеки jQuery, библиотеки пров е рки до­
стоверности jQuery и библиотеки ненавязчивой проверки достоверности от Microsoft,
которы е все можно видеть в листинге 27.22.

Листинг 27 .22. Добавление элементов проверки достоверности JavaScript


в файле мakeBooking. cshtml
@model Appointment
@{ Layout = " Layout ";
@section scripts {
<script asp-src-include="lib/jquery/dist/ * .min.js"></script>
<script asp - src-include="liЬ/jquery-validation/dist/jquery . *.min.js">
</script>
<script asp-src-include="liЬ/jquery-validation-unoЬtrusive/*.min.js">
</script>

<div class =" bg-primary panel - body" ><h2>Book an Appointment</h2></di v>


<form clas s = " pan el-b ody " a sp- ac ti o n="MakeBooking " method= " post " >
<div asp - validat i on - summa r y= "Model On ly " class =" text - danger " ></div>
<div c lass= " form - group " >
<label asp - for =" Client Na me " >You r name : </labe l >
<div>< s pan asp -validation- fo r ="ClientName" clas s =" text - da nger " ></span>
</div>
856 Часть 11. Подробные сведения об инфраструктуре ASP.NET Саге MVC

<input asp -for= " ClientName " class= " form - control " />
</div>
<div class= " form- group " >
<label asp -for= "Date " >Appointment Date : </ label>
<div><span asp-validation-for= " Date " class= " text -d anger " ></s pan>
</div>
<input asp-for=" Date " type="text " asp- format= " {О : d}" class=" form-control" />
</div>
<span asp - validation-for= "TermsAccepted " class= " text - danger " ></span>
<div class= " radio form- group " >
<input asp - for= "TermsAccepted " />
<label asp - for= "TermsAccept ed " >I accept the terms & conditions</label>
</div>
<button type= " submit " class= " Ьtn Ьtn-p r imary">Make Booking</button>
</fo rm>

Файлы должны добавляться в показанном порядке. Когда дескрипторные вспо­


могательные классы трансформируют элементы input, они инспектируют атрибуты
проверки достоверности, примененные к свойствам класса модели, и добавляют ат­
рибуты к выходным элементам. Запустив приложение и просмотрев НТМL-разметку,
отправленную браузеру, вы заметите элемент следующего вида:

<input class= " form - control " type= " text " data-val="true"
data-val-required="The name field is required." id= "ClientName "
name= " Cl i entName" value= "" />
Код JavaScript ищет элементы с атрибутом data - val и выполняет локальную про­
верку достоверности в браузере. когда пользователь отправляет форму. не посылая
НТГР-запрос серверу. Чтобы увидеть эффект, запустите приложение и отправьте фор­
му, одновременно используя инструменты <F12> для наблюдения за тем, что сообще­
ния об ошибках проверки достоверности отображаются, хотя никакие НТГР-запросы
серверу не отправлялись.

Избегание конфликтов с проверкой достоверности, встроенной в браузеры

Ряд браузеров текущего поколения, поддерживающих HTML5, обеспечивают простую про­


верку достоверности на стороне клиента, которая основана на атрибутах, применяемых к
элементам input . Общая идея в том, что элемент input, к которому применен атрибут
requir ed , например, вызовет отображение браузером сообщения об ошибке проверки
достоверности, если пользователь попытается отправить форму, не предоставив значение
для этого элемента.

В случае генерации элементов форм из моделей никаких проблем со встроенной в браузеры


проверкой достоверности возникать не будет. Дело в том, что для указания правил провер­
ки инфраструктура MVC генерирует и использует атрибуты данных (таким образом, элемент
input , который должен иметь значение, помечается атрибутом data - val - required, не
распознаваемым браузерами).

Тем не менее, вы можете столкнуться с проблемами, если не в состоянии полностью кон­


тролировать разметку в приложении, что часто происходит при поступлении содержимо­

го, сгенерированного в другом месте . В результате с формой может работать и провер ка


достоверности jQuery, и проверка достоверности, встроенная в браузер, что всего лишь
сбивает с толку пользователя. Чтобы избежать такой проблемы , к элементу form можно
добавить атрибут novalidate.
Глава 27. Проверка достоверности моделей 857
Одна из замечательных характеристик проверки достоверности на стороне клиента
МVС связана с тем. что те же самые атрибуты. используемые для указания правил про­
вер1ш достоверности, применяются внутри клиента и на сервере. Это означает. что дан­
ные, поступающие из браузеров , которые не поддерживают JavaScript. подВергаются та­
кой же проверке достоверности. что и данные из браузеров. поддерживающих JavaScript.
безо всяких дополнительных усилий. Однако это также означает, что специальные атри­
буты проверки достоверности при проверке на стороне клиента не поддерживаются, по­
тому что код JavaScript не имеет возможности реализовать специальную логику внутри
клиента. Иными словами, при желании использовать проверку достоверности на стороне
клиента необходимо придерживаться встроенных атрибутов, описанных в табл. 27.7.

Сравнение проверки достоверности на стороне клиента в MVC


и проверки достоверности с помощью библиотеки jQuery Validatioп
Функциональность проверки достоверности на стороне клиента в MVC построена на ос­
нове библиотеки jQuery Validation. Если хотите, то можете работать с библиотекой
jQuery
Validatioп напрямую, игнорируя средства MVC. Библиотека jQuery Validation обладает высо­
кой гибкостью и большими возможн остями . Ее имеет смысл исследовать хотя бы для того,
чтобы понять, каким образом настраивать средства MVC, чтобы извлечь максимальную
пользу из доступных вариантов проверки. Библиотека jQuery Validation подробно описана в
к ниге jQuery 2.0 для профессионалов (ИД " Вильяме", 2016 год).

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


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

стоверности на стороне клиента. В качестве части такоrо процесса серверу отправля­


ется запрос Ajax для проверки имени пользователя. Если имя пользователя уже было
выдано, тогда отображается сообщение об ошибке проверки и пользователю предо­
ставляется возможность ввести другое имя.

Процесс может выглядеть похожим на обычную проверку достоверности на сто­


роне сервера . но данный подход обладает рядом преимуществ. Во-первых. удаленно
проверяться будут только некоторые свойства; ко всем остальным значениям данных.
которые ввел пользователь, будет по-прежнему применяться проверка достоверности
на стороне клиента. Во-вторых, запрос является относительно легковесным и сосре­
доточенным на проверке достоверности, а не на обработке целого объекта модели.
Третье отличие связано с тем, что удаленная проверка достоверности выполнятся
в фоновом режиме. Пользователю не приходится щелкать на кнопке отправки и ожи­
дать визуализации нового представления. В итоге пользователи получают более от­
зывчивый интерфейс, что особенно важно в случае медленного сетевого соединения
между браузером и сервером.
Тем не менее, удаленная проверка достоверности сопряжена с компромиссом. Она

соблюдает баланс между проверками на стороне клиента и на стороне сервера. но


требует отправки запросов серверу приложений, поэтому не будет настолько быст­
рой. как обычная проверка достоверности на стороне клиента.
858 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

Первый шаг в сторону использования удаленной проверки достоверности предпо­


лага ет создание метода действия, который может проверить одно и з сво йств модел и .
Мы будем пров ерять сво й ство Date модели Appo in tment , чтобы обеспечить нахож­
дени е запроше нной даты встречи в будушем . (Это одно из исходных правил проверки
достоверности, применяемых в н ачале главы, но его нево з можно ре а лизовать с помо­

щью стандартных средств проверки н а стороне клиента.) В листинге 27.23 прив еден
код метода действия ValidateDate () ,добавленного в контроллер Home .
Листинг 27.23. Добавление метода действия для проверки достоверности
в файле HomeController. cs

using System ;
using Micro s of t. As pNetCore . Mvc ;
using ModelVal i dat i on . Models ;
using Microso f t . AspNetCore . Mvc . ModelBinding ;
name s pace Mode l Val i dation . Control l ers {
pu Ы ic class HomeC ontrol le r : Contro l le r
puЫic I ActionResu l t In dex () =>
View ( "MakeBook i ng ", new Appo i ntment () Date = DateTime . Now } ) ;
[HttpPost]
puЫic ViewRes ul t MakeBooking(Appointment appt)
i f (Mode l State. GetVa li da ti onS t ate(name of(appt . Date))
== ModelVa l idationState . Val i d
&& Mode l St ate . GetVa l idat i o nState(name of(appt . Cl i entName))
== Mode l ValidationSta t e . Va l id
&& appt . ClientName . Equals( " Joe", St ringCompari s on . OrdinalignoreCase)
&& appt . Da te. DayOfWeek == DayOfWeek . Monda y) {
ModelState . AddModel Erro r ("",
" Joe cannot book appoin t ments on Mondays " ) ;

i f (ModelState . I sVa l id) {


re t urn View ( "Comple t ed ", appt) ;
else {
r eturn Vi ew () ;

puЬlic JsonResult ValidateDate(string Date) {


DateTime parsedDate;
if (!DateTime.TryParse(Date, out parsedDate)) {
return Json ( "Please enter а valid date (mm/dd/yyyy) ") ;
else if (DateTime. Now > parsedDate) {
return Json ("Please enter а date in the future");
else {
return Json(true);

Методы действий, поддержив ающие удаленную проверку достоверно сти, должны


возвращать объект типа JsonResu l t, который сообщает инфраструктуре MVC о том,
что работа производится с данными JSON , как объяснялось в главе 20 . В дополне­
ние к возвращению такого ре зул ьтат а методы д ействий для проверки достоверности
Глава 27 . Проверка достов е рности моделей 859
дол жны опр еделять п араметр, который имеет то же самое имя , ч то и проверяемо е
пол е данных ; в рассм атриваемом примере это Date. Внутри метода действий прове р­
ка достов е рности вьmолняется путем преобразования значения в объект Da tе т i me и
выя снения. отно с ится ли дата к будущему.

Совет. Можно было бы воспользоваться привязкой моделей, так что параметром метода дейс­
твия стал бы объект Dat eTime, но тогда метод проверки достоверности не вызывался бы в
ситуации, когда пользователь ввел бессмысленное значение вроде apple . Причина в том,
что связывателю модели не удается создать объект DateT i me из app l e и генерируется
исключение . Средство удаленной проверки достоверности не способно обработать такое
исключение , поэтому исключение молча отбрасывается . В итоге возникает нежелательный
эффект: поле данны х не подсвечивается, создавая впечатление , что введенное пользовате­
лем значение является допустимым. Как правило, наилучший подход к удаленной проверке
достоверности предусматривает получение методом действия параметра string и явное
выполнение любого преобразования типа, разбора или привязки модели .

Результаты проверки достоверности выражаются с применением метода Json () .


создающего результат в формате JSON, который может разобрать и обработать сце­
н арий удаленной проверки достоверности на стороне клиента. Если значение допус ­

тимо. тогда в качестве параметр а методу Js on () передается t rue :

retu r n Json( true ) ;

При наличии проблемы в параметре передается сообщение об ошибке проверки.


к оторое должен увидеть пользователь:

return Json ( "Please enter а date in the future" ) ;

Чтобы использовать метод удаленной проверки достоверности. к нужному свойс­


тву класса модели применяется атрибут Remo t e (листинг 27.24) .

Листинг 27 .24. Использование атрибута Remote в файле Appoin tmen t. cs


using System ;
using System . Compone ntModel . DataAnnotat i ons ;
using ModelValidation .Infrast r uc tu r e;
using Мicrosoft.AspNetCore.Mvc;
namespace ModelVa li dat i on . Mo del s
p uЫic class Appoi nt me n t {
[Requ i red]
[Display(Name = " name " ) ]
puЫic string Cl ien t Name { get ; set; }
[UIHint ( " Date " )]
[Required(ErrorMe ss age = " Please enter а date " )]
[Remote ("ValidateDate", "Home")]
puЫic DateTime Date { ge t ; set; }
[MustBeTrue( ErrorMes sage = " Уои mu s t accep t t he terms " ) ]
puЬlic bool TermsAc cepted { get; se t ; }
860 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

В качестве аргументов атрибуту Remo t e указываются имя действия и контроллер,


предназначенные для генерации URL, по которому библиотека проверки достов е рнос­
ти JavaScript будет обращаться с целью выполнения проверки - в рассматриваемом
случае это действие Va l idate Date из контроллера Home .
Чтобы посмотреть на работу удаленной проверки достоверности, запустите при­
ложение, п е рейдите на URL вида /Home и введите дату, относящуюся к прошлому.
После того как фокус ввода переместится на другой элемент, появится сообщение об
ошибке проверки достоверности (рис. 27.8).

.
С1 Mod~1 Valld-'tion х

1 ~ с-~~~'"',:;;;~~~'-·· ~:::-=:.э ··1

1 Yoшname: 1

1 Joe 1

1 Appointment Date: ,

1 · Please enler а date iп the fulure


11 01/01 /2000
1

, О 1accept the terms & coпdit ions

1 ,.,,,,,,.
1
1
L---··- ---·-·· - - - --·-- - - -······--·-· - - - - ··-·-· -- . ---··--. ·- -· -- _\
Рис. 27 .8. Выполнение удаленной проверки достоверности

Внимание! Метод действия для проверки достоверности будет вызываться, когда пользова­
тель впервые отправляет форму, и затем каждый раз , когда данные редактируются . Для
элементов ввода текста любое нажатие клавиши будет иметь следствием обращение к
серверу. В некоторых приложениях это может вылиться в значительное количество запро­
сов, что должно быть учтено при описании требований к производительности сервера и
ширине полосы пропускания в производственной среде . Кроме того, может быть принято
решение отказаться от удаленной проверки достоверности для свойств, проверка кото­
рых сопряжена с высокими затратами (например, если выяснение уникальности имени
пользователя требует обращения к медленному серверу).

Резюм е
В настояще й главе был представлен широкий спектр приемов , доступных для
выполнения проверки достоверности моделей, которая гарантирует, что предостав ­
ляемые пользователем данные удовлетворяют ограничениям, налагаемым на модель

данных. Пров е рка достоверности моделей является важной темой, и наличи е в при­
ложении подходящих средств проверки жизненно важно для обеспечения пользова­
телям комфортных условий работы. В следующей главе будет показано, к ак защитить
приложение МVС с применением ASP.NET Соге Identity.
ГЛАВА 28
Введение в ASP.NET
Core Identity

с истема ASP.NET Core Identity представляет собой АРI-интерфейс от


предназначенный для управления пользователями в приложениях
В этой главе демонстрируется процесс настройки ASP.NET Саге Identity
Microsoft,
ASP.NET.
и создания
простого инструмента администрирования пользователей, который управляет инди­
видуальными пользовательскими учетными записями, хранящимися в базе данных .
Система ASP.NET Core Identity поддерживает другие виды пользовательских учет­
ных записей, такие как записи, хранящиеся с использованием Active Dlrectory, но
здесь они не рассматриваются, потому что редко применяются вне корпораций (где
р еализации Active Directive оказываются настолько замысловатыми, что очень трудно
отыскать полезные общие примеры).

На заметку! Настоящая глава требует наличия установленного средства SQL Server LocalDB
для Visual Studio. Чтобы добавить LocalDB, запустите программу установки Visual Studio и
установите компонент Microsoft SQL Server Data Tools (Инструменты данных Microsoft
SQL Server) .

В главе 29 будет показано, как выполнять аутентификацию и авторизацию с по­


мощью таких пользовательских учетных записей, а в главе 30 - каким образом вый­
ти за рамки основ и применять ряд более сложных при емов. В табл. 28. 1 приведена
сводка, позволяющая поместить систему ASP.NET Core Identity в контекст.

Таблица 28.1. Помещение системы ASP.NET Core ldentity в контекст

Вопрос Ответ

Что это такое? Система ASP.NET Core ldentity - это АРl-интерфейс для управления
пользователями и запоминания пользовательских данных в хра­

нилищах, таких как реляционные базы данных, посредством Entity


Framework Core
Чем она полезна? Управление пользователями является важной возможностью для
большинства приложений, и ASP.NET Core ldeпtity предлагает готовую
хорошо протестированную платформу, которая не требует создания
специальных версий распространенных функций

Как она Система ldentity используется через службы и промежуточное ПО , до­


используется? бавляемое в класс Star tup, и посредством классов, которые действу­
ют в качестве шлюзов между приложением и функциональностью ldentity
862 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

Окончание табл. 28. 1


Вопрос Ответ

Существуют ли В Microsoft скомпенсировали жесткость ранних АРl-интерфейсов уп­


какие-то скры­ равления пользователями ASP.NET, сделав систему ldeпtity настолько
тые ловушки или гибкой и конфигурируемой, что выяснение того, что возможно и что
ограничения? необходимо, может оказаться проблематичным . В книге мы лишь
слегка коснемся поверхности этой глубокой и сложной систе м ы

Существуют ли Можно было бы реализовать собственные АРl-интерфейсы, но такая


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

Изменилась ли она Система ASP.NET Core ldeпtity работает с инфраструктурой ASP.N ET


по сравнению с Core аналогично тому, как было в предшествующих версиях, хотя
версией MVC 5? она обновлена для согласования с системой служб и промежуточ но­
го ПО , а также имеет большее число компонентов, доступных через
внедрение зависимостей . Слож ную авторизацию можно выполнять
с помощью проверок , основанных на политиках и ресурсах, как будет
описано в главе 30

В табл. 28.2 приведена сводка для настоящей главы.

Таблица 28.2. Сводка по главе

Задача Решение Листинг

Добавление ldeпtity в проект Добавьте пакеты и промежуточное ПО для 28.1-28.15


ASP.NET ldentity Core и Entity Framework
Core, создайте класс пользователя и класс
контекста базы данных , а также создайте
миграцию базы данных

Чтение пользовательски х Выполните запрос к базе данных ldeпtity с 28.16, 28.17


данных применением класса контекста

Создание пользовательской Вызовите метод UserManager . 28.18-28.20


учетной записи CreateAsync ()
Изменение стандартной поли­ Установите параметры паролей в классе 28.21
тики проверки паролей Startup
Реализация специальной про­ Реализуйте интерфейс 28.22-28.24
верки паролей IPasswordValidator либо унаследуйте
класс от класса PasswordValidator

Изменение политики проверки Установите параметры пользовательски х 28.25


учетных записей учетных записей в классе Startup
Реализация специальной про­ Реализуйте интерфейс IUserValidato r 28 .26-28 .28
верки учетных записей либо унаследуйте класс от класса
UserValidator
Удаление пользовательской Вызовите метод 28.29 , 28.30
учетной записи UserManager.DeleteAsync()
Редактирование пользователь­ Вызовите метод 28.31-28.33
ской учетной записи UserManager . UpdateAsync()
Глава 28. Введение в ASP.NET Core ldentity 863

Подготовка проекта для примера


Создайте новы й проект типа Empty (Пусто й) по имени Users с использовани­
ем шабл о на ASP.NET Core Web Application (.NET Core) (В е б-приложени е ASP.NET
Core (.NET Core)) . Добавьте требуем ые п акеты NuGet в р аздел dependen cies файл а
proj ect . j son и настройте инструментарий Razor в разделе t ool s, как показано в
листинге 28.1. Разделы, которые не нужны для данной главы , понадобится удалить.
Пакеты , требующиеся для Identity, будут добавляться отдельно, чтобы подчеркнуть
р аз ницу между пакетами, н е обходимыми для общей разработки приложений MVC, и
пакетами, предназначенными для аутентификации и авторизации.

Л и стинг 28.1. Добавление пакетов в файле proj ect. j son

"dependencies ": (
"Microso f t . NETCo r e.App ":
"version " : " 1 . 0 . О ",
" type ": "platform"
} 1

"Microsoft.AspNetCore . Diagnostics " : " 1.0 . 0 ",


"Microsoft . AspNetCore . Server. I IS i ntegration ": " 1 . 0 . 0 ",
"Microso f t . AspNetCo r e . Se r ve r. Kes tr el ": " 1 . О . О ",
"Microsoft . Extens i ons . Log gi ng . Console " : "1. 0 . 0",
"Мicrosoft.AspNetCore.Mvc : 11 1.0 . 0 11 , 11

"Microsoft.AspNetCore . StaticFiles": "1.0.0",


11
Мicrosoft.AspNetCore . Razor.Tools 11 :
"version": "1. О. O-preview2-final 11 ,

"type": "build"

},

"tools ":
Мicrosoft.AspNetCore.Razor.Tools : 11 1.0.0-preview2-final 11 ,
11 11

"Mi crosoft .AspNetCore . Server . I I Si ntegration .Tools ": "l . O. O-preview2 - fi nal "
}'
11
f r ameworks 11 : {
"netcoreappl . O":
" imports 11 : [ "dotnetS. 6", 11
porta Ы e - net4 5+win8 " ]

) '
"buildOptions ":
11
emitEntryPoint 11 : true , 11
preserveCompilationContext ": true
) '
" runtimeOpt i ons ": {
" conf igProperties ": { 11
Sys tem . GC . Server ": true }

В листинге 28 .2 показан код класса Startup, который конфигурирует средства,


пр едоставляемы е пакетами NuGet.
864 Часть 11. Подробные сведения об инфраструктуре ASP.NET Соге MVC

Листинг 28.2. Содержимое файла Startup. cs

using Microsoft.AspNetCore.Builder;
using Microsoft .Extensions.Dependencyinjection ;
namespace Users {
puЬlic class Startup
puЫic void ConfigureServices{IServiceCol lecti on services) {
services.AddМvc();

puЫic void Configure(IApp li cationBuilder арр) {


app.UseStatusCodePages();
app.UseDeveloperExceptionPage();
app.UseStaticFiles();
app.UseМvcWithDefaultRoute();
}

Создание контроллера и представления


Создайте папку Controllers, добавьте файл класса по имени HomeController. cs
и поместите в него определение контроллера из листинга 28.З. Этот контроллер будет
применяться для описания деталей пользовательских учетных записей и данных, а
его метод действия Index () передает словарь значений стандартному представле­
нию через метод View ().

Листинг 28.З. Содержимое файла HomeController. cs из папки Controllers

using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
namespace Users .Controllers {
puЫic class HomeController : Controller
puЫic ViewResul t Index () =>
View(new Dictionary<string, object>
{ [ " Placeholder "] = "Placeholder" } ) ;

Чтобы снабдить контроллер представлением, создайте папку Vi ews/Home и до­


бавьте в нее файл представления по имени Index. cshtml с разметкой, приведенной
в листинге 28.4.

Листинг 28.4. Содержимое файла Index. cshtml из папки Views/Home

@model Dictionary<string, object>


<div class= " bg-primary panel-body"><h4>User Details</h4></div>
<tаЫе class="taЫe taЬle-condensed taЫe-bordered">
@foreach (var kvp in Model) {
<tr><th>@kvp.Key</th><td>@kvp.Value</td></tr>

</tаЫе>
Глава 28. Введение в ASP.NET Саге ldentity 865
Пр едс тавлен ие отображает содержимое словаря модели в таблице . Для подде ­
ржки пр едставл ения создайте папку Views/ Shared и добавьте в не е файл по им е ни
_ Layout . cs h t ml с раз меткой , показанной в листинге 28 .5.

Листинг 28.5. Содержимое файла_Layout. cshtml из папки Views/Shared


< !DOCTYPE html>
<html>
<head>
<t i tle>Users</tit l e>
<meta name= "viewport " content= "wi dth=device - width " />
<link href= " /liЬ/bootstrap/d i st / css / boo t st r ap . css " rel= " stylesheet " />
</head>
<body class= "pane l-body " >
@RenderBody ()
</body>
</html>

При стили за ции НТМL- э л е ментов представлени е полаг аетс я на СSS-пакет

Bootstrap . Создайте в 1юрневой папке проекта файл bower . j son с использованием


шаблона элеме нта Bower Configuration File (Файл конфигурации Bower) и добавьте па­
кет Bootstrap в ра здел depende nc i es (листинг 28.6).

Листинг 28.б. Добавление пакета Bootstrap в файле bower. j son

" пате ": "asp.net ",


"private ": true ,
"dependencies " : {
"bootstrap": 11 3.3.6 11

Последний подготовительный шаг связан с созданием файл а _V i ewimports .


cshtml в п апк е Views, в котором настраиваются встроенные д е снрипторны е вспо­

могательные массы для применения в представлениях (листинг 28.7).


Листинг 28. 7. Содержимое файла _Viewimports. csh tml из папки Views
@addTagHelper *, Microsoft . AspNetCore . Mvc.TagHe l pers

Н а кон е ц, со з дайте в п а пке Views файл запуска пр едставле ния по имени


_ ViewStart. cshtm l с содержимым из листинга 28.8. Он обесп е чит использовани е
компоновки, созданной в листинге 28.5, всеми представл ениями в приложении.

Листинг 28.8. Содержимое файла_ViewStart. cshtml из папки Views


@{
Layout " Layout ";

Запустив приложе ни е , вы увидите вывод , приведенный на рис. 28.1.


866 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

Use rs х

r::--
..:;..- ~ С CJ localhost:53221

Placeholder Placeholder

Рис. 28.1. Запуск примера приложения

Настройка ASP.NET Core ldentity


Процесс настройки системы Identity затрагивает почти каждую часть приложе­
ния, требуя новых классов модели, изменений конфигурации, а также контроллеров
и действий для поддержки операций аутентификации и 'авторизации. В последующих
разделах мы пройдем по процессу настройки Identity в базовой конфигурации, что­
бы продемонстрировать разнообразные шаги, которые с ним связаны. Задействовать
систему Identity в приложении можно многими разными способами, но конфигура­
ция, применяемая в этой главе, предусматривает использование самых простых и хо­
довых параметров.

Добавление пакета ldentity в приложение


В случае применения шаблона Empty среда Visual Studio не добавляет пакеты
ASP.NET Core Identity или Entity Framework Core в создаваемые проекты, поэтому они
должны быть добавлены вручную. Добавьте требуемые пакеты в файл proj ect. j son
(листинг 28.9).

Листинг 28.9. Добавление пакетов ldentity в файле project. json

"depe ndenci е s" : {


"Mi crosoft.NETCo re.App ":
"versi on ": "1. О. О ",
"t ype ": "p latform "
}'
"Microsoft . AspNetCore .Diagnostics": "1. 0 .О",
"Microsoft.AspNetCore .Se rver .IISintegration ": "1. 0 . 0 ",
"Mic rosoft .Asp NetCore .Server.Kestrel": "1.0.О",
"Microsoft.Extens i ons .Logging .Console": "1.0. О ",
"Micros oft.AspNetCore .Mvc": "1. 0 .0",
"Mi crosoft .As pNetCore . StaticFiles": " 1 . О . О ",
"Microsoft .AspNe tCore.Razor .Tools ":
"vers i on ": "l .0 . 0-p review2 -final",
" type ": "build "
}'
Глава 28. Введение в ASP.NET Саге ldentity 867
"Мicrosoft.Extensions.Configuration": "1.0.0",
"Мic r osoft . Extensions.Configuration.Json":
11
1.0.0 11 ,
"Microsoft.AspNetCore.Identity.EntityFrameworkCore": 11 1.0.0 11 1
"Мicrosoft . EntityFrameworkCore.SqlServer": 11 1.0.0 11 1
"Мicrosoft.EntityFrameworkCore.Tools": "l.0.0-preview2-final"
} 1

" tools ": {


"Microsoft . AspNetCore . Razor .Tools ": " 1 . 0 . 0- preview2 - final ",
"Microsoft . AspNetCore . Server .I ISintegration .Too ls": " l .0. 0- previ ew2 - f i nal ",
"Мicrosoft.EntityFrameworkCore.Tools": "l.0.0-preview2-final"
} 1

" frameworks ": {


"netcoreappl.0 ":
"imports ": [ " dotne t 5 . 6 11 1 " po r t aЬl e -n e t 45+w in 8 " ]

} 1

"buildOptions ":
"emitEntryPoint ": true ,
"preserveCompi l ationCo ntext ": t r ue
} 1

" ru ntimeOptions ": {


"configPropert i es ": { " Sys t em . GC . Server": t r ue}

У1шз анны е п акеты добавляют в проект ASP.NET Core Identity и Entity Framework
Core. Доб а вл ени е в разделе too l s уст анавливает инструм е нты командной строки,
котор ы е по зволяют настр а ивать базу данных , используемую для хран е ния данных
Identity, что вскор е будет сделано.

С оздан и е класса пользо в ателя


Сл едующий ш аr заключа ется в опр еделении класса , предназн ач е нного для пр ед­
ставления поль з ователя в приложен ии , который называется классом пользовате­
ля. Класс п ол ьз ователя: наследу ется: от классаI dentityUser, опр едел е нного в про ­
странств е им ен Micro s oft . AspNetCore . Identi ty . En ti tyFrameworkCore . Кл асс
Identi tyUser обеспечивает базовое представление поль з ователя , которое можно
ра с ширять, добавляя свойства к производному классу , как будет описано в глав е 30.
В табл. 28.3 пер е числ ены наиболее пол езные встро енные свойства, которы е опр еде­
лены в Identi tyUser , включая применяемые в этой главе.
И ндивидуал ьны е свойства в настоящий момент неважны. Важно то, что кл асс
IdentityUser пр едоставляет доступ к б азовой информ а ции о пользов ат ел е: имя
пол ьз ователя, адр е с электронной почты, телефонный номер, хеш пароля, членство в
рол я х и т. д . При желании хранить дополнительные св едения о поль з овател е придется
добавить свойства в кла сс, унасл едованный от Identi tyUser, который будет исполь­
зоваться для представл е ния пользователей в приложении.
Ч т обы со з дать класс пользователя для приложени я, со здайте папку Mode l s и до­
бавьте в н ее файл класса по имени AppUser . cs с опр еделени е м кл асса AppUser , при­
в еденным в листинге 28.10.
868 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

Таблица 28.З. Свойства класса Identi tyUser


Имя Описание

Id Это свойство содержит уникальный идентификатор пользователя

UserName Это свойство возвращает имя пользователя

Claims Это свойство возвращает коллекцию заявок (claim) пользователя,


которая будет описана в главе 30

Email Это свойство содержит адрес электронной почты пользователя

Logins Это свойство возвращает коллекцию входов пользователя, используе­


мую для сторонней аутентификации, как объясняется в главе 30
PasswordHash Это свойство возвращает хешированную форму пароля пользователя ,
которая применяется в разделе "Реализация возможности редактиро­
вания" далее в главе

Roles Это свойство возвращает коллекцию ролей , к которым принадлежит


пользователь (глава 29)
PhoneNumЬer Это свойство возвращает телефонный номер пользователя

SecurityStamp Это свойство возвращает значение, которое изменяется , когда меняет­


ся удостоверение пользователя, например, из-за смены пароля

Листинг 28.1 О. Содержимое файла AppUser. cs из папки Model s

using Microsoft.AspNetCore.Identity .E ntityFrameworkCore ;


namespace Users . Models {
puЬlic class AppUser : IdentityUser
11 Для базовой установки Identity
11 дополнительные члены не требуются

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

Конфигурирование импортирования представлений

Несмотря на то что это не относится непосредственн о к настройке ASP.NET Core


Identity, в следующем разделе мы будем работать с объ ектами AppUser в пр едстав­
лениях. Чтобы упростить написание представл ений, добавьте про странств о имен
Users . Models в файл импортирования представл ений (листинг 28.11).

Листинг 28.11. Добавление пространства имен в файле_Viewimports. csh tml

@using Users.Models
@addTagHelper *, Microsoft . AspNetCore . Mvc . TagHelpers
Глава 28. Введение в ASP.NET Core ldentity 869

Создание класса контекста базы данных


Следующий шаг предусматривает создание класса контекста базы данных Entity
Framework Core, который оперирует с классом AppUser . Класс контекста унаследован
от Ident i tyDbContext<T>, где т - IOiacc пользователя (App User в текущем приме­
ре). Добавьте в папку Models файл класса по имени Appidenti tyDbCo n t ex t . cs и
определите в нем класс, как показано в листинге 28.12 .

Листинг 28.12. Содержимое файла Appidenti tyDЬContext. cs из папки Models

using Mi crosoft . AspNetCore . Identity .EntityFrameworkCore ;


using Microsoft . EntityFrameworkCore ;
names pace Users . Model s {
puЬlic class AppidentityDbC ontex t : Ident i tyDbConte x t <AppUser> {
puЫic AppidentityDbContext(DbContextOptions<AppidentityDbContext>
options) : base (options) { }

Класс контекста базы данных может быть расширен для изменения способа, ко­
торым база данных настраивается и используется, но в случае элементарного прило­
жения ASP.NET Core Identity простого определения класса вполне достаточно, чтобы
начать и получить заполнитель для любой настройки в будущем.

На заметку! Не пере живайте, если роль та ких классов не особенно ясна. Если ранее вы не
имели дела с инфраструктурой Entity Framework Core, тогда можете трактовать ее как
"черный ящик ". После того, как основные строительные блоки окажутся на месте (и вы
можете их копировать в свои проекты, чтобы все работало), потребность в их редактиро­
вании будет возникать редко .

Конфигурирование настройки строки подключения к базе данных


Первый шаг по конфигурированию ASP.NET Core Identity заключается в опреде­
лении строки под1<mочения к базе данных. По соглашению строка подключения по­
мещается в файл appsettings . j son, который затем загружается в I\Лассе Startup.
Создайте в корневой папке проекта файл appsettings. j son с использованием шаб­
лона эл ем ента ASP.NET Configuration File (Файл конфигурации ASP.NET) и добавьте в
него содержимое листинга 28.13.
Листинг 28.1 З. Содержимое файла appsettings. j son

" Data ": {


"SportStoreidentity ":
"ConnectionString ": "Server=(localdb)\\MSSQLLocalDB;Database=
IdentityUsers ; Trusted_Connec tion=True;
Mu l tipleAc tiveResultSets =true "
}
870 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Соге MVC

В строке подключения указан параметр loca l d b, который предоставляет удобную


поддержку баз данных для разработчиков. Кроме того, в каче стве имени базы данных
указ ывается IdentityUsers.

На заметку! Ширина печатной страницы не позволяет соблюдать правильный формат строки


подключения, которая дол ж на выглядеть как одиночная неразрывная строка. В редакто­
ре Visual Studio это не проблема, но в листинге строку пришлось разбить на части. При
добавлении строки подключения в свой проект удостоверьтесь, что вводите ее в единс­
твенной строке.

Имея строку подключения к базе данных, можно обновить класс Startup для чте ­
ния конфигурационного файла и об е спечения доступности настроек (листинг 28.14).

Листинг 28.14. Чтение настроек приложения в файле Startup. cs

u sing Mi crosoft . AspNe tCore . Bu ilde r;


using Microsoft .Extens i ons . Depend e n cy! n j ection ;
using Мicrosoft.Extensions . Configuration;
using Мicrosoft.AspNetCore.Hosting;
namespace Users {
puЫ i c clas s Star tup
IConfigurationRoot Configuration;
puЫic Startup(IHostingEnvironment env)
Configuration = new ConfigurationBuilder()
.SetвasePath(env.ContentRootPath)
.AddJsonFile ("appsettings. json") .Build();

pu Ыi cvo id Con figu reSe rvi ces (IServ iceCo llec ti on s e r v i ces ) {
services . AddMvc();

p uЫ ic vo id Con figu re(IAppl icatio n Bui lder ар р ) {


a pp. Use Sta t u sCod e Page s ( );
app. UseDeveloperExcep t ionPage() ;
a p p . Use St ati c Fi l e s () ;
a pp.U seMv cWit hDef a ul tRou te ();

Внесенные в код изменения загружают файл a pp s ett i ngs . j son и предст авляют
соде ржимое через свойство Conf ig ura t i on , которое применяется в следующем раз ­
дел е при настройке служб и промежуточного ПО ASP.NET Core Identity.

Конфигурирование служб и промежуточного


программного обеспечения ldentity
Финальный шаг настройки системы Identity пр едусм атривает добавлени е служб и
промежуточного ПО в нласс S t a rtup , чтобы внедрить Identity в конв е йер обработни
запросов и предоставить средства, которые используются для управл е ния пользов а ­

телями где-то в других местах приложения. Необходимые изме нения поназаны в лис ­
тинге 28.15.
Глава 28 . Введение в ASP.NET Core ldentity 871
Листинг 28.15. Конфигурирование служб и промежуточного ПО ASP.NET Core ldentity
в файле Startup. cs

using Microsoft . AspNetCore.Builder ;


using Microsoft . Extensions . Dependencyinjection ;
using Microsoft . Extensions . Configurat i on ;
using Microsoft . AspNetCore .H osting ;
using Мicrosoft.EntityFrameworkCore;
using Microsoft . AspNetCore.Identity.EntityFrameworkCore;
using Users.Model s ;
namespace Users {
puЫic class Startup
IConfigurationRoot Configuration;
puЬlic Startup(IHostingEnvironment env) {
Configuration = new Configu r ationBuilder()
. SetBasePath(env .C ontentRootPath)
. AddJsonFile( " appsettings . json " ) . Build() ;

puЫic void ConfigureServices(IServiceCollection services)


services.AddDЬContext<AppidentityDЬContext>(options =>
options.UseSqlServer(
Configuration["Data:SportStoreidentity:ConnectionString"]));
services.Addidentity<AppUser, IdentityRole>()
. AddEntityFrameworkStores<AppidentityDbContext>();
services.AddMvc() ;

puЫic void Conf igur e(IApplicat ionBu ilder арр) (


app . UseStatusCodePages() ;
app . UseDeveloperExceptionPage() ;
app.UseStaticFi l es() ;
app .U seidentity() ;
app.UseMvcWithDefaultRoute() ;

Для создания базовой установки ASP.NET Core Identity требуются три набора изме ­
нений. Сначала настраивается инфраструктура Entity Framework (EF) Core, которая
предоставляет приложениям МVС службы доступа к данным:

services . AddDbContext<App i dentit y DbContext>(options =>


options . UseSqlServer(Configurat i on[
" Data:SportStoreidentity : ConnectionString " ])) ;

Метод AddDbCon text () добавляет службы, требующиеся для EF, а метод


UseSglServer () настраивает поддержку, необходимую для хранения данных с при­
менением Microsoft SQL Server. Метод AddDbContext () позволяет использовать ранее
созданный класс контекста базы данных и указать, что он будет копироваться с ба­
зой данных SQL Server, строка подключения для которой получается из конфигурации
приложе ния (в пример е приложения это файл appsettings . j son).
872 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC

Понадобится также настроить службы для ASP.NET Core Identity, что делается сле­
дующим образом:

services.Addidentity<AppUs e r, IdentityRole>()
.AddEntityFramewo rkSt ores<Appiden t ityDbCont ex t >() ;

Метод Addidenti ty () имеет параметры типов, которые указывают класс, приме­


няемый для представления пользователей, и класс, используемый для представления
ролей. Здесь задается класс AppUser для пользователей и класс Ide ntityRo l e для
ролей. Метод AddEnti tyFrameworkStores () указывает, что система Identity долж­
на использовать инфраструктуру Entity Framework Core для сохранения и извлече­
ния своих данных с применением созданного ранее класса контекста базы данных.
Последнее изменение в ю~ассе Startup касается добавления системы ASP.NET Core
Identity в конвейер обработ1<и запросов . Это позволяет ассоциировать пользователь­
ские учетные данные с запросами на основе сооkiе-наборов или переписывания URL,
т.е. детали пользовательских учетных записей не включаются напрямую в НТТР­
запросы, отправляемые приложению, или в ответы, которые оно генерирует:

app . Useidentity() ;

Создание базы данных ldentity


Почти все на месте, и осталось лишь фактически создать базу данных, которая
будет использоваться для хранения данных Identity. Откройте окно консоли диспет­
чера пакетов, выбрав пункт меню Toolsc::>NuGet Package Manager (Сервис е::> Диспетчер
пакетов NuGet) в Visual Studio, и введите следующую команду:

Add - Migration Initial


Как объяснялось при настройке базы данных для приложения SportsStore. инфра­
структура Eпtity Framework Core управляет изменениями в схемах баз данных через
средство, которое называется миграции. В случае модификации 1тассов модели, при­
меняемых для генерации схемы, можно сгенерировать файл миграции, 1<0торый содер­
жит команды SQL, предназначенные для обновления базы данных. Приведенная выше
команда создает файлы миграции, которые будут настраивать базу данных Identity.
Когда команда завершит выполнение. вы увидите в окне Solution Explorer папку
Migrations. Просмотрев содержимое файлов в этой папке, вы обнаружите команды
SQL, которые будут использоваться для создания начальной базы данных. Чтобы за­
действовать файлы миграции для создания базы данных, введите такую команду:

Update - Database
Выполнение команды может занять некоторое время, а после ее завершения база
данных будет создана и готова к применению.

Использование ASP.NET Core ldentity


После проведения базовой настройки можно приступать к применению ASP. NEТ
Core Identity для добавления поддерж1ш управления пользователями в пример при­
ложения. В последующих разделах будет продемонстрировано, как использовать АР!­
интерфейс Identity для создания инструментов администрирования, которые делают
возможным централизованно е управление пользователями.
Глава 28. Введение в ASP.NET Соге ldentity 873
Инструменты централизованного администрирования пользователей полезны
практически во всех приложениях , даже в тех, которые позволяют пользователям со­

здавать и управлять собственными учетными записями. Всегда найдутся пользовате­


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

В рамках настоящей главы инструменты администрирования удобны из-за того, что


они организуют множество основных функций управления пользователями в неболь­
шое число классов, делая их полезными примерами для демонстрации фундаменталь­
ных возможностей системы ASP.NET Core Identity.

Перечисление пользовательских учетных записей


Отправной точкой этого раздела является перечисление всех пользовательских
учетных з аписей в базе данных, что позволит увидеть эффект от кода, который будет
добавлен в приложение позже. Первым делом добавьте в папку Co nt ro ll ers файл
класса по имени AdminContr ol l er . c s и определите в нем контроллер, как пшшзано
в листинге 28 .16, который будет применяться для реализации функциональности ад­
министрирования пользователей.

Листинг 28.16. Содержимое файла AdminController. cs из папки Controllers

using Mi c r osof t. AspNetCore . I de ntit y ;


using Micro s o ft. As pN et Core . Mvc ;
using Users . Model s ;
namespace Use r s .Contro ll ers {
puЫ i c c l as s Admin Contr ol l e r : Controller {
private Use rMa na ger<AppU se r > us e rMana ge r;
puЬlic Admi nCont rolle r( Us erMana ger <AppUs er> us rMgr) {
userManage r = usrMg r;

puЫic ViewRes ult I ndex() => Vi e w(userManager. Users) ;

Метод действия Ind ex () перечисляет пользователей, управляемых систе­


мой I dentity ; разумеется, в текущий момент пользователи отсутствуют, но вско­
ре они появятся . Доступ к поль з овательским данным осуществляется через объе1<Т
UserManager<Ap p Us er> . который конструктор контроллера получает поср едством
внедрения зависимостей .
С помощью объекта UserManage r <App Us er> можно запрашивать хранилище
данных. Свойство Use r s возвращает перечисление объектов пользователей (экземп­
ляров класса AppUser в рассматриваемом приложении), с которым удобно работать,
используя LINQ. Внутри метода действия значение свойства Us er s, которое будет пе­
речислять всех пользователей в базе данных, передается методу View (),так что он
сможет отобразить детали учетных записей. Чтобы снабдить метод действия пред­
ставл е ни ем, создайте папку Vi ew s /Admi n , добавьте файл по имени Ind e x . c sht ml и
поместите в него разметку из листинга 28.17.
874 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC

Листинг 26.17. Содержимое файла Index.cshtrnl из папки Views/Adrnin


@model IEnumeraЫe<AppUser>

<di v class= "b g-primary panel-body"><h4>User Accounts</h4></div>


<tаЫе class= " taЫe taЫe-condensed taЬle - bordered " >
<tr><th>ID</th><th>Name</th><th>Email</th></tr>
@if (Model.Count() == 0) {
<tr><td colspan= " З " class= " text-center " >No User Accounts</td></tr>
else {
foreach (AppUser user in Model) {
<tr>
<td>@user.Id</td><td>@user.UserName</td><td>@user.Email</td>
</t r>

)
</tаЫе>

<а class= " btn Ьtn - primary " asp-action= " Create " >Create</a>

Представление содержит таблицу, в которой для каждого пользователя предусмот ­


рена строка с колонками, отображающими уникальный идентификатор, имя пользо­
вателя и адрес электронной почты. Если пользователи в базе данных отсутствуют.
тогда выводится соответствующее сообщение, как показано на рис. 28.2, для чего по­
надобится запустить прилож ение и запросить URL вида / Admin .

+- -i" С 1
[j localhost:53221/Adinin

ID Name Email
No User Accounts

'•' Рис. 28.2. Отображение (пустого) списка пользователей

В представлени е включена ссылка Create (Со здать), стилизованная под кнопку,


которая нацелена на действие Crea te контроллера Admin. Это действие будет подде ­
ржив ать добавление пользователей.
Глава 28. Введение в ASP.NET Core ldentity 875

Переустановка базы данных

Запустив приложение и перейдя на URL вида / Admin, вы заметите небольшую паузу перед
отображением содержимого, визуализируемого из представления . Причина в том, что инф­
раструктура Entity Framework Core должна создать и подготовить базу данных к ее первому
при менени ю.

Увидеть созданную базу данны х можно, открыв окно SQL Server Object Explorer (Проводник
объектов SQL Server) в Visual Studio. SQL Server
Если вы впервые используете окно
Object Explorer, то должны выбрать в меню Tools (Сервис) пункт Connect to Database
(Подключиться к базе данных) , чтобы сообщить среде Visual Studio о базе данных, с кото­
рой нужно работать. В качестве источника данных выберите Microsoft SQL Server, для имени
сервера ука жите ( localdЬ) \mssqllocaldЬ , оставьте отмеченным флажок Use Windows
Authentication (Использовать аутентификацию Windows) и щелкните на стрелке, раскрыва­
ющей поле Select Or Enter а Database Name (Выберите или введите имя базы данных) .
Спустя несколько секунд отобразится список доступных баз данных LocalDB, в котором
дол ж на быть возможность выбора базы данных IdentityUsers, относящейся к примеру
приложения . Щелкните на к нопке ОК , после чего в окне SQL Server Object Explorer поя­
вится новая запись. Среда Visual Studio запомнит базу данных, так что описанный процесс
необходимо выполнить толь ко один раз .

Для просмотра базы данных понадобится раскрыть элемент ( localdЬ) \ms sqll ocaldb<>
Databases <>Identi tyUsers в окне SQL Server Object Explorer. Вы получите воз­
можность увидеть таблицы , которые были созданы файлами миграции, с именами вроде
AspNetusers и AspNetRoles . После добавления пользователей, как будет показано в
следующем разделе, в базу данных можно отправлять запросы для просмотра содержимого
таблиц.

Чтобы удалить базу данны х, щелкните правой кнопкой мыши на элементе Identi tyusers
в окне SQL Server Object Explorer и выберите в контекстном меню пункт Delete (Удалить) .
В диалоговом окне Delete Database (Удаление базы данных) отметьте оба флажка и щелк­
ните на кнопке ОК для удаления базы данных.

Чтобы заново создать базу данных , откройте окно консоли диспетчера пакетов и введите
следующую команду:

Update - Database
База данны х будет воссоздана и готова к применению, когда вы снова запустите приложение .

Создание пользователей
В отношении входных данных, получаемых приложением, будет использоваться
проверка достоверности моделей MVC, и легче всего это сделать за счет создания про­
стых моделей представлений для каждой операции, поддерживаемой контроллером .
Создайте в папке Models файл IOJacca по имени UserViewModels . cs с содержимым,
приведенным в листинге 28.18.

Листинг 28.18. Содержимое файла UserViewModels. cs из папки Models

using System.ComponentModel . DataAnnotations;


namespace users . Models {
puЫic class CreateModel {
[Required]
876 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC

puЫic string Name { get ; set; )


[Required]
puЫic string Email { get ; set ; }
[Required]
puЫic string Password { get ; set ;

Начальная модель называется Crea teModel и определяет основные свойства,


которые требуются для создания пользовательской учетной записи: имя пользова­
теля, адрес электронной почты и пароль. Атрибут Required из пространства имен
Systern. CornponentModel . DataAnnotations применяется для указания на обяза­
тельность значений всех трех свойств, определя емых моделью.
В листинге 28.19 к контроллеру Adrnin добавле на пара методов действий Crea te (} ,
на которые нацелена ссылка в представлении Index из предыдущего ра здел а ; они ис­
пользуют стандартный прием для отображения пользовател ю представления в случае
запроса GET и обработки данных формы при поступлении запроса POST.
Листинг 28.19. Определение методов действий Create () в файле AdminController. cs

using Microsoft . AspNetCore . Identity ;


using Microsoft . AspNetCore . Mvc ;
using Use rs . Models;
using System.Threading.Tasks ;
namespace Users . Controllers {
puЫic class AdminController : Controller {
private UserManager<AppUser> userManager;
puЫic AdminController(UserManager<AppUser> usrMgr) {
userManager = usrMgr ;

puЫic ViewResu l t Index () => View(use rManager.Users ) ;


puЬlic ViewResul t Create () => View () ;
[HttpPost]
puЫic async Task<IActionResult> Create(CreateModel model)

if (ModelState.IsValid) {
AppUser user = new AppUser
UserName = model.Name,
Email = model . Email
};
IdentityResult result
= await userМanager.CreateAsync(user, model.Password);
if (result.Succeeded) {
return RedirectToAction("Index");
else {
foreach (IdentityError error in result.Errors)
ModelState.AddМodelError("", error.Description);
}

return View(model);
Глава 28 . Введение в ASP. NET Core ldeпtity 877
Важной частью листинга является метод действия Crea te () , принимающий
аргумент CreateModel, который будет вызываться, когда администратор отправ­
ляет данные формы. Свойство ModelState. IsValid применяется для проверки
того, что данные содержат обязательные значения, и если это так, тогда создает­

ся новый экземпляр класса


AppUser, который передается асинхронному методу
UserManager.CreateAsync() :

AppUser user = new AppUser { UserNarne = rnodel . Narne , Ernail = rnodel . Ernail };
IdentityResult result = await userМanager.CreateAsync(user, model.Password);

В качестве результата метод CreateAsync () возвращает объект IdentityResult,


который описывает исход операции посредством свойств, перечисленных в табл. 28.4.

Таблица 28.4. Свойства класса Identi tyResul t

Имя Описание

Succeeded Возвращает t rue, если операция выполнилась успешно

Errors Возвращает последовательность объектов I denti tyError, описываю­


щих ошибки, которые возникли при попытке выполнения операции. Класс
Ident i tyEr r or предлагает свойство Description со сводкой по проблеме

В методе действия Create () с помощью свойства Succeeded выясняется , была


ли создана новая запись о пользователе в базе данных. Если свойство Succeeded воз­
вращает true , тогда клиент перенаправляется на действие Index , так что отобразит­
ся список пользователей:

if (result.Succeeded)
return RedirectToAction("Index");
else {
foreach (Iden t ityError error in result . Errors)
ModelState . AddModelError( "", error .Descr iption) ;

Если свойство Succeeded возвращаетfalse, тогда производится перечисление


последовательности объектов I den t i t y Er ror , которую предоставляет свойство
Errors . Свойство Description используется для создания ошибки проверки досто­
верности на уровне модели с помощью метода ModelState . AddMode l Error (), как

объяснялось в главе 27.


Чтобы снабдить новые методы действий представлением, создайте в папке
Views/Adrnin файл представления по имени Create . cshtrn l и добавьте в него раз­
метку, п01шзанную в листинге 28.20.
Листинг 28.20. Содержимое файла Create. cshtml из папки Views/Admin

@model CreateModel
<div class= "bg - prirnary panel - body "><h4>Create User</h4></div>
<div asp - validation -s ummary= " All " class= " text - dange r" ></div>
878 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC

<fo rm asp - action= " Create" method="post " >


<d i v class= " form - group " >
<label asp -f or= " Name" ></label >
<input asp-f or= " Name " class= " fo rm- control " />
</di v>
<d iv class= "form- group " >
<label asp -for= " Email " ></label>
<input asp -for= " Email " class="form-control" />
</div>
<div c l ass= " form - group " >
<label asp -f or= "P assword " ></label>
<input asp -for= "Pa sswo rd" class ="form- control " />
</div>
<button type="submit" class= " Ьtn Ьtn -prim ary " >Create</button>
<а asp - action="Index " class="btn btn-defau lt">Cancel</a >
</ form >

В этом представлении нет ничего особо примечательного - в нем присутствует


простая форма для сбора значений, которые инфраструктура MVC привяжет к свойс­
твам объекта м одели, переданного методу действия Crea te ( ) , а также сводка по воз­
можным ошибкам проверки достоверности.

Тестирование функциональности создания


Чтобы протестировать возможность создания новой пользовательской учетной за­
писи, запустите приложение, п ерейдите на URL вида / Admi n и щелкните на кнопке
Create (Создать). Заполните форму значениями из табл. 28.5.

Таблица 28.5. Значения для создания примера пользователя

Имя Описание

Name Joe
Email joe@example . com
Password Secretl23$

Совет. Существуют домены, зарезервированные для целей тестирования, в том числе


example . сот . Полный список таки х доменов доступен по адресу https : / /tools .
ie tf. o rg/html/rfc 2606 .

После ввода значений щелкните на кнопке Create. Система ASP.NET Core ldentity
создаст пользовательскую учетную запись, которая будет отображаться после перена­
правления браузера на метод действия Index () , что видно на рис. 28.3 . (Вы получите
другой идентификатор, т.к. идентификаторы для пользовательских учетных записей
генерируются случайным образом.)
Щелкните на кнопке Create еще раз и введите в элементах формы значения из
табл. 28.5. На этот раз после отправки формы вы получите ошибку, о которой сооб­
щается в сводке по проверке достоверности модели (рис. 28.4).
Глава 28. Введение в ASP.NET Core ldentity 879

+- ~ С [J localhost:532.21/Admin ~: =

ю Name Email
75c90f9b-aff7-4818-ab52-Зa640b4501 fd Joe joe@example.com

1ЫМ1

Рис. 28.З. Добавление новой пользовательской учетной записи

+- ~ С ' CJ localhost:53221/Adniin/Create

• User name 'Joe' is already taken.

Name

Joe

Рис. 28.4. Ошибка при попытке создать нового пользователя

Проверка паролей
Одним из наиболее распространенных требований, особенно в корпоративных
приложениях, является принудительное применение политики проверки парол ей.
Чтобы взглянуть на стандартную политику, запустите приложение, запросите URL
вида /Admi n/Create и заполните форму данными, приведенными в табл. 28.6; важ­
ное отл ичие их от данных из предыдущего раздела связано со значением. вводимым

в поле пароля.
880 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Соге MVC

Таблица 28.6. Значения для создания примера пользователя

Имя Описание

Name Alice
Ernail alice@example.com
Password secret

Когда форма отправляется серверу. система ldentity проверяет пароль-кандидат и


генерирует ошибки, если он не удовлетворяет требованиям (р ис . 28.5).

U s~_r s х

__________
+- 7 С :a1ocal host: sз2 211Atimi11/Cre;;;----------~: :
--,.:.;.:..;_."-..-·---------· ···-·- --- ...__.:._
.:.:.:........---..:.:. , ~.:.....;.:.:.:_:__:,._ _______ __

• Passwords must have at least опе поп letter and non digit character.
• Passwords musl have at least one digit ('О'-'9'} .
• Passwords must have at Jeas! one uppercase ('A'-'Z').

Name

Alice

Рис. 28.5. Ошибки в результате проверки пароля

Правила проверки парол ей можно сконфигурировать в классе Startup, как пока ­


зано в листинге 28.21.

Листинг 28.21. Конфигурирование правил проверки паролей в файле Startup. cs

puЫic void ConfigureServices(IServiceCollection services)


services . AddDbContext<AppidentityDbContext>(options = >
options . UseSqlServer(
Configuration ["Data : SportStoreidentity : ConnectionString " ])) ;
services.Addidentity<AppUser, IdentityRole>(opts =>
opts.Password.RequiredLength = 6;
opts.Password.RequireNonAlphanumeric = false;
opts.Password.RequireLowercase = false;
opts.Password.RequireUppercase = false;
opts.Password.RequireDigit = false;
}) .Add.EntityFrameworkStores<AppidentityDbContext>();
services . AddMvc() ;
Глава 28. Введение в ASP.NEТ Саге ldentity 881
МетодAddI den t i t у ( ) может использоваться с фующией, которая принима­
ет объект
Identi tyOptions, чье свойство Password возвращает экземпляр класса
PasswordOptions . Класс PasswordOptions предоставляет свойства для управления
политикой пров ер ки паролей, описанные в табл. 28.7.

Таблица 28. 7. Свойства класса PasswordOptions

Имя Описание
RequiredLength Это свойство типа int применяется для указания мини­
мальной длины паролей

RequireNonAlphanumeric Установ к а этого свойства типа bool в true требует, чтобы


пароли содержали хотя бы один символ, не являющийся
буквой или цифрой

RequireLowercase Установка этого свойства типа bool в true требует, чтобы


пароли содержали хотя бы один символ нижнего регистра

RequireUppercase Установка этого свойства типа bool в true требует, чтобы


пароли содержали хотя бы один символ верхнего регистра

RequireDigit Установка этого свойства типа bool в true требует, чтобы


пароли содержали хотя бы один цифровой символ

В листинге 28.21 указано, что пароли должны иметь минимальную длину в


шесть символов , а другие ограничения отключены. Это не должно делаться в ре­
альном проекте, но позволяет получить эффективную демонстрацию. Запустив
приложение, перейдя на URL вида /Admin/Create и повторив отправку формы , вы
обнаружите, что пароль se c ret теперь принимается и новая пользовательская учет­
ная запись успешно создается (рис . 28.6).

[') Us.ers х

г---·--·-------- --·----·-·------ ~
f- С 1 ф localhost:623 i 1/Adn1in/ Create '(:(
--· --------- ·-~-=:=·=::о.=.:---==-==-::::==-..::;:::-=:-.:::=:.:.~-=:-·.=::.===-=--- - -

• Password canr10t contaiп userriame


• Password сап11оt contain numeric sequence

Name

ВоЬ

Ри с. 28.6. Изменение политики проверки паролей


882 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

Реализация специального класса проверки паролей


Встроенной проверки паролей вполне достаточно для большинства приложений,
но может возникнуть необходимость в реализации специальной политики, особенно
при разработке корпоративного производственного приложения, в котором сложные
политики проверки паролей - обычное явление. Функциональность проверки па­
ролей определяется интерфейсом IPasswordValidator<T> из пространства имен
Microsoft . AspNetCore . Identi ty, где Т - класс пользователя, специфичный для
приложения (AppUser в рассматриваемом примере):

using System.Threading.Tasks ;
namespace Microsoft . AspNetCore.Identity {
puЫic interface IP asswordVa lidator<TUser> where TUser : class {
Task<IdentityResult> ValidateAsync(UserManager<TUser> manager,
TUser user , string password);

Для проверки пароля вызывается метод ValidateAsync (), которому передают­


ся данные контекста через объект UserManager (позволяющий выполнять запросы
к базе данных Identity), представляющий объект пользователя и пароль-кандидат.
В результате возвращается объект IdentityResult, создаваемый с использованием
статического свойства Success, если проблемы отсутствуют, или вызывается стати­
ческий метод Failed (),которому передается массив объектов Identi tyError, опи­
сывающих возникшие во время проверки проблемы.
Чтобы продемонстрировать применение специальной политики провер­
ки, создайте папку
Infrastructure и добавьте в нее файл класса по имени
CustomPasswordValidator . cs с определением из листинга 28.22.

Листинг 28.22. Содержимое файла CustomPasswordValidator. cs


из папки Infrastructure

using System.Collections . Generic;


using System.Threading.Tasks;
us ing Microsoft.AspNetCore . Identity;
using Users.Models;
namespace Users .Infrastructure
puЫic class CustomPasswordValidator : IPasswordValidator<AppUser> {
puЫic Task<IdentityResult> ValidateAsync(UserManager<AppUser> manager,
AppUser user , string password) {
List <IdentityError> errors = new List<IdentityError>();
if (password . ToLower() .Contains (user . UserName . ToLower() ))
errors.Add(new IdentityError {
Code = "Pa sswordContainsUserName ",
Description = "Password cannot contain username"
}) ;

if (password.Contains("l2345"))
errors . Add(new IdentityError {
Code = "PasswordCon ta insSequence",
Description = "Pas sword cannot contain numeric sequence "
}) ;
Глава 28 . Введение в ASP.NET Core ldentity 883
return Task . FromResult(errors . Count ==О?
IdentityResult . Success : IdentityResul t .Failed(errors.ToArray()) );

К.ласе
CustomPasswordValidator проверяет, не содержит ли пароль имя поль­
зователя или последовательность 12345 . В листинге 28.23 класс CustomPassword
Validator р е гистрируется как средство проверки паролей для объектов AppUser.

Листинг 28.23. Регистрация специального класса проверки паролей


в файле Startup. cs

using Microsoft .AspNetCore .Builder ;


using Microsoft . Extensions . Dependencyinjection;
using Microsoft . Extensions . Configuration;
using Microsoft . AspNetCore .H osting ;
using Microsoft . EntityFrame workCore ;
using Microsoft . AspNetCore .I denti t y.E nt ity FrameworkCore ;
using Users . Models ;
using Users.Infrastructure;
using Мicrosoft.AspNetCore.Identity;
namespace Users {
puЬlic class Startup {
IConfigurationRoot Con f iguration ;
puЫic Startup(IHostingEn vironment env) {
Configuration = new Configuratio n Bui l der()
. SetBasePath(env . ContentRootPath)
. AddJsonFile( " appsettings . json " ) . Build() ;

puЫic void ConfigureServices(IServiceCollection services)


services.AddTransient<IPasswordValidator<AppUser>,
CustomPasswordValidator>();
services . AddDbContext<Appident i tyDbContext>(options =>
options . UseSqlServer(
Configuration [" Data :SportSto r e id entity:ConnectionString " ]) ) ;
services . Addidentity<AppUser , IdentityRole>(opts => {
opts . Password . RequiredLength = 6 ;
opts . Password . RequireNonAl phanumeric = false;
opts.Password . RequireLowerca se = false ;
opts . Password . RequireUppercase = false;
opts . Password . RequireDig it = false;
}) . AddEntityFrameworkStores<AppidentityDbContext>() ;
services.AddMvc();

puЫic void Configure(IApplicationBuilder арр) {


app . UseStatusCodePages() ;
app . UseDeveloperExceptionPage() ;
app . OseStaticFi les() ;
app . Useidentity() ;
app . UseMvcWithDefaultRoute() ;
884 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC

Чтобы протестировать специальную политику, запустите приложение, запросите URL


вида /Admin/Create и заполните форму значениями, приведенными в табл. 28.8.

Таблица 28.8. Значения для создания примера пользователя

Имя Описание

Narne ВоЬ

Email bob@exarnple. com


Password ЬоЫ2345

Пароль в табл. 28.8 нарушает оба правила проверки, навязываемые специальным


классом, и вызывает отображение сообщений об ошибках , как показано на рис. 28.7.

С) Users х

1 ~ С 1 Ф localhost:623 11 /Adn;i11/C r~ate


1--
1
i
!
! Password caлnot contain username
• Password cannot contair1 rшmeric sequence

Name

ВоЬ

Рис. 28. 7. Использование специального класса проверки паролей

Можно также реализовать специальную политику проверки, построенную на ос­


нове встроенного класса , который применяется по умолчанию. Стандартный класс
называется PasswordValidator и определен в пространстве имен Microsoft .
AspNetCore . Identi ty . В листинге 28.24 специальный класс проверки изменен так,
что теперь он унаследован от класса PasswordValidator и основывается на подде­
рживаемых им базовых проверках.

Листинг 28.24. Наследование от встроенного класса проверки


в файле CustornPasswordValidator. cs
using System .Collections .Ge neric ;
us ing System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Users . Models ;
using System.Linq;
namespace Users.Infrastructure
puЬlic class CustomPasswordValidator PasswordValidator<AppUser> {
Глава 28. Введение в ASP.NET Core ldentity 885
puЬlic override async Task<IdentityResult> ValidateAsync(
UserManager<AppUser> manager, AppUser user , string password) {
IdentityResult result = awaitbase .ValidateAsync(manager, user, password};
List<IdentityError> errors = result.Succeeded?
new List<IdentityError>(} : result.Errors.ToList(};
if (password.ToLower() .Contains(user.UserName. ToLower()))
errors . Add(new IdentityError {
Code = 11 PasswordContainsUserName 11 ,
Description = 11 Password cannot contai n username 11
}) ;

if (password . Contains ( 11 12345 11 ) )


errors . Add(new IdentityError {
Code = " PasswordContainsSequence 11 ,
Descrip ti on = 11 Password cannot contain numeric sequence 11
}) ;

return errors . Count ==О ? IdentityResult .Success


: IdentityResult . Failed(errors .ToArray()) ;

Для тестирования объединенной проверки запустите приложение и заполните


данными из табл. 28.9 форму, возвращаемую для URL вида /Admin/Create .

Таблица 28.9. Значения для создания примера пользователя

Имя Описание

Name ВоЬ

Email bob@example.com
Password 12345

После отправки формы вы увидите сочетание сообщений об ошибках специаль­


ной и встроенной проверки (рис. 28.8).

[J Usш х

J ~ С ГQ-~~lhos~:S1296/дdn11ri/Cr,;;;;----- -------~
г ---'==--::-:::-:--==-=-=--"=-.-=-===:--_;__-=-::---·--'

1
• Pass\vords must Ье at least 6 characters.
1 • Pass\vord canno1 con tain nLJmeric sequence

Name

ВоЬ

Рис . 28.8. Объединение специальной и встроенной провер ки паролей


886 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC

Проверка деталей, связанных с пользователем


При создании учетной записи проверка выполняется также в отношении им е ни
пользователя и адреса электронной почты. Чтобы взглянуть на работу встроенной
пров е рки, запустите приложение , запросите URL вида /Admin/Crea t e и заполните
форму данным и из табл. 28.10.
Таблица 28.1 О. Значения для создания примера пользователя

Имя Описание

Name В оЫ
Ema il al i ce@example . com
Passwo rd secret

Отправив форму. вы получите сообщение об ошибке (рис. 28.9).

С) Uиrs

• User name 'ВоЬ!' is invalid, сап only contain letters or digits.

Name

ВоЫ

Email
.~· ~ ..,,,,....,, ~"~.~.,,.,.,,..~ -#~- - ~.

Рис. 28.9. Ошибка при проверке пользовательской учетной записи

Проверку можно конфигурировать в классе St ar tu p с использованием свойс­


тва I de nti tyOpt i o ns. Use r, которое возвращает экземпляр класса UserOptions.
Свойства Use r Opt i ons описаны в табл. 28.11.

Таблица 28.11. Свойства класса UserOptions

Имя Описание

All owedUs e r NameCharacters Это свойство типа s t r i ng содержит все разрешенные


символы, которые могут применяться в имени пользова­
теля . В стандартном значении указаны символы a - z , A- Z,
0- 9, переноса, точки, подчеркивания и @. Данное свойство
не является регулярным выражением, и каждый разрешен­
ный символ должен задаваться явно в строке

RequireUniqueEma il Установка этого свойства типа bool в true требует, чтобы


для новых учетных записей указывались адреса электрон­
ной почты, которые не использовались ранее
Глава 28. Введение в ASP.NET Core ldentity 887
В листинге 28.25 конфигурация приложения изменена так, чтобы уникальные
адреса электронной почты были обязательными и в именах пользователей допус­
кались только алфавитные символы нижнего регистра.

Листинг 28.25. Изменение настроек проверки пользовательских учетных записей


в файле Startup. cs

puЫic void ConfigureServices(IServiceCollection services)


services.AddTransient<IPasswordValidator<AppUser> ,
CustomPasswordValidator>();
services.AddDbContext<AppidentityDbContext>(options =>
options.UseSqlServer(
Configuration["Data : SportStoreidentity : ConnectionString"]));
services.Addidentity<AppUser, IdentityRole>(opts => {
opts.User.RequireUniqueEmai1 = true;
11
opts.User.AllowedUserNameCharacters = aЬcdefghijklmnopqrstuvwxyz 11
;

opts . Password.RequiredLength = 6;
opts.Password . RequireNonAlphanumeric = false;
opts.Password.RequireLowercase = false;
opts.Password . RequireUppercase = false;
opts.Password.RequireDigit = false;
}) . AddEntityFrameworkStores<AppidentityDbContext>() ;
services.AddMvc();

Повторно отправив данные из предыдущего теста, вы заметите, что адрес элект­


ронной почты сейчас приводит к ошибке, а символы в имени пользователя по-пре­
жнему отклоняются (рис. 28.10).

D Users х

f- с . €]
·--- -------·-------------·--------------------

• User пате 'ВоЬ!' is iпvalid. сап опlу сопtаiп letters or digits.


• Email 'alice@example.com' is already takeп .

Name

ВоЬ!

Рис. 28.10. Изменение настроек проверки пользовательс ких учетных записей


888 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC

Реализация специальной проверки пользователей


Функциональность проверки указывается с помощью интерфейса IUserValidator<T>,
который определен в пространстве имен Microsoft . AspNetCore . Identi ty:
using System . Threading.Tasks ;
namespace Microsoft . AspNetCore . Identity
interface IUserValidator<TUser> where TUser : class {
puЬlic
Ta sk<Identit yRes u l t > ValidateAsync(UserManager<TUser> manager ,
TUser user);

Метод ValidateAsync () вызывается для выполнения проверки. В результате воз­

вращается объект того же самого класса Identi tyResul t . который применялся при
проверке паролей. Чтобы продемонстрировать работу специального масса проверки,
добавьте в папку Infrastructure файл класса по имени CustomUserValidator . cs
с определением, представленным в листинге 28.26.
Листинг 28.26. Содержимое файла CustomUserValidator. cs из папки Infrastructure
us i ng System . Threading . Tasks;
using Microsoft.AspNe tCore . Ident i ty ;
using Users.Mode l s ;
namespace Users . Infrastr uc ture {
puЬlic class CustomUserValida t or : IU serValidator<AppUser> {
puЬlicTask<IdentityResult> Va lidateAsync(UserManager<AppUser> rnanager ,
AppUser user) {
if (user . Email . ToLower() . EndsWith( " @exarnple . com " ))
return Task .F rornResult(IdentityResult . Success) ;
else {
return Task . FrornResult(IdentityResult . Failed(new IdentityError
Code = "ErnailDornainError ",
Description = "Only exarnple.com emai l addresses are allowed "
}) ) ;

Класс Cus t ornUs erValidator проверяет домен адреса электронной почты, чтобы
удостовериться в том, что он является частью домена examp le . com. 28.27
В листинге

специальный класс регистрируется как средство проверки для объе1пов AppUser .


Листинг 28.27. Регистрация специального класса проверки пользователей
в файле Startup. cs

puЫic void ConfigureServices( I ServiceCol l ection services)


services.AddTransient<IPasswordValidator<AppUser> ,
CustomPasswordValidator>() ;
services.AddTransient<IUserValidator<AppUser>, CustomUserValidator>();
Глава 28. Введение в ASP.NET Соге ldentity 889
services.AddDbContext<AppidentityDbContext>(options =>
options.UseSqlServer(
Configuration[ "Data : SportStoreidentity : ConnectionString " ])) ;
services.Addidentity<AppUser, IdentityRole>(opts => {
opts.User . RequireUniqueEmail = true;
opts.User . AllowedUserNameCharacters = "ab cdefghijklmnopqrstuvwxyz ";
opts . Password . RequiredLength = 6 ;
opts.Password . RequireNonAlphanumeric = false;
opts . Password .RequireLowercase = false;
opts.Password . RequireUppercase = false;
opts .Password .RequireDigit = false;
}) .AddEnt i tyFrameworkStores<Appide ntityDbContext>() ;
services.AddMvc();

Для тестирования специального IOiacca проверки запустите приложение и запол­


ните форму. возвращаемую для URL вида /Admin/Create, данными из табл . 28.12.
Таблица 28.12. Значения для создания примера пользователя

Имя Описание

Name ЬоЬ

Email bob@invalid . com


Password secret

Имя пользователя и пароль успешно проходят проверку. но адрес электронной


почты не относится к корректному домену. В результате отправки формы отобра­
зится сообщение об ошибке проверки (рис. 28.11) .

1
+- ;".! С . D localhost:S3221/Ac!mio/Create

• Only example.com email addresses are allowed

Name

ЬоЬ
,,_...... ,~ ~.,....
Рис. 28.11.
"".,,. . ~~.
Выполнение специальной проверки пользователей
890 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

Процесс сочетания встроенной проверки, обеспечиваемой классом UserValida tor<T>,


и специальной проверки аналогичен такому процессу для проверки паролей (листинг
28.28) .
Листинг 28.28 . Расширение встроенного класса проверки пользователей
в файле CustomUserValidator. cs

using System.Collections.Generic;
using System.Linq;
using System . Threading . Tasks ;
using Mi crosoft .AspNetCore .I dentity ;
using Users . Models;
namespace Users .I nfrastructure {
puЬlic class CustornUserValidator : UserValidator<AppUser> {
puЫic override async Task<IdentityResult> ValidateAsync(
UserManager<AppUser> manager ,
AppUser user) {
IdentityResult result = await base.ValidateAsync(manager, user);
List<IdentityError> errors = result.Succeeded?
new List<IdentityError>() : result.Errors.ToList();
if (!user.Email .T oLower().EndsWith( " @example . com " ))
errors . Add(new IdentityError {
Code = "EmailDomainError ",
Description = " Only example. сот email addresses are allowed "
}) ;

return errors.Count ==О? IdentityResult.Success


: IdentityResult.Failed(errors.ToArray());

Завершение средств администрирования


Для завершения инструмента администрирования осталось только реализовать
средства для редактирования и удаления пользователей. В листинге 28.29 показа ­
ны изменения, внесенные в файл Views/Admin/Index . cshtml для нацеливания на
действия Edi t и Delete контроллера Admin.

Листинг 26.29. Добавление кнопок редактирования и удаления


в файле Index.cshtml из папки Views/Admin

@model IEnumeraЫe<AppUser>

<div class= " bg-primary panel - body " ><h4>User Accounts</h4></div>


<div class="text-danger" asp-validation-summary="ModelOnly"></div>
<tаЫе class= " taЫe taЬle - condensed taЬle - bordered " >
<tr><th>ID</th><th>Name</th><th>Email</th></tr>
@if (Model . Count() == 0) {
<tr><td colspan= " З " class= " text - center " >No User Accounts</td></tr>
Глава 28 . Введение в ASP.NET Саге ldentity 891
else {
foreach (AppUser user in Model) {
<tr>
<td>@user . Id</td><td>@user .Use rName</td><td>@user . Email</td>
<td>
<form asp-action="Delete" asp-route-id="@user.Id" method="post">
<а class="Ьtn Ьtn-sm Ьtn-primary" asp-action="Edit"
asp-route-id="@user.Id">Edit</a>
<Ьutton type="suЬmit"
class="ьtn ьtn-sm ьtn-danger">Delete</button>
</form>
</td>
</tr>

</tаЫе>
<а class= " Ьtn Ьtn-primary" asp - action= " Create " >Create</a>

Кнопка Delete (Удалить) отправляет форму действию Delete контроллера Admin,


что важно, поскольку при изменении состояния приложения требуется за про с POST.
Кнопка Edit (Редактировать) является якорным элементом, который будет отправлять
запрос GET , т.к. первый шаг в процессе редактирования предусматривает отображе­
ние текуших данных. Кнопка Edit содержится в элементе form, поэтому СSS-стили
Bootstrap не укладывают ее вертикально.
Кроме того, в представление добавлена сводка по проверке достоверности модели,
так что можно легко отображать сообщения об ошибках, которые поступают из ос­
тальных средств администрирования.

Реализация средства удаления


В классе UserManager<T> определен метод DeleteAsync (), который принимает
экземпляр класса пользователя и удаляет его из базы данных. В листинге 28.30 ме­
тодDeleteAsync () используется для реализации средства удаления в контроллере

Admin.

Листинг 28.ЗО. Удаление пользователей в файле AdminCon troller. cs


using Microsoft . AspNetCore . Identity;
using Microsoft . AspNetCore . Mvc ;
using Users . Models ;
using System.Threading . Tasks;
namespace Users . Contro l lers {
puЫic class AdminController : Controller {
private UserManager<AppUser> userManag er ;
puЬlic AdminController(UserManager<AppUser> usrMgr) {
userManager = usrMgr;

/ / ... для краткости другие действия не показаны ...

[HttpPost]
puЫic async Task<IActionResult> Delete (string id)
AppUser user = await userМanager.FindВyidAsync(id);
892 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC

if (user != null) {
IdentityResult result = await userмanager.DeleteAsync(user);
if (result.Succeeded) {
return RedirectT0Action( 11 Index");
} else {
AddErrorsFromResult(result);
}
else {
ModelSta te. AddМodelError ( 11 11 ,
11
User Not Found 11 ) ;

return View ( 11 Index 11 , userManager. Users) ;

private void AddErrorsFromResult(IdentityResult result)


foreach (IdentityError error in result.Errors) {
ModelState.AddМodelError( , error.Description); 1111

Метод действия Delete () получает в своем аргументе уникальный идентифика­


тор пользователя и применяет метод FindByi dAsyn c () для нахождения соответс ­
твующего объе кта пользователя. который можно пе редать методу Dele t eAsync () .
В качестве результата метод Dele t eAs ync () возвращает объект I dent i tyResult ,
который обрабатывается таким же образом, как в предшествующих примерах, чтобы
обеспечить отображени е пользователю сообщений о любых ошибках. Для тестирова­
ния функциональности удаления создайте нового пользователя и затем щелкните на
кнопке Delete, расположенной рядом с отображаемыми деталями об этом пользов ате­
ле в представлении Index (рис. 28. 12).

l~ u,..,.
J
г-·-
~ ---- ----- -·--- -------------
с [Ф ~~~~~~~1n -.~- - - - - -·- -----------·--'

j
Na~
ID Name Email
1 ID
j 45Ьdf470-Оd6З-441 5· 80 9е-dЬВ9Зсб d7еЫ le•tuser testeexample.com 1111 ! Ь908Ь741-7аа5-4е70-9367-06dа226467ЗЬ AI~
ii Ь90ВЬ74f - 7 аа5 ·4 е70-9З67 -06d а226467З Ь Alice alicelij)example.cшn 18118 1i еаЬсd121 -ЗЬЗ6-4Jа2-а298-еdЗd857аЗ485 Joe(
1 еаЬСd12 1-ЗЬЗб-4fа2-а298-еdЗd857а3485 Joe
joet'texampJecom " " !" [ )
!i 1111 1: _________________,
l---- ----- - --- ----------·--- ------- ---- _ _j
Рис. 28.12. Удаление пользовательской учетной записи

Реализация возможности редактирования


Для завершения инструмента администрирования необходимо добавить подде­
ржку редактирования адреса электронной почты и пароля, которые связаны с поль­
зовательской учетной записью.
Глава 28. Введение в ASP. NET Core ldentity 893
В настоящий момент имеются только свойства, определяемые пользователями , но
в главе 30 будет показано. как расширить схему специальными свойствами. В листин­
ге 28.31 приведен код методов действий Edit () ,добавленных в контроллер Admi n.

Листинг 28.31. Добавление действий Edi t в файле AdminController. cs


using Mi crosoft . AspNetCore . Identity ;
using Mi crosoft . AspN e tCore . Mvc ;
using Users . Models ;
using System. Threading .Tasks ;
namespace Users.Con t rollers {
puЬlic class Adm i nContro l ler : Co nt rol l er {
private UserManager<App User> u se rManag e r;
private IUserValidator<AppUser> userValidator;
private IPasswordValidator<AppUser> passwordValidator;
private IPasswordНasher<AppUser> passwordНasher;
puЫic AdminController(UserМanager<AppUser> usrMgr,
IUserValidator<AppUser> userValid,
IPasswordValidator<AppUser> passValid,
IPasswordHasher<AppUser> passwordHash)
userМanager = usrMgr;
userValidator = userValid;
passwordValidator = passValid;
passwordHasher = passwordHash;

11 . . . для краткости другие действия не показаны ...

puЫic async Task<IActionResult> Edit(string id) {


AppUser user = await userМanager.FindВyidAsync(id);
if (user != null) {
return View(user);
else {
return RedirectToAction("Index");

}
[HttpPostJ
puЫic async Task<IActionResult> Edit(string id, string email,
st:r:ing password) {
AppUser user = await userManager.FindВyidAsync(id);
if (user != null) {
user.Email = email;
IdentityResult validEmail
= await userValidator.ValidateAsync(userМanager, user);
if (!validEmail.Succeeded) {
AddErrorsFromResult(validEmail);

Identi tyResul t validPass = null;


if (!string.IsNullOrEmpty(password))
validPass = await passwordValidator.ValidateAsync(userМanager,
user, password);
if (validPass.Succeeded) {
user.PasswordНash = passwordНasher.HashPassword(user,
password) ;
894 Часть 11. Подробные сведения об инфраструктуре ASP.NET Соге MVC

} else {
Add.ErrorsFromResult(validPass);

if ( (valid.Email. Succeeded && validPass == null)


11 (validEmail.Succeeded
&& password ! = string. Empty && validPass. Succeeded))
IdentityResult result =
await userМanager.UpdateAsync(user);
if (result.Succeeded) {
return RedirectToAction("Index");
} else {
Add.ErrorsFromResult(result);

else
ModelState.AddМodelError("", "User Not Found");
}
return View(user);

private void AddErrorsFromResult(IdentityResult result)


foreach (IdentityError error in result.Errors) {
ModelState .AddModelErr or( "", error . Description) ;

Действие Edi t, на которое нацелены запросы GET, использует строку идентифи­


катора, встроенную в представление Index, чтобы вызвать метод FindByidAsync ()
для получения объекта AppUser, представляющего пользователя. Более сложная реа­
лизация действия Edi t получает запрос POST и имеет аргументы для идентифш,ато­
ра пользователя, нового адреса электронной почты и пароля. Завершение операции
редактирования требует выполнения нескольких задач.
Первая задача связана с проверкой полученных значений. В настоящий момент
работа ведется с простым объектом пользователя (хотя в главе 30 объясняется, как на­
страивать данные, сохраняемые для пользователей), но даже в этом случае нужно про­
верить пользовательские данные, чтобы обеспечить соблюдение специальных политик,
которые были определены в разделах "Проверка паролей" и ··проверка деталей, связан­
ньrх с пользователем" ранее в главе. Сначала проверяется адрес электронной почты:

user.Email = email;
IdentityResult validEmail = await userValidator.
ValidateAsync(userмanager, user);
i f ( ! validEmail . Succeeded) {
AddErrorsFromResu lt (validEma il ) ;

Кконструкторуконтроллерадобавленазависимостьот IUserValidator<AppUser>,
чтобы можно было проверять новый адрес электронной почты. Обратите внимание ,
что перед выполнением проверки значение свойства Email должно быть изменено,
т.к. метод ValidateAsync () принимает только экземпляры класса пользователя.
Глава 28. Введение в ASP. NET Core ldentity 895
Вторая задача связана с изменением пароля, если он был предоставлен. Система
ASP.NET Core Identity сохраняет хеши паролей, а не сами пароли . Целью является пре­
пятствование похищению паролей. Следующая задача заключается в получении про ­
веренного пароля и генерации хеш-кода. который будет сохранен в базе данных. так
что пользователь м ожет быть аутентифицирован, как демонстрируется в главе 29.
Паролипреобразуютсявхешичерезреализациюинтерфейса IРаsswоrdНаshеr<АррUsеr> ,
которая получается за счет объявления зависимости конструктора, распознаваемой
посредством внедрения зависимостей. В интерфейсе IPasswordHasher определен
метод HashPassword () , который принимает строковый аргумент и возвращает его
хешированное значение :

if (!string . IsNullOrEmpty(password))
validPass = await passwordValidator.ValidateAsync(userМanager,
user, password) ;
if (validPass . Succeeded) {
user.PasswordHash = passwordHasher.HashPassword(user, password);
else {
AddErrorsF r omResu l t(validPass) ;

Изменения , внесенные в класс пользователя, не сохраняются в базе данных до тех


пор. пока не будет вызван метод UpdateAsync () :

if ((validEmail . Succeeded && va lidPas s == null ) 11 (validEmail . Succeeded


&& password != string .Ernpty && valid Pass .Succeeded)) {
IdentityResult result = await userМanager.UpdateAsync(user);
if (result . Succeeded) {
return RedirectToAction( "Index" ) ;
else {
AddErrorsFrornResult(result) ;

Соэдание представления
Финальным компонентом является представление, которое будет отображать теку­
щие значения для пользователя и позволит отправлять контроллеру новые значения.

Добавьте в папку Views/Adrnin файл по имени Edi t. cshtml с содержимым, приве­


денным в листинге 28.32.

Листинг 28.32. Содержимое файла Edi t. cshtml из папки Views/Admin

@rnodel AppUser
<div class= "bg - prirnary panel - body " ><h4>Edi t Use r </h4></div>
<div asp - validation-surnrnary= "Al l " c las s =" text - danger "></div>
<forrn asp - action= " Edit " rnethod= "post " >
<div class= " form - group " >
<label asp - for= "I d " ></ l abel>
<input asp-for= " Id " class= " form -contr ol" d is aЫed />
</div>
896 Часть 11 . Подробные сведения об инфраструктуре ASP. NET Core MVC

<div class="form-group">
<l abel asp- for= " Ema il" ></label>
<input asp-for="Email" c lass="form-con trol " />
</di v>
<div class= " form - group " >
<l abe l for= " password " >Password</ l abe l >
<inp ut name = "p asswo rd" c las s= "form-contr o l" />
</di v>
<b utt on type="submit " class= " btn Ьtn-primary">Save</button>
<а asp-action="Index" class= " Ьtn Ьtn-default">Cancel</a>
</form>

Представление отображает идентификатор пользователя. который не может быть


изменен , как статический текст и предлагает форму редактирования адреса элект­
ронной почты и пароля (рис . 28.13). Обратите внимание, что для элементов пароля
дескрипторный вспомогательный класс не применяется, т.к. класс пользователя не
содержит информации о пароле, поскольку в базе данных хранятся только хеширо­
ванные значения.

----·~·-·--'"'---·----"---·~~~---.....;.;:.,_·-·---··--·--·---·-~ __
__._.._-_.. ..

ld

1 f26621ЗО-З26З4се7-Ьс4а·З4dсЗd663715

Email

joe~exa mple . com

Password

Cancel

Рис. 28.1 З. Редактирование пользовательской учетной записи

Последнее изменение связано с помещением в комментарий настроек проверки


пользователей в классе Startup, чтобы для имен пользователей использовались стан­
дартные символы (листинг 28.33). Ввиду того, что некоторые учетные записи в базе
данных были созданы до изменения настроек проверки. отредактировать их не удаст ­
ся, т.к. имена пользователей не пройдут проверку . А поскольку проверка применяется
ко всему объекту пользователя , когда проверяется адрес электронной почты, резуль­
татом является пользовательская учетная запись, которую невозможно изменить.
Глава 28. Введение в ASP.NET Core ldentity 897
Листинг 28.ЗЗ. Комментирование настроек проверки пользователей
в файле Startup. cs

services . Addldentity<AppUser , IdentityRo l e>(opts => {


opts . Password.RequiredLength = 6 ;
opts.Password . RequireNon LetterOrDigit = false ;
opts.Password . Require Lowercase = fa l se ;
opts.Password . RequireUppercase = false ;
opts . Password . RequireD igi t = false ;
opts . User . RequireUniqueEmail = true;
11 opts. User .AllowedUserNameCharacters = "aЬcdefghijklmnopqrstuvwxyz";
}) .AddEntityFrameworkStores<Appide n t ityDbCo nt ext>() ;

Чтобы протестировать средство редактирования , запустите приложение, запроси­


те URL вида / Admin и щелкните на одной из кнопок Edit. Изм ените адрес электронной
почты или введите новый пароль (л ибо сделайте то и другое), после чего щелкните
на 1шопке Save (Сохранить) для обновления базы данных и возвращения к URL вида
/Admin .

Резюме
В главе было показано, как создавать конфигурацию и классы , требующиеся для
использования системы ASP.NET Core ldentity, а также проде монстрировано , каким
образом их можно применять для создания инструмента администрирования поль­
зователей. В следующей главе вы узнаете, как выполнять аутентификацию и автори­
зацию с помощью ASP.NET Core ldentity.
ГЛАВА 29
Применение ASP.NET
Core Identity

в этой главе будет показано,


29.1
предыдущей глав е. В табл.
как применять систему ASP.NET Core Identity для ау­
тентификации и авторизации пользовательских учетных записей, созданных в
приведена сводка для настоящей главы.

Таблица 29.1. Сводка по главе

Задача Решение Листинг

Ограничение доступа Применяйте атрибут Authorize 29 .1


к методу действия

Аутентификация Создайте контроллер Account, которы й полу- 29.2-29.5


пользователей чает пользовательские учетные данные и прове-

ряет их с использованием класса UserManager


Создание и управление Используйте класс RoleManager 29.6-29.10
ролями

Авторизация доступа Добавьте пользовательские учетные записи к 29.11-29 .18


к действию ролям и применяйте атрибут Au thorize для
указания, какие роли могут иметь доступ к ме­

тодам действий

Обеспечение наличия учет­ Поместите в базу данных начальные данные 29.19-29.23


ной записи администратора для автоматичес к ого создания учетной записи

Подготовка проекта для примера


В главе будет продолжена работа с проектом Users, созданным в главе 28. В качес ­
тве подготовительных шагов запустите приложение, перейдите на URL вида /Admin
и, щелкая на кнопке Create (Со здать) , добавьте в базу данных пользовательские учет ­
ные запи си из табл. 29.2.
Таблица 29.2. Пользовательские учетные записи, требующиеся для настоящей главы

Имя пользователя Адрес электронной почты Пароль

Joe joe@example . com secret123


Alice alice@example.com secret123
ВоЬ bob@example . com secret123
Глава 29. Применение ASP.NEТ Соге ldentity 899
По завершении запрос URL вида / Ad.min должен привести к отображению списка
пользователей, включая описанные в табл. 29.2 (не имеет значения, если вы создади­
те дополнительных пользователей; важно, чтобы присутствовали пользователи, ука­
занные в таблице), как показано на рис. 29.1.

1· Users Х
,...---- - - - -·- ·-----·-----·--------·- - - · -----, !
1_!"~1:· С · [') ::_j
loc~lhost:625.::_:/дd_n~~~-..:...;;:...:......~·-·-·-·".~-~;::."..:..........=...."--·---·--."~ .'CJ·.•

ID Name Email

1 4fc1 f959-Ыc8-4ede-ad41-6bc4d6953011

d4ca5981 -Зf2c-4Cff-83ee-75cOeb2919a3
ВоЬ

Joe
bob@example.com

joe @example.com ••
,.,
1

1 f4d948e3-888c-4a3c-90f4-1 Зc4f8211324 Alice alice @example.com

L.~~~~~-~~- _J

Рис. 29.1. Запуск примера приложения

Аутентификация пользователей
Наиболее фундаментальной работой для системы ASP.NET Core ldentity является
аутентификация пользователей. Основной инструмент для ограничения доступа I<
методам действий - атрибут Authorize, который сообщает инфраструктуре МVС о
том , что обрабатываться должны только запросы от аутентифицированных пользова­
телей. В листинге 29.1 атрибут Authorize применяется к действию Index контрол­
лера Home.

Листинг 29.1. Ограничение доступа в файле HomeController. cs


using System.Collections . Generic;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
namespace Users . Controllers (
puЫic class HomeController Controller (
[Authorize]
puЫic ViewResul t Index () =>
View(new Dictionary<string , object> { ["Placeholder"J "Placeholder" }} ;
900 Часть 11 . Подробные сведения об инфраструктуре ASP. NET Core MVC

После запуска приложения браузер отправит запрос на стандартный URL, кото­


рый будет нацелен на метод действия, декорированный атрибутом Au t horize. Пока
что у пользователей нет никакой возможности аутентифицировать себя, поэтому в
результате возникает ошибка (рис. 29.2).

~-2._ С ' [J localhost:62S4YAc~ount/=~$!~et_~~U1·f= ~:.::_..:~ ::__


Status Code: 404; Not Found 1
.....___ _ _ _ _ _ _ _ _ __J
Рис. 29.2. Нацеливание на защищенный метод действия

Атрибут Authorize не указывает, как пользователь должен быть аутентифициро­


ван, и не имеет прямой ссылки на ASP.NET Core Identity. Службы ldentity и промежу­
точное ПО охватывают всю платформу ASP.NET, что делает их интеграцию в прило ­
жения МVС простой и бесшовной. Работа производится путем модификации объектов
контекста, которые описывают НТТР-запросы, и снабжения МVС результатом процес­
са аутентификации без необходимости в предоставлении любых других деталей.
Платформа ASP.NET предоставляет информацию о пользователе через объект
HttpContext, который используется атрибутом Authorize для пров е рки состояния
текущего запроса и выяснения, был ли пользователь аутентифицирован. Свойство
HttpContext . User возвращает реализацию интерфейса IPrincipal, который оп­
ределен в пространстве имен System. Security. Principal. Интерфейс IPrincipal
определяет свойство и метод, показанные в табл. 29.3.

Таблица 29.3. Избранные члены, определяемые интерфейсом IPrincipal

Имя Описание

Identity Возвращает реализацию интерфейса IIdentity, который описывает


пользователя, ассоциированного с запросом

IsinRole(role) Возвращает true, если пользователь является членом указанной


роли. Управление авторизацией с помощью ролей объясняется в раз­
деле "Авторизация пользователей с помощью ролей" далее в главе

Реализация интерфейса IIdentity, возвращаемая свойством IPrincipal .


Identi ty, предлагает базовую, но полезную информацию о текущем пользователе
через свойства, которые описаны в табл. 29.4.

Совет. В главе30 рассматривается класс реализации по имени Claimsidenti ty, который


ASP.NET Core ldentity применяет для интерфейса IIdentity.
Глава 29. Применение ASP.NET Core ldentity 901

Таблица 29.4. Избранные свойства, определяемые интерфейсом IIdenti ty


Имя Описание

AuthenticationType Возвращает строку, которая описывает механизм, используемый


для аутентификации пользователя

IsAuthe nticated Возвращает t r ue, если пользователь был аутентифицирован

Name Возвращает имя текущего пользователя

Промежуточное ПО ASP.NET Core Identity применяет сооkiе-наборы. посылаемые


браузером, для выяснения, был ли пользователь аутентифицирован. Если пользова­
тель прошел аутентификацию, тогда свойство IIde n t i ty. I sAuth en t ica te d уста­
навливается вtrue . Поскольку пример приложения пока не располагает механизмом
аутентификации, свойство I s Au th enticated всегда возвращает fa ls e , что приво­
дит к ошибке аутентификации. В результате клиент перенаправляется на URL вида
/Accoun t/Login , который является стандартным URL для предоставления учетных
данных аутентиф икации.
Браузер запрашивает URL вида /Acc o unt / Log i n , но из-за того, что в проекте он
не соответствует какому-либо контроллеру или действию , сервер возвращает ответ
404 - No t Found (404 - не найдено). давая в итоге сообщение об ошибке, показанное
на рис .
29.2.

Изменение URL для входа

Хотя /Accoun t/Login - это стандартный URL, на который клиенты перенаправляются,


когда требуется авторизация, в методе Con f igur e Servi ces () класса Start up можно
указать собственный URL, изменив параметр конфигурации при настройке служб ASP.NET
Core ldeпtity:

servi ce s.Add i de nt i ty<AppUs e r, IdentityRol e>( opt s => (


opts.Cookies.ApplicationCookie.LoginPath = "/Users/Login";
})
.AddE nt i t yFramewor kSt ore s <Appiden t ityDb Co ntex t>() ;

Система ldeпtity не может полагаться на систему маршрутизации при генерации своих URL,
так что цель перенаправления должна указываться буквально. В случае изменения схемы
маршрутизации, используемой приложением, потребуется также обеспечить изменение на­
стройки ldeпtity, чтобы URL по-прежнему достигал целевого контроллера.

Подготовка к реализации аутентификации


Несмотря на то что запрос з аканчивается выводом сообщения об ошибке, он ил­
люстрирует, каким обр азом система ASP.NET Core Identity вписывается в стандартный
жизненный цикл запросов ASP.NET. Следующий шаг заключается в реализации. конт­
роллера. который будет получать запросы для URL вида /Accoun t / Log i n и аутенти­
фицировать польз ователя. Добавьте новый класс модели в файл UserViewMode l s . cs
(листинг 29.2).
902 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

Листинг 29.2. Добавление нового класса модели в файле UserViewModels. cs

using System . ComponentModel . DataAnnotat i ons ;


namespace Users.Models {
puЫic class CreateModel
[Required ]
puЫic string Name { get ; set; }
[Required ]
puЫic string Email { get ; set ; }
[Required ]
puЬlic string Password { get ; set;

puЫic class LoginМodel


[Required]
[UIHint("email")]
puЬlic string Email get; set; }
[Required]
[UIHint("password")]
puЬlic string Password { get; set; }

Новый класс модели имеет свойства Email и Password, декорированные атр и­


бутом Required, так что можно применять проверку достоверности моделей для
контроля, предоставил ли пользователь значения. Свойства такж е декорированы
атрибутом UIHint. который гарантирует. что элементы input, ви зуализируемые де ­
скрипторным вспомогательным классом в представлении, будут иметь соответствую­
щим образом установленные атрибуты t уре .

Совет. В реальном проекте для выяснения, предоставил ли пользователь значения для име­
ни и пароля, перед отправкой формы серверу можно использовать проверку достовер­
ности на стороне клиента, которая была описана в главе 27 .

Добавьте в папку Controllers файл класса по имени AccountController . cs и


поместите в него определ ение контроллера, приведенное в листинге 29.3.

Листинг 29.3. Содержимое файла AccountController. cs


из папки Controllers

using System . Threading .Tasks ;


using Microsoft . AspNetCore . Autho ri zation ;
using Microsoft . AspNetCore . Mvc ;
using Users.Models;
namespace Users.Controllers
[Authorize]
puЬlic c l ass AccountController Controller {
[AllowAnonymous]
Глава 29 . Применение ASP.NET Core ldentity 903
puЫic IActionResult Login(string returnUrl) {
ViewBag.returnUrl = returnUrl;
return View();

[HttpPost ]
[Al l owAnonymous]
[ValidateAntiForgeryToken]
puЫic async Task<IActionResult> Login(LoginModel details,
string returnUrl) {
return View(details);

В листинге 29.3 логика аутентификации не бьmа реализована, потому что плани­


руется определить представление и затем пройти через процесс проверки пользова­
тельских учетных данных и входа пользователей в приложение.
Хотя контроллер Accoun t пока еще не аутентифицирует пользователей, он содер­
жит удобную инфраструктуру. заслуживающую объяснения отдельно от кода ASP.NET
Соге Identity, который вскоре будет добавлен в метод действия Log in ( ) .
Первым делом обратите внимание, что обе версии метода действия Log in ( ) при­
нимают аргумент по имени returnUrl. Когда пользователь запрашивает ограничен­
ный URL, он перенаправляется на URL вида /Account/Login со строкой запроса, в
которой указан URL, куда пользователь должен быть направлен после того, как он
успешно пройдет аутентификацию. Удостовериться в этом можно, запустив приложе­
ние и запросив URL вида /Horne/Index.
Браузер будет перенаправлен примерно так:

/Account/Login?ReturnUrl= %2FHome %2Findex


Значение параметра ReturnUrl строки запроса делает возможным такое перена­

правление пользователя, что навигация междУ открытыми и защищенными частями

приложения превращается в простой и гладкий процесс.


Далее обратите внимание на атрибуты, которые были применены в контроллере
Account. Контроллеры, управляющие пользовательскими учетными записями, содер­
жат функциональность, которая должна быть доступна только аутентифицированным

пользователям, такую как сброс пароля, например. С этой целью к классу контролле­
ра был применен атрибут Authorize, а к индивидУальным методам действий - ат­
рибут Al l owAnonymous .
В итоге доступ к методам действий по умолчанию ограничивается аутентифицирован­
ными пользователями, но пользователям, не прошедшим аутентификацию, разрешено
входить в приложение. Кроме того, применяется атрибут ValidateAntiForgeryToken,
описанный в главе 24, который работает в сочетании с дескрипторным вспомогатель­
ным классом для элемента forrn в целях противодействия подделке межсайто вых
запросов.

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


дет визуализироваться для сбора учетных данных от пользователя. Создайте папку
Views/Account и добавьте в нее файл представления по имени Login. cshtrnl с раз­

меткой из листинга 29.4.


904 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Соге MVC

Листинг 29.4. Содержимое файла Login. cshtml из папки Views/Account


@model Log i nMode l
<d i v c la ss =" bg -pr i ma r y pa nel- bod y " >< h4 >Log In </h4></di v>
<div c l ass= " text-danger " as p -val i datio n- s umrnary= " Al l" ></div >
< f orm asp - actio n = " Log in" met h od= " post " >
<i n put type= "h idde n" name= " re t urn Url " va l u e = " @Vi e wBag . ret u r nUrl " />
<div c l ass= " form- group " >
<l abe l asp - fo r = " Ema il" ></label>
<inp u t asp - for= "Email " c l ass= "form- co n t r ol " />
</div>
<d i v c l ass= " form - gro up " >
<l abel asp -for= " Password " ></ l abel>
<i nput asp - for = " Password " class= " form- con tro l" />
</d i v>
<b u tton class= " btn b tn - primary " type= " s u bmi t " >Log In < / b utton>
</form>

Единственный примечательный асп е кт данного пр едставл ения - скрытый эле­


мент i n pu t , который сохраняет аргумент retu r n Ur l . Во всех остальных отношениях
это стандартное представление Razor, но оно завершает подготовку к аутентифика­
ции и демонстрирует способ перехвата и перенаправления запросов , не прошедших
аутентификацию. Чтобы протестировать новый контролл е р, запустите приложени е.
Когда браузер запрашивает стандартный URL приложения, он пер енаправляется на
URL вида /Accoun t / Login , что выдает содержимое, показанное на р и с. 29.3 .

1 u"" х

~ -:i
---...:.::.·--~-'" ---·. ". ..__.":..::."'·-·"
"~::.....::..._..___. ·---~--~-·-· --1
С :D-loca lh-;;st:62S42iAccou~tfl-;g i11?R-;,t~~11U rJ :%2FHom;%2 Flnd-;,-;---~: S 1

Email

Password

-
~-------------------··

Рис. 29.З. Вывод пользователю приглашения предоставить свои учетные данные


J
Добавление аутентификации пользо вателей
Запросы к защищенным методам действий корректно пер енаправляются контрол­
леру Acco u n t, но учетные данные , предоставляемы е пользователем , пока еще не ис-
Глава 29 . Применение ASP.NET Соге ldentity 905
пользуются для аутентификации. В листинге 29.5 завершается реализация действия
Login за счет применения служб ASP.NET Core Ideпtity для аутентификации пользо­
вателя с участием деталей, хранящихся в базе данных.

Листинг 29.5. Добавление аутентификации в файле AccountController. cs


using System . Threading .Tasks ;
using Microsoft . AspNetCore . Authorization ;
using Microsoft . AspNetCore.Mvc ;
using Users . Models ;
using Microsoft.AspNetCore.Identity;
namespace Users . Controllers {
[Authorize]
puЫic class AccountController : Cont roller {
private UserManager<AppUser> userManager;
private SigninМanager<AppUser> signinМanager;
puЬlic AccountController(Userмanager<AppUser> userMgr,
SigninМanager<AppUser> signinМgr) {
userмanager = userMgr;
signinМanager = signinМgr;

[AllowAnonymous]
puЫic IActionResult Login(string returnUr l ) {
ViewBag.returnUrl = returnUrl ;
return View () ;

[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
puЫic async Task<IActionResult> Login(LoginМodel details,
string returnUrl) {
if (ModelState . IsValid) {
AppUser user = await userManager.FindВyEmailAsync(details.Email);
if (user != null) {
await signinМanager.SignOutAsync();
Microsoft.AspNetCore.Identity.SigninResult result
await signinмanager.PasswordSigninAsync(
user, details.Password, false, false);
if (result.Succeeded) {
return Redirect (returnUrl ? ? "/ ") ;

ModelState.AddМodelError(nameof (LoginМodel . Email),


"Invalid user or password") ;

return View(details);

Простейшей частью является получение объекта AppUser, который представ­


ляет пользователя. что делается посредством метода FindByEmailAsync () класса
UserManager<AppUser>:
AppUser user = await userManager.FindВyEmailAsync(details.Email);
906 Часть 11. Подробные сведения об инфраструктуре ASP.NET Саге MVC

Метод FindByEmailAsync () находит пользовательскую учетную запись, исполь­


зуя адрес электронной почты, который применялся при ее создании. Имеются также
альтернативные методы поиска по идентификатору. по имени и по входу. Адрес элек­
тронной почты используется для входа из-за того, что такой подход принят в боль­
шинстве веб-приложений, доступных через Интернет, и он набирает популярность
также в корпоративных приложениях.

В случае если учетная запись с указанным пользователем адресом электрон ­


ной почты существует, тогда производится аутентификация с прим енением класса
SigninManager<AppUser>. для которого добавляется аргумент конструктора, рас­
познаваемый с помощью внедрения зависимостей . Класс SigninManager использу ­
ется для выполнения двух шагов аутентификации:

await signinManage r.SignOutAsync () ;


Microsoft.AspNetCore.Identity.SigninResult result =

await signinManager .PasswordSigninAsync (use r, details.Password ,


false , false) ;

Метод SignOutAsync () аннулирует любой имеющийся у пользователя сеанс, а


метод PasswordSignin () проводит саму аутентификацию. В качестве аргументов
метод PasswordSignlnAsync () получает объект пользователя, предоставленный
пользователем пароль, булевское значение, управляющее постоянством сооkiе-набора
аутентификации (отключ ено), и признак , должна ли учетная запись блокироваться в
случае некорректного пароля (отключ ено ) .
Результатом метода PasswordSigninAsync () будет объект SigninResu l t, в
котором определено булевское свойство Succeeded, указывающее на успешность
аутентификации.
В рассматриваемом примере проверяется свойство Su cce e ded ; если оно рав­
но true, то пользователь перенаправляется на местоположение returnUrl, а если
false, тогда добавляется ошибка проверки достоверности и затем представление
Login отображается заново, чтобы пользователь смог повторить попытку.
Как часть процесса аутентификации система Identity добавляет к ответу сооkiе­
набор, который браузер затем включает в любые последующие запросы, чтобы иден­
тифицировать сеанс пользователя и ассоциированную с ним учетную запись. Вы не
обязаны создавать или управлять зтим сооkiе-набором напрямую, т.к. он поддержи­
вается автоматически про межуточным ПО Identity.

Учет двухфакторной аутентификации

В настоящей главе выполнялась однофа кторная аутентификация, при которой пользователь


может быть аутентифицирован с применением одиночной порции информации, известной
ему заранее: пароля .

Система ASP.NET Core ldentity поддер жи вает та кже двухфакторную аутентификацию, когда
пользователю нужно кое-что дополнительное, обычно получаемое в момент, когда он желает
пройти аутентификацию. Наиболее распространенными примерами могут быть значение из
маркера SecurelD или код аутентификации, который отправляется в виде сообщения элек­
тронной почты или текстового сообщения. (Строго говоря, в качестве двух факторов может
выступать что угодно , включая отпечатки пальцев , результаты сканирования радужной обо­
лочки глаз или распознавания голоса , хотя в большинстве веб-приложений такие варианты
востребованы редко.)
Глава 29. Применение ASP.NET Соге ldentity 907
Защита в итоге усиливается, потому что злоумышленнику необходимо знать пароль пользо­
вателя и иметь доступ к тому, что предоставляется как второй фактор, наподобие учетной
записи электронной почты или сотового телефона.

Двухфакторная аутентификация в книге не рассматривается по двум причинам. Во-первых,


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

Во-вторых, двухфакторная аутентификация вынуждает пользователя помнить о необходи­


мости прохождения второго шага аутентификации, для чего держать поблизости, например,
сотовый телефон или маркер безопасности, что в случае веб-приложений не всегда оказы­
вается подходящим. Я более десяти лет проносил с собой маркер SecurelD того или иного
вида на различных работах и потерял счет, сколько раз не мог войти в систему работодате­
ля из-за того, что забывал маркер дома.
Если вы заинтересованы в двухфакторной аутентификации, тогда рекомендуется опираться
на стороннего поставщика вроде Google, который позволяет пользователю самостоятельно
выбрать, желает ли он иметь дополнительную защиту (и терпеть неудобства), обеспечива­
емую двухфакторной аутентификацией. Использование сторонней аутентификации демонс­
трируется в главе 30.

Тестирование аутентификации
Чтобы протестировать аутентификацию пользователей, запустите приложение и
запросите URL вида / Home /I ndex. После перенаправления на URL вида /Accoun t/
L og i n введите учетные данные одного из пользователей, перечисленных в начале
главе (скажем. адрес электронной почты j o e @ex amp l e.c om и пароль se c retl 23).
Щелкните на кнопке Log ln (Вход) и браузер будет перенаправлен обратно на /Home /
Index, но на этот раз он отправит сооkiе-набор аутентификации , который предоста­
вит доступ к методу действия (рис. 29.4).

Совет. Для просмотра сооkiе-наборов, применяемых при идентификации аутентифицирован­


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

1:------------·
! •.
!!
( Us~"

+- -:' С D lo~h-

Email
os-
><
t:б-'2==
54""
,,
21""
дc==co=,"~\1Log-
1n - - - - - - - - - - - - '5' Е

~~ ~;~<:.~-°--~~~~~~~-~------------~
1
joe@example.com 1

J
1

1 "'_:"' ~ "~'"""" ' """'"" ____

1~'•- - · - - - - - - - - - - - - - - - - -]
Рис. 29.4. Аутентификация пользователя
908 Часть//. Подробные сведения об инфраструктуре ASP.NET Соге MVC

Авторизация пользователей с помощью ролей


В предыдущем разделе атрибут Authorize применялся в самой базовой форме,
которая позволяет любому аутентифицированному пользователю выполнять метод
действия. Его также можно использовать для уточнения авторизации, чтобы полу­
чить более детальный контроль над тем, какие пользователи могут выполнять те или
иные действия, основываясь на принадлежности пользователя к роли.
Роль - это всего лишь произвольная метка, которая определяется для представле­
ния разрешения выполнять набор действий внутри приложения. Практически каж­
дое приложение проводит различие между пользователями, которые могут выполнять

административные функции, и пользователями, которые не могут. В мире ролей это


делается путем создания роли Administrat o rs и ее назначении пользователям.

Пользователи могут принадлежать ко многим ролям, а связанные с ролями разреше­


ния могут быть настолько крупнозернистыми или мелкозернистыми, насколько это
желательно. Таким образом, с применением разных ролей можно проводить различие
между администраторами, которым позволено выполнять базовые задачи, подобные
созданию новых учетных записей, и администраторами, которым разрешено выпол­
нять более I{ритичные операции вроде доступа к данным о платежах.
Система ASP.NET Core Identity берет на себя ответственность за управление на­
бором ролей, определенных в приложении, и отслеживание членства пользователей
в них. Но ей ничего не известно о том, что означает каждая роль; эта информация
содержится внутри части MVC приложения, где доступ к методам действий ограни­
чивается на основе членства в ролях.

Для доступа и управления ролями в ASP.NET Core ldentity предусмотрен строrо


типизированный базовый класс по имени RoleManager<T>, где Т - нласс, 1юторый
представляет роли в механизме хранения. Инфраструктура Entity Framework Core ис­
пользует для представления ролей класс Identi tyRole, в котором определены свойс­
тва, перечисленные в табл. 29.5.

Таблица 29.5. Избранные свойства класса Identi tyRole

Имя Описание

Id Определяет уникальный идентификатор для роли

Name Определяет имя роли

Users Возвращает коллекцию объектов Identi t yUserRole,


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

При желании расширить встроенную функциональность, которая описана в


главе 30 для объектов пользователей, можно создать нласс роли, специфичный для
приложения, но здесь будет применяться класс IdentityRole, т.к. он делает все,
в чем нуждается большинство приложений. Когда конфигурировалось приложение в
главе 28, системе ASP.NET Core Identity уже было указано на необходимость исполь­
зования класса IdentityRole для представления ролей, что демонстрирует следую­
щий оператор в методе ConfigureServices () класса Startup:

services.Addidentity<AppUser, IdentityRole>(opts => {


opts . User . Require Un iqueEmail = true;
11 op t s . User.All owedUs e rNameCharac t ers =
Глава 29. Применение ASP.NET Core ldentity 909
" abcdefghijklmnopqrstuvwxyz ";
opts . Password.RequiredLength = 6 ;
opts.Password . RequireNonAlphanumeric = false;
opts.Password . RequireLowercase = false;
opts . Password . RequireUppercase = false;
opts . Password . RequireDigit = false;
}) . AddEntityFrameworkStores<AppidentityDbContext>() ;

Параметры типов в методе Addidenti ty () указывают классы , которые будут при­


меняться для представления пользователей и ролей. В примере приложения для пред­
ставле ния пользователей используется масс AppUser, а для представления ролей -
встроенный класс Identi tyRole .

Создание и удаление ролей


Чтобы продемонстрировать применение ролей, мы создадим инструмент ад­
министрирования для управления ими, начав с методов действий, которые могут
создавать и удалять роли. Добавьте в паш<у Controllers файл класса по имени
RoleAdminController . cs и определите в нем контроллер, как показано в листинге
29 .6.

Листинг 29.6. Содержимое файла RoleAdminController. cs из папки Controllers

using System . ComponentModel.DataAnnotations ;


using System . Threading . Tasks ;
using Microsoft . AspNetCore . Identity ;
using Microsoft . AspNetCore . Identity . Entity Fr ameworkCore ;
using Microsoft . AspNetCore . Mvc ;
namespace Users . Controllers {
puЫic class RoleAdminController : Cont r oller (
private RoleManager<IdentityRole> r o l eManager ;
puЫic RoleAdminController(RoleManager<IdentityRole> roleMg r ) {
roleManager = roleMgr;

puЬlic ViewResult Index() => View(roleManager . Roles);


puЫic IActionResult Create() => View() ;
[HttpPost]
puЫic async Task<IActionResult> Create( [ Required]string name) {
if (ModelState. IsValid) {
IdentityResult result
= await roleManager . CreateAsync(new I dentityRole(name)) ;
i f (result . Succeeded) (
return RedirectToAction{ " Index " ) ;
else {
AddErrorsFromResult(result) ;

return View{name) ;

[HttpPost]
910 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC

puЫic async Task< I Act i onResu l t> De l ete(s t ring id) {


Ide ntityRole ro l e = awa it ro l eManager . Fi ndByi dAsync(id) ;
i f (role !=null) {
IdentityResult result = await ro l eManager . De l eteAsync(role) ;
i f (result.Succeeded) {
return Redirect ToAction( " Index " ) ;
else {
AddErrorsFromResult(resu lt ) ;

else {
Mode l State . AddMode l Error( "", "No role foun d" ) ;

return View( "I ndex ", roleManage r.Roles) ;

priva t e void AddErrorsFromResult(IdentityResult res ult)


fo r each ( I dentit yError error in res ult.Errors) {
Mode l State . AddModelError( "", error . Description) ;

Управлен и е ролями производится с использованием класса RoleManager< T>, где


т -тип, предназначенный для пр едставления ролей (встроенный класс Identi tyRole
в данном прил ож е нии) . Конструктор RoleAdminController объявля ет зави с имость
от RoleManager<IdentityRole>. которая распознается посредством внедр ения за ­
висимостей при создании объекта контроллера.
В класс е RoleManager<T> определены мет оды и сво й ства , пер е численны е в
табл . 29.6, которы е по з воляют со здавать и управлять ролями.

Таблица 29.6. Члены, определяемые классом RoleМanager<T>

Имя Описание

Cre at eAsync( r ole) Создает новую роль

DeleteAsync(role) Удаляет указанную роль

FindByidAsync(id) Находит роль по ее идентификатору

FindByNameAsync( name) Находит роль по ее имени

Rol eEx i stsAs ync(name) Возвращает true, если роль с указанным именем
существует

UpdateAsync( r ole) Сохраняет изменения в указанной роли

Roles Возвращает перечисление ролей, которые были


определены

Index () нового контроллера отобр аж ает вс е роли в пр иложении .


М етод де йствия
М етод действияCrea te ( ) применяется для отображения и получения формы , данные
которой используются при создании новой роли с помощью метода Cr eateAsync ().
М етод действия De l ete () получ ает запрос POST и приним ает уникал ьн ый иденти­
фикатор роли , который применяется для е е удаления из приложе ния ч е р ез метод
DeleteAsync (), находя объект роли с применением м етода FindByidAsync () .
Глава 29 . Применение ASP.NET Core ldentity 91 1
Создание представлений

Чтобы ото б р азить детали рол ей в прилож ении, создайте п апку Views/RoleAdmin
и добавьте в н ее файл Index. csht ml с ра зм еткой из листинг а 29.7 .

Л исти нг 29.7. С одержимое файла Index. cshtml из папки Views/RoleAdrnin


@model IEnumeraЬle<Ident i tyRole>

<div class= " bg - p r imary p anel - body " ><h4>Ro l es</h4></div>


<div c l ass= " text - danger " asp - va l idation - summary=" Mode l Only " ></div>
<tаЫе class= " taЫe taЬle - condensed taЬle - bordered taЬle - bordered " >
<tr><th>ID</th><th>Name</th><th>User s </th><th></th></tr>
@if (Model.Count() == 0) {
<tr><td colspan=" 4 " class= " text - center " >No Roles</td></tr>
else {
foreach (var role in Mode l )
<tr>
<td>@role . Id</td>
<td>@role . Name</td>
<td identity - role= " @ro l e .I d " ></ t d>
<td>
<form asp - action =" Delete " asp - rou t e - id= " @ro l e . Id " method= " post " >
<а class= " bt n btn - sm btn - p r imary " asp-action =" Edit "
asp - route - id=" @role . Id " >Edit</a>
<button typ e="s ubm i t "
class= " Ьtn Ьtn - sm Ьtn - danger " >
Delete
</button>
</form>
</td>
</tr>

</tаЫе>
<а class= " btn btn - pr imary " asp - act i on=" Create " >Create</a>

Для отображения деталей о ролях в приложении представл ени е использует табли ­


цу. В тр етьей колонке применяется специальный атрибут эл ем ента:

<td identity-role="@role. Id" ></ t d>

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


роли, что требует включения в представление очень большого объема кода. Чтобы
сохр анить представление простым, добавьте в папку I n f rast r uct u re файл класса
по имени RoleUsersTagHelper . cs и пом естите в него опр еделени е де скрипторного

вспомогательного класса , приведенное в листинге 29.8.

Листинг 29.8. С одержимое файла RoleUsersTagHelper. cs


и з папки Infrastructure
using System.Collections . Gen e ric ;
using System . Threading . Tasks ;
using Microsoft . AspNetCore . Identity ;
912 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

using Microsoft . AspNetCore.Identity . EntityFrameworkCore ;


using Microsoft.AspNetCore . Razor . TagHe l pers ;
us i ng Users . Mode l s ;
namespace Users . Infrastructure
[HtmlTarget El e ment( " td ", At tr ib utes = "identity- ro l e " ) J
puЫic c l ass Role UsersTagHelper : TagHelper {
private UserManager<App User> u serManager ;
private RoleManager< I dent i tyRole> roleManager ;
puЬlic RoleUsersTagHelpe r (UserMa n ager<Ap pUser> usermgr ,
Ro l eMana g er< I den ti tyRol e > rolemg r ) {
use rM anage r u se r mgr ;
ro l eMa n ager = rolemg r;

[Html AttributeName( " identit y- ro l e " ) ]


p uЫ i cs tr i ng Ro l e { get ; set ; }
puЬlic override async Task ProcessAsync(TagHelperContext context ,
TagHe l pe r Outp u t o u tput) {
List<str i ng> n a mes = n ew List< s tring> ( ) ;
IdentityRo l e role = awa it roleManager .FindByidAsync(Role) ;
i f (role !=null ) {
foreach (var user in userManager . Users) {
if (use r != nu ll
&& await userMa n ager .I s i nRo leAsy n c( u s e r , role . Name)) {
names . Add(user . Use r Name) ;

output . Content . SetConte n t(names . Cou n t О?


" No Users ": string . Jo in ( ", ", names)) ;

Этот дескрипторный вспомогательный класс опер ирует на эле ментах t d поср едс ­
твом атрибута iden ti t y - role, который используется для получ е ния им ени обраба­
тываемой роли. Объекты Ro l eManager<IdentityRole> и UserManager <AppUser>
позволяют отправлять запросы базе данных Identlty для построения спи с к а и ме н
пользователей в роли. В листинге 29.9 к файлу импортирования представл ений до ­
бавляется дескрипторный вспомогательный класс RoleUse r sTagHe l per и выраже ­
ние @us i ng , чтобы на типы EF Core можно было ссылаться внутри пр едставлени й,
не указывая пространство имен .

Листинг 29.9. Добавление дескрипторного вспомогательного класса


в файле _ Viewimports . csh tml

@u sing Users .Mode l s


@using Microsoft.AspNetCore.Identity.EntityFrameworkCore
@addTagHe l per * , Mi crosoft . Asp NetCore . Mvc .T agHe l pers
@addTagHelper Users.Infrastructure.*, Users
Глава 29. Применение ASP.NET Core ldentity 913
Добавьте в папку Views/RoleAdmin файл представления по имени Create .
cshtml с разметкой из листинга 29.10 для поддержки добавления новых ролей.
Листинг 29.1 О. Содержимое файла Create. cshtml из папки Views/Ro leAdmin

@model stri ng
<div class= "bg - primary panel - body " ><h4>Create Ro l e</ h 4></div>
<div asp - validation- summary= "All" class="text-danger"></div>
<form asp - action= "Create " method="post">
<div c l ass="form- group " >
<label for= " name " ></label>
<inp ut name="name" class= "form-cont rol " />
</div>
<button type= " submit " c l ass= "bt n btn-primary">Create</button>
<а asp - action= " Index " class ="Ьtn Ьtn-default">Cancel</a>
</form>

Для создания роли из данных формы требуется только имя - вот почему в Crea te .
cshtml имеется возможность применения string в качестве класса модели представ­

ления. Мы хотим воспользоваться преимуществами проверки достоверности модели.


чтобы удостовериться в предоставлении пользователем значения, когда форма отправ­
лена, но для такой простой задачи не стоит создавать отдельный класс модели. Взглянув
на метод Crea te () . принимающий запросы POST в листинге 29.6. вы заметите, что
атрибут проверки достоверности Required применяется прямо к параметру . Результат
будет таким же, как в случае применения этого атрибута в классе модели , и появляется

возможность задействовать встроенный процесс проверки достоверности моделей.

Тестирование создания и удаления ролей


Чтобы протестировать новый контроллер , запустите приложение и перейдите на
URL вида /RoleAdmin. Щелкните на кнопке Create (Создать), введите имя в элементе
input и щелкните на второй кнопке Create. Новая роль будет сохранена в базе дан­
ных и отображена после перенаправления браузера на действие Index (рис . 29.5) .
Щелкнув на кнопке De lete (Удалить), роль можно удалить из приложения.

1О Name Users
Ьае5а ОП-706Ь-407f- Ь89Ь·4170Ь4437ЬеЬ Admins No Users
111111
ccc2df5c-ЗC1e-4c6c-ae5d - 5db140с45а00

lL---------------------------~
lEI
Users No Users

."
Рис. 29.5. Создание новой рол и
914 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC

Управление членством в ролях


Следующий шаг - обеспечение возможности добавления и удаления пользовате­
лей из ролей. Сам процесс несложен ; он предусматривает взятие данных роли из клас­
са RoleManager и их ассоциирование с деталями индивидуальных пользователей.
Для начала понадобится определить несколько классов моделей представлений,
которы е будут представлять чле нство в роли и получать новый н абор инструкций от­
носительно членства от поль зо вателя. В листинге 29.11 показаны добавления, вне­
сенные в файл UserViewModels . cs из папки Models.
Листинг 29.11. Добавление моделей представлений в файле userViewModels. cs
using System . ComponentModel . DataAnnotations ;
using System . Collections.Generic;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore ;
namespace Osers.Models {
puЬlic class CreateModel
[Required]
puЫic string Name { get ; set ; }
[Required]
puЬlic string Emai l { get ; set ; }
[Required]
puЫic string Password { get; set ;

puЬlic class LoginModel


[Requi red]
[UIHint( " email " )J
puЫic string Email get ; set ; }
[Required]
[UIHint ( "password " )]
puЫic string Password { get ; set ; }

puЬlic class RoleEditмodel {


puЫic IdentityRole Role { get; set; }
puЬlic IEnumeraЫe<AppUser> MemЬers { get; set; }
puЫic IEnumeraЫe<AppUser> NonМemЬers { get; set;

puЫic class RoleModificationМodel {


[Required]
puЬlic string RoleName { get; set; }
puЬlic string Roleid { get; set; }
puЫic string[J IdsToAdd { get; set;
puЬlic s tring [] IdsToDelete { get; set;
}

Кл асс RoleEdi tModel пр едставляет роль и детали о польз ователях в системе, ка­
тегоризированные по членству в роли . Класс RoleModificationModel представля ет
набор изменений роли . В листинге 29.12 к контроллеру
RoleAdrnin добавляют ся но­
вые методы действий, которые используют модели представл ений из листинга 29.11
для управления членством в ролях.
Глава 29. Применение ASP.NET Core ldentity 915
Л и стинг 29.12. Добавление методов действий в файле RoleAdminController. cs

using System . ComponentModel . DataAn notations ;


using System . Thread i ng . Tasks ;
using Microsoft . AspNetCore . Identity ;
using Microsoft . AspNetCore .I de n tity . En tityFrameworkCore ;
using Microsoft . AspNe t Core . Mvc ;
using System.Linq;
using Users.Models;
using System.Collections.Generic;
namespace Users . Control l ers {
puЫic class RoleAdm i nControl l er : Control l er {
private RoleManager<Ide n ti t yRo l e> roleManager ;
private UserManager<AppUser> userManager;
puЫic RoleAdminCont r o l ler{RoleManager< I de n ti t yRo l e> r o l eMgr ,
UserManager<AppUser> userMrg) {
ro l eManager = roleMgr ;
userManager = userMrg;

11 .. . для краткости другие методы действий не п оказаны ...

puЫic async Task<IActionResult> Edit(string id) {


IdentityRole role = await roleМanager.FindВyidAsync(id);
List<AppUser> memЬers =
new List<AppUser> () ;
List<AppUser> nonМemЬers = new List<AppUser>();
foreach (AppUser user in userмanager.Users) {
var list = await userManager.IsinRoleAsync(user, role.Name)
? memЬers : nonМemЬers;
list.Add(user);

return View (new RoleEditмodel


Role = role,
MemЬers = memЬers,
NonМemЬe rs = nonМemЬers
}) ;

[HttpPost]
puЬlic async Task<IActionResult> Edit(RoleModificationМodel model)
IdentityResult result;
if (ModelState.IsValid) {
foreach (string userid in model.IdsToAdd ?? new string[] { }) {
AppUser user =
await userManager.FindВyidAsync(userid);
if (user != null) {
result = await userManager.AddToRoleAsync(user,
model . RoleName);
if (!result.Succeeded) {
AddErrorsFromResult(result);

foreach (string userid in model . IdsToDelete ?? new string[] { }) {


AppUse r user =
await userмanager.FindВyidAsync(userid);
916 Часть 11. Подро бные сведения об инфраструктуре ASP.NET Core MVC

if (user != null) {
result = await userManager.RemoveFromRoleAsync(user,
model.RoleName);
if (!result.Succeeded ) {
AddErrorsFromResult(result);
}

if (ModelState. IsValid) {
return RedirectToAction(nameof(Index));
else {
return awai t Edi t (model. Roleid) ;

private void AddErrorsFrornResult(IdentityResult result)


foreac h (IdentityError error in result.Errors) {
ModelState . AddMode l Error( "", error.Description);

Большая часть кода в версии метода действия Edi t () для запросов GET отвечает
за генерацию наборов членов и не членов выбранной роли. После того как все поль­
зователи категоризированы, новый экземпляр класса RoleEdi tModel передается ме­
тоду View (), так что данные могут быть отображены с применением стандартного
представления. Версия метода действия Edi t () для запросовPOST отвечает за до­
бавление и удаление пользователей в и из ролей. Класс UserManager<T> предлагает
методы для работы с ролями, которые описаны в табл. 29.7.

Таблица 29.7. Методы, связанные с ролями, которые определяет класс Userмanager<T>

Имя Описание

AddToRoleAsync( user , narne) Добавляет идентификатор пользователя к роли


с указанным именем

Ge tRolesAsync(user) Возвращает список имен ролей, членом которых


является пользователь

IsinRoleAsync(user, narne) Возвращает true , если пользователь имеет


членство в роли с указанным именем

RemoveFrornRoleAsync(user ,name) Удаляет пользователя как члена из роли


с указанным именем

Причудливость методов, относящихся к ролям, связана с тем, что они опериру­


ют с именами ролей , хотя роли имеют также и уникальные идентификаторы. По
этой причине класс модели представления RoleModificationModel имеет свойс­
тво RoleName. В листинге 29.13 приведено содержимое файла Edi t . cshtrnl . добав­
ленного в папку Views/RoleAdrnin, который позволяет пользователю редактировать
членство в роли.
Глава 29 . Применени е ASP.NET Core ldentity 917
Листинг 29.13. Содержимое файла Edit . cshtml из папки Views/RoleAdmin

@model RoleEditModel
<div class= " b g- primary panel - body " ><h4>Edit Ro l e</ h 4></ di v>
<div asp - val i dation - summary= "Al l" c la ss = " text - danger " ></div>
<form asp - action="Edit " method= " pos t" >
<input type= " hidden " name= " roleName " value= " @Model . Role . Name " />
<input type= " hidden " name =" roleid " va l ue= " @Model.Role . Id " />
<hб class= " bg -i nfo pane l -body " >Add То @Model.Ro l e . Name</hб>

<tа Ые class= " taЫe taЫe - bordered taЫe - condensed " >
@if (Model . NonMembers . Count() == 0) {
<tr><td col s pan= " 2 " >Al l Use rs Ar e Memb ers</ t d></ tr >
else {
@foreach (AppUser user i n Mode l.No nMembers)
<tr>
<td>@user . Us erName</td>
<td>
<input type= " checkbox " name= " IdsToAdd " value =" @user.Id " >
</td>
</tr>

</tаЫе>

<hб class= " bg -i nfo panel - body " >Remov e From @Model . Role . Name</hб>
<tаЫе class= " taЫe taЫe - bordered ta Ы e - condensed " >
@if (Model . Members . Count() == 0) {
<tr><td colspan= " 2 " >No Users Are Members</td></tr>
else {
@foreach (App User user i n Mode l. Me mbers)
<tr>
<td>@user . UserName</td>
<td>
<input type= " checkbox " name=" IdsTo Delete " value= " @u ser .I d " >
</td>
</tr>

</tаЫе>

<button type= " submit " class= " btn Ьt n - pr i mary " >Save</button>
<а asp - action= "I ndex " class= " btn btn - default " >Cancel</a>

</form>

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


членам и выбранной роли, и е ще одну для пользовател е й, принадлежащих роли. Имя
каждого пользователя отображается вместе с флажком, который позволяет изменять
чл е нство . Таблицы находятся внут ри формы , которая отпр авляется методу действия
Edit () и привязана к кл ассу модели RoleModificationModel, обеспечивая легкий
доступ к списку изменений членства в роли .
918 Часть 11 . Подробные сведения об инфрастру ктуре ASP.NET Core MVC

Тестирование редактирования членства в роли


Чтобы протестировать поддержку членства в ролях, запустите приложение, пе­
рейдите на URL вида /RoleAdmin и создайте новую роль по имени Users. Щелкните
на кнопке Edit (Редактировать) ; все пользователи в приложении отобразятся внутри
списка не членов (рис. 29.6).

l Users
~ ~ С t)i.;call,ost:б2542/Rol.;дd;;;-i.n/Edit/f2910ea6-:-;-734.4t;;:i=;;926.0"d1cз641if6f --~( ;:
--~----~.-:.----·---~ - ~ - - _____1

ВоЬ [;J

Joe о

Alice о

No Users Are Members

- Cancel 1

i......-------------------------·--------·--------___J
Рис. 29.6. Просмотр и редактирование членства в ролях

Отметьте флажки для пользователей Alice и Joe (две из учетных записей, добав­
ленных в систему Identity в начале главы) и щелкните на кнопке Save (Сохранить) .
В списке членов роли Users появятся пользователи Alice и Joe, как показано на
рис. 29.7.

! Uш1

!· +--- --·---·-··--·-·.
-1' С' [J localhos t:625 42/RoleAdrnir1
----'-"-'---·"'.:.с......;, __.'-'-_ _ _ . _ _..- · - - ·----

1 ю Name Users

,. .""
1 2ea3d582· 1f5b-4:kЗ-8602-b997e38df992 Admins No Users

1291 oea6-1734-4fa4-a926·0d1 c36417f6f Users Joe. Alice

"
Рис. 29. 7. Управление членством в ролях
Глава 29. Применение ASP. NЕТ Core ldentity 919

Использование ролей для авторизации


Те перь, когда в приложении имеются роли , их можно применять в качестве осно­
вы для авторизации посредством атрибута Authorize . Чтобы облегчить тестирова­
ние автори з ации на основ е ролей, добавьте в контроллер Accou nt метод Logout () ,
который сделает возможным выход и последующий вход от имени другого пользова­
теля для де монстрации членства в ролях (листинг 29.14).
Листинг 29.14. Добавление метода Logou t () в файле Accoun tCon trol ler. cs

using System . Threading .T asks ;


using Microsoft .As p NetCore . Author i zation ;
using Microsof t. AspNetCore . Mvc ;
using Users . Mo del s ;
us i ng Microsof t . Asp NetCore .I de ntity;
namespace Use r s.Contro l lers (
[Authorize]
puЫic class AccountController : Controller (
private UserManager<AppUser> use r Manager ;
private SigninManager<AppUser> s igninManager ;
11 .. . для краткости другие методы действий не показаны .. .
[Authorize]
puЫic async Task<IActionResult> Logout() {
await signinМanager.SignOutAsync();
return RedirectToAction ("Index", "Home");

Обновите контроллер Home , добавив новый метод действия и передав пр едставле­


нию инфор мацию об аутенти фицированном польз овател е (листинг 29.15).
Листинг 29.15. Добавление метода действия и информации об учетной записи
в файле HomeController. cs

us i ng System.Co ll ections . Ge n eric ;


using Microsof t. AspNetCo r e . Mv c ;
using Microsoft . AspNetCore . Authorization ;
namespace Users . Controllers {
puЫic class HomeController : Contro l ler (
[Authorize]
puЫic IActionResul t Index () => View (GetData (nameof (Index))) ;
[Authorize(Roles = " Users " ) ]
puЬlic IActionResul t OtherAction () => View ( "Index" ,
GetData(nameof(OtherAction)));
private Dictionary<string, object> GetData(string actionName) =>
new Dictionary<string, object> {
[ "Action"] = actionName,
[ "User"] = HttpContext. User. Identi ty . Name,
[ "Authenticated"] = HttpContext. User. Identity. IsAuthenticated,
["Auth Туре"] = HttpContext.User.Identity . AuthenticationType,
["In Users Role"] = HttpContext.User.IsinRole("Users")
};
920 Часть 11. Подробные сведения об инфраструктуре ASP.NET Соге MVC

Атрибут Aut horize для метода действия Index () не изменялся, но в случае его
применения к методу OtherAction () было установлено свойство Roles с целью ука­

зания на то, что доступ к этому методу должен быть разрешен только членам роли
Users. Кроме того, определен метод GetData (),который добавляет базовые сведения
об удостоверении пользователя с использованием свойств, доступных через объект
HttpContext.

Совет. Атрибут Authorize можно также применять для авторизации доступа на основе
списка индивидуальных пользовательских имен. Это привлекательная возможность в не­
больших проектах , но она требует изменения кода в контроллерах всякий раз , когда изме­
няется набор авторизуемых пользователей, и обычно означает необходимость в повтор­
ном проходе через цикл тестирования и развертывания. Использование для авторизации
ролей изолирует приложение от изменений в отдельных пользовательских учетных запи­
сях и позволяет управлять доступом к приложению посредством информации о членстве

в ролях, хранящейся в системе ASP.NET Core ldeпtity.

Последнее изменение касается файла Index. cshtml из папки Views/Home, кото­


рый применяется обоими действиями в контроллере Home, и связано с добавлением
ссылки, нацеленной на метод Logout () контроллера Account (листинг 29.16).

Листинг 29.1 б. Добавление ссылки на метод Logout () в файле Index. cshtml


из папки Views/Home
@model Dictionary<string , object>
<div class="bg-primary panel-body"><h4>User Details</h4></div>
<tаЫе class= "taЫe taЫe-condensed taЬle-bordered">
@foreach (var kvp in Model) {
<tr><th>@kvp.Key</ th> <td>@kvp.Value</td></t r>

</tаЫе>
@if (User?.Identity?.IsAuthenticated ?? false) {
<а asp-controller="Account" asp-action="Logout"
class="Ьtn ьtn-danger">Logout</a>

Чтобы протестировать аутентификацию, запустите приложение и перейдите на


URL вида /Ноте/ Index. Браузер будет перенаправлен так, что можно ввести поль­
зовательские учетные данные . Не имеет значения, данные какого пользователя из
табл. 29.2 будут выбраны, поскольку атрибут Authorize, примененный к действию
Index , разрешает доступ любому аутентифицированному пользователю .
Однако если запросить URL вида /Home/OtherAction, то выбор пользователя из
табл. 29.2 будет иметь значение, т.к. членами роли Users, требующейся для доступа
I< методу OtherAction (), являются только пользователи Alice и Joe. В случае вхо­
да от имени пользователя ВоЬ браузер будет перенаправлен на URL вида /Account/
AccessDenied , который используется, когда пользователь не имеет возможности
получить доступ к методу действия. Для обработки данной ситуации в контроллер
Account добавлен метод AccessDenied (),так что теперь имеется действие, обраба­
тывающее запрос (листинг 29. 17).
Глава 29 . Применение ASP.NET Соге ldentity 921

С овет. Устанавливая свойство I denti t yOption s . Cookies . Appl i ca tio n Cook i e .


AccessDen i edPa t h , можно изменять URL вида / Account/AccessDenied. Во врезке
" Изменение URL для входа" ранее в главе был приведен похожий пример .

Л истинг 29. 17 . Доба вление метода действи я в файле AccountController.cs


using System . Threading . Tasks ;
using Microsoft . AspNetCore . Author i zation;
us i ng Mi crosoft . As pNetCore . Mvc ;
using Users.Mode l s ;
using Mi crosoft.AspNetCore . Ide n t ity ;
namespace Users . Control l ers {
[Authorize]
puЫic class AccountContro l ler : Contro l ler {
private UserManager<AppUser> use r Manager ;
private SigninManager<AppUser> s i gninManager ;
puЫic AccountContro ll e r (Use r Manage r <AppUser> userMgr ,
Sign i nManager<AppUser> s i gninMg r ) {
userManager = userMgr ;
signinManager = signinMgr ;

// .. . для краткости другие методы действий не показаны ...


[AllowAnonymous]
puЫ i c IActionResult AccessDenied()
r e turn Vi.ew () ;

Чтобы сна бдить действие Acces s Den ied представление м для от ображения, со­
здайте в папке Vi ews/Account файл по имени Access Den i ed . cs ht ml с соде ржимым
из л истинг а 29.18.

Листи нг 29.18. Содер жи м ое файла AccessDenied. cshtml из папки Views/Account


<div class= "bg - danger pane l- body " ><h4>Access Denied</h4></div>
<а asp - action=" Index " a sp-contro ll e r="Home " class= "btn Ьt n- prima r y " >OK</a>

Запу стите приложение, запросите URL вида /Account/Log i n и войдите от име­


ни bob@examp l e . com. Когда процесс аутентификации завершится , бр аузер будет п е ­
ренаправлен на URL вида / Home/ I ndex , который отобразит детали учетно й з аписи,
к ак пок аз ано слева н а рис. 29.8, проясняя, что пользователь ВоЬ не явля ется чле­
но м рол и Use r s . Затем запросите URL вида / Home/Ot h erAction, который нацелен
на действ и е, з ащищенное с пом ощью доступа на основе ролей. Пользователь ВоЬ не
им еет тр е буем ого членства в роли , по этому браузер будет п е ренапр авле н на URL вида
/Account/Ac c essDenied, как демонстрируется справа н а рис. 29.8.

Совет. Роли загружаются во время входа пользователя, т.е. изменение ролей для пользова­
теля, который в текущий м омент аутентифицирован, вступит в силу только после того, ка к
пользователь выйдет и затем аутентифицируется заново.
922 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC

:~;;:-.··~;~"' ', 1 ...,/ >{,;;-,: ',r_•:.::1,:-,~Y.~'

;,U~er Detail~ ::. ·.. )::


'
1
1 ~:. '. , ' • ' 1 • '

Action lndex

User ВоЬ

Auth entlcat.e d Tru e

Auth Туре Microsoft.AspNet.ldentity.Application

ln Users Rol e False

-----------------~

Рис. 29.8. Применение авторизации на основе ролей

Помещение в базу данных начальных данных


В рассматриваемом проекте осталась одна проблема - доступ к контроллерам
Admin и RoleAdmin не ограничен. Это классическая проблема курицы и яйца. Дело в
том, что для ограничения доступа необходимо создать пользователей и роли, но кон­
троллеры Admin и RoleAdmin являются инструментами управления пользователями,
и если защитить их с помощью атрибута Authorize, тогда не будет никаких учет­
ных данных, которые предоставят к ним доступ, особенно при первом развертывании
приложения.

Проблема решается помещением в базу данных начальных данных, когда приложе­


ние запускается. В листинге 29.19 приведено содержимое файла appsettings. j son
с добавленными конфигурационными данными, указывающими детали для учетной
записи, которая будет создана.

Листинг 29.19. Добавление конфигурационных данных в файле appsettings. j son

" Data ": {


"AclminUser":
"Name 11 : "Admin",
"Email": "aclmin@example.com",
"Password": "secret",
"Role": "Aclmins"
} '
" SportStoreidentity ":
"ConnectionString": "Server= (localdb) \ \MSSQLLocalDB ; Datab a se=
IdentityUsers;Trusted Connection=True;Mu l tipleActiveResul t Sets=true"
) -

В категории Data: AdminUser предоставлены четыре значения, требующиеся для


создания учетной записи и ее назначения роли, которая обеспечит возможность ис­
пользования административных инструментов.
Глава 29. Применение ASP.NET Core ldentity 923

В нимание! Помещение паролей в текстовые конфигурационные файлы означает необхо ­


димость добавления в процесс развертывания приложения возможности изменять па­
роль стандартной учетной записи и инициализировать новую базу данны х при начальном
развертывании.

Добавьте в класс Appidenti tyDbContext статический метод, как показано в лис­


тинге 29.20. Код для создания стандартной учетной записи вовсе не обязан находить­
ся в данном классе, но для меня это место выглядит вполне естественным, и я посту­

паю так в своих проектах.

Листинг 29.20. Добавление метода в файле AppidentityDbContext. cs


using Microsoft . AspNetCore .Identity .E ntityFrameworkCore ;
using Microsoft.EntityFrameworkCore ;
using System.Threading.Tasks;
using Microsoft . Extensions.Configuration;
using System;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Dependencyinjection;
namespace Users . Models {
puЬlic class AppidentityDbContext : IdentityDbContext<AppUser>
puЫic AppidentityDbContext(DbContextOptions<AppidentityDbContext> options)
: base ( options) { }
puЬlic static async Task CreateAdminAccount(IServiceProvider serviceProvider,
IConfiguration configuration) {
Userмanager<AppUser> userManager =
serviceProvider.GetRequiredService<UserManager<AppUser>>();
Roleмanager<IdentityRole> roleмanager =
serviceProvider.GetRequiredService<RoleManager<IdentityRole>>();
string username = configuration["Data:AdminUser:Name"];
string email = configuration["Data:AdminUser:Email"];
string password = configuration [ "Data : AdminUser: Password"] ;
string role = configuration [ "Data :AdminUser: Role"] ;
if (await userManager.Find.ВyNameAsync(username) == null) {
if (await roleМanager.FindВyNameAsync(role) == null) {
await roleмanager.CreateAsync(new IdentityRole(role));

AppUser user = new AppUser


UserName = username,
Email = email
};

IdentityResult result = await userManager


.CreateAsync(user, password);
if (result.Succeeded) {
await userManager.AddToRoleAsync(user, role);
924 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

Метод Cr e a t eAdmi nAcc ount () принимает объект реализации I Se r v i ceProvi der ,


который прим е няется для получе ния объектов Use r Manager и RoleManager, а также
объект реализации IConfigurat i on , используемый для извл ечения данных из файл а
app s et ting . j son . Код в методе Crea teAdrninAccoun t () проверяет, существует ли
пользовател ь , и если нет, то создает его и назнач ает указанной роли, которая таюк е
при н е обходимости создается. В листинг е 29.21 в 1шасс Startup добавле н опер атор.
вызывающий м етод CreateAdrni nAccount () после того , как остальная часть прил о ­
жения настроена и сконфигурирована.

Листинг 29.21. Вызов метода базы данных в файле Startup. cs

p u Ыic void Configure(IApplicationBuilder арр) {


app . UseStatus CodePages() ;
app.UseDeveloperExceptionPage() ;
app . UseStat i cFi les() ;
app . Us eidentity() ;
app . UseMvcWithDefaultRoute() ;
AppidentityDbContext . CreateAdminAccount(app . ApplicationServices,
Configuration) .Wait();

Имея надежную стандартную уч етную запись в базе данных ldentJty, м ожно при­
менить атр и бут Authorize для защиты контроллеров Adrnin и RoleAdrnin . В листин­
ге 29.22 пр и в едены изменения , внес енные в контроллер Adrnin .

Листинг 29.22. Ограничение доступа в файле AdminController. cs


using Microsoft . AspNetCore . Ident i ty ;
using Microsoft . AspNetCo re. Mvc ;
u sing Users . Models ;
us ing System . Th r ea ding .Tasks ;
using Microsoft.AspNetCore.Authorization;
namespace Users . Controllers {
[Authorize (Roles = "Admins") ]
puЫic class Admi nControl l er : Contro ll e r {
11 ... для краткости оп ера торы не п оказаны . ..

В листинг е 29.23 показано соответствующее изменение , внесенное в контролле р


Ro l e Adrni n.
Листинг 29.23. Ограничение доступа в файле RoleAdminController. cs
using System.ComponentModel . Da t aAnnotations ;
using System . Threading . Tas ks ;
using Microsoft . AspNe t Cor e. Ide nt ity ;
u s ing Microsoft . AspNetCo r e . Identity . EntityFrameworkCore ;
using Microsoft . AspNetCo r e . Mvc ;
us i ng System.Linq ;
using Users . Models ;
using System. Co l lection s . Generic ;
using Microsoft.AspNetCore.Authorization;
Глава 29 . Применение ASP. NET Core ldentity 925
namespace Users . Contro lle rs {
[Authorize (Roles =
"Admins")]
puЫic class RoleAdmi n Co n t r ol le r : Cont r olle r
/ / . . . для краткости о пера торы не показаны . ..

Запустите приложение и запросите URL вида /Admin или /RoleAdm i n. Если вы


уже вошли от им ени какого-то другого пользователя. тогда понадобится выйти. В про­
тивном случае вам будет предложено предоставить учетные данные . так что можете
вв е сти admin@e xampl e . с от и пароль secre t и получить доступ к административ­
ным функциям.

Резюме
В настоящей главе объяснялось. как использовать систему ASP.NET Core Identity
для аутентификации и авторизации пользователей . Вы узнали, каким образом соби­
рать и проверять пользовательские учетные данные и ограничивать доступ к методам

действий на основе ролей. членом которых является пользователь. В следующей главе


будут проде м онстрированы некоторые расширенные средства, предлагаемые систе­

мой ASP.NET Core Identity.


ГЛАВА 30
Расширенные ср едства
ASP.NEТ Core Identity

в этой главе описание системы ASP.NET Core ldentity завершается рассмотрением


ряда расширенных средств, которые она предлагает. Будет продемонстрирова­
но, как расширять схему базы данных за счет определения специальных свойств в
классе пользователя, и каким образом использовать миграции базы данных для при­
менения этих свойств, не удаляя данные из базы данных ASP.NET Core Identity. Кроме
того, вы узнаете, как система ASP.NET Core Identity поддерживает концепцию заявок
(claim), и ознакомитесь с их использованием для гибкой авторизации доступа к мето­
дам действий через политики. Ближе к концу главы будет показано, каким образом
система ASP.NET Core Identity облегчает аутентификацию пользователей с участием
третьих сторон. Вы увидите способ аутентификации с помощью учетных запис е й
Google, но имейте в виду, что в ASP.NEТ Core Identity имеется встроенная поддержка
также для учетных записей Microsoft, Facebook и Twitter. В табл. 30.1 приведена свод­
ка для настоящей главы.

Таблица 30.1. Сводка по главе

Задача Решение Листинг

Сохранение специальных данных для Добавьте свойства в класс паль- 30.1-30.3


пользователей зователя и обновите базу данных
ldeпtity

Выполнение детализированной Используйте утверждения 30.4-30.6


авторизации

Создание специальных утверждений Применяйте трансформацию 30.7, 30.8


утверждений

Использование данных утверждений Создайте политики 30.9-30.13


для оценки доступа пользователей

Применение политик для доступа к Оценивайте политики внутри ме­ 30.14-30.19


ресурсам тодов действий

Разрешение третьим сторонам выпол­ Принимайте утверждения от пос­ 30.20-30.23


нять аутентификацию тавщиков аутентификации , таких
как Microsoft, Google и Facebook
Глава 30. Р асшире нн ые средства ASP.NEТ Core lden tity 927

Подготовка проекта для примера


В главе будет продолжена работа с проектом Users, созданным в главе 28 и усо­
вершенствованным в главе 29. Запустите приложение и удостоверьтесь в наличии
пользователей в базе данных. На рис . 30.1 показано состояние базы данных, 1юто ­
рая содержит пользователей Admin, Alice, ВоЬ и Joe из предыдущей главы . Чтобы
проверить пользователей, запустите приложение, запросите URL вида /Admin и ау ­
тентифицируйтесь как пользователь Admin , используя адрес admin@ e xamp le .com и
пароль secret .

1~. Use r ~ х

+- ~ С
------..:...·---:..:.--·---=--
' ••
D localhost:6254 2/Adrп in
... / •• • ' ~ ' '
-

:

:" ~ ~
--"'~--------------·..;,._

' - , '•' о " , ' ~ '1 • •


___-
• ' i
.
: ,

" User.Accounts - . · .., " :· . -, - "· - ·


" ' ~ ·"' ' • • • \.- • • .·: • • _' : 9 : ' • ' • • l 1

1О Name Email

""
2fe2bВf9-e1 d9-4da4-a833-c4881ed01358 Admin admin@example.com

""
65 1aзbda-7fe5·45Зc-b32d-f50f1 a5126d2 ВоЬ bob@example.com

9787а 1Ьа-6726-4762-ас7d-57еЗеЗе9dс9с Alice alice@example.com

."
""
bd71а9е7 -d376-4a2c-abbc-71 b87afcffe2 Joe joe@example.com

*1
Рис . 30 . 1. Н ач альн ы е пользовател и в ба з е да нн ы х ldentity

В настоящей главе также понадобится несколько ролей. Перейдите на URL вида


/RoleAdmin , создайте роли с именами Use rs и Employees и назначьте им пользова­

телей согласно табл. 30.2 .


Таблица 30.2. Роли и их члены , требующиеся для примера приложения

Роль Чл ены

Users Alice, Joe


Employees Alice,Bob

На рис . 30. 2 показана требуемая конфигурация ролей, отображаемая !{ОНтролле­


ром RoleAdmin .

Добавление специальных свойств


в класс пользователя
При создании класса AppUser для представления пользователей в главе 28 было
упомянуто , что базовый класс определяет основной набор свойств, описывающих
пользователя, таких как адрес электронной почты и телефонный номер.
928 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC

U s er~ х

г,:;---·----·------ ---·-·---·-------·-----------

+- 'i'> С [] localhost:62542/RoleAclmin/Edit/e7861409- 3b9e- 4f81-8aOb-c50c 146abef7 f..? _


·--~---------·

ID Name Users
c56fd8dd-3336-4340-a775-e4e57959d235 Admins Admin

d71e56ad- 84ac- 4 3cf-b60З-3643Ьf4cf6c1 Users Allce, Joe

e786 1 409-Зb9e-4f81-8a0b-<:50c146abef7 Employees ВоЬ, Alice

Рис. 30.2. Конфигурирование ролей для целей главы

В большинстве приложений необходимо хранить дополнительную информацию о


пользователях , включая постоянные предпочтения в приложении и сведения вроде

адресов - короче говоря, любые данные, которые полезны при выполнении прило­
жения и должны предохраняться между сеансами. Поскольку по умолчанию для хра­
нения своих данных система ASP.NET Core Identity применяет инфраструктуру Entity
Framework Core, определение дополнительной информации о пользователе означает
добавление свойств в класс пользователя и предоставл ение EF Core возможности со­
здать схему базы данных для их сохранения.
В листинге 30.1 приведен код класса AppUser с добавленными двумя простыми
свойствами, предназначенными для представления города, в котором живет пользо­
ватель , и уровня его квалификации.

Листинг 30.1. Добавление свойств в файле AppUser. cs

using Microsoft . AspNetCore . Identity .EntityFrameworkCore;


namespace Use rs.Mode ls {
puЬlic enum Ci ties {
None, London, Paris, Chicago

puЫic enum QualificationLevels


None, Basic, Advanced

pu Ыic class AppUser : IdentityUser


puЬlic Ci ties Ci ty { get; set; }
puЫic QualificationLevels Qualifications { get; set; }

Перечисления Ci tie s и QualificationLevels определяют значения для не­


скольких городов и уровней квалификации. Эти перечисления используются свойс­
твами City и Qualifica tion, добавленными в класс AppUs er.
Глава 30. Расширенные средства ASP.NET Core ldentity 929
Действия, которые добавлены к контроллеру Home в листинге 30.2, позволяют поль­
зователю просматривать и редактировать свои свойства City и Qualification .
Листинг 30.2. Добавление поддержки для специальных свойств из класса
пользователя в файле HomeController. cs

using Systern . Collections . Generic ;


using Microsoft . AspNetCore . Mvc ;
using Microsoft . AspNetCore . Authorization;
using Users.Models;
using Мicrosoft.AspNetCore.Identity;
using System.Threading.Tasks;
using System.Componentмodel.DataAnnotations;
namespace Users . Controllers {
puЫic class HomeControl le r : Controller {
private UserManager<AppUser> userмanager;
puЫic HomeController(Userмanager<AppUser> userMgr) {
userManager = userMgr;

[Authorize]
puЫic IActionRe sult Index() => View(GetData(narneof(Index))) ;
[Authorize(Roles = "Users " ) ]
puЫic IActionResul t OtherAction () => View ( " Index ",
GetData(narneof(OtherAct i on))) ;
private Dictionary<string , object> GetData(string actionName) =>
new Dictionary<string , object> {
["Action"J = actionName , [ "User "J = HttpContext . User.Identity.Name,
[ "Authenticate d "] = HttpContext . User . Identity.IsAuthenticated,
[ "Auth Туре " ] = HttpContext.User .I dentity . Authen t icationType ,
["In Users Role "] = HttpContext . User . IsinRo l e( "Users " ) ,
["City"] = CurrentUser.Result.City,
["Qualification"] = CurrentUser.Result.Qualifications
};
[Authorize]
puЫic async Task<IActionResult> UserProps()
return View(await CurrentUser);

[Authorize]
[HttpPost]
puЫic async Task<IActionResult> UserProps(
[Required]Cities city, [Required]QualificationLevels qualifications)
if (ModelState. IsValid) {
AppUser user = await CurrentUser;
user.City = city;
user.Qualifications = qualifications;
await userManager.UpdateAsync(user);
return RedirectToAction("Index");

return View(await CurrentUser);

private Task<AppUser> CurrentUser =>


userManager.FindByNameAsync(HttpContext.User.Identity.Name);
930 Часть 11 . Подробные сведения об инфраструктуре ASP. NET Core MVC

Ново е свойство Cu r rentUser с помощью класса UserManager<AppUser> извле­


кает э кзе мпляр App User для представления текуlli;его пользователя. Объект AppUser
применяется в качестве объекта модели представления в версии GET метода действия
UserProps (), а версия
POST этого метода использует его для обновления з на ч ений
новых с в ойств City и QualificationLeve l.
Метод Ge tData ( ) был модифицирован так, чтобы возвращаемый им словарь со­
де ржал значения специальных свойств для текущего пользователя, т.е. значения спе­
циальных свойств будут видны в представлениях, которые отображаются методами
действий Index () и OtherAction ().
Чтобы снабдить методы действий Us e r Props () представлением , добавьте в пап­
ку Views/ Home файл по им ени UserP r ops. cshtml и поместите в н е го разм етку из
листинга 30.3.

Листинг 30.З. Содержимое файла UserProps. cshtml из папки Views/Home

@model AppUse r
<div class= " bg - primary pa n el - body " ><h4>@Mode l. UserName</h4></div>
<div asp - validat i on - summary= "Al l" cl as s= " tex t- danger " ></d i v>
<form asp - ac ti o n="U serProps " me t hod="pos t" >
<div c l ass= " form - group " >
<labe l asp - for =" City " ></ l abel>
<select asp - for= " City " class =" fo r m- cont r o l"
a sp- items= " @new Sel e ct List (Enum . GetName s(typeof(Cities) ) ) " >
<option di sa Ыed selected v al ue="" >Select а Ci t y</option>
</se l ect>
</d i v>
<div class ="f orm- group " >
<label asp -for= " Qualifications " ></ l abe l >
<select asp - for= " Qualifications" class= " form - control "
asp- items= " @new SelectList(Enum . Ge t Names(typeof(QualificationLevels) )) " >
<option d i saЫed selected val u e ="" >Se l ect а City</option>
</select>
</div>
<butt o n typ e=" submit " class= " Ьtn Ьtn -p r i mary " >Submit</button>
<а asp - act i o n=" Index " class= " Ьt n Ьt n - de f aul t" >Cancel</a>
</ f orm>

Представление соде ржит форму с элементами s elect, которые заполняются зна­


чениями из перечислений, опр еделенных в листинге 30.1. Когда форма отправляет­
ся, из базы данных Identity извлекается объект App Us er , представляющий те кущего
пользователя, а значения специальных свойств обновляются с применением значе­
ний , выбранных пользов ателем :

AppUser u s e r = awa i t CurrentU ser;


user.City = city;
user.Qualifications =
qualifications;
await userManager.UpdateAsync(user);
return Redir e ctToAct i on( " Index " ) ;
Глава 30. Расширенные средства ASP.NET Core ldentity 931
Обратите внимание , что диспетчеру пользователей потребу ется явно указать на
необходимость обновления записи базы данных для пользователя , чтобы отразить из­
м ене ния. путем вызова метода Up dateAs yn c ( ). Ранее делать это было не обязатель­
но, т.к. внутри м етодов, которые использовались для внесения изменений в Identity,
метод UpdateAsyn c () вызывался автоматически, но при изменении свойств напря­
мую ответственность за сообщение диспетчеру пользователей о том, что нужно вы­
полнить обновление, возлагается на вас.

Подготовка миграции базы данных


Все связующие механизмы приложения, предназначенные для поддержки новых

свойств, теперь на месте, и осталось лишь обновить базу данных, чтобы ее таблицы
сохраняли значения специальных свойств.
Первым делом понадобится создать новый файл миграции базы данных, кото­
рый будет содержать команды SQL, требуемые для обновления схемы базы данных.
Откройте окно консоли диспетчера пакетов и введите следующую команду:

Add- Migrat i on Cus tomProp erti e s


После заверш е ния команды вы заметите в палке Mi gra t ion s новый файл , в име ­
ни которого содержится строка Cus tomP ropert ies , а точное имя включает числовой
иде нтифюштор. Открыв этот файл , вы увидите в нем класс С#, содержащий метод по
имени Up (), который выполняет команды SQL, необходимые для добавления в базу
данных поддержки специальных свойств. Имеется также метод Down ( ) , который вы­
полняет команды. восстанавливающие предыдущую схему базы данных.

Внимание! Инструменты Entity Framework Core текущей версии не добавляют в файл миграций про­
странство имен, которое содержит классы моделей. Для устранения проблемы придется отре­
дактировать в папке Mi grat i o n s файл, имя которого содержит строку CustomPropert ie s ,
и добавить в его начало оператор usin g для пространства имен Us e rs. Model s .

Затем с помощью показанной ниже команды выполняется миграция базы данных


в новую схему:

Upd a t e - Data b ase


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

Внимание! Будьте внимательны при выполнении миграций производственных баз данных,


содержащих реальные данные о пользователях. Помните, что довольно легко создать
миграцию , которая отбросит столбцы или целые таблицы и может привести к разруши­
тельным последствиям. Удостоверьтесь в том, что миграции баз данных тщательно про­
тестированы, и позаботьтесь о создании резервной копии важных данных на случай , если
что-то пойдет не так, как ожидалось.

Тестирование специальных свойств


Чтобы протестировать результат проведенной миграции , запустите приложение и
войдите как один из пользователей, хранящихся в Identity (с применением , например ,
адреса alice@examp l e . сот и пароля
secre t123). После аутентификации вы увидите
стандартные значения для свойств Ci ty и Qualificati o n Lev e l. Изменить эти свойс-
932 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC

тва можно, запросив URL вида /Horne/UserProps , выбрав новые знач ения и щелкнув
на кнопке Submit (Отправить), что приведет к обновлению базы данных и перенаправ ­
лению опять на URL вида / Ноте с отображением новых значений (рис. 30.3).

Acllon lndex
Action lndex
User
User Alice
Aullieпli ca te d London
Au t h e п t i ca t ed Tru e
Auth Туре
Oualiflcatlons
Auth Туре Mlcrosofl.AspNet. ldentity.Application
ln Users Role True
Advanced ln Users Role True
J City None

I Oua lification None


'
1 •
j
Cancel
Oualilicalion
London

Advanced
L--------l -
l. " --L__
Рис. 30.З. Использование специальных свойств в классе пользователя

Работа с заявка ми и полити ка ми


В более старых системах управления пользоват елям и, т аких как ASP.NET
Membership , которая предшествовала ASP.NET Core Identity, приложение считалось
полномочным источником всей информации о пользователе , по существу трактуя
приложение как замкнутый мир и доверяя содержащимся в нем данным.
Такой подход к разработке ПО укоренился настолько прочно, что м ожет быть труд ­
но осознать, что это произошло. Пример приема с замкнутым ми ром приводился в
главе 29, когда пользователи аутентифицировались с пом ощью уч етных данных, хра­

нящихся в базе данных , а доступ предоставлялся на основе ролей, ассоциированных


с учетными данными. То же самое делалось еще раз в настоящей гл аве, когда в класс
пользователя были добавлены сп ециальные свойства. Каждый фрагмент информа ­
ции, н е обходимой для управл е ния ауте нтификаци ей и автори з аци е й пользоват ел е й ,
поступал изнутри приложения; такой подход полностью подходит для большинства
веб - приложений , и потому он демонстрировался здесь настол ько подробно.
Система ASP.NEТ Core ldentity также поддерживает альтернативный подход к рабо­
те с пользователями, который приемлем в ситуации, когда приложение МVС не является
единственным источником информации о пользователях , и может применяться для ав­
торизации пользователей более гибкими способами, чем позволяют традиционны е роли .
Альтернативный подход предусматривает использование заявок, и в этом разделе будет
описано , как система ASP.NEТ Core Identity поддерживает авторизацwо на основе заявок.

Совет. Вы вовсе не обязаны применять заявки в своих приложениях и, как было показано в
главе 29, система ASP.NET Core ldeпtity прекрасно обеспечивает приложения службами
аутентификации и авторизации безо всякой потребности в понимании заявок.
Глава 30. Расширенные средства ASP.NET Core ldentity 933

Понятие заявок
Заявка - это порция информации о пользователе наряду со сведениями о том,
откуда информация поступила. Изучать заявки легче всего на практических демонс­
трациях, в отсутствие которых любое обсуждение становится слишком абстрактным,
чтобы быть по - настоящему пол езным. Прежде всего, добавьте в папку Contro ller s
ф айл класса по имени Claims Con t ro ll er . cs и определите в нем контроллер, как
пока з ано в л истинге 30.4.

Совет. Вы можете пребывать в некоторой растерянности, следя за определением кода и зна­


комясь с описаниями классов для текущего примера. В настоящий момент переживать по
поводу деталей не стоит - просто придерживайтесь инструкций до тех пор, пока не уви­

дите вывод из метода действия и представление, которое будет определено. Это лучше
всего другого содействует пониманию заявок.

Листинг 30.4. Содержимое файла ClaimsController. cs из папки Controllers


using Mi crosoft . AspNetCore. Aut hor iza ti on ;
us i ng Mi crosoft . AspNetCore . Mvc ;
namespace Users . Controller s {
puЫic c l ass Cl aimsCont r ol l e r : Cont r olle r {
[Authorize]
puЫic ViewResult Index() => View( Use r?. Cla i ms);

Получать заявки, ассоциированные с пользователем, можно разными путями.


Свой ство User (также доступное как свойство HttpContext . User) возвращает объ­
е кт ClaimsPrincipal , который р е ализует принцип, задействованный в данном при­
м е р е . Набор заявок, свя з анных с пользователем, доступен через методы и свойства
кл асс а ClairnsPrinci pal , перечисленные в табл. 30.3.

Таблица 30.З. Изб ранные члены класса ClaimsPrincipal


Имя Описание

Identity Получает объект реализации Пdentity , который ассо­


циирован с текущим пользователем, как рассказывается в

последующи х разделах

FindAll (type) Эти методы возвращают все заявки специфического типа


FindAll(<predicate>) или те, которые удовлетворяют предикату

FindFirst(type) Эти методы возвращают первую заявку специфического


FindFirst(<predicate>) типа или ту, которая удовлетворяет предикату

HasClaim(type , value) Эти методы возвращают true , если пользователь имеет


заявку указанного типа с заданным значением или если есть
HasClaim(<predicate>) заяв к а, удовлетворяющая предикату

IsinRo l e(name) Возвращает t r ue , если пользователь является членом роли


с указанным именем
934 Часть 11. Подробные сведения об инфраструктуре ASP.NET Соге MVC

Как объяснялось в главе 28, свойство HttpContext. User. Identi ty возвра­


щает реализацию интерфейса IIdenti ty, которой в случае использования ASP.NET
Core Identity является объект Claimsidentity. В табл. 30.4 описаны члены класса
Claimsidentity, имеющие отношение к этой главе.

Таблица 30.4. Избранные члены класса Claimsidenti ty

Имя Описание

Claims Возвращает перечисление объектов Claim, представляющих


заявки для пользователя

AddClaim(claim) Добавляет заявку к удостоверению пользователя

AddClaims(claims) Добавляет перечисление объектов Claim к удостоверению


пользователя

HasClaim(predicate) Возвращает true, если удостоверение пользователя содер­


жит заявку, которая совпадает с указанной

RemoveClaim(claim) Удаляет заявку из удостоверения пользователя

Доступны также другие методы и свойства, но перечисленные в табл. 30.4 чле­


ны применяются в веб-приложениях наиболее часто по причинам, которые станут
очевидными по мере демонстрации места заявок в рамках более широкой платфор­
мы ASP.NEТ Core.
В листинге 30.4 используется свойство Controller. User для получения объекта
ClaimsPrincipal и передачи значения свойства Claims как модели представления
стандартному представлению. Объект Claim представляет одиночную порцию дан­
ных о пользователе, а в классе Claim определены свойства, показанные в табл. 30.5.

Таблица 30.5. Свойства класса Claim

Имя Описание

Issuer Возвращает имя системы, которая предоставила заявку

Subject Возвращает объект Claimsidenti ty для пользователя, к которому


относится заявка

Туре Возвращает тип информации, которую представляет заявка

Value Возвращает порцию информации, которую представляет заявка

Чтобы отобразить детали заявок, ассоциированных с пользователем, создайте


папку Views/Claims и добавьте в нее файл по имени Index. cshtml с содержимым
из листинга 30.5.

Листинг 30.5. Содержимое файла Index. cshtml из папки Views/Claims


@model IEnumeraЫe<System.Security.Clairns.Clairn>

<div class="bg-primary panel-body"><h4>Clairns</h4></div>


<tаЫе class="taЫe taЫe-condensed taЬle-bordered">
<tr>
<th>Subject</th><th>Issuer</th><th>Type</th><th>Value</th>
</tr>
Глава 30. Расширенные средства ASP.NET Core ldentity 935
@if (Mode l == null 1 1 Model. Count () == О) {
<tr><td colspan= " 4 " class= " text - center " >No Claims</td></tr>
else {
@foreach (var claim in Model.OrderBy(x => х . Туре)) {
<tr>
<td>@claim . Subject . Name</td>
<td>@claim.Issuer</td>
<td identity - claim-typ e="@claim.Type " ></td>
<td>@claim.Value</td>
</tr>

</tаЫе>

Для отображения заявок, переданных в модели представления, применяется таб­


лица. Значением свойства Claim. Туре является URI для схемы Microsoft, которая не
особенно полезна. В качестве значений для полей класса System. Securi ty . Claims .
ClairnTypes используются популярные схемы, поэтому чтобы облегчить чтение выво­
да из представления Index . cshtml, к элементу td, отображающему свойство Туре,
был добавлен специальный атрибут:

<td identity-claim-type="@claim.Type"></td>

Добавьте в папку Infrastructure файл класса по имени ClairnTypeTagHelper . cs


и создайте в нем дескрипторный вспомогательный класс, который транслирует зна­
чение атрибута в более читабельную строку (листинг 30.6).
Листинг 30.6. Содержимое файла ClaimТypeTagHelper. cs из папки Infrastructure
using System .Linq;
using Systern . Reflection ;
using Systern . Security.C lairns;
using Microsoft .AspNetCore . Razor .TagHelpers ;
namespace Users . Infras tructure {
[HtrnlTargetElernent( " td ", Attributes = " identity - clairn- type " )]
puЫic class ClaimTypeTagHelper : TagHelper {
[HtrnlAttributeNarne( " identity-claim- type " )]
puЬlic string ClaimType { get; set ; }
puЬlic override void Process(TagHelperContext context,
TagHelperOutput output) {
bool foundType = false;
Fieldinfo[] fields = typeof(ClairnTypes) .GetFields() ;
foreach (Fieldinfo field in fields) {
if (field . GetValue(null) .ToString() == ClaimType)
output . Content . SetContent(field . Narne);
foundType = true ;

if (!foundType) {
output . Content . SetContent(ClairnType . Split( ' /' , '. ' ) .Last( )) ;
936 Часть 11. П одробные сведения об инфраструктуре ASP.NET Core MVC

Чтобы выяснить, поч е му контроллер, в котором применяются заявки , был создан


без подробных объяснений, запустите приложение и аутентифицируйтесь как поль­
зователь Alice (используя адрес a lice@exarnple . corn и пароль secret123). После
прохождения аутентификации запросите URL вида /C l aims для просмотра заявок,
ассоциированных с пользователем (рис. 30.4).

Subject lssuer Туре Value


Alice LOCAL AUTHORITY SecurityStamp ebЬd9127-5bbe-49f2-BOce-OOOЗ6076690d

Alice LOCAL AUTHORIТY Role Users


Alice LOCAL AUTHORITY Role Employees
Alice LOCAL AUTHORIТY Name Alice
Alice LOCAL AUTHOR I ТY Name/dentifier 97В7а 1Ьа-6726-4762 -a c7d-57e3e3e9dc9c

Рис . 30 .4 . Вывод из действия I ndex контроллера Clairns

Для удобства детали з аявок дополнительно воспроизведены в табл . 30.6.

Таблица 30.6. Данные, показанные на рис. 30.4


Субъект Выдавшая система Тип Значение

Al ice LOCAL AUTHORI TY Secu r i tyStarnp Уникальный идентификатор

Alice LOCAL AUTHOR I TY Role Ernp loyees


Alice LOCAL AUTHORI TY Ro l e Us e rs
Alice LOCAL AUTHORITY Narne Al ice
Alice L OCAL AUTHORI TY Name!dent i f ier Идентификатор пользователя Alice

В табл. 30.6 отраж ен самый важный аспект заявок , который зюиrючается в том,
что они уже применялись при реализации стандартных ср едств аутентификации и
авторизации в главе 29. Здесь видно, что некоторые из заявок относятся к удостове ­
рению пользователя (заявка это Al i ce , а заявка Name!dent i f ie r -уникаль­
Name -
ный идентификатор пользователя Al ice в базе данных ASP.NET Core Identity). Друrие
заявки показывают членство в ролях - есть две заявки Ro l e , свидетельствующие о
том факте, что пользователю Alice назначены роли User s и Emp l oyees.
Когда информация подобного рода выражается как набор заявок , отличие состоит
в том , что появляется возможность определить, откуда поступили данные. Для всех
заявок, приведенных в табл . 30.6, св ойство I ss u e r установл ено в L OCAL AUTHORITY ,
указывая на то, что удостоверение пользователя бьmо установлено приложением .
Глава 30. Расширенные средства ASP.NET Core ldentity 937
Теперь, когда вы увидели примеры заявок, определить, что собой представляет за ­
явка, гораздо легче: заявка - это любая порция информации о пользователе, которая
доступна приложению, включая удостоверение пользователя и его членство в ролях.

Кроме того. информация о пользователях, которая была определена в пр едшествую­


щих главах, автоматически делается доступной в виде заявок системой ASP.NET Core
Identity. Хотя поначалу заявки могут сбивать с толку, ничего магического в них нет, и
подобно любому другому аспекту приложений МVС, они оказываются гораздо менее
пугающими, как только заглянуть "за кулисы" и выяснить, каким образом они на са­
мом деле работают.

Создание заявок
Заявки интересны тем, что приложение может получать их из многих источников, а
не просто полагаться на локальную базу данных, хранящую информацию о пользовате­
лях. В разделе "Использование сторонней аутентификации" далее в главе на реальном
примере будет показано, как аутентифицировать пользователей с помощью сторонней
системы, а пока в проект нужно добавить класс, эмулирующий систему, которая пре­
доставляет информацию о заявках. Добавьте в папку Infrastructure файл класса по
имени LocationClaimsProvider. cs с содержимым из листинга 30.7.
Листинг 30. 7. Содержимое файла Loca tionClaimsProvider. cs
из папки Infrastructure
using System . Security . Claims ;
using System . Threading.Tasks ;
using Microsoft .AspNetCore .Au thentication ;
namespace Users. I nfrastructure {
puЫic static class LocationClaimsProvider
puЫic static Task<ClaimsPrincipal> AddClaims(
ClaimsTransformationContext context) {
ClaimsPrincipal principal = context . Principa l;
if (principal != null
&& !principal.HasClaim(c =>с.Туре== ClaimTypes . PostalCode))
Claimsidentity identity = principal.Identity as Cl a imsidentity ;
if (identity != null && identity . IsAuthenticated
&& identity . Name != null) {
if (identity . Name . ToLower() == "alice") (
identity.AddClaims(new Claim[] {
CreateClaim(C l aimTypes .PostalCode , "DC 20500 " ) ,
CreateClaim(ClaimTypes . StateOrProvince , " ОС " )
)) ;
else {
identity . AddClaims(new Claim[ ] {
CreateClaim(ClaimTypes . PostalCode , " NY 10036 " ) ,
CreateClaim(ClaimTypes .StateOrProvince , " NY")
) ) ;

}
return Task.FromResult(principal);

private static Claim CreateClaim(string type, string value) =>


new Claim(type , value , ClaimValueTypes.String , " RemoteCla i ms " );
938 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

Метод AddClaims () принимает объект Cla i msTransformationContext и полу­


ча ет ассоциированный с ним экземпляр ClaimsPrincipal. чтобы прив ести значе­
ние его свойства Identi ty к типу Cla imsi denti ty. Затем значение свойства Name
применяется для создания заявок, касающихся почтового кода и штата, в котором

проживает пользователь.

Класс LocationClaimsProvider эмулирует систему вроде центральной базы дан­


ных человеческих ресурсов, которая могла бы служить полномочным источником ин­
формации о месте ж ительств а персонала. н апример .
Для применения специальных заявок необходимо включить функцию промежу ­
точного ПО, известную как трансформация заявок, которая будет вы з ывать метод
AddClaims () LocationClaimsProvider при получении каждого запроса ин­
класса
фраструктурой ASP.NET Core. В листинге 30.8 функция трансформации з аявок вклю­
чается в методе Configure () класса Startup.

Листинг 30.8. Включение трансформации заявок в файле Startup. cs

puЫic void Configure(IApplicationBuilder арр) {


app . UseStatusCodePages() ;
app . UseDeveloperExceptionPage() ;
app . UseStaticFiles() ;
app . Use!dentity() ;
app.UseClaimsTransformation(LocationClaimsProvider.AddClaims);
app . UseMvcWithDefaultRoute() ;
AppidentityDbContext . CreateAdminAccount(app . ApplicationServices ,
Configuration) . Wait() ;

Метод UseClaimsTransformation () используется для назначения метода. кото­


рый будет получать объектClaimsPrincipal и трансформировать его; здесь указан
статический метод AddClaims () класса LocationClaimsProvider.

Трансформация заявок

Применение средства трансформации заявок требует определенной осторожности, пос­


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

При наличии многочисленных трансформаций, подле жащих выполнению, которые обычно


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

Cl aimsTransformer transform = new ClaimsTransformer();


transform . OnTransform += LocationClaimsProvider.AddClaims ;
app . UseClaimsTransformation(transform . TransformAsync) ;

Класс ClaimsTransformer предоставляет событие OnTransform, которое будет обра­


щаться к мно ж еству методов по мере получения запросов.
Глава 30. Расширенные средства ASP.NET Core ldentity 939
Каждый раз , когда поступает запрос, промежуточное ПО трансформации заявок
вызывает метод Loca tionClaimsProvider. AddClaims (), который эмулирует ис­
точник данных человеческих ресурсов и создает специальные заявки. Для просмотра
специальных заявок запустите приложение, войдите от имени разрешенного пользо ­
вателя и запросите
URL вида /C laim. На рис. 30.5 показаны заявки для пользователя
Alice. Можете выйти и снова зайти, чтобы понаблюдать за изменениями.

Subjec1 Jssuer Туре Value

Alice LOCAL AUTHORIТY SecuritySlamp ebbd9 127-5bbe-49f2-80ce-00036076690d

Alice LOCAL AUTHORIТY Role Users

Alice LOCAL AUTHORIТY Role Employees

1 Alice LOCAL AUTHORIТY Name Alice


1
Alice LOCAL AU THORIТY Name ldentifier 9787а 1Ьа-6726-4762-ас7d-57еЗеЗе9dс9с

1 Alice RemoteClaims PostalCode ос 20500

1 Alice RemoteClaims StaleOrProvince ос


1
1
1
[_
Рис . 30.5. Определение дополнительных заявок для пользователей

Получение заявок из множества местоположений означает, что приложение не


обя зан о дублировать данные , которые хранятся где-то в другом месте, и делает воз­
можной интеграцию данных от внешних участников. Свойство Cl aim. Issuer сооб­
щает, откуда происходит заявка, что помогает судить о том, насколько точными веро­

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

Создание специальных заявок удостоверений

Если в приложение необходимо добавить специальные локальные заявки, то это можно


делать при создании новых пользователей . Класс UserManager<T> предлагает методы
AddC laimAsync () и AddC laimsAsync (), предназначенные для определения локальных
заявок, которые затем хранятся в базе данных и автоматически извлекаются, когда пользо­
ватель аутентифицирован (т. е . нет нужды полагаться на средство трансформации заявок) .
Однако перед применением упомянутых методов следует обдумать, каким образом хранящи­
еся данные будут поддержи ваться в актуальном состоянии, и не лучше ли, чтобы приложение
обслужи валось путем динамического извлечения данных из их источника. Как объясняется в
следующем разделе, заявки используются при проверках авторизации, и устаревшие данные
заявок могут предоставить пользователям доступ к частям приложения, к которым он должен

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


940 Часть 11. Подробные сведения об инфраструктуре ASP.NET Соге MVC

Использование политик
Имеющиеся рабочие заявки можно применять для управления доступом пользо­
вателей к приложению бол ее гибким образом, ч е м с помощью стандартных рол е й.
Пробл е ма с ролями в том, что они статичны , и после того, как пользователю была на­
значена роль, он остается е е чл еном до тех пор, поr<а не будет явно удален. Вот почему
длительно работающие сотрудники крупных корпораций в итоге обладают букв ал ьно
немыслимым доступом к внутренним системам: им назначаются роли, требуемые для
выполнения каждого нового задания, но старые роли удаляются р едко.

Заявки используются для построения политик авторизации , которые являются


частью конфигурации приложения и применяются к методам действий или контрол ­
лерам посредством атрибута Author i ze . В листинге 30.9 представлена простая по ­
литика, которая разрешает доступ только пользователям со специфическим типом и
з начением з аявки .

Листинг 30.9. Создание политики авторизации в файле Startup. cs

using Micros o ft . AspNetCore . Builde r;


u sing Microsoft . Ext e n s ions . Dependenc yinjection ;
using Microsoft . Ex t ensions . Con f iguration ;
using Microsoft . AspNetCore . Hosting ;
us ing Mi crosoft . En t i t y FrameworkCo r e ;
u sing Mi crosof t. AspNe tCo re . I d e n ti ty. Enti tyFra me wor kCor e;
u sing Us ers . Models ;
u sing Users . Infrast r ucture ;
using Microsoft.AspNetCor e .Identity ;
using System.Security.Claims;
namespace Users {
p u Ыic class Star t up
I Con f igurat i onRoot Co nf igura t ion;
puЫic Startup(IHo s tingEnvi ronme nt env) {
Configuration = ne w Configu rat i onBuilder()
. SetBasePath(e nv . Co n te nt RootPa t h)
.AddJsonFil e( " ap p sett i ngs . json " ) . Build() ;

puЬl i c vo i d Conf i gu r eService s ( IServ i ceCollec ti o n s e r v i ces)


services . AddTran s ient<IPasswordValidator<AppUs e r> ,
Cu s tomPasswo r dValidato r >() ;
se r v i ces . AddTran si e nt<IUserVal i dator<AppUse r >, CustomUse r Validator>( ) ;
services .AddAuthorization (opts => {
opts. AddPolicy ( "DCUsers", policy => {
policy.RequireRole("Users");
policy.RequireClaim(ClaimTypes.StateOrProvince, "DC");
}) ;
}) ;

s e rv i ces . AddDb Con te x t <Appide n t ityDbCont e x t >(option s =>


op t ions . UseSqlSe rve r (
Configurat i o n["Data : Sp o r tSto r e ide n t ity:Con nectionSt ri n g" ])) ;
s ervices . Addiden t ity<AppUser , Iden ti t yRol e >(op t s => {
opts . User . Req uir eUniqueEma i l = tr u e ;
Глава 30. Расширенные средства ASP.NET Саге ldentity 941
opts . Password . RequiredLength = 6 ;
opts . Password . RequireNonAlphanumeric = false ;
opts . Password .RequireLowercase = false ;
opts . Password .RequireUppercase = false ;
opts . Password . RequireDigit = false;
}) . AddEntityFrameworkStores<AppidentityDbContext>(};
services . AddMvc() ;

puЫic void Configure(IApplicationBuilder арр) {


app . UseStatusCodePages() ;
app.UseDeveloperExceptionPage() ;
app.UseStaticFiles() ;
app . Useidentity() ;
app.UseClaimsTransformation(LocationClaimsProvider.AddClaims) ;
app.UseMvcWithDefau l tRoute();
AppidentityDbContext . CreateAdminAccount(app . ApplicationServices ,
Configuration) . Wait() ;

Метод AddAuthorizat i on () настраивает политику авторизации и предоставляет


экземпляр класса AuthorizationOptions , в котором определены члены , описанные
в табл. 30.7.
Таблица 30.7. Члены класса AuthorizationOptions

Имя Описание

DefaultPolicy Это свойство возвращает стандартную политику авторизации,


которая используется, когда атрибут Authorize был применен
без каких-либо аргументов. По умолчанию политика проверяет,
что пользователи прошли аутентификацию

AddPolicy(name , Этот метод используется для определения новой политики, как


expression) объясняется ниже

Политики определяются с применением метода AddPolicy () ,который р аботает с


лямбда-выражением, оперирующим на объекте AuthorizationPolicyBuilder для
построения политики с помощью методов из табл. 30.8.
Таблица 30.8. Избранные методы класса AuthorizationPolicyBuilder
Имя Описание

RequireAuthenticatedUser() Этот метод требует, чтобы запрос был ассоциирован с


аутентифицированным пользователем

RequireUserName(name) Этот метод требует, чтобы запрос был ассоциирован с


указанным пользователем

RequireClairn(type) Этот метод требует, чтобы пользователь имел заявку


указанного типа . Проверяется только присутствие за­
явки и принимается любое значение
942 Часть 11. Подробные сведения об инфраструктуре ASP.NET Соге MVC

Окончание табл. 30.8


Имя Описание

RequireClaim(type , values) Этот метод требует, чтобы пользователь имел заяв­


ку указанного типа с одним из диапазона значений.
Значения могут быть выражены как аргументы, разде­
ляемые запятыми, или в виде экземпляра реализации

IEnumeraЫe<string>

Requ ireRole(ro l es) Этот метод требует, чтобы пользователь имел членство
в роли. Множество ролей может быть указано как аргу­
менты, разделяемые запятыми, или в виде экземпляра

реализации IEnumeraЬle<string>, и членство в лю­


бой роли обеспечит удовлетворение требования

AddRequirements(requirement) Этот метод добавляет к политике специальное требо­


вание, как описано в разделе " Создание специальных
требований политики " далее в главе

Политика в листинге 30.9 требует, чтобы пользователь имел членство в роли Users
и заявку StateOrProvince со значением ос. Когда требований н еск олько, тогда все
они должны быть удовлетворены, чтобы авторизация была одобрена.
П е рвы й аргумент метода AddPo li cy () - это имя. посредством которого можн о
ссылаться на политику при ее применении. Именем политики из листинга 30.9 явля­
ется DCUsers, и оно используется в атрибуте Authorize для применения политики к
контроллеру Ноте в листинге 30.10.
Листинг 30.1 О. Применение политики авторизации в файле HomeController. cs

using System . Collections . Generic ;


using Microsoft.AspNetCore . Mvc;
using Microsoft.AspNetCore . Authorization;
using Users . Models ;
using Microsoft . AspNetCore.Identity ;
using System .Threading . Tasks ;
using System . ComponentModel . DataAnnotations ;
namespace Users . Controllers {
puЫic class HomeController : Controller {
private UserManager<AppUser> userManager;
puЫic HomeController(UserManager<AppUser> userMgr) {
userManager = userMgr ;

[Authorize]
puЫic IActionResult Index() => View(GetData(nameof(Index))) ;
/ / [Authorize (Roles = 11 Users 11 ) ]
[Authorize(Policy = "DCUsers")]
puЫic IActionResul t OtherAction () => View ( Index ,
11 11

GetData(nameof(OtherAction))) ;
11 .. . для краткости другие методы не показаны ...
private Task<AppUser> CurrentUser =>
userManager . FindByNameAsync(HttpContext.User . Identity . Name) ;
Глава 30. Расширенные средства ASP.NET Core ldentity 943
Свойство Policy используется для указания имени политики, которая будет при­
меняться, чтобы защитить метод действия. Результатом окажется комбинированная
проверка имеющихся у пользователя ролей и заявок, которая выполняется, когда за­
прос нацелен на метод OtherAction ().Правильным сочетанием членства в ролях и
заявок располагает только учетная запись Alice, в чем можно удостовериться, за­
пустив приложение, войдя от имени разных пользователей и запрашивая URL вида
/Horne/OtherAction.

Создание специальных требований политики


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

К счастью, систему политик можно расширять специальными требованиями, которые


представляют собой классы, реализующие интерфейс IAuthorizationRequirernent , и
специальными обработчиками авторизации, которые являются подклассами класса
AuthorizationHandler и занимаются оценкой требования для заданного запроса.
Добавьте в папку
Infrastructure файл класса по имени BlockUsersRequirernent. cs,
после чего определите в нем специальное требование и обработчик (листинг 30.11).
Листинг 30.11. Содержимое файла BlockUsersRequirement. cs
из папки Infrastructure
using System ;
using System.Linq;
using System . Threading . Tasks;
using Microsoft.AspNetCore.Authorization ;
namespace Users . Infrastructure {
puЫic class BlockUsersRequirement : IAuthorizationRequirement
puЬlic BlockUsersRequirernent(pararns string[J users) {
BlockedUsers = users;

puЫic string [] BlockedUsers { get ; set ; }

puЫic class BlockUsersHandler : AuthorizationHandler<BlockUsersRequirement>


protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context, BlockUsersRequirement requirement)
if (context . User.Identity != null && context . User . Identity . Name != null
&& !requirement.BlockedUsers
. Any(user => user . Equals(context .User .I dentity . Name ,
StringComparison.OrdinalignoreCase) )) {
context.Succeed(requirement);
else {
context . Fail() ;

return Task . CompletedTask ;


944 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

Кл асс BlockUs e r Requ irement представляет требование и применя ет ся для ука ­


з ания данных, исполь зуемых при создании политики; в рассматрива ем ом случа е э то

списо1< пользователей , которые не будут авторизованы . Класс BlockUsersHandler


ответств ен ен з а оце юч запроса на авторизацию с примен ением данных тр е бования и
унаследован от класса Authorizat i onH a nd l er<T> , где Т - тип класса тр ебования.
М етод Handl e () вызывается для класса обр аботчика , когда система авторизации
нужда ется в проверке доступа 1< ресурсу. В качестве аргументов методу п е ред аются
объект Au t h or i zation Hand l erCon t ext , который определяет члены, опи с анные в
табл. 30.9, и объект требования, который предоставляет доступ к данным, необходи­
мым для выполнения проверки .

Таблица 30.9. Избранные члены класса AuthorizationHandlerContext


Имя Описание

User Это свойство возвращает объект ClaimsPr i nc i pal ,


ассоциированный с запросом

Succeed(requirement) Этот метод вызывается , если запрос удовлетворяет


требованию. Аргументом является объе кт реализации
I Au thorizat i onRequi rement , получаемый методом
Handl e ()
Fail () Этот метод вызывается, если запрос не удовлетворяет
требованию
Resource Это свойство возвращает объект, который используется для
авторизации доступа к одиночному ресурсу прило ж ения ,

как объясняется в разделе "Использование политик для


авторизации доступа к ресурсам" далее в главе

Обр аботчик требования в листинге 30. l l пров еряет и мя пол ь зов ателя. чтобы
выяснить, находится ли оно в списке запрещенных пользователей , которы й пре­
доставляет объе кт BlockUsersRequ i remen t, и вызывает метод Succeed () ил и
Fail () соответственно. Применение специального требования сопряжено с двумя
и зменениями конфигурации (листинг 30 .12) .

Листинг 30.12. Применение специального требования авторизации в файле Startup. cs


using Mi cro s o f t . AspNetCore . Builder;
using Mi c r osoft . Exte nsions . Dependencyin je ct i on ;
us i ng Microsoft.Ex t ens i ons . Configu r ation;
using Microso f t . As pNetCore .Host ing ;
using Microso f t . Ent i tyFrameworkCo r e ;
usi ng Microso ft. As pNe t Co re.Ident it y . Enti tyFrameworkCore ;
us i ng Users . Mode l s ;
us in g Users . I nfrastructure ;
usi ng Micro s oft. AspNetCore .I denti ty;
usi ng System.Se cu rity . Cla ims ;
using Microsoft.AspNetCore.Authorization;
namespace Use r s {
p uЬl i c c l as s Start up
IConfigu r a t ionRoo t Configu rat i on ;
Глава 30. Расширенные средства ASP.NET Core ld entity 945
puЫic Startup(IHostingEnvironrnent env) {
Configuration = new ConfigurationBui l der()
. SetBasePath(env . ContentRootPath)
.AddJsonFile( " appsettings .js on ") .Buil d() ;

puЫic void ConfigureServices(IServi ceCollection services)


services . AddTransient<IPasswordValidator<AppUser> ,
CustomPasswordVa lida tor>() ;
services . AddTransient<IUserValidator<AppUser> , CustornUserValidator>();
services.AddTransient<IAuthorizationHandler, BlockUsersHandler>();
services . AddAuthorization(opts => {
opts . AddPolicy( " DCUsers ", policy => {
policy . RequireRole( " Users ");
policy . RequireClaim(C la imTypes . StateOrProvince , "ОС");
}) ;
opts. AddPolicy ( "NotBob" , pol.icy => {
policy.RequireAuthenticatedUser();
policy.AddRequirements(new BlockUsersRequirement("Bob"));
}) ;
) ) ;

services.AddDbContext<AppidentityDbContext>(options =>
opt i ons . UseSqlServer(
Configuration[ "Da ta : SportStoreidentity :ConnectionString " ] ));
services.Addidentity<AppUser , IdentityRole>(opts => {
opts . User .RequireUnique Email = true;
opts.Password.RequiredLength = б;
opts . Password.RequireNonAlphanurneric = false ;
opts . Password .RequireLowercase = false;
opts . Password .Requ ir eUppercase = false;
opts . Password.RequireDigit = false;
)) . AddEntityFrameworkStores<AppidentityDbContext>() ;
services . AddMvc() ;

puЬlic void Conf igur e(IApp li cationBuilder арр) {


app . UseStatusCodePages() ;
app . UseDeveloperExceptionPage() ;
app.UseStaticFiles() ;
ap p.Useide ntity();
app .Us eClaimsTransformation(LocationClaimsProvider . AddClairns) ;
app . UseMvcWithDefaultRoute() ;
AppidentityDbContext . CreateAdrninAccount(app.ApplicationServices,
Configuration) .Wai t() ;

Первый шаг предусматривает регистрацию 1<ласса обработчика с помощью пос­


тавщика служб как реализации интерфейса IAuthorizationHandler. Второй шаг
предполагает добавление к политике специального требования, что делается с ис­
пользованием метода AddRequ irements () :
946 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

opts .AddPolicy("NotBob" , policy => {


policy . RequireAuthenticatedUser();
policy.AddRequirements(new BlockUsersRequirement("Bob"));
}) ;

Результатом будет политика, которая требует аутентифицированных пользовате­


лей, отличных от ВоЬ, и может применяться посредством атрибута Authorize сука­
занием имени политики (листинг 30.13).

Листинг 30.13. Применение специальной политики в файле HomeController. cs

11 [Authorize(Roles = "Users")]
[Authorize (Policy = "DCUsers")]
puЫic IActionResult OtherAction() => View("Index",
GetData(nameof(OtherAction)));
[Authorize (Policy = "NotBob") ]
puЫic IActionResult NotBob () => View ("Index", GetData (nameof (NotBob))) ;

В случае аутентификации 1<ак пользователя ВоЬ вы не сможете получить до­


ступ к URL вида /Home/NotBob, но всем остальным учетным записям доступ будет
предоставлен.

Использование политик для авторизации доступа к ресурсам


Политики могут также применяться для контроля доступа к индивидуальным ре­
сурсам, что представляет собой общий термин, означающий любой элемент данных,
который используется в приложении и требует более детализированного управления,
чем возможно на уровне методов действий. Добавьте в папку Models файл по имени
ProtectedDocument. cs и определите в нем класс, который представляет документ с
рядом свойств, касающихся владения (листинг 30.14).

Листинг 30.14. Содержимое файла ProtectedDocumen t. cs из папки Models


namespace Users.Models {
puЬlic class ProtectedDocument
puЬlicstring Title { get; set; }
puЫic string Author { get; set; }
puЫic string Editor ( get; set; }

Это всего лишь заполнитель для реального до1<умента, а его ключевой аспект в
том, что править каждый д01<умент разрешено только двум лицам: автору и редакто­
ру. Реальный документ потребовал бы содержимого, хронологии изменений и многих
других средств, но для примера вполне достаточно и такого простого класса. Добавьте
в папку Controllers файл класса по имени DocumentController. cs и создайте в
нем контроллер, как показано в листинге 30.15.
Глава 30. Расширенные средства ASP.NET Core ldentity 947
Л истин г 30.15. Содержи м ое фа й ла DocumentController. cs из папки Controllers

using Microsoft.AspNetCore . Authorizat i on ;


using Microsoft . AspNetCore . Mvc;
using System . Linq;
using Users . Models ;
namespace Users.Controllers
[Authorize]
puЬlic class DocumentController : Controller {
private ProtectedDocument[J docs = n ew Pro t ectedDocument[J
new ProtectedDocument { Title " QЗ Budget ", Author = "Alice ",
Editor = " Joe " } ,
new ProtectedDocument Tit l e "P ro j ect Pl an ", Author = " ВоЬ ",
Edi tor = " Alice " }
};
puЫic ViewResult I ndex() => View(doc s ) ;
puЬlic ViewResult Edit(string title) {
return View("Index ", docs . Firs t OrDe f aul t (d => d . Tit l e title)) ;

Контролле р поддержи в ает фиксированный н абор объе ктов ProtectedDocument.


Объе1пы ProtectedDocument при м еняются в де йствии I ndex, п е р едающе м вс е до­
куме нты методу V i ew ( ) , и в действии Edi t , которо е выбир ает оди н докуме нт н а
ос нове аргумента ti tle. Об а м етода действий используют представление по имени
Index . chstml, по этому создайте новую папку Views/Document и пом е стите в н ее
ф айлIndex . chstml с ра зметкой из листинга 30.16.

Листинг 30. 16. Содержимое файла Index. cshtml из пап к и Views/Docurnent

@if (Model is IEnumeraЫe<ProtectedDo c u ment>) {


<div class = " bg - p ri mary pane l- body" >
<h4>Documents (@User? .I dentity? .Na me)</ h 4>
</div>
<tаЫе class= " taЫe taЫe - condensed t a Ьl e - bordered " >
<tr><th>Title</th><th>Author</th><th>Editor</th><th></th></tr>
@foreach (var doc in Model) {
<tr>
<td>@doc . Title</td>
<td>@doc . Author</td>
<td>@doc . Editor</ t d>
<td>
<а class="btn btn - sm btn - primary " asp - action= " Edit"
asp-route-title= " @doc . Ti t l e " >
Edit
</а>
</td>
</tr>
}
</tаЫе>
else {
<div class= " bg - prirnary panel - bo d y " >
948 Часть 11. Подробные сведения об инфраструктуре ASP.NET Соге MVC

<h4>Editing @Model . Title (@User? .I dentity? . Name) </h4 >


</ div>
<div class= "panel-body">
Document editing feature would go here ...
</div>
< а asp-action="Index" class="Ьtn Ьtn-primary" > D o ne < /a >

<а asp-action="Logout" asp-controller="Account"


class= "btn btn-danger">Logout</a>

Если модель представления - последовательность объектов ProtectedDocument,


тогда представление отображает таблицу, в которой для каждого документа предус­
мотрена строка с именами автора и редактора, а также ссылкой на действие Edi t.
Если модель представления - одиночный объект ProtectedDocument, тогда пред­
ставление отобразит заполнитель, на месте которого реальное приложение предло­
жило бы средства редактирования .
В настоящий момент единственным ограничением авторизации является атри­
бут Authorize, примененный к классу DocumentController, который означает,
что любой документ может редактировать любой пользователь, а не только автор
или редактор . В этом легко убедиться, запустив приложение, запросив URL вида
/Document, войдя от имени любого пользователя приложения и щелкая на кнопках
Edit (Редактиров ать) для документов. На рис. 30.6 иллюстрируется редактирование
документа Project Plan пользователем Joe.

Title Author
1
аз Budgel Alico Joe 18 i1
Document ediHng feature \•1ould go t1ere".
Allce
В оЬ

111 11111181
L___т·-------"-·---·----~
_J

Рис. 30.6. Редактирование документов

Создание политики и обработчика авторизации доступа к ресурсам


Ограничение доступа к индивидуальным документам на уровне методов действий
обеспечить трудно, потому что атрибут Authorize оценивается перед вызовом метода
действия. Другими словами, решение относительно авторизации принимается до того,
как объект ProtectedDocument извлечен и может быть проинспектирован с целью
выяснения, каким пользователям должен быть предоставлен доступ к документу.
Пробле ма решается созданием политики и обработчика авторизации, которым из­
вестно, каким образом иметь дело с объектами ProtectedDocume nt, и их использо­
ванием внутри метода действия после того, как были выявлены детали о пользовате­
ле. Добавьте в папку Infrastructure файл по имени DocumentAuthorization. cs
и определите в нем классы, представленные в листинге 30.17.
Глава 30. Расширенные средства ASP.NET Core ldentity 949
Ли сти нг 30.1 7 . С одержим ое файла Documen tAu thori za tion. cs
и з папки Infras t r uctur e
using System ;
using System . Threading . Tasks ;
using Microsoft . AspNetCore . Author i zat i on ;
using Users . Models ;
namespace Users.Infrastructure
puЫic class DocumentAuthorizationRequirement
IAuthorizationRequirement {
puЫic bool AllowAuthors { get ; set ;
puЬlic bool AllowEditors { get ; set ;

puЫic class DocumentAuthorizationHandler


: AuthorizationHandler<DocumentAuthorizationRequirement>
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context ,
DocumentAuthorizatio nRequirement requirement)
ProtectedDocument doc = context . Resource as ProtectedDocument ;
string user = context . User . Identity . Name ;
StringComparison compare = St ringComparison . OrdinalignoreCase ;
if (doc != null && user != null &&
(requirement.AllowAuthors && doc . Author . Equa l s(use r, compare))
11 (requirement . AllowEditors && doc . Editor . Equals(user , compa r e) ))
context . Succeed(requirement) ;
else {
context . Fail() ;

return Task . CompletedTask ;

Объ е кт AuthorizationHandlerContext пр едлагает свойство Resource , об е спе­


чивающее до ступ к объе кту, который может быть проинспектирован для авторизации.
Кл асс DocumentAuthorizationHandl er проверяет, содержит л и сво й ство Resource
объ е кт ProtectedDocument , и е сл и это так, тогда выясн я ет, явл я ется ли текущий
пол ь з ов ател ь автором или редактором , а также разрешает ли объект
DocumentAutho
rizationRequirement авторам или редакторам иметь доступ к документу.
В ли стинг е 30 .18 кл асс DocumentAuthorizationHandler р е гистрируется в ка­

че стве о б работчи ка дл я тр еб ований DocumentAuthorizationRequirement и опре­


деля ется политика , котор ая имеет это требование.

Листинг 30. 18. Регистра ци я обработчи ка и определение политики в фа йле Startup. cs

puЫic void ConfigureServices(ISe r viceCol l ection services)


services . AddTransient<IPasswordValidator<AppUser> ,
CustomPasswordValidator>() ;
services.AddTransient<IUserVal idator<AppUser> , CustomUserValidator>() ;
services . AddTransient<IAuthorizationHandl er , BlockUsersHandler>() ;
950 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

services.AddTransient<IAuthorizationHandler,
DocumentAuthorizationHandler>();
services . AddAuthorization(opts => {
opts . AddPo l icy( " DCUsers ", policy => {
po li cy . RequireRole("Users ");
policy . RequireClaim(ClaimTypes.StateOrProvince , "DC");
}) ;
opts . AddPolicy ( " NotBob ", policy => {
policy .Requir eAuthenticatedUser() ;
policy.AddRequirements(new BlockUsersRequirement( " Bob " ));
}) ;
opts. AddPolicy ( "Au thorsAnd.Edi tors" , policy => {
policy.AddRequirements(new DocumentAuthorizationRequirement
AllowAuthors = true,
AllowEditors = true
}) ;
}) ;
}) ;
services . AddDbCo nt ext<App identityDbContext>(opt i ons =>
options . UseSqlServer(
Configuration[ "Data : SportStoreidentity : Connect i onString " ]} ) ;
services . Add identity<AppUser , Iden tit yRole>(opts => {
opts . User .RequireUniqueEmail = true;
opts .Password . Required Length = 6 ;
opts . Password . RequireNonAlphanumeric = fa l se ;
opts . Password .RequireLowercase = false ;
opts . Password . RequireUppercase = false ;
opts . Password . RequireDigit = false;
}) . AddEntityFrameworkStores<Appident i tyDbContext>() ;
servi ces .AddMvc ();

Финальный шаг связан с применением политики авторизации в методе действия


(ли стинг 30. 19).

Листинг 30.19. Применение политики авторизации в файле Documen tCon troller. cs

using Microsoft . AspNetCore . Authorization ;


using Microsoft . AspNetCore.Mvc ;
using Sys tem.Linq ;
using Users.Models ;
using System.Threading.Tasks;
namespace Users.Controllers {
[Authorize ]
puЫic class DocumentContro ll er : Controller {
private ProtectedDocument[] docs = new ProtectedDocument[]
new ProtectedDocument { Ti tle " QЗ Budget ", Author = "Al ice ",
Editor = " Joe " } ,
new ProtectedDocument Title "P roject Plan ", Author = " ВоЬ ",
Editor = "Alice " }
};
private IAuthorizationService authService;
Глава 30 . Расширенные средства ASP.NET Саге ldentity 951
puЫic DocumentController(IAuthorizationService auth) {
authService = auth;

puЬlic ViewResult I ndex() => Vi ew(doc s ) ;


puЫic async Task<IActionResult> Edit{string title) {
Protecte dDocument doc = docs.FirstOrDefault(d => d.Title title);
bool authorized = await authService.AuthorizeAsync(User,
doc, "AuthorsAndEdi tors") ;
if (authorized) {
return View { 11 Index 11 , doc);
else {
return new ChallengeResult{);

Конструктор контроллера определяет аргумент IAut h or iz atio n Se r vice , кото­


рый предоставляет методы. предназначенные для оценки политик авторизации, и
распознается с использованием внедрения зависимостей . В методе Ed i t () вызы­
вается метод AuthorizeA s y n c () с передачей ему текущего поль з ователя. объек­
та ProtectedDocume nt и имени политики . подлежащей применению. Если метод
AuthorizeAsyn c () в результате возвращает t ru e , то авторизация одобрена и вы­
зывается м етод Vi ew () . Если результатом оказьmается false , тогда есть проблема с
авторизацией. и возвращается объект Chal l e ngeResu l t , описанный в главе 17, ко­
торый сообщает инфраструктуре MVC о том, что произошел отказ в автори зации.
Чтобы посмотреть на эффект. запустите приложение и запрашивайте URL вида
/Docume n t , аутентифицируясь от имени разных пользователей . Если, например ,
вы аутентифицируетесь как Joe , то получите возможность редактировать документ

QЗ Budget , но не документ Pro j ec t Pl an.

Использование сторо н ней аутентификации


Одно из преимуществ системы на основе заявок, такой как ASP.NEТ Саге Identity. свя­
з ано с тем, что заявки могут поступать из внешней системы, причем даже те, которые
идентифицируют пользователя для приложения. Это означает, что другие системы могут
аутентифицировать пользователя от имени приложения, и система ASP.NEТ Саге Identity
построена с учетом такой идеи, чтобы упростить добавление поддержки для аутентифика­
ции польз ователей через третьи стороны, вюпочая Microsoft, Google, Facebook и 1Witter.
Прим енение сторонней аутентификации обеспечивает важные преимущества:
многие пользователи уже имеют учетные записи , пользователи получают возмож­

ность выбирать исполь з ование двухфакторной аутентификации и отсутствует пот­


ребность в управлении пользовательскими учетными данными внутри приложения.
В последующих разделах будет показано, как настроить и задействовать стороннюю
аутентификацию для пользователей Google.

Р егистрация приложения в Google


Службы сторонней аутентификации обычно требуют рег истрации приложений перед
те м. как они смогут аутентифицировать пользователей . Результатом процесса регист­
рации будут учетные данные, которые включаются в запрос аутентификации к сторон­
н ей служб е. Процесс р егистрации Google инициируется по адр е су h t tp : / / console .
952 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Соге MVC

de ve l ope r s. g oog l e . сот и выполняется согласно инструкциям , доступным по ссылке


h t tp : //deve l opers . google . com /identity/sign- in/weЬ/devconsole - pro j ect.
Понадобится указать URL обратного вызова, который для ст андартной конфигурации
выглядит как / signin - google . В среде разработ1ш URL обратного вызова должен
быть установлен в http : / /localhos t: пopт/signin - google. Для прои зводств е нно­
го приложения н е обходимо создать URL. который включает общедоступно е имя хоста
и порт.

После прохождения процесса р егистрации будет получен идентификатор кли ента ,


идентифицирующий приложение в Goog\e, и секретный код клиента, который при­
меняется в качестве меры безопасности, не позволяя другим прилож ениям выдав ать
себя за ваше приложение .

На заметку! Вы должны зарегистрировать собственное приложение и использовать иденти­


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

информацию за пределами конфигурационного файла, но ради простоты предполагается,


что конфигурационные файлы не доступны публично и могут безопасно содержать учет­
ные данные аутентификации Google.

Включение аутентификации Google


Система ASP.NET Core Identity располагает встроенно й подде р жкой для аутенти­
фикации пользователей с помощью их учетных записей Microsoft, Google, Facebook
и Тwitter, а также общую поддержку для любой службы аутентификации. которая р а­
бота ет по протоколу OAuth. Добавьте в файл pro j ect . j son пак ет NuGet, который
включает специфичные для Google дополнения к системе ASP.NET Core Ideпtity (лис­
тинг 30.20) .

Листинг 30. 20. Добавление пакета NuGet в файле proj ect. j son

"depe ndenci e s ": {


"Microsoft . NE TCo r e . App":
"version ": " 1 . 0 . 0 ",
"type ": "platform "
},
"Mic r osoft . AspNetCore .Diagnostics": "1. 0 . 0 ",
"Mic r osoft . AspNetCore . Server . IISintegra t ion ": "1. 0 . О ",
"Microsoft . AspNetCore . Server . Kestrel ": " 1 . 0 . 0",
"Mic r osof t .Extensions . Logging . Console ": " 1 . 0 . 0",
"Microsoft. AspNetCore . Mvc " : "1. О . О ",
"Microsoft . AspNetCore . St a t ic Files ": " 1.0 .0",
"Microsoft . AspNetCore . Razor . Tools ":
"version ": " 1 . О . 0- preview2 - final ",
"type ": "bui l d "
}'
"Mic r osoft . Exte nsions . Configura t ion": "1 .0 . 0 ",
"Microsoft . Extensions . Configurat i on. Json ": " 1 . 0 . 0",
"Microsoft . AspNetCore . Identity . Entity FrameworkCore ": " 1.0 . 0 ",
Глава 30 . Расширенные средства ASP.NET Core ldentity 953
" Microsoft . EntityFrameworkCore . Sql Se rver ": " 1 . 0 . 0 ",
"Microsoft . EntityFrameworkCore . Tools ": "1.0. 0 - preview2 -f ina l",
"Microsoft.AspNetCore.Authentication.Google": "1.0.0"
},

Для каждой службы, поддерживаемой ASP.NET Core Identity, предусмотрен свой па­
кет NuGet, как показано в табл. 30.10.

Таблица 30.1 О. Пакеты NuGet для аутентификации

Имя Описание

Microsoft . AspNetCore .Authentication.Googl e Аутентифицирует пользо­


вателей с применением
учетных записей Google
Microsoft .AspNetCore .Authent i cati on .Facebook Аутентифицирует пользо­
вателей с использованием
учетных записей Facebook
Microsoft .AspNetCore . Authentication .Twi tter Аутентифицирует пользо­
вателей с применением
учетных записей Twitter
Mi crosoft . AspNetCore . Authentication . Microso f tAccount Аутентифицирует пользо­
вателей с использованием
учетных записей Microsoft
Microsoft . AspNetCore .Authentication . OAuth Аутентифицирует пользо­
вателей с помощью любой
службы OAuth 2.0

Каждый пакет имеет метод для добавления промежуточного ПО в конвейер обра­


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

Листинг 30.21. Включение аутентификации Google в файле Startup. cs

puЫic void Configure(IApplicationBuilder арр) {


app.UseStatusCodePages() ;
app.UseDeveloperExceptionPage() ;
app.UseStaticFiles() ;
app.Useidentity() ;
app.UseGoogleAuthentication(new GoogleOptions
Clientid = "<enter client id here>",
ClientSecret = "<enter client secret here>"
}) ;
app.UseClaimsTransformation(LocationC l a imsP rovider . AddC l aims) ;
app . UseMvcWithDefaultRoute();
AppidentityDbContext . CreateAdminAccount(app . ApplicationServices ,
Configuration) . Wait();
954 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Саге MVC

Метод UseGoo gl eAuthenticat i on () настраивает промежуточное ПО, тр е буемое


для аутентификации пользов ателей с помощью Google, и указ ывает идентификатор и

секр етный код клиента, которые были созданы в процессе регистрации.


При аутентификации пользователя с участием третьей стороны можно выбрать
создание пользоват еля в баз е данных I de ntit y, которая впоследствии будет при­
меняться для управления ролями и заявками как в случае обычных пользов ателей .
В главе 28 был добавлен класс проверки пользователей , пр едотвращающий создание
пользователя, если его адрес эл е 1пронной почты не относится к домену example . com.
По скольку планируется иметь дело с пользователями из любого дом е на, пров ерку ад­
реса электронной почты для этого примера необходимо отключить (листинг 30.22).

Листинг 30.22. Отключение проверки домена в файле CustornUserValidator. cs

u s ing System . Collect i ons . Generic ;


u sing Sy s t em .L inq ;
u si ng System.Th readi n g .T asks ;
using Mi crosoft . AspNetCore .I dentity ;
using Users . Models ;
namespace Use r s .I n f rastructure
puЬlic class CustomUserVal i dator : UserValidator<AppUser> {
p u Ьl i c ove rri de async Task<Identi t yRes u lt> Va l idateAs ync(
UserManager<AppUser> manager ,
AppUser user) {
I dentity Resu l t r esult = await ba s e . Va li da teAsync(ma n ager , user) ;
Li s t<IdentityEr r or> e r ro r s = r esult.Succeeded ?
new List< I dentityError>() : result .E rrors . ToList() ;
// if ( !user. Email. ToLower () . EndsWith ("@example.com"))
// errors. Add (new Iden t i tyError {
11 Code = "EmailDomainError",
11 Description = "Only exarnple. com email addresses are allowed"
11 }) ;
11
ret u rn errors . Count ==О ? Identi t yRe s ult . Success
: I d ent it yRe s ul t. Fai led (erro r s . ToA r ray() ) ;

Далее нуж но добавить в файл пр едставл е ния View s /Account/Login . cshtml


кнопку. которая по зволит пользоват елям входить ч е р е з Google (листинг 30.23). Для
кнопок в Google предусмотрены изображения, чтобы обеспечить согласованный вне ­
шний вид с остальными пр ил ожениями, поддержив ающими учетны е з аписи Google,
но ради простоты достаточно создать стандартную кнопку.

Листинг 30.23. Добавление кнопки в файле Login. cshtml из папки Views/Account

@model LoginModel
<d i v c l ass= " bg -p rimary pane l- body " >< h 4> Log In </h 4 ></div>
<div cla s s= " text-danger " asp - validation -s ummary= " All " ></div>
Глав а 30. Расширенные средства ASP.NET Соге ldentity 955
<form asp - action= " Login " method= " post " >
<input type= " hidden " name= " returnUrl " value= " @ViewBag . return Ur l" />
<div class= " form - group " >
<label asp - for= " Email " ></label>
<input asp - for= " Email " class= " form - control " />
</div>
<div class= " fo r m- group " >
<label asp - for= " Password " ></label>
<input asp - for= " Password " class= " form - control " />
</div>
<button class= " btn btn - pr i mary " t ype= " submit " >Log In </ b u t ton>
< а class="Ьtn Ьtn- i nfo" asp-action="GoogleLogin"
asp-route-returnUr1="@ViewBag.returnUr1">Log In With Goog1e
</а>
</form>

Н о в ая кнопк а нацел ена на действие GoogleLog i n контролл ера Account . В лис ­


ти нге 30.24 приведен код метода действия GoogleLogin () и другие и зменения, вне­
с е нны е в к о нтро лл ер.

Л и ст и нг 30.24. Добавление поддержки аутентификации Google


в файле AccountController. cs

using System . Threading . Tasks ;


using Microsoft . AspNetCore . Authorization ;
using Microsoft.AspNetCore . Mvc ;
using Users.Models ;
using Microsoft . AspNetCore . Iden ti ty ;
using System . Security.C1aims;
using Microsoft.AspNetCore .Http.Authentication;
namespace Users.Controllers {
[Authorize]
puЫic class AccountController : Controller {
private UserManager<AppUser> userManager ;
private SigninManager<AppUser> s i gninManager ;
/ / ... для краткости другие методы не показаны . ..
[AllowAnonymous]
puЬ1ic IActionResu1t Goog1eLogin(string returnUr1)
string r edirectUrl = Url. Action ( "GoogleResponse", "Account",
new { ReturnUrl = r e turnUrl }) ;
AuthenticationProperties properties = signinМanager
.ConfiqureEx ternalAuthenticationProperties("Google", redirectUrl);
return new ChallengeResult("Google", properties);

[AllowAnonymous]
puЫic async Task<IActionResul t> GoogleResponse (string returnUrl = "/")
{
ExternalLogininfo info = await signinМanager.GetExternalLogininfoAsync();
if (info == nu11) {
r e tur n RedirectToAction(nameof(Login));

var resu1t = a wait signinМanager.Externa1LoginSigninAsync(


956 Часть 11. Подробные сведения об инфраструктуре ASP.N ET Core MVC

info.LoginProvider, info.ProviderKey, false);


if (result.Succeeded) {
return Redirect(returnUrl);
} else {
AppUser user = new AppUser {
Email = info.Principal.FindFirst(ClaimTypes.Email) .Value,
UserName =
info.Principal.FindFirst(ClaimTypes.Email) .Value
};
IdentityResult identResult = await userManager.CreateAsync(user);
if (identResult .Succeeded) {
identResult = await userManager.AddLoginAsync(user, info);
if (identResult .Succeeded) {
await signinМanager.SigninAsync(user, false);
return Redirect(returnUrl);

return AccessDenied();

Метод Goog leL ogin () создает экземпляр класса AuthenticationProperties


и устанавливает свойство Redi rec t Ur i в URL, который нацелен на действие
Goog leResponse того же контроллера. Следующая часть кода заставляет систему ASP.NEТ
Core Identity реагировать на отсутствие авторизации перенаправлением пользователя на
страницу аутентификации Google вместо страницы, определенной в приложении:

return new ChallengeResu lt ( "Google", properties) ;

Это означает, что когда пользователь щелкает на кнопке Log ln With Google (Во йти
с помощью Google), браузер перенаправляется на страницу аутентификации Google и
затем после успешного прохождения аутентификации направляется обратно на метод
действия GoogleResponse ().
Внутри метода GoogleResponse () детали внешнего входа получаются вызовом
метода GetExternalLogininfoAsync () объекта SigninManager:

ExternalLogininfo info = await signinManage r .


GetExternalLogininfoAsync () ;

Класс ExternalLogininfo определяет свойство ExternalPrincipal, возвраща­


ющее объект ClairnsPrincipal, который содержит заявки, предоставленные пользо­
вателю механизмом Google. Для входа пользователя в приложение используется метод
ExternalLoginSigninAsync() :
var result = await sign!nManager.ExternalLoginSigninAsync(
info . LoginProvider , info . ProviderKey, false);

Если вход произвести не удалось, то причина в том. что в базе данных отсутствует
запись , которая представляла бы пользователя Google. Для решения проблемы с при­
менением следующих двух операторов создается новый пользователь и ассоциируется
с учетными данными Google:
Глава 30. Расширенные средства ASP.NET Соге ldentity 957

IdentityResult i dentResu l t = await use r Manager .CreateAsync (u s er) ;


identResult = await userMa nager .AddLoginAsync (user , info) ;

На заметку! При создании польз ователя ldentity ис пользуется заявка адреса эл ектронно й поч­
ты, предоставленная Google для свойств Email и Us e r Name объе кта App Us er , та к что
никаких конфликтов имен с пользователями , существующими в базе данных, не возникает.

Чтобы протестировать аутентификацию, запустите приложение, щелкните на


кнопке Log lп With Google и введите учетные данные для допустимой учетной запи­
си Google. Когда процесс аутентификации завершится, браузер будет перенаправлен
обратно на приложение. Перейдя на URL вида /Claims, вы заметите, что заявки для
пользователя , показьmающие выполнение аутентификации системой Google, были до­
бавл е ны к удостоверению пользователя (рис. 30. 7).

D IJ~tfS - ·. "· ·.· .· • - о х

с rС!?..~~~~h~т~~5!·;1 1~:~~~ -_ --=-~--~-- --=~~~-~~=:.~-: ~~~- ~-

Subject lssuer Ту ре Value


t es lu se r ~ gmail.com LOCAL AUTHORIТY SecurityStamp 6Ь2а4092·Ы2е·46f6 ·ас Ь 1-5d01 aa5d8aeb

Adarn Freeman Google Email 1esteruser6 gmail.com

Adam Freeman Google Give11Name Adam


l es t нsor@g rn ai l . com LOCAL AUTHORITY Name t estus er~g m ail.com

Ado.m Freeman Google Nnme Adain Freeman

1 es111 se r ~ gmail.com LOCAL AUTHORIТY Nameldenlifier 0689е2сЗ-4с84·4dЬ2·91 cd·8141494a6d96

Adam Freeman Google Name\dentifier 101888805463382593408


testusor@ gma.il.com RemoteClaJms PostalCode NY 10036
tesiusor6gmall.com RemoteClaims StaleOrProvlnce NY
Adai11 Fteemд n Google Surnaine Free1na11

- - - - - ----
-~ - - J'
Рис . 30 . 7. Пр име р исп ользов а ния сторонне й аутентификации

Резюм е
В настоящей главе были описаны некоторые расширенные средства, поддержи­
ваемые системой ASP.NET Core Identlty. Здесь демонстрировалось применение специ­
альных свойств пользователя и обеспечение их поддержки за счет обновления схемы
базы данных с использованием миграций базы данных. Далее в главе объяснялась
работа заявок и их применение для создания более гибких способов авторизации
пользователей посредством политик. Также было показано , как испол ьзовать поли­
тики для контроля доступа к индивидуальным ресурсам, управляемым приложением .

В завершение главы рассматривалась задача аутентификации пользователей через


Google, которая построена на идее, л ежашей в основе прим е нения заявок. В следую­
щей главе вы ознакомитесь с тем, как в действительности реализованы самые важ­
ные соглашения, используемые в приложениях МVС , и каким образом их можно на ­
страивать в собственных приложениях .
ГЛАВА 31
Соглашения по модели
v v
и ограничения деиствии

н а протяжении всей книги неоднократно подчеркивалось, что никакой магии с


разработкой приложений МVС не связано, и беглый взгляд "за кулисы" позво­
ляет легко выявить, каким образом все части подгоняются друг к другу для доставки
функциональности , которая была описана в предшествующих главах.
В последней главе книги рассматриваются два полезных средства, позволяющие
настраивать способ работы приложения МVС. Соглашения по модели дают возмож­
ность заменять соглашения, используемые при создании контроллеров и действий, с
переопределением тех, которые применяются по умолчанию. Ограничения действий
позволяют указывать, для какого вида запросов может использоваться то или иное

действие. Они предоставляют инфраструктуре MVC инструкции относительно выбора


действия, которое будет обрабатывать запрос.
Если хотите, то можете не читать эту главу (из-за того, что местами материал ха­
рактеризуется довольно высокой сложностью}, тем не менее, имейте ее в виду, ког­
да приложение поведет себя не так, как бьmо задумано. Описанные в данной главе
средства не придется задействовать особенно часто (или даже вообще никогда}, но
чем больше вы узнаете о работе инфраструктуры МVС, тем лучше будете подготовле­
ны к решению возникающих проблем. В табл. 31.1 приведена сводка для настоящей
главы.

Таблица 31 . 1. Сводка по главе

Задача Решение Листинг

Настройка модели приложения Используйте один из встроенных ат­ 31 .1-31.15


рибутов или создайте специальное
соглашение по модели

Применение настройки повсюду в Определите глобальное соглашение 31.16, 31.17


приложении по модели

Проведение различия между двумя Используйте ограничения действий 31.18-31 .26


методами действий, которые могли
бы обрабатывать запрос
Глава 31 . Соглашения по модели и ограничения действи й 959

Подготовка проект а для пр и м е ра


Создайт е новый проект типа Empty (Пустой) по имени Con ven t ionsAndCons t r а i n t s
с ис пользованием шаблон а ASP.NET Core Web Application (.NET Core) (Веб-приложение
ASP.NET Core (.NET Core)). Добавьте требуемые пакеты NuGet в раздел dep endencie s
файла proj ect . j son и настройте инструментарий Razor в разделе tool s, как по­
казан о в л и стинг е 31 . 1. Разделы, которые не нужны для данной главы, понадобится
удалить .

Листинг 31 .1. Добавление пакетов в файле proj ect. j son

"dependencies ": {
"Microsoft .NETCo r e . App ":
"versio n": " 1 . 0 . 0 ",
" type " : "platfo r m"
}'
"Microsoft . AspNetCore . Diagnostics ": "1. 0 . 0 ",
"Microsoft . AspNe tCo r e . Server . IISintegration ": " 1 . 0 . 0 ",
"Microsoft . AspNetCore . Serve r. Kest re l ": " 1 . 0 . 0 ",
"Microsoft . Extensions . Logging . Con s ole ": " 1 . 0 . О ",
11
Мicrosoft. AspNetCore. Mvc 11 : 11 1 . О. О 11 ,
11
Microsoft.AspNetCore.StaticFiles 11 : 11 1.0.0 11 ,
"Microsoft . AspNetCore.Razor.Tools": {
11
version": "1.0.0-preview2-final 11 ,
"type": "build"

}'
"tools ":
"Microsoft.AspNetCore . Razor.Tools": 11 1.0.0-preview2-final 11 ,

"Microsoft . AspNetCore . Server . IIS in tegration . Tools ":


" 1 . 0 . 0- preview2 - final "
}'
" frameworks ": {
" netcoreapp l. 0":
" imports ": [ "dotnetS . 6", " portaЫe -n et45+wi n 8 "]

}'
"buildOptions ":
"emitEntryPoin t": true , " p r ese r v e Comp i lationContext ": t ru e
}'
" runtimeOpt i ons ": {
"configPrope rtie s ": { " System . GC. Server ": true }

В л истинге 31.2 приведен код класса Sta rt up, который конфигурирует средства,
пр едо ставляемые пакетами NuGet.
960 Часть 11. Подробные сведения об инфрастру ктуре ASP.NET Core MVC

Листинг 31 .2. Содержимое файла Startup. cs


usi ng Microsoft . AspNetCore . Builde r ;
using Microsoft . Extensions . Dependencyinjection ;
names pace Conventio nsAndCo nstraint s
puЬli c c l ass Startup {
puЫic void ConfigureServices(IServi ceCol l ect i on s e rvices) {
services.AddМvc();

puЫic void Configure(IApp l ica t io nBui l de r арр) {


app.UseStatusCodePages();
app.UseDeveloperExceptionPage();
app . UseStaticFiles();
app.UseMvcWithDefaultRoute();

Создани е модели представл ен и я , контроллера и пр едста вле ния


Во многих примерах гл ав ы полезно знать , какой м етод и спол ьзовал ся для обр а ­
ботки запроса. С этой целью со здайте папку Mode l s и добавьте в нее файл кл асс а по
имени Resu l t . cs с определ ением, показанным в листинге 31.3. Данный кл а сс по з ­
волит контрол е рам в настоящей глав е пер едавать пр едста вл е нию информацию о то м ,
как был обработан запрос.

Листинг 31 .З. Содержимое файла Resul t. cs из папки Models

usi ng System . Collections . Gener i c ;


names pace Conve nt i onsAndC on st r ai nt s. Mode ls
puЫic c l ass Resu l t {
puЫic string Contro l ler { get ; set ;
p u Ыic s t ring Ac t ion { get ; set ; }

В этой глав е требуется еди нственный контроллер и представление. Со здайте пап­


ку Controllers и добавьте в н ее файл класса по имени HomeCon troll e r . cs с опр е ­
деле нием из листинга 31.4.

Листинг 31 .4. Содержимое файла HomeController. cs из папки Controllers


using ConventionsAndConstra in ts . Models ;
usi ng Microsoft . As pNetCore . Mvc ;
namespace Convent i onsAndConstra i nt s . Controllers
puЫic c l ass HomeCon tr o ll er : Cont rol l er {
puЫ i c IActionResu l t I ndex() => Vi ew( "Resu lt", new Result {
Cont r o l ler = nameof(HomeCont r ol l e r ) ,
Actio n = nameof( I ndex)
}) ;
Глава 31. Соглашения по модели и ограничения действий 961
puЫic IActionResult List() => View( "Re sult", new Result {
Controller = nameof(HomeContro ller),
Action = nameof(Lis t)
}) ;

Оба метода действий в контроллере Home визуализируют представление Result.


Создайте папку Views/Home и поместите в нее файл Resul t . cs h tml с разметкой,
приведенной в листинге 31.5.

Листинг 31 .5. Содержимое файла Resul t. cshtml из папки Views/Home

@model Resu l t
@{ Layout = null ;
<!DOCTYPE html>
<html>
<head>
<meta name="viewport " content="width=device-width" />
<l ink asp -href - include= " liЬ/boo ts tr ap /dis t /css/* . min . css " rel= "stylesheet " />
<title>Result</title>
</head>
<body c l ass= "pane l- body " >
<tаЫе c la ss= "t aЫe taЫe-condensed t a Ы e -b ordered " >
<tr><th>Controller : </th>< td>@Model .Control l er</ t d></tr>
<tr><th>Action : </t h><td>@Model . Action</td></tr>
</tаЫе>
</body>
</html>

При стилизации НТМL-элементов представление полагается на СSS-пакет


Bootstrap. Создайте в корневой папке проекта файл bower . j son с использованием
шаблона элемента Bower Configuration File (Файл конфигурации Bower) и добавьте па­
кет Bootstrap в раздел dependencies (листинг 31.6).

Листинг 31 .6. Добавление пакета Bootstrap в файле bower. j son

"name ": "asp . net",


" private ": true ,
"dependencies ": {
"bootstrap": "3.3.6"

Финальный подготовительный шаг связан с созданием внутри папки Views файла


_ Viewimports. cshtml . в котором настраиваются встроенные дес1<рипторные вспо­

могательные юшссы для применения в представлениях, а также импортируется про­

странство имен модели (листинг 31. 7).


962 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Соге MVC

Листинг 31. 7. Содержимое файла_Viewimports. cshtml из папки Views


@u s ing Co nven t i onsAndConstr a int s . Mode l s
@addTa g He lper * , Mi c r oso f t . As pNetCo r e . Mvc . TagHelpers

Запустив приложение, вы увидите вывод, показанный на рис. 31.1.

Controller: HomeController

Action: lndex

Рис. 31 .1. Запуск примера приложения

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


и соглашений по модели
В MVC поддерживается соглашение по конфигурации, благодаря чему можно
просто создать класс с именем, заканчивающимся на Con troller, и приступить к
определению методов действий. Во время выполнения инфраструктура МVС приме­
няет процесс обнаружения для нахождения всех контроллеров и действий в прило ­
жении и инспектирует их, чтобы выяснить, используют ли они средства , подобные
фильтрам.
Итоговым результатом процесса обнаружения является модель приложения, состо­
ящая из объектов, которые описывают каждый найденный класс контроллера, метод
действия и параметр. Соглашения, на которые опирается MVC, применяются к моде­
ли приложения по мере ее построения . Например, когда обнаруживается класс конт­
роллера, имя этого класса используется в качестве основы для контроллера, который
представляет его в модели ; другими словами, класс HomeCon tro l le r применяется
для создания контролл е ра Н оте . Когда система маршрутизации идентифицирует за­
прос, подлежащий обработке контроллером Home, именно модель приложения предо­
ставляет отображение на ~тасс HomeCon tr ol l er.
Модель приложения можно настраивать с использованием соглашений по модели .
Соглашения по модели - это классы, которые инспектируют содержимое модели при­
ложения и вносят корректировки, такие как синтез новых действий либо изменение
способа применения классов для создания контроллеров . В последующих разделах
объясняется, как структурирована модель приложения, рассматриваются различные
типы соглашений по модели и демонстрируются приемы использования этих согла­
шений. В табл. 31 .2 приведена сводка, позволяющая поместить модель приложения и
соглашения по модели в контекст.
Глава 31. Соглашения по модели и ограничения действий 963
Таблица 31 .2. Помещение модели приложения и соглашений по модели в контекст

В опрос Ответ

Что это такое? Модель приложения представляет собой полное описание


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

Чем они полезны? Соглашения по модели удобны, потому что они делают
возможным внесение изменений в способ отображения
классов и методов на контроллеры и действия. Могут вы­
полняться и другие настройки, такие как ограничение НТТР­
методов, которые принимаются действиями, или примене­
ние ограничений действий (рассматриваются далее в главе)

Как они используются? Соглашения по модели определяются с использованием


набора интерфейсов, описанных в последующих разделах,
и применяются посредством атрибутом или конфигуриру­
ются в классе Startup

Существуют ли какие­ Со способом применения соглашений по модели связаны


то с к рытые ловуш к и или определенные странности, которые обсуждаются в после­
ограничения? дующих разделах

Существуют ли Нет, хотя можно ввести собственные компоненты для со­


альтернативы? здания специальной модели приложения , если стандарт­
ная модель не соответствует имеющимся потребностям

Изменились ли они по сравне­ Модель приложения - это новое добавление к инфраструк­


нию с версией MVC 5? туре MVC

Модель приложения
Во время процесса обнаружения инфраструктура МVС создает экземпляр класса
ApplicationModel и заполняет его сведениями о найденных контроллерах и дейс­
твиях. Посл е завершения процесса обнаружения применяются соглашения по модели,
чтобы внести все специальные изменения, которые были указаны. Отправной точ1<ой
для поним ания модели приложения является исследование свойств, определяемых
клaccoм Microsoft . AspNetCore . Mvc . ApplicationModels . ApplicationModel,
которые описаны в табл. 31.3.

На заметку! Может показаться, что мы начинаем с очень простого места, особенно если
вы стремитесь немедленно погрузиться в детали, но полезно оценить, насколько полно

классы, рассматриваемые в этом разделе, описывают основные части приложения MVC.


Понимание работы модели приложения поможет понять внутреннее функционирование
более сложных средств, что способствует лучшей подготовке к диагностированию про­
блем, когда проекты выдают непредвиденные результаты .
964 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC

Таблица 31 .3. Избранные свойства класса ApplicationМodel

Имя Описание

Contro llers Это свойство возвращает объект реализации IList<ControllerModel> ,


который содержит все контроллеры в приложении

Filters Это свойство возвращает объект реализации I List< I Fi 1 terMetada ta>,


который содержит глобальные фильтры в приложении

В настоящей главе интерес представляет свойство Cont r o ll ers , возвращающее


список, который содержит объекты Con t r ol l e r Model для каждого контроллера, об­
наруженного в приложении . Наиболее важные свойства класса ControllerModel
описаны в табл . 31.4.

Таблица 31 .4. Избранные свойства класса ControllerModel


Имя Описание

ControllerName Это свойство типа string определяет имя контроллера.


Данное имя будет использоваться при сопоставлении
с сегментом маршрутизации controller
ControllerType Это свойство типа Typeinfo определяет тип класса
контроллера

Control l erPr opertie s Это свойство возвращает объект реализации


IL ist<Prope r tyMod e l >, который описывает все свойства ,
определяемые контроллером (табл . 31.5)
Action s Это свойство возвращает объект реализации
ILi st<Act i onModel >, который описывает все действия , оп­
ределяемые контроллером (табл . 31 .6)
Fi lters Это свойство возвращает объект реализации
IList<IFilterMetadata>, который содержит фильтры,
применяемые ко всем действиям в контроллере

RouteCon str a i nts Это свойство возвращает объект реализации


ILi s t <I Rout eCon s traintPr ovider> , который использу­
ется для ограничения нацеливания маршрутов на действия,
определяемые контроллером

Se l ector s Это свойство возвращает объе кт реализации


I List<Sel ect o r Model>, который содержит детали ограни­
чений действий (описанные в разделе " Использование ограни­
чений действий " далее в главе) и информацию маршрутизации ,
применяемую к контроллеру посредством атрибутов , как было
показано в главе 15

Как видите, классы модели приложения охватьmают часть основной фун1{цион аль­
ности MVC. Свойство ControllerName, наприм е р, используется для установки им е ­
ни , которое будет задействовано системой маршрутизации при сопоставлении URL,
а свойство ControllerType применяется для установки класса контроллера , к кото­
рому это имя относится.
Глава 31 . Соглашения по модели и ограничения действий 965
Свойство ControllerProperties возвращает список объектов PropertyModel,
каждый из которых описывает свойство, определяемое контроллером. Самые важные
свойства класса PropertyModel перечислены в табл. 31.5.

Таблица 31 .5. Избранные свойства класса PropertyModel

Имя Описание

PropertyNarne Это свойство типа string возвращает имя свойства

Attributes Это свойство возвращает список атрибутов, которые были применены


к свойству

Свойство Actions возвращает список объектов ActionModel, каждое из которых


описывает метод действия, определяемый одиночным классом контроллера. Наиболее
важные свойства класса ActionModel перечислены в табл. 31 .6.

Таблица 31 .6. Избранные свойства класса ActionМodel

Имя Описание

ActionName Это свойство типа string определяет имя действия, которое


будет использоваться при сопоставлении с сегментом маршрути­
зации action
ActionMethod Это свойство типа Methodinfo применяется для указания мето­
да, который реализует действие

Controller Это свойство возвращает объект ControllerModel , описываю­


щий контроллер, которому принадлежит действие

Filters Это свойство возвращает объект реализации


IList<IFil terMetadata>, содержащий фильтры, которые при­
меняются к действию

Parameters Это свойство возвращает объект реализации


IList<PropertyModel>, который содержит описания парамет­
ров, требуемых методом действия

RouteConstraints Это свойство возвращает объект реализации


IList<IRouteConstraintProvider>, который используется
для ограничения нацеливания маршрутов на действие

Selectors Это свойство возвращает объект реализации


IList<SelectorModel>, который содержит детали ограничений
действий (описанные в разделе "Использование ограничений дейс­
твий" далее в главе) и информацию маршрутизации, применяемую
к контроллеру посредством атрибутов , как объяснялось в главе 15

Последний уровень детализации доступен через свойство Parameters, возвраща­

ющее список объектов ParameterModel, которые описывают каждый параметр, оп­


р едел я емый методом действия. Наиболее важные свойства класса ParameterModel
п еречислены в табл. 31.7.
966 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

Таблица 31.7. Избранные свойства класса ParameterModel


Имя Описание

ParameterName Это свойство типа stri ng используется для указания имени


параметра

Parameter in fo Это свойство типа Proper tyinfo применяется для указания


параметра

Bi nding i n f o Это свойство типа Bin d i n g i n f o используется для конфигурирова­


ния процесса привязки моделей, как было показано в главе 27

Типы App li ca ti onMo d el , Co ntrol lerMode l, Proper t yMo d el , Ac t ionModel и


Par a me t erModel применяются для описания любого аспекта классов I{ОНтроллеров
в прилож ении, а тю<же их методов, свойств и параметров , опр еде ля емых к аждым

методом .

Настройка модели приложения


Инфраструктур а MVC имеет несколько встроенных соглаш е ний, которые она
применя е т в х оде з а пол нения Appli c ati o nMode l объект ами Control l erModel,
PropertyMode l , ActionMode l и Pa r ame t e r Model с целью описания обнаруживае­
мых контроллеров .

Одни соглашения являются явными, такие как удаление строки Controller и з


име ни 1macca контроллера и затем использование р езультата для уст ановки свойства
Contro l le r Name объекта Con t r o ll e rModel. Это соглаш ение по зволя ет опр еделять
класс вроде HomeController, но нацеливаться на него с помощью сегм ента URL, ко­
торый содержит Home.
Другие соглашения являются неявными, например, прим енени е каждого класса
для создания одного контроллера и каждого метода для создания одного действия.
Большинство разработчиков приложений MVC воспринимают эти с огл аш е ния как не ­
что само собой разумеющееся и совершенно не задумываются о них. однако каждый
аспект модели приложения может быть изменен.
В пр едшествующих главах были описаны атрибуты. которы е и з м е няют способ
работы инфраструктуры MVC. и в действительности это соглаш е ния по модели.
Упомянутые атрибуты перечислены в табл. 31.8.

Таблица 31 .8. Базовые атрибуты, которые изменяют стандартные соглашения приложения

Имя Описание

Act i onName Этот атрибут позволяет явно указывать значение для свойства
Act i onName объекта Act i onModel вместо того , чтобы выводить
его из имени метода

Non Control l er Этот атрибут предотвращает применение класса для создания


объекта Con trollerMode l
NonAction Этот атрибут предотвращает использование метода для создания
объекта Act i onМode l

В листинге 31 .8 атрибут Actio nName применяется для измен е ния им е ни дейс­


твия, которое создано для представления метода Li s t () в класс е HomeController.
Глава 31 . Соглашения по модели и ограничения действий 967
Листинг 31 .8. Настройка модели приложения в файле HomeController. cs

using Conventions AndConstra i nts.Models;


using Mi crosoft . AspNe t Co re. Mvc ;
n ame sp ace Conve nt i o ns AndCons tra in t s.Cont r o l l ers
p uЫi c clas s Home Contr oller : Cont ro l l er {
puЫic I ActionRe sul t Index () => Vi ew ( " Resul t ", new Re s ul t {
Controller = nameof(HomeCo ntr ol l er) ,
Action = nameo f (Inde x )
}) ;

[ActionName ( "Details") ]
puЬl i cIActionResul t List () => View ( " Re sul t", new Resul t {
Controlle r = nameof(HomeCon t rol ler ) ,
Act i on = nameof( List)
}) ;

Здесь указано , что для создания действия должно использоваться имя Det ail s ,
заменяя собой стандартное имя List. Чтобы взглянуть на результат, запустите при­
ложение и запросите URL вида /Home/Detai l s. Как показано на рис . 31.2, запрос
обрабатывается методом Li s t () .

..::> С [] localhost:65325/Home/Details -
=
Controller: HomeController

Action: List

Рис. 31 .2. Настройка модели приложения

Р оль соглашений по модели


Атрибуты, описанные в табл. 31.8, дают возможность вносить базовые измене­
ния в модел ь приложения , но ограничены в своей области действия. Для более су­
щественных настроек требуются соглашения по модели (также известные как просто
соглашения) .
Атрибуты из табл. 31 .8 разрешают указывать изменения в объектах модели прило­
жения до их создания , например, переопределение имени, назначаемого действию. По
контрасту с этим создание соглаш е ния по модели позволяет изменять модель прило­

жения, модифицируя объекты модели после их создания, что открывает возможности


для применения бол е е обширных изменений . Доступны ч етыре вида соглашений по

модели, каждый из которых определяется своим интерфейсом (табл. 31.9) .


968 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Соге MVC

Таблица 31 .9. Интерфейсы соглашений по модели приложения

Имя Описание

IApplicationModelConvention Этот интерфейс используется для применения согла­


шения к объекту App l icationМode l

I Controll erModelConvention Этот интерфейс используется для применения соглаше­


ния к объектам ControllerModel в модели приложения

IActionModelConvention Этот интерфейс используется для применения согла­


шения к объектам ActionМode l в модели прило жения

IParameterModelConvention Этот интерфейс используется для применения соглаше­


ния к объектам ParameterMode l в модели приложения

Вс е ч етыр е и нте рф ейса работ ают аналогично : меняется только уровень, н а к ото­
ром они функционируют внутри модел и предста влен ия . Н апри м ер. вот опр еделе ние
интe pфeйc aIControllerModelConvention :

namespace Microsoft . AspNetCore . Mvc . ApplicationModels


puЫic interface I Controlle r Mode l Convention
void App l y(ControllerModel contro l ler) ;

С помощью вызов а метода App l y ( ) соглаше нию по модели предоставля ется во з ­


можно сть внести изменения в объект Cont r o ll erMode l , к к оторому оно был о при­
менено и который передается м етоду в качестве аргумента. Другие интерф е йсы
также определяют методы Арр l у () , причем каждый и з них получает объект моде ­
ли м одифицируе мого им тип а, так что интерфейс IActionModelConvention при ­
ни м ает объект
ActionModel, а инте рфейс I ParameterModelConvention - объ ект
ParameterMode l.

Создание соглашения по модели


Соглаш е ния по модели для контроллеров , действий и парам етров могут п рим е ­
няться как атрибуты, что обл е гчает установку обла сти де йствия вно симых и ми и з ­
мен е ний. Создайте папку Infrastr u c tu re и добавьте в нее файл класс а по им е ни
ActionNamePrefixAttribute . cs , содержимо е которого приведено в листинге 31.9.

Листинг 31 .9. Содержимое файла ActionNamePrefixAttribute. cs


из папки Infrastructure
using System ;
using Microsoft . Asp NetCore.Mvc . Appl i ca t ionMode l s ;
namespace ConventionsAndConstraints .I nfrastructure
[AttributeUsage (Attribute Targets . Me t hod , Al l owMultiple = false) ]
puЫic cl ass ActionName PrefixAttribute : Attribute , I ActionMode l Convention
private str i ng namePrefix ;
puЫicActionNamePrefixAttribute (str i ng prefix) {
namePrefix = prefix ;
Глава 31 . Соглашения по модели и ограничения действий 969
puЬlic void Apply(Acti onMode l act i on) {
ac t io n. Ac t ionName = name Prefix + a ction. Ac ti onName ;

Класс Ac tionNamePre fi xAt tr i but e унаследован от класса Attribu te и реали­


зует инт е рфейс I ActionModel Conven ti on. Его конструктор принимает строку, ис­
пользуемую в кач естве префикса, который применяется путем модификации свойства
ActionName объ екта Acti onModel . полученного методом App ly () .

Совет. Обратите внимание, что использование атрибута Act i onNamePrefix ограничива ­


ется так, что он может быть применен только к методам. Когда соглашения по модели
применяются как атрибуты, соглашения по контроллерам оказывают воздействие только
в случае их применения к классам, соглашения по действиям - только в случае приме­
нения к методам, а соглашения по параметрам - только в случае применения к парамет­

рам . Соглашение , примененное на неправильном уровне , будет просто проигнорировано


без возникновения ошибки. Во избежание путаницы используйте Att ribu t eUs age , что­
бы ограничить область действия создаваемых атрибутов .

В листинге 31. l О атрибут соглашения по модели применяется к одному из методов


действий контроллера Horne .

Листинг 31 .1 О. Применение соглашения по модели в файле HomeController. cs

using Conve ntionsAndCon st raints . Mode ls ;


using Microsof t. As pNe tCo re.Mvc ;
using ConventionsAndConstraints.Infrastructure;
namespace ConventionsAn dConstra i nts. Con trollers
puЫic class HomeCon t ro l ler : Control l er {
puЬl i c IAc t ionRes ul t I ndex () => Vi e w ( "Res ul t 11
, new Res ul t {
Cont r o l ler = nameo f (HomeCont r o ll e r ) ,
Action = nameof (I ndex)
}) ;
[ActionNamePrefix ( 11 Do 11 ) ]

puЫic I Act i onResu l t Li st () => Vi ew ( " Res ul t", ne w Res ul t {


Control l er = nameof (HomeContr oll e r ) ,
Action = name of(L i s t )
}) ;

Во время прохождения процесса обнаружения инфраструктура МVС создаст объ­


ект ActionModel, описывающий метод List () , выявит Ac t ionNameP r e fi x и вызо­
вет его метод Apply () . Чтобы посмотреть на эфф ект, запустите приложение и запро­
сите URL вида / Horne/ DoList, заменяющий URL, который был бы нацелен на метод
Li st () при стандартных соглашениях (рис. 31 .3).
970 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Саге MVC

Controller: HomeController

Action: List

Рис. 31 .З. Применение соглашения по модели

Использование соглашений, добавляющих


или удаляющих объекты модели
Со способом применения соглашений по модели связана одна индивидуальная
особенность, которая предотвращает добавление или удаление ими объектов модели .
Например, предположим, что нужно создать соглашение, которое определенные мето­
ды смогли бы достигать через два разных действия. Добавьте в папку Infrastructure
файл класса по имени AddActionAttribute . cs с содержимым из листинга 31.11.

Листинг 31, 11. Содержимое файла Ad.dActionAttribute. cs


из папки Infrastructure
using System;
using Microsoft . AspNetCore . Mvc.ApplicationModels ;
namespace ConventionsAndConstraints.Infrastructure
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
puЫic class AddActionAttribute : Attribute, IActionModelConvention
private string additionalName ;
puЫic AddActionAttribute(string name) {
additionalNarne = narne;

puЬlicvoid Apply(ActionModel action) {


action . Controller . Actions.Add(new ActionModel(action)
ActionNarne = additionalNarne
}) ;

Это соглашение по действиям использует конструктор ActionModel. который дуб­


лирует настройки существующего объекта и затем изменяет свойство ActionName
нового экземпляра. Новый объект ActionModel добавляется в коллекцию действий
контроллера за счет навигации с помощью свойства ActionModel. Controller . В
листинге 31. 12 демонстрируется применение данного соглашения по модели к конт­
роллеру Home.
Глава 31 . Соглашения по модели и ограничения действий 971
Листинг 31 .12. Применение соглашения по модели в файле HomeController. cs

using ConventionsAndConstra ints.Models ;


using Microsoft .AspNe t Core .Mvc;
using ConventionsAndConstraints .I nfrastructure ;
namespace ConventionsAndConstraints . Controllers
puЫic class HomeController : Controller {
puЬlic IActionResult Index() => View("Result", new Result {
Control l er = nameof(HomeController),
Action = nameof(Index)
}) ;
[AddAction("Details")]
puЬlic IActionResult List() => View( "Result ", new Result [
Controller = nameof(HomeController),
Action = nameof(List)
}) ;

После запуска приложения инфраструктура МVС начнет процесс обнаружения и


сообщит о следующей ошибке:

InvalidOperationException : Collection was modified;


enumeration operation may not execute .
InvalidOperationException : коллекция была модифицирована;
операция перечисления может не выполниться.

Соглашение по модели пытается изменить набор объектов действий модели по


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

Листинг З 1.13. Создание безопасного соглашения по модели в файле


Add.ActionAttribute.cs

using System ;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using System.Linq;
namespace ConventionsAndConstraints . Infrastructure
[AttributeUsage(AttributeTargets.Method, AllowMultiple true)]
puЬlic class AddActionAttribute : Attribute {
puЫic string AdditionalName { get; }
puЫic AddActionAttribute(string name)
AdditionalName = name;

[AttributeUsage(AttributeTargets . Class, AllowMultiple false)]


puЫic class AdditionalActionsAttribute : Attribute,
IControllerModelConvention {
puЫic void Apply(ControllerModel controller)
var actions = controller.Actions
972 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

. Select (а => new {


Action =а,
Names = a.Attributes.Select(attr =>
(attr as AddActionAttribute)?.AdditionalName)
}) ;

foreach (var item in actions.ToList()) {


foreach (string name in item.Names) {
controller.Actions.Add(new ActionМodel(item.Action)
ActionName name =
}) ;

Модифицировать набор действий. ассоциированных с контроллером. внутри со­


глашения по действиям невозможно, но все равно необходим какой-нибудь способ
обозначения требующихся изменений. По этой причине класс AddActionAttribute
был сделан просто атрибутом, а не соглашением по модели.
Изменять набор действий внутри соглашения по контроллерам допускается, поэ­
тому был создан класс AdditionalActionsAt tribute. Метод Apply () использует
LINQ для нахождения методов, к которым был применен класс AddActionAttribute .
и создает новы е объекты Ac t ionModel с указанными именами.
Самой важной частью данного класса является вызов метода ToList (), приме­
ненный к результатам LINQ:

foreach (var item i n actions .ToList ())

Метод ToList () инициирует перечисление запроса LINQ и помещает результат в


новую коллекцию, а это значит, что цикл foreach проходит по другому набору объек­
тов, который отличается от набора, перечисляемого инфраструктурой МVС во время
применения соглашений по модели. Без вызова ToL i st () было бы получено то ж е
самое сообщение об ошибке, которое сгенерировало соглашение по модели из лис­
тинга 31 .12; благодаря вызову To List () появляется возможность создавать новые
объ екты действий в модели. В листинге 31.14 иллюстрируется применение переде­
ланных атрибутов к контроллеру Home.

Листинг 31.14. Применение переделанного соглашения по модели в файле


HomeController.cs

using ConventionsAndConstraints . Models;


using Microsoft.AspNetCore . Mvc ;
using Convent i onsAndConstra in ts . I nfrast ru cture ;
namesp ace ConventionsAndConstraints . Contro ll ers
[AdditionalActions]
puЫic class HomeController : Co ntro ller {
puЫic IActionResul t Index () => View ( " Resul t ", new Resul t (
Contro l ler = nameof(HomeCon troller) ,
Action = nameof(Index)
}) ;
Глава 31 . Соглашения по модели и ограничения действий 973
[AddAction ( " Details " ) ]
puЬlic I ActionRe su l t List ( ) => Vi ew ( "Re s ul t ", new Re sul t {
Contro ll er = nameof( HomeCo nt r ol l er) ,
Action = nameof( List)
}) ;

Для просмотра эффекта от переделанного соглашения по модели запустите при­


ложение и запросите URL вида /Home/Deta il s и / Home / Lis t . Соглашение по мо­
дели добавл яет новое действие, которое обрабатывается методом Li st () ,дополняя
со зданную по умолчанию модель (рис . 31.4).

1 . R•sult х

/ +- -t С : D localhost:65325/Home/List

1--::0~:;--- HomeController

~-~~~~L-is-t~-
Controller: HomeController

Action: List

Рис. 31 .4. Результат создания действия в модели

П орs1Док выполнен и я соглашений по модели


Соглашения по модели применяются в специфическом порядке, начиная с со­
глашений, имеющих н аиболее широкую область де йствия: первыми применяются
соглашения по контроллерам , затем соглашения по действиям и, наконец, согла­
ш е ния по парам етрам. Для демонстрации такого порядка к методу List () класса
HomeControl l er применяются оба специальных соглашения , созданные в предшес­
твующих при мерах (листинг 31.15).

Листинг З 1.15. Применение нескольких соглашений по модели в файле


HomeController.cs
using ConventionsAndCons traints . Model s ;
using Microsoft . AspNetCore.Mvc ;
using ConventionsAndCon s traints .I nfr a structure ;
namespace Co nve ntion s AndCo nst ra ints. Co ntr o ll e rs
[AdditionalActions ]
puЫic class HomeController : Con t ro ll er {
puЫic IActionRes ult Index() => Vi ew( " Result ", new Result {
Cont r oller = nameof(HomeControl l er) ,
Action = nameof(Index)
}) ;

[ActionNamePrefix ( "Do")]
[AddAct i on( " Details " ) ]
974 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

puЫic IActionResul t List () => View ( "Resul t ", new Resul t


Controller = nameof(HomeController),
Action = nameof(List)
}) ;

Атрибут Addi tionalActions, который является соглашением по контроллерам,


применяется первым и создает новое действие по имени Details . Затем применяет­
ся атрибут ActionNamePrefix , представляющий собой соглашение по действиям, ко­
торый применяет префикс Do ко всем действиям, ассоциированным с методом. В ре­
зультате метод List () реализует два действия, DoList и DoDetails, которых можно
достичь через URL вида /Home/DoList и /Home/DoDetails (рис. 31.5).

1. Result х

Action: List ---- --~-...:. : ___ _____________ ___


. ~:.....- _.....,. ,.. ____ _

~-:_:_,~_:r_:_:l-e-r:_____~_i:_~_e_C_o_n_tr-o-ll_e r______J
Рис. 31 .5. Иллюстрация порядка выполнения соглашений по модели

Создание глобальных соглашений по модели


Может возникнуть необходимость изменить стандартные соглашения по модели
для каждого контроллера, действия или параметра в приложении. В таком случае
вместо того, чтобы обеспечить согласованное применение атрибутов к каждому классу
контроллера, можно создать глобальное соглашение по модели. глобальные соглаше­
ния по модели конфигурируются в классе Startup, как показано в листинге 31.16.

Листинг 31 .16. Соэдание глобальных соглашений по модели в файле Startup. cs

using Microsoft .AspNetCore.Builder ;


using Microsoft .Ext ensions.Dependencyinjection;
using ConventionsAndConstraints .I nfrastructure;
namespace ConventionsAndConstraints
puЫic class Startup {
puЫic void ConfigureServices(IServiceCollection services) {
services.AddМvc() .AddМvcOptions(options => {
options.Conventions.Add(new ActionNamePrefixAttribute("Do"));
options.Conventions.Add(new AdditionalActionsAttribute());
}) ;
Глава 31 . Соглашения по модели и ограничения действий 975
puЫic void Configure(IApplicationBuilder арр) {
app . UseStatusCodePages() ;
app.UseDeveloperExceptionPage() ;
app.UseStaticFiles() ;
app . UseMvcWithDefaultRoute() ;

Объект MvcOpt i ons, получаемый расширяющим методом AddMvcOptions () ,


определяет свойство Conventions. Это свойство возвращает коллекцию, в которую
можно добавлять объекты соглашений по модели. В листинге 31 .16 оба специаль­
ных соглашения по модели применены глобально, т.е. имена всех действий будут
снабжены префиксом Do, а все методы будут инспектироваться на предмет атрибута
AddAction . Поскольку соглашения по модели применяются глобально, атрибуты из
класса HorneController можно удалить (листинг 31.17).

Листинг 31 .17. Удаление соглашений по модели в файле HorneController. cs


using ConventionsAndConstraints . Models ;
using Microsoft . AspNetCore . Mvc ;
using ConventionsAndConstraints . Infrastructure ;
namespace ConventionsAndConstraints . Controllers
// [AdditionalActions]
puЬlic class HomeController : Controller {
puЫic IActionResult Index() => View( " Result ", new Result {
Controller = nameof(HomeControlle r ) ,
Action = nameof(Index)
}) ;

// [ActionNamePrefix ( "Do")]
[AddAction( " Details ") ]
puЫic IActionResult List () => View ( "Result", new Result {
Controller = nameof(HomeController) ,
Action = nameof(List)
) ) ;

IЛобальны е соглашения по модели применяются перед соглашениями, применен­


ными напрямую к классам. Если глобальных соглашений несколько, то они приме­
няются в порядке , в котором регистрировались, безотносительно их типов. В лис­
тинге 31.16 соглашение по действиям было зарегистрировано перед соглашением по
контроллерам, а это означает, что действие Deta ils, указанное посредством атрибу­
та AddAction, создается после того, как соглашение
ActionNamePrefixAttribu te
будет применено ко всем именам действий. В результате метод List () реализует два
действия , DoList и Details , которые достижимы через URL вида /Horne/DoList и
/Home/Details (рис. 31.6).
976 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC

1 Result Х

~ ~~· С : CJ localh~~~.: 6S3~5/.H~~ef_?~a~~~~ c{J_~ =-.!


Controller: HomeController 1

/ Action: List 1

Рис. 31 .6. Иллюстрация порядка выполнения глобальных соглашений по модели

Использование ограничений действий


Ограничения действий решают, подходит ли метод действия для обработки специ­
фического запроса, и это может навести на мысль, что ограничения действий подоб­
ны фильтрам авторизации, которые рассматривались в главе 19.
На самом деле применение ограничений действий намного уже. Когда инфраструк­
тура МVС получает НТГР-запрос , она проходит через процесс выбора с целью иден­
тификации метода действия, который будет использоваться для обработки запроса.
При наличии нескольких методов действий, которые могли бы обработать запрос, ин­
фраструктуре MVC нужен какой-нибудь способ решения, какой из них применить, и
именно здесь используются ограничения действий. В табл. 31.10 приведена сводка,
позволяющая поместить ограничения действий в контекст.

Таблица 31 .10. Помещение ограничений действий в контекст

Вопрос Ответ

Что это такое? Ограничения действий - это классы, которые инфраструктура


MVC применяет для выяснения, может ли запрос быть обрабо­
тан специфическим действием

Чем они полезны? Если обработать запрос способны два или большее число дейс­
твий, то MVC необходимы средства для решения, какое дейс­
твие подходит лучше других. Ограничения действий использу­
ются для предоставления такой информации

Как они используются? Ограничения действий применяются как атрибуты, что позволяет
их многократно использовать повсюду в приложении, а также оз­

начает, что логика определения , должно ли действие обработать


запрос, не обязана находиться внутри самого метода действия

Существуют ли какие-то Ограничения действий могут быть применены слишком широко


скрытые ловушки или и препятствовать обработке запроса любым подходящим мето­
ограничения? дом действия, приводя к тому, что клиенту отправляется ответ
4 О 4 - Not Found (404 - не найдено)

Существуют ли Фильтры более удобны, если нужно ограничить доступ к дейс­


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

Изменились ли они по Ограничения действий теперь являются неотъемлемой частью


сравнению с версией модели приложения, которая отсутствовала в предшествующих

MVC 5? версиях MVC


Глава 31. Соглашения по модели и ограничения действий 977

П одгото в ка п р оекта для примера

Цель ограничений действий - помочь инфраструктур е МVС сделать выбор среди


двух и бол ее похожих методов, когда любой из них мог бы использ оваться для обра­
ботки запроса. Такая ситуация создается в листинге 31. 18 за сч ет добавления нового
метода действия в контроллер Home .

Листинг З 1.18. Создание двух одинаково подходящих действий


в файле HomeController. cs

using Co n ventionsAn dConstra i nts . Mod e ls;


usi n g Mi crosoft.AspNetCore . Mvc ;
using ConventionsAndConstra int s .Inf rastr u ct u re ;
n amespace Convent i onsAnd Cons t r a ints.C o n t r ol l e r s
11 [Add i t i ona l Actions ]
p u Ыic class HomeControl l er : Co n trol l er {
puЫic IActionResult Inde x( ) => View( " Result ", new Result {
Controller = nameof( HomeContro ll er) ,
Action = na meof(Index)
}) ;

[ActionName("Index")]
puЫic IActionResult Other() => View("Result", new Result {
Controller =
nameof(HomeController),
Action = nameof(Other)
}) ;
11 [Act i onNamePrefix( "Do " ) ]
[AddAction ( " Details " )]
p u ЫicIActio n Res u lt Li s t () => Vie w ( " Res ult ", new Result {
Contro l ler = nameof( Home Co n t r o ll er) ,
Action = nameof(List)
}) ;

Здесь бьш добавл е н новый метод по имени Oth er ( ) , к которому применен атрибут
ActionName, так что он выпускает действие под названием I ndex . Кро м е того, был
обновлен класс Startup для удаления глобальных соглашений по модели, прим е нен­
ных ранее в главе (листинг 31.19).

Листинг 31.19. Удаление глобальных соглашений по модели в файле Startup. cs

puЫic void Conf i gu r eServices(IServi ceCo l lection services) {


services . AddMvc() . AddMvcOpt i ons(opti ons => {
11 options . Conventions.Add(new ActionNamePrefixAttribute("Do"));
11 options.Conventions.Add(new AdditionalActionsAttribute());
}) ;
978 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC

Таким образом, в контролле ре Ноте есть два действия по имени Index. Запустив
приложение , вы увидите сообщение об ошибке, показанное на рис. 31.7, которо е сви­
детельствует о том, что МVС не известно, какое действие должно использоваться.

J lntemol Servor Error Х

+- ~ С D loca ll10st:65325
---------------'---'
An unhandled exceptio п оссштеd while processing the request. ~
AmЬiguousActionE xceptюn : lviultiple actions matched. The fo!lo\'Jiпg actions matched
rou е cJata апd hacl all coпstra ints satisfied:

Co nst 1·a in t sA ndCoпvent i o n s.Con·юllers.HomeCo ntro l lecl ndex


ConstraintsAndC oпveпt ion s.C ontгolleгs. HomeC ont ю l ler. Otl1eг
Microsoft.AspNet.Mvc.1 nfrastructure.De fau ltActionSelectcr.SelectAsync(Rou teContext context)

Рис. З 1. 7. Результат создания двух одинаково подходящих методов действий

Для удобства ниже приведена важная часть сообщения:

Ambi guousActi on Exc ep t i on : Mul ti ple a cti on s matc he d . The fol l owing
acti ons matched route data and had a ll con s traints sat i sfied :
Conve nti onsAndConstra i nts.Cont r ol lers.H ome Con t r ol l er .Index
Convent i onsAndCons t ra i nt s . Controller s. Home Contro l l e r . Othe r
AmbiguousActionEx cep t ion : соотве т с тв ие нескольким действиям .
Следующи е дейс твия со отв ет ств ую т д а нным маршрута и все их
ограничения удовле тв орены :

Conven t ionsAn dConstraint s. Cont r ollers . HomeContro ll er .Index


Co nven t ionsAn dCons tra i n t s . Con tro l le rs.Hom e Contro l ler . Ot her

Огран и чения действий


Огран ичения действий применяются для указания инфраструктуре МVС , может
ли метод действия использоваться для обработки запроса, и для реализации интер­
фейса I Ac ti onCons tra i nt, который определен следующим образом:

names pace Mi c ro so ft.Asp NetCo r e . Mvc . Acti onCons t r ai nts {


p u Ы ic in te rf ace IActionCo ns tr a i nt : I Acti onConst r a i ntMetadata
i nt Order { get ; }
boo l Accept (Act i onCon st ra i nt Co nt e xt context) ;

Когда инфраструктура МVС проходит через процесс выбора метода действ ия для
обработки запроса, она выясняет, связаны ли с ним ограничения. При нали ч и и ог­
раничений они упорядочиваются в последовательность на основе значения свойства
Or de r, и по очереди для каждого ограничения вызывается метод Accept () .Если дл я
любого ограничения метод Accept () возвращает f a ls e, тогда инфраструктура МVС
считает, что метод действия не может применяться для обработки текущего запроса.
Глава 31. Соглашен и я no м одел и и о гран и чения де й стви й 979

С о вет. Интерфейс IAct i onConstraint является производным от IActionConstraint


Metadata, который представляет собой интерфейс , не определяющий членов . Он
не используется напрямую, и при определении специальных ограничений вы всег­
да дол ж ны применять интерфейс IActionConstaint , а при создании ограничения,
к оторое имеет зависимости , подле ж ащие распознаванию, использовать интерфейс
IActionConstra i ntFactory , как описано в разделе "Распознавание зависимостей в
ограничения х де й ствий" далее в главе.

Чт обы п ом очь огр ан ичен и я м дей ств и й пр инять р е ш ение , инфраструкту ра MVC
снаб жает их экземпляро м клас с а Act i onConstraintContext для данных контекс­
та, который определяет свойства, описанные в табл. З 1.11.

Таблица 31 .11. Свойства класса ActionConstraintContex t


И мя Описание

Candidates Это свойство возвращает список объектов


ActionSelectorCandidate, описывающий набор методов
действий, которые инфраструктура MVC включила в окончатель­
ный сп и со к для обработки текущего запроса

CurrentCandidate Это свойство возвращает объект ActionSel ectorCandidate,


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

RouteContext Это свойство возвращает объе кт RouteContext , который пре­


доставляет информацию о данны х маршрутизации (чер е з свойс­
тво RouteData ) и НТТР-запросе (через свойство HttpContext )

Создание ограничения действи я


С ам ы й р асп рост р ан ен ны й тип огр анич е ния исс л едует з апрос , чтобы удо стове ­
р иться в уд о вл е твор ени и некоторо й п олитики, так о й как нал ичи е опр едел енного
з н а ч ения в заг ол овке НТГР. Чтобы посмотр еть , к аким обр азом с о здается данный вид
огр а н и ч ения дей ств ия, до б авьте в папку Infrastructure ф айл клас са по и ме ни
UserAgentAttribute . cs и поместите в него содержимо е и з листинга 31.20.
Листи нг 31.20. Содержимое файла UserAgentAttribute.cs
из папки Infras tructure
using System ;
using System.Linq ;
using Microsoft.AspNetCore.Mvc . ActionConstraints ;
namespace ConventionsAndConstraints .I nfrast r ucture
puЫic class UserAgentAttribute : Attribute , IActionConstraint
private string substring ;
puЬlic UserAgentAttribute(string sub) {
substring = sub . ToLower() ;

puЬlic int Order { get ; set ; О;


980 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

puЫ i c bool Accept(ActionConstraintContext context)


return context.RouteContext . HttpContext
. Request . Headers[ " User-Agent " ]
. Any(h => h . ToLower() . Contains(substring));

Это атрибут ограничения действия, который предотвращает соответствие запроса


действиям, когда заголовок User-Agent не содержит указанную строку. Внутри ме­
тода Accept () и з объект а HttpContext извлекаются заголовки НТТР и посредством
LINQ выясняется , содержит ли какой-нибудь из них подстроку. которая получ ена ч е ­
рез конструктор.

На заметку! Не полагайтесь на заголовок User-Agent при идентификации браузеров в ре­


альных прило жениях, потому что значения этого заголовка часто обманчивы . Например ,
версия браузера Microsoft Edge, которая являлась текущей на момент написания главы,
отправляет заголовок User - Agent , содержащий Android, Apple, Chrome и Safari,
из-за чего Microsoft Edge легко по ошибке принять за другой браузер. Более надеж­
ный подход предусматривает применение библиотеки JavaScript, такой как Modernizr
(http://modernizr . com ), для обнаружения функциональных возможностей, от кото­
рых зависит приложение .

В листинге 31.21 новое ограничени е применяется к одному из методов класса


HomeController .
Листинг 31 .21. Применение ограничения действия в файле HomeController. cs
using ConventionsAndConstraints . Models ;
using Microsoft . AspNetCore.Mvc ;
using ConventionsAndConstraints.Infrastructure ;
narnespace ConventionsAndConstraints .Controllers
11 [AdditionalActions]
puЬlic class HorneController : Controller {
puЫic IActionResult Index() => View( " Result ", new Result {
Contro l ler = narneof(HorneController) ,
Action = narneof(Index)
) ) ;
[ActionNarne( " Index " )]
[UserAgent ( "Edge")]
puЬlic IActionResul t Other () => View ( "Resul t ", new Resul t {
Controller = nameof(HomeControl le r) ,
Action = narneof(Other)
}) i
/ / [ActionNarnePrefix ( " Do " ) ]
[AddAction( " Details " )]
puЫic IActionResult List() => View( " Result" , new Result {
Controller = narneof(HorneController) ,
Action = nameof(List)
} ) i
Глава 31. Соглашения по модели и ограничения действий 981
К методу Other () применяется атрибут Us erAgent , который указывает, что дейс­
твию не разрешено получать запросы с заголовком Use r- Ag e n t , не содержащим эле­
мент Ed ge . Запустив приложение и запросив URL вида /Home/I n d e x в браузерах
Google Chrome и Microsoft Edge , вы заметите, что запросы обрабатываются разными
м етодами (рис. 31.8).

х L ______ Е1 RE!Stllt

+- ·• С : CJ locall1ost:65325

*'
locall1ost.65325 1.·

Controller: HomeController
Controller: HomeController
Action: lndex
Action: Other

Рис. 31 .8. Эффект от ограничения действия

Влияние ограничения на выбор действия

Предыдущий пример раскрыл один аспект, касающийся использования ограничений, ко­


торый может стать очевидным не сразу: действию с ограничением, чей метод Accept ( )
возвращает true для запроса, отдается предпочтение перед действием , к которому огра­
ничения вообще не применялись.

Контроллер Home определяет два действия I ndex - созданные из методов I ndex () и


Other () - и оба они могут применяться для обработки запросов, заголовок Us er-Agen t
которых содержит элемент Edge . Причина использования для обработки запроса от брау­
зера Edge метода Other () связана с тем, что к нему было применено ограничение и метод
Accept () этого ограничения возвращает t rue . Идея в том, что действие, которое имеет
ограничение , принимающее запрос , является лучшим кандидатом, чем действие, вообще
не имеющее ограничений.

Соэдание сравнивающего ограничения действия

Посредством свойств Cand idates и Curr entCandidate объекта ActionCon st r ain t


Context огранич ения снабжаются деталями о других действиях, которые являются
кандидатами на обработку запроса. Каждое потенциальное соответствие описывается
с и с пользованием э кз емпляра класса Act i o n Se l ecto r Candidate , который опреде­
ля ет свойства, приведе нные в табл. 31.12.

Таблица З 1.12. Свойств а класса ActionSelectorCandida te

Имя Описание

Action Это свойство возвращает объект ActionDescriptor, который


описывает действие-кандидат

Constraints Это свойство возвращает список объектов реализации


IAct i onConstra int, содержащий набор ограничений, которые
были применены к действию-кандидату
982 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

Класс ActionDescriptor применяется для описания действия через свойства,


перечисленные в табл . 31 .13 , многие из которых аналогичны свойствам, пр едостав ­
ляемым другими объектами контекста.

Таблица 31.13. Избранные свойства класса ActionDescriptor

Имя Описание

Name Это свойство возвращает имя действия

RouteConstraints Это свойство возвращает объект реализации


IList<IRouteConstraintProvider>, который использу­
ется для ограничения нацеливания маршрутов на действие

Parameters Это свойство возвращает объект реализации


IList<PropertyModel>, который содер жит описания пара­
метров, требующихся методу действия

Act ionConstraints Это свойство возвращает объект реализации


IList<IActionConstraintMetadata>,coдepжaщий
ограничения для данного действия

Ограничения могут инспектировать действия-кандидаты и иметь сведения о том,


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

работы. В качестве примера р ассмотрим способ, которым применяются ограничения


к контроллеру Ноте в листинге 31.22.

Листинг 31.22. Применение ограничения к контроллеру в файле HomeController. cs


using ConventionsAndConstraints.Models;
using Microsoft . AspNetCore . Mvc ;
using ConventionsAndConstraints.Infrastructure ;
namespace ConventionsAndConstraints . Controllers
puЫic class HorneController : Controller (
puЫic IActionResult Index() => Vi ew("Result ", new Result {
Controller = nameof(HomeController),
Action = nameof(Index)
}) ;
[ActionName ( "I ndex " )]
[UserAgent("Edge")]
puЬlic IActionResul t Other () => View ( " Resu l t", new Resul t (
Controller = nameof(HomeController),
Act i on = nameof(Other)
1);

[UserAgent("Edge")]
puЫic IActionResu l t Li st() => View( "Res ul t ", new Result (
Controller = nameof(HomeController) ,
Action = nameof(List)
}) ;
Глава 31. Соглашения по модели и ограничения действий 983
В приложении имеется только одно действие List и применени е к нему ограниче­
ния означает, что его могут использовать лишь запросы, чей заголовок User-Agent
содержит элемент Edge. Например, если вы отправите запрос из браузера Chrome, то
получите ответ 404 - Not Found .
Пользы в этом мало, т.к. пользователи не поймут. почему они получили ошибку, из­
за отсутствия поясняющего текста, который бы предложил применить взамен другой
браузер. Ограничения удобны, когда необходимо управлять выбором метода действия
для обработки запроса, но не когда нужно полностью предотвратить использование
специфического действия . Если вашей целью является как раз последнее, тогда при­
мен е ние фильтра позволит перенаправить клиента на описательную страницу ошиб­
ки, что будет намного более полезным ответом.
Чтобы решить проблему, понадобится обновить класс UserAgentAttribute так,
чтобы ограничение не отклоняло запросы, :когда для обработки запроса доступно
только одно действие-кандидат (листинг 31.23).

Листинг 31 .23. Проверка наличия других действий-кандидатов в файле


UserAgentAttribute.cs
using System ;
us i ng System .L inq ;
using Microsoft.AspNetCore . Mvc . ActionConstraints ;
namespace ConventionsAndConstraints . Infrastructure
puЬlic class UserAgentAttribute : Attribute , IActionConstraint
private string substring ;
puЫic UserAgentAttribute(string sub) {
substring = sub.ToLower() ;

puЫic int Order { get ; set; } = О ;

puЫic boo l Accept(ActionConstraintContext context) {


return context . RouteContext.HttpContext
. Request .He aders[ " User - Agent " ]
. Any(h => h.ToLower() . Contains(substring))
11 context.Candidates.Count() == 1;

Дополнительный запрос LINQ проверяет, что действие - кандидат, возвращенное

свойством CurrentCandidate, является единственным в :колле1щии, которую воз­


вратило свойство Candida t es. Если это так, тогда ограничение знает, что инфра­
структура MVC не располагает альтернативным действием, и разрешает обработку
запроса. Результат можно просмотреть, запустив приложение и запросив URL вида
/Home/List в браузере Google Chrome. Хотя заголовок User - Agent , отправляемый
Chrome, не содержит элемент Edge, который указан с помощью атрибута для метода
List (), класс ограничения выясняет. что другие действия-кандидаты отсутствуют, и
позволяет обработать запрос.
984 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC

Распознавание зависимостей в ограничениях действий


Интерфейс I Ac tionConst r aintFac t o r y используется, когда необходимо распоз ­
навать зависимости в ограничении действия поср едством поставщика служб, кото ­
рый был описан в главе 18. Бот определение этого интерфейса:

us i ng System ;
name s pace Microsof t. AspN et Core . Mvc . Ac t io nCo ns tr ai nt s
puЫ i c interface IActionConstraintFa cto r y : I Ac tionConstraintMetadata
I Ac t ionConstrai nt Create in stance (I Se r v i ceProvider services) ;
bool IsRe u sa Ьl e { get ; }

М е тод Create in stance () вы з ывается для создания новых эк з емпляров кл асса

ограничения действия, а свойство IsReus aЫ e применяется для указания , могут ли


объекты, возвращаемые методом Createins t ance ( ),использоваться для множества
з апросов .

Для демонстрации применения интерфейса I Ac ti on Con s traintFac t ory нуж­


на з ависимость, которая потребует распознавания. С этой целью добавьте в папку
Infrastructure файл класса по имени UserAgen t Comp a r er . cs , содержимое кото­
рого приведе но в листинге 31.24.
Листинг 31.24. Содержимое файла UserAgentComparer. cs из папки
Infrastructure
usi ng System . Li nq;
using Microsoft . AspNetCore .H ttp ;
namespace ConventionsAndConstraints .In frastruc tu re
pu Ьl ic class UserAgentCompare r {
puЬli c bool Co nta i nsS tring( Ht t pRe ques t r e que s t , str i ng agent) {
string searchTerm = agent . ToLower( );
r e t urn reque st. Headers [" User - Agent "J
. Any(h => h. To Lower ( ) . Conta i ns (s earchTe r m) ) ;

В классе Use r Age n tComparer опр еделен единственный м етод . который ищет
строку внутри заголовка User - Agent в НТГР-запросе. Это та же самая функциональ­
ность, которая использовалась ранее , но упакованная в отдельный класс , так что его

жизненным циклом можно управлять с применением пост авщика служб. сконфигу­


рированного в листинге 31.25.

Листинг 31.25. Регистрация типа для поставщика служб в файле Startup. cs


using Mi crosoft . AspNetCore . Bu i lder ;
using Microsoft . Extensions. Depende ncy i njection ;
us i ng ConventionsAndConstra i nts .I n fr astructure ;
name s pace Convent i onsAndConstraints
p uЬlic class Start up {
Глава 31 . Соглашения по модели и ограничения действий 985
puЫic void ConfigureServices(IServiceCo l lection services) {
services.AddSingleton<UserAgentComparer>();
serv i ces . AddMvc() . AddMvcOpt i o n s(opt i o n s => {
11 options . Conventions . Add(new ActionNamePrefixAttr i b ut e( "Do " )) ;
11 options . Conven t ions . Add(new Additiona l ActionsAtt r ibu te()) ;
}) ;

puЫic void Conf i gure ( IApp l icat i onBu i lde r арр) {


app . UseStatusCodePages() ;
app . UseDeve l ope r ExceptionPage() ;
app .UseStat i c Fi l es() ;
app . UseMvcWithDefau l t Route() ;

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


э кземплярUserAgentComp a rer. В листинге 31 .26 показ ано обновл е нное ограничение
UserAgent , которо е теп е рь делеги рует работу по инспектированию з аголовка объекту
UserAgen t Comparer , получаемому с помощью поставщика служ б.

Листинг 31.26. Распознавание зависимостей в файле UserAgentAttribute. cs

using System ;
using System.Linq ;
using Microsoft . AspNetCore . Mvc . Ac ti o n Constraints ;
using Microsoft.Extensions . Dependencyinjection;
namespace Conven ti onsAndConstra i nt s. Infrast r ucture
puЬlic class UserAgentAttribute : Attribute, IActionConstraintFactory

private string s ubstring ;


puЫic UserAgentAttribute(string sub) {
s ubstring = s ub ;

puЫic IActionConstraint Createinstance(IServiceProvider services) {


return new UserAgentConstraint(services.GetService<UserAgentComparer>(),
suЬstring) ;

puЫic Ьооl IsReusaЫe => false;


private class UserAgentConstraint : IActionConstraint
private UserAgentComparer comparer;
private string suЬstring;
puЬlic UserAgentConstraint(UserAgentComparer comp, string suЬ) {
comparer = comp;
substring = suЬ.ToLower();

puЫic int Or der { get ; set ; О;


986 Часть 11. Подробные сведения об инфраструк туре ASP.NET Core MVC

puЫ ic bo ol Acc ep t( Ac t i onConstra i n tCont ex t c on te x t )


return compa r er . Co ntainsString(con te xt . RouteCon t ext
. Ht t pCon te xt . Re qu e st, substring)
1 1 c o ntex t . Candida t es. Count () == 1;

В приведенной модели атрибут. примененный к методам действий, отвеча­


ет за создание экземпляров класса ограничения, когда вызывается его м е тод

Create i nstance () . В качестве аргумента м етод Create i nst a nce () получает объ­
ект реализации I Servi ceP rovi de r, используемый в примере для получения объекта
Us e r AgentCompare r, поэтому можно создавать экземпляр закрытого класса ограни­
чения, который затем будет задействован в процессе выбор а.

Избежание ловушки, связанной с областью действия


Подобно другим средствам, основанным на атрибутах , применение атрибута ограничения
к классу контроллера эквивалентно применению данного атрибута к каждому отдельному
методу. Тем не менее, обычно это приводит к нежелательным результатам, поскольку целью
ограничений является помощь инфраструктуре MVC в выборе метода действия, а примене­
ние одного и того же ограничения ко всем действиям контроллера в целом не способствует
достижению указанной цели.

Например, если применить атрибут Us erAge nt к классу HomeControl l er , то действия


Index перестанут быть достижимыми из любого браузера . Оба действия Index будут оди­
наково подходящими для браузеров, которые включают в заголовок User -Agent элемент
Edge , что в итоге даст исключение. Всем остальным браузерам не подойдет ни одно из
действий Index , что в результате приведет к ответу 4 04 - Not Found.
Внутри ограничения можно было бы воспользоваться объектом контекста, чтобы найти дру­
гие ограничения и посмотреть, отклонят ли они запрос, но тогда метод Accept ( ) каждого
ограничения будет многократно вызываться для каждого запроса , что окажется затратным
процессом, которого лучше избегать.
Ограничения хорошо работают в ситуации, когда есть множество методов действий , кото­
рые могут обработать один и тот же запрос, из-за чего к ним и применяется ограничение .

Р езюме
В настоящей главе были описаны два средства, которые используются для на­
стройки работы MVC. Вы узнали, как применять соглашения по модели для изменения
способа отображения классов и методов на контроллеры и действия . Кроме того. было
пока зано, каким образом использовать ограничения действий для сужения диап азо­
на запросов, которые действие может обрабатывать, и как их применять для выбора
действия из списка кандидатов , идентифицированных при поступлении запроса.
Итак. изучение инфраструктуры ASP.NET Core MVC завершено. Оно начиналось с
создания простого приложения, после чего вы совершили комплексный тур по разно­
образным компонентам в инфраструктуре, научившись их конфигурировать, настра­
ивать или полностью заменять . Желаю вам успехов в раз работке собств енных при­
ложений MVC и надеюсь на то, что вы получили не меньшее удовольстви е от чте ния
этой книги, чем я во время ее н аписания .
Предметный указатель

А N
Ajax (Asynchronous JavaSciipt and ХМL). 631 . NEТCore, 140; 351
Arrange/act/ assert (А/А/А). 179 Node.js. 349: 351
АSР.NЕТСоге, 355 Node Package Manager (NPM), 351
ASP.NEТ Соге Identity, 325 NuGet. 204
ASP.NEТ Core Web Application (.NЕТ Core). 140
ASP.NET Саге Web Application о
(.NET Framework). 140 ОRМ (Object-relational mapping). 218
ASP.NEТWeb Forms, 21

R
в
Razor. 115: 147; 654
Browser Link (Ссылка на браузер), 156

с
s
SportsStore, 201: 244: 276: 298: 325
CDN (Content delivery network). 776
CRUD (Create, read. update, delete), 201: 302 т
CSRF (Cross-site request fогgегу) , 7 46
TDD (Thst-Diiven Development), 189
D u
DAL (Data access lауег). 73
URL (Uniform resource locator), 25
Dl (Dependency injection). 543 относительные к приложению. 789

Е v
Entity Framework Core (EF Core), 218 Visual Studio. 151
Visual Studio Code. 348; 353
G
Git, 350 х
GU!D (Globally unique identifier), 566
XUnit.net. 175
J А
JavaScript, 161 Администрирование,298
JSON (JavaScript Object Notation), 377: защита средств администрирования. 325
535;629 Архитектура
МVС , 24
к "модель-представление", 73
Kestrel, 384 Атака CSRF, 7 4 7

L Б
LINQ (Language Integrated Query). 26 База данных
LocalDB. 363 Identity. 326
LТS (LongThrm Support), 349 добавление пакетов в базу данных, 364
миграции баз данных, 225: 343
м переустановка базы данных. 289
расширение базы данных. 288
МVС (Model-view-controller), 20; 24
создание,337:363
МVР (Model-view-presenter). 74
классов базы данных, 220
МWМ (Model-view-view model), 75
988 Предметный указатель

в данных, 783
установка времени истечения для кеша. 786
Веб-сервер Kestrel, 384
Кеширование, 783
Виджет, 281 Класс
Визуализация представлений. 659 ActionExecutedContext. 589
Внедрение в действия, 573 AddressSummary, 809
Внедрение в свойства. 573 Anchor'lagHelper. 781
Внедрение зависимостей (DI), 51; 543; AppldentityDbContext, 327
550:556 ApplicationDbContext, 362
для фильтров, 600 Appointment, 835
использование для конкретных типов. 563 Assert, 180
конфигурирование, 559 CacheThgHelper, 788
Выражение Cart, 263; 276
Razor. 128 CartController, 279
лямбда. 101 Controller, 443; 508; 520; 590
CustomeгController, 461
г CustomHtmlResult, 519
DbContext. 220
Генерация
DiagnosticsFilter, 604
URL. 478 DictionaryStorage. 561
списка категорий, 255
EFRepository, 362
EnvironmentтagHelper. 768
д ErrorMiddleware. 399
Данные ExceptionF!lterAttribute. 599
добавление данных модели представления, 232 FoгmThgHelper, 7 45
создание формы для ввода данных. 172 GuestResponse, 361
Действия,500 HeaderModel, 823
Диспетчер пакетов HomeController, 408; 430
Node (NPM), 351 HttpContext. 391
NuGet, 141; 197; 225 ImageThgHelper. 782
LegacyRoute, 482; 484; 489
з LinkТag, 779

Запрос LinkThgHelper, 778; 779


Ajax, 631 MemoryRepository. 54 7
использование тел запросов в качестве
Mock, 200
824
источников данных привязки, ModelExpression. 735
подделка межсайтовых запросов
(CSRF), 746 ModelStateD!ctionary, 836
Зтцита средств администрирования, 325 ModelValidationContext, 853
MvcRouteHandler, 487
и OrderController, 291
PocoController, 506
Идентификатор (GUID), 566 Product, 182
Инициализатор Program, 374; 383
коллекции, 95 RazorV!ewEngineOptions. 671
объекта,94 ResultF!lterAttribute. 593
Инструмент ScriptThgHelper, 769
Bower, 143; 351 Startup, 207; 268; 374; 385; 429; 578
Gulp. 146 StartupDevelopment, 425
NPM, 146 StatusCodeResult, 541
NuGet, 141 ThgHelper, 716
Интерполяция строк, 93 ThgHelperContent, 728
Инфраструктура имитации. 196 ThgHelperOutput, 728
ThxtAreaThgHelper, 763
к ТimeFilter, 603

Категория ТypeBroker, 553


генерация списка категорий, 255 ValidationSummaryThgHelper. 841
Кеш, 788 ViewComponent. 257
аннулирование кеша, 775 ViewResult. 520
Предметный указа т е л ь 989
VJewResнltDetailsAttribute, 594 м
VirtuaJPathContext, 490
дескрипторный вспомогательный, 229; Маршрут, 36
708;719;764 добавление нового маршрута. 469
длл проверки достоверности форм, 764 ограничение маршрутов, 451
использование . 741 на основе типов и значений, 455
с использованием регулярного выраже-
соэдание, 715
усовершенствованные возможности, 726 нил. 454
контекста,326 переменной длины, 448
соэдание класса
применение ограничений к маршрутам, 463
корзины, 276 сложный. 463
хранилища , 221 Маршрутизация, 400
базы данных, 220 ASP.NEТ, 236
Коллекцил URL, 26; 426
инициализатор коллекции, 95 входящих URL, 481
привлзка коллекций, 815 генерация URL, 4 78
сложных типов, 81 7 конфигурация маршрутизации. 212
применение коллекции в качестве типа
стандартная.448
модели,816 на контроллер . 486
на контроллеры МVС. 485
Компоненты
представлений,252;686
на основе соглашений, 426; 460
РОСО, 686 настройка системы маршрутизации, 4 79
соэдание, 686 помещение маршрутизации в контекст, 427
создание простого маршрута,434
приложен.ил МVС. 85
создание. 85 426; 460
с помощью атрибутов ,
упорядочение маршрутов, 440
слабо свлэанные, 211
создание,549 Массив
Компоновки, 124
привязка массивов . 813
создание, 123 Методы

Контроллер,33;70;500
асинхронные, 109
расширяющие, 97
Account. 332
фильтрующие, 100
АР!, 614 : 623
CRUD. 303 Механизм
для отображения сообщений об ошибках , 338 Razor, 654; 657; 658; 671
виэуалиэации,644
добавление контроллера, 213
создание. 117: 139;365: 617: 682;711;831 Миграция, 225
Конфигурацил маршрутизации, 212 Минификацил, 166
Конфигурирование Модель, 43
внедрения зависимостей, 559 создание модели, 545
поставщика служб. 558 представлений. 69
приложенил,327;369;713 привязка модел ей, 50; 270; 793; 799

сериалиэатора JSON, 637 стандартные эначенил привязки , 801


служб указание источника данных привязки

мvс. 419 моделей, 820


приложения. 345 проверка достоверности модели, 314: 828;
эле ментов input, 7 49 834
Корзина конфигурирование стандартных сообще­

добавление виджета с итоговой информа­ ний об ошибках проверки достовер­

цией по корзине. 281 ности, 842


правила проверки достоверности с помо ­
создание

класса корзины, 276 щью метаданных . 849


службы корзины, 278 на стороне клиента. 854
удаление элементов из корзины, 279 на стороне сервера, 854
создание
класса модели, 284
л модели. 710
Лямбда-выражение, 101 Модель-представление-контроллер ( МVС ) , 24;
68
990 Предметный указатель

Модель-представление-модель пр едстав­ Платформа


ления (МVVМ). 75 ASP.NEТ Core, 23
Модель-представление-презентатор Политика безопасности, 336
(MVP). 74 Представление.37;70:332:644
Модель представления, 734 Razor, 71 ; 147
арх:итектура , 73 визуализация представлений, 659
Модульное тестирование. 201; 230: 233 для отображения сообщений об ошибках . 338
мvс. 170; 174 добавление и конфигурирование. 215
Visual Studio Code, 369 компонентыпредставлений.252;678:686

действий , 519 обновление представлений, 164


контроллера с з ависимостью, 560 подготовка представлений, 503
контроллеров,519 создание,38; 118; 139;255;365; 431;682
создание проекта модульного тестирова­ гибридных представлений. 704
навигационного компонента
ния. 548
фильтров, 587 представления , 252

Модульные тесты строго типизированное, 120: 524


написание и выполнение, 178 тестирование представлений, 40
файл запуска представления. 126
частичное,241;666;686
о
Привязка
Области , 426; 491 атрибуты для источников данных привяз­
влияние области на приложение МVС, 495 ки . 820
генерирование ссылок на действия, 496
моделей,50;270;793;799
Объединение ограничений, 456 указани е источника данных привязки
Объект моделей,820
инициализатор объекта , 94 простых типов, 802
Операция сложных типов, 803
CRUD , 302 из заголовков, 823
null-условная операция(?). 87 свойств
объединения с null (??), 89 избирательная , 811
Организация/ действи е /утверждение стандартные значения привяз ки, 801
(arrange/act/assert), 179 Приложение
Отладчик Visual Studio, 151 мvс
Ошибки проверки достоверности, 842 минификация, 166
конфигурирование стандартных сообще­ модульное тестирование, 170; 174
ний. 842 пакетирование. 166
SportsStore, 241
п конфигурирование, 223; 327; 340; 369: 619;
Пакет 713;798
Bootstrap, 238 службы приложения . 345
Git, 350; 351 развертьmание, 336; 343
Identity, 325 Проект АSР. NЕТСоrе, 355
Moq, 196
NuGet, 204; 357; 414 р
Уе ошаn , 355
Уеошаn (уо), 351 Разработка через тестирование (TDD), 189
диспетчер пакетов NuGet. 141 Редактор
управление программными пакетами , 141 Visual Studio Code. 353; 361: 365: 372
Пакетирование, 166
Паттерн с
МVС,68
REST,625 Сегмент. 433
интеллектуальный пользовательский ин­ URL
терфейс (Sш art UI) , 71 необязательный , 446
локатор служб (Service Locator), 575 Сеть доставки содержимого (CDN), 776
модель-представление-модель пр едставле­ Система управления базами данных
ния (МVVМ). 75 SQLite, 363
модель-представление - презентатор (МVР), 74
Предметный указатель 991
Служба, 386 ф
ASP.NEт. 387
Файл
мvс. 419
жизненный цикл службы, 565 CSS, 161
конфиrурирование поставщика служб , 558 JavaScript, 161
регистрация службы, 277 запуска представления, 126
хранилища, 211 импортирования представлений. 121
соэдание службы корзины, 278 Фильтрация по категории, 248
Среда
Фильтры, 576; 581
.NEТCore. 140; 351 авторизации,584:585
Node.js, 349; 351 асинхронные, 591: 595
Visual Studio, 151 гибридные, 596
глобальные, 608
Ubuntu, 349; 351
Средство BrowserLink, 156; 159
действий.584:588:590
исключений,584;598;599;601
Ссылка
генерация ссылок
порядок применения фильтров, 610
изменениепорядка,612
на категории с помощью компонента
результатов, 584; 592
представления,256
исходящих, 468 с зависимостями. 605
Формат
на метод действия, 46
отображение ссылок на страницы, 228; 234 JSON (JavaScript Object Notation), 379; 535;
создание ссылок на страницы , 230 629
Стилизация представления , 63; 65
Страница
х
разбиение на страницы , 226 Хранилище, 545
редактирования,302 класс хранилища, 221
списка.302 соэдание , 221
Строка фиктивное
интерполяция строк, 93 соэдание , 210
подключения ,222
Схема URL, 432 ц
Цепочки зависимостей, 560
т
ш
Таблица стилей CSS, 60; 778
создание , 163 Шаблон
Тест
URL, 432
проекта
избирательный прогон тестов, 182
модульный
ASP.NEТCoreWebApplication ( . NEТCore), 140
ASP.NEТCore Web Applicatioп (.NЕТ
создание. 371
параметризованный, 191 F'ramework), 140
прогон тестов, 371 формирование шаблонов (scaffolding), 212
Тестирование
универсализации имен, 770
модульное, 201; 230; 233
Технология
э
ASP.NEТWeb F'orms, 21 Элемент
тип img, 782
анонимный, 108 input, 749
ТИпизация label. 754
неявная, 107 option. 756
Точка останова, 152 select. 756; 758
textarea, 763
якорный, 781
у

Управление программными пакетами, 141


Уровень доступа к данным (DAL), 73
ЯЗЫК ПРОГРАММИРОВАНИЯ
С# б.О И ПЛАТФОРМА .NET 4.б
7-Е ИЗДАНИЕ

Эндрю Троелсен, Новое 7-е издание этой


Филипп Джепикс книги было ПОЛНОСТЬЮ
пересмотрено и переписано

с учетом последних изменений


спецификации языка С# и
дополнений платформы .NET
FгamewOl'k. Отдельные главы
посвящены важным новым
средствам, которые делают

.NET FгamewOl'k 4.6 самым


передовым выпуском, в том

числе: усовершенствованная

модель программирования

ADO.NET Entity Fгamework;


многочисленные улучшения

IDЕ-среды и архитектуры
MVVM для разработки
настольных приложений WPF;
многочисленные обновления
в ASP.NET Web API.
Помимо этого, предлагается
исчерпывающее рассмотрение

всех ключевых возможностей


языка С#, как старых, так
и новых, что позволило обрести
www.williamspuЬlishing.com популярность предыдущим

изданиям этой книги. Читатели


получат основательные

знания приемов объектно­


ориентированной обработки ,
атрибутов и рефлексии,
обобщений и коллекций,
а также обретут понимание
МНОГИХ сложных тем.

ISBN 978-5-8459-2099-7 в продаже


www.dialektika.com
9 785990 891043
лpress ®
www.apress.com

Вам также может понравиться