В финальной части книги рассматривается ASP.NET Core — последняя версия инфраструктуры для разработки веб-приложений, которая использует C# и .NET Core. В этой главе предлагается введение в инфраструктуру ASP.NET Core и раскрываются ее отличия от предыдущей версии, т.е. ASP.NET.
После ознакомления с основами паттерна "модель-представление-контроллер" (Model-View-Controller — MVC), реализованного в ASP.NET Core, вы приступите к построению двух приложений, которые будут работать вместе. Первое приложение, REST-служба ASP.NET Core, будет закончено в главе 30. Вторым приложением является веб-приложение ASP.NET Core, созданное с применением паттерна MVC, которое будет завершено в главе 31. Уровнем доступа к данным для обоих приложений послужат проекты
AutoLot.Dal
и AutoLot.Models
, которые вы создали в главе 23.
Выпуск инфраструктуры ASP.NET MVC в 2007 году принес большой успех компании Microsoft. Инфраструктура базировалась на паттерне MVC и стала ответом разработчикам, разочарованным API-интерфейсом Web Forms, который по существу был ненадежной абстракцией поверх HTTP. Инфраструктура Web Forms проектировалась для того, чтобы помочь разработчикам клиент-серверных приложений перейти к созданию веб-приложений, и в этом отношении она была довольно успешной. Однако по мере того, как разработчики все больше и больше привыкали к процессу разработки веб-приложений, многим из них хотелось иметь более высокую степень контроля над визуализируемым выводом, избавиться от состояния представления и ближе придерживаться проверенных паттернов проектирования для веб- приложений. С учетом указанных целей и создавалась инфраструктура ASP.NET MVC.
Паттерн "модель-представление-контроллер" (Model-View-Controller — MVC) появился в 1970-х годах, будучи первоначально созданным для использования в Smalltalk. Относительно недавно его популярность возросла, в результате чего стали доступными реализации в различных языках, в том числе Java (Spring Framework), Ruby (Ruby on Rails) и .NET (ASP.NET MVC).
Модель — это данные в приложении. Данные обычно представляются с помощью простых старых объектов CLR (plain old CLR object — POCO). Модели представлений состоят из одной или большего числа моделей и приспособлены специально для потребителя данных. Воспринимайте модели и модели представлений как таблицы базы данных и представления базы данных.
С академической точки зрения модели должны быть в высшей степени чистыми и не содержать правила проверки достоверности или любые другие бизнес-правила. С практической точки зрения тот факт, содержит модель логику проверки достоверности или другие бизнес-правила, целиком зависит от применяемого языка и инфраструктур, а также специфических потребностей приложения. Например, в инфраструктуре EF Core присутствует много аннотаций данных, которые имеют двойное назначение: механизм для формирования таблиц базы данных и средство для проверки достоверности в веб-приложениях ASP.NET Core. Примеры, приводимые в книге, сконцентрированы на сокращении дублированного кода, что приводит к размещению аннотаций данных и проверок достоверности там, где в них есть наибольший смысл.
Представление — это пользовательский интерфейс приложения. Представление принимает команды и визуализирует результаты команд для пользователя. Представление обязано быть как можно более легковесным и не выполнять какую-то фактическую работу, а передавать всю работу контроллеру.
Контроллер является своего рода мозговым центром функционирования. Контроллеры принимают команды/запросы от пользователя (через представления) или клиента (через обращения к API-интерфейсу) посредством методов действий и надлежащим образом их обрабатывают. Результат операции затем возвращается пользователю или клиенту. Контроллеры должны быть легковесными и использовать другие компоненты или службы для обработки запросов, что содействует разделению обязанностей и улучшает возможности тестирования и сопровождения.
С помощью ASP.NET Core можно создавать много типов веб-приложений и служб. Двумя вариантами являются веб-приложения, в которых применяются паттерн MVC и службы REST. Если вы имели дело с "классической" инфраструктурой ASP.NET, то знайте, что они аналогичны соответственно ASP.NET MVC и ASP.NET Web API. Типы веб-приложений МУС и приложений API разделяют часть "модель" и "контроллер" паттерна МУС, в то время как веб-приложения МУС также реализуют "представление", завершая паттерн МУС.
Точно так же, как Entity Framework Core является полной переработкой Entity Framework 6, инфраструктура ASP.NET Core — это переработка популярной инфраструктуры ASP.NET Framework. Переписывание ASP.NET было нелегкой, но необходимой задачей, преследующей цель устранить зависимости от
System.Web
. Избавление от указанной зависимости позволило запускать приложения ASP.NET под управлением операционных систем, отличающихся от Windows, и веб-серверов помимо Internet Information Services (IIS), включая размещаемые самостоятельно. В итоге у приложений ASP.NET Core появилась возможность использовать межплатформенный, легковесный и быстрый веб-сервер с открытым кодом под названием Kestrel, который предлагает унифицированный подход к разработке для всех платформ.
На заметку! Изначально продукт Kestrel был основан на LibUV, но после выпуска ASP.NET Core 2.1 он базируется на управляемых сокетах.
Подобно EF Core инфраструктура ASP.NET Core разрабатывается в виде проекта с полностью открытым кодом на GitHub (
https://github.com/aspnet
). Она также спроектирована как модульная система пакетов NuGet. Разработчики устанавливают только те функциональные средства, которые нужны для конкретного приложения, сводя к минимуму пространство памяти приложения, сокращая накладные расходы и снижая риски в плане безопасности. В число дополнительных улучшений входят упрощенный запуск, встроенное внедрение зависимостей, более чистая система конфигурирования и подключаемое промежуточное программное обеспечение (ПО).
В оставшихся главах этой части вы увидите, что в ASP.NET Core внесено множество изменений и усовершенствований. Помимо межплатформенных возможностей еще одним значительным изменением следует считать унификацию инфраструктур для создания веб- приложений. В рамках ASP.NET Core инфраструктуры ASP.NET MVC, ASP.NET Web API и Razor Pages объединены в единую инфраструктуру для разработки. Разработка веб-приложений и служб с применением полной инфраструктуры .NET Framework предоставляла несколько вариантов, включая Web Forms, MVC, Web API, Windows Communication Foundation (WCF) и WebMatrix. Все они имели положительные и отрицательные стороны; одни были тесно связаны между собой, а другие сильно отличались друг от друга. Наличие стольких доступных вариантов означало, что разработчики обязаны были знать каждый из них для выбора того, который подходит для имеющейся задачи, или просто отдавать предпочтение какому-то одному и положиться на удачу.
С помощью ASP.NET Core вы можете строить приложения, которые используют Razor Pages, паттерн MVC, службы REST и одностраничные приложения, где применяются библиотеки JavaScript вроде Angular либо React. Хотя визуализация пользовательского интерфейса зависит от выбора между MVC, Razor Pages или библиотеками JavaScript, лежащая в основе среда разработки остается той же самой. Двумя прежними вариантами, которые не были перенесены в ASP.NET Core, оказались Web Forms и WCF.
На заметку! Поскольку все обособленные инфраструктуры объединены вместе под одной крышей, прежние названия ASP.NET MVC и ASP.NET Web API официально были изъяты из употребления. Для простоты в этой книге веб-приложения ASP . NET Core, использующие паттерн "модель-представление-контроллер", упоминаются как MVC, а REST-службы ASP.NET - как Web API.
Многие проектные цели и функции, которые побудили разработчиков применять ASP.NET MVC и ASP.NET Web API, по-прежнему поддерживаются в ASP.NET Core (и были улучшены).
Ниже перечислены некоторые из них (но далеко не все):
• соглашения по конфигурации (convention over configuration; или соглашение над конфигурацией, если делать акцент на преимуществе соглашения перед конфигурацией);
• контроллеры и действия;
• привязка моделей;
• проверка достоверности моделей;
• маршрутизация;
• фильтры;
• компоновки и представления Razor.
Они рассматриваются в последующих разделах за исключением компоновок и представлений Razor, которые будут раскрыты в главе 31.
ASP.NET MVC и ASP .NET Web API сократили объем необходимой конфигурации за счет введения ряда соглашений. В случае соблюдения таких соглашений уменьшается объем ручного (или шаблонного) конфигурирования, но при этом разработчики обязаны знать соглашения, чтобы использовать их в своих интересах. Двумя главными видами соглашений являются соглашения об именовании и структура каталогов.
В ASP.NET Core существует множество соглашений об именовании, предназначенных для приложений MVC и API. Например, контроллеры обычно содержат суффикс
Controller
в своих именах (скажем, HomeController
) и вдобавок порождены от класса Controller
(или ControllerBase
). При доступе через маршрутизацию суффикс Controller
отбрасывается. При поиске представлений контроллера отправной точкой поиска будет имя контроллера без суффикса. Такое соглашение об отбрасывании суффикса встречается повсюду в ASP.NET Core. В последующих главах вы встретите немало примеров.
Еще одно соглашение об именовании применяется относительно местоположения и выбора представлений. По умолчанию метод действия (в приложении MVC) будет визуализировать представление с таким же именем, как у метода. Шаблоны редактирования и отображения именуются в соответствии с классом, который они визуализируют в представлении. Описанное стандартное поведение может быть изменено, если того требует ваше приложение. Все это будет дополнительно исследоваться при построении приложения
AutoLot.Mvc
.
Существует несколько соглашений о папках, которые вы должны понимать, чтобы успешно создавать веб-приложения и службы ASP.NET Core.
По соглашению папка
Controllers
является тем местом, где реализации ASP.NET Core MVC и API (а также механизм маршрутизации) ожидают обнаружить контроллеры для вашего приложения.
В папке
Views
хранятся представления для приложения. Каждый контроллер получает внутри главной папки Views
собственную папку с таким же именем, как у контроллера (исключая суффикс Controller
). По умолчанию методы действий будут визуализировать представления из папки своего контроллера. Например, папка Views/Home содержит все представления для класса контроллера HomeController
.
Внутри папки
Views
имеется специальная папка по имени Shared
, которая доступна всем контроллерам и их методам действий. Если представление не удалось найти в папке с именем контроллера, тогда поиск представления продолжается в папке Shared
.
Улучшением по сравнению ASP.NET MVC стало создание особой папки по имени
wwwroot
для веб-приложений ASP.NET Core. В ASP.NET MVC файлы JavaScript, изображений, CSS и другое содержимое клиентской стороны смешивалось в остальных папках. В ASP.NET Core все файлы клиентской стороны содержатся в папке wwwroot. Такое отделение скомпилированных файлов от файлов клиентской стороны значительно проясняет структуру проекта при работе с ASP.NET Core.
Как и в ASP.NET MVC и ASP.NET Web API, контроллеры и методы действий являются основополагающими компонентами приложения ASP.NET Core MVC или API.
Ранее уже упоминалось, что инфраструктура ASP.NET Core унифицировала ASP.NET MVC5 и ASP.NETWeb API. Такая унификация также привела к объединению базовых классов
Controller.ApiController
и AsyncController
из MVC5 и Web API 2.2 в один новый класс Controller
, который имеет собственный базовый класс по имени ControllerBase
. Контроллеры веб-приложений ASP.NET Core наследуются от класса Controller
, тогда как контроллеры служб ASP.NET — от класса ControllerBase
(рассматривается следующим). Класс Controller
предлагает множество вспомогательных методов для веб-приложений, наиболее часто используемые из которых перечислены в табл. 29.1.
Класс
ControllerBase
обеспечивает основную функциональность для веб-приложений и служб ASP.NET Core, и вдобавок предлагает вспомогательные методы для возвращения кодов состояния HTTP. В табл. 29.2 описана основная функциональность класса ControllerBase
, а в табл. 29.3 перечислены некоторые вспомогательные методы для возвращения кодов состояния HTTP.
Действия — это методы в контроллере, которые возвращают экземпляр типа
IActionResult
(или Task
для асинхронных операций) либо класса, реализующего IActionResult
, такого как ActionResult
или ViewResult
. Действия будут подробно рассматриваться в последующих главах.
Привязка моделей представляет собой процесс, в рамках которого инфраструктура ASP.NET Core использует пары "имя-значение", отправленные НТТР-методом POST, для присваивания значений свойствам моделей. Для привязки к ссылочному типу пары "имя-значение" берутся из значений формы или тела запроса, ссылочные типы обязаны иметь открытый стандартный конструктор, а свойства, участвующие в привязке, должны быть открытыми и допускать запись. При присваивании значений везде, где это применимо, используются неявные преобразования типов (вроде установки значения свойства
string
в значение int
). Если преобразование типа терпит неудачу, тогда такое свойство помечается как имеющее ошибку Прежде чем начать подробное обсуждение привязки, важно понять предназначение словаря ModelState
и его роль в процессе привязки (а также проверки достоверности).
Словарь
ModelState
содержит записи для всех привязываемых свойств и запись для самой модели. Если во время привязки возникает ошибка, то механизм привязки добавляет ее к записи словаря для свойства и устанавливает ModelState.IsValid
в false
. Если всем нужным свойствам были успешно присвоены значения, тогда механизм привязки устанавливает ModelState.IsValid
в true
.
На заметку! Проверка достоверности модели, которая тоже устанавливает записи словаря
ModelState
, происходит после привязки модели. Как неявная, так и явная привязка модели автоматически вызывает проверку достоверности для модели. Проверка достоверности рассматривается в следующем разделе.
В дополнение к свойствам и ошибкам, добавляемым механизмом привязки, в словарь
ModelState
можно добавлять специальные ошибки. Ошибки могут добавляться на уровне свойств или целой модели. Чтобы добавить специфическую ошибку для свойства (например, свойства PetName
сущности Car
), применяйте такой код:
ModelState.AddModelError("PetName","Name is required");
Чтобы добавить ошибку для целой модели, указывайте в качестве имени свойства
string.Empty
:
ModelState.AddModelError(string.Empty, $"Unable to create record: {ex.Message}");
Неявная привязка моделей происходит, когда привязываемая модель является параметром для метода действия. Для сложных типов она использует рефлексию и рекурсию, чтобы сопоставить имена записываемых свойств модели с именами, которые содержатся в парах "имя-значение", отравленных методу действия. При наличии совпадения имен средство привязки применяет значение из пары "имя-значение", чтобы попробовать установить значение свойства. Если совпадение дают сразу несколько имен из пар "имя-значение", тогда используется значение первого совпавшего имени. Если имя не найдено, то свойство устанавливается в стандартное значение для его типа. Вот как выглядит порядок поиска пар "имя-значение":
• значения формы из HTTP-метода POST (включая отправки JavaScript AJAX);
• тело запроса (для контроллеров API);
• значения маршрута, предоставленные через маршрутизацию ASP.NET Core (для простых типов);
• значения строки запроса (для простых типов);
• загруженные файлы (для типов
IFormFile
).
Например, следующий метод будет пытаться установить все свойства в типе
Car
. Если процесс привязки завершается без ошибок, тогда свойство ModelState.IsValid
возвращает true
.
[HttpPost]
public ActionResult Create(Car entity)
{
if (ModelState.IsValid)
{
// Сохранить данные.
}
}
Явная привязка моделей запускается с помощью вызова метода
TryUpdateModelAsync()
с передачей ему экземпляра привязываемого типа и списка свойств, подлежащих привязке. Если привязка модели терпит неудачу, тогда метод возвращает false
и устанавливает ошибки в ModelState
аналогично неявной привязке. При использовании явной привязки моделей привязываемый тип не является параметром метода действия. Скажем, вы могли бы переписать предыдущий метод Create()
с применением явной привязки:
[HttpPost]
public async Task Create()
{
var vm = new Car();
if (await TryUpdateModelAsync(vm,"",
c=>c.Color,c=>c.PetName,c=>c.MakeId, c=>c.TimeStamp))
{
// Делать что-то важное.
}
}
Атрибут
Bind
в HTTP-методах POST позволяет ограничить свойства, которые участвуют в привязке модели, или установить префикс для имени в парах "имя-значение". Ограничение свойств, которые могут быть привязаны, снижает опасность атак избыточной отправкой (over-posting attack). Если атрибут Bind
помещен на ссылочный параметр, то значения будут присваиваться через привязку модели только тем полям, которые перечислены в списке Include
. Если атрибут Bind
не используется, тогда привязку допускают все поля.
В следующем примере метода действия
Create()
все поля экземпляра Car
доступны для привязки, поскольку атрибут Bind
не применяется:
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Create(Car car)
{
if (ModelState.IsValid)
{
// Добавить запись.
}
// Позволить пользователю повторить попытку.
}
Пусть в ваших бизнес-требованиях указано, что методу
Create()
разрешено обновлять только поля PetName
и Color
. Добавление атрибута Bind
(как показано в примере ниже) ограничивает свойства, участвующие в привязке, и инструктирует средство привязки моделей о том, что остальные свойства должны игнорироваться.
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(
[Bind(nameof(Car.PetName),nameof(Car.Color))]Car car)
{
if (ModelState.IsValid)
{
// Сохранить данные.
}
// Позволить пользователю повторить попытку.
}
Атрибут
Bind
можно также использовать для указания префикса имен свойств. Если имена в парах "имя-значение" имеют префикс, добавленный при их отправке методу действия, тогда атрибут Bind
применяется для информирования средства привязки моделей о том, как сопоставлять эти имена со свойствами типа. Код в следующем примере устанавливает префикс для имен и позволяет привязывать все свойства:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(
[Bind(Prefix="MakeList")]Car car)
{
if (ModelState.IsValid)
{
// Сохранить данные.
}
}
Источниками привязки можно управлять через набор атрибутов на параметрах действий. Допускается также создавать специальные средства привязки моделей, но эта тема выходит за рамки настоящей книги. В табл. 29.4 перечислены атрибуты, которые можно использовать для управления привязкой моделей.
Проверка достоверности происходит немедленно после привязки модели (явной и неявной). В то время как привязка модели добавляет ошибки в словарь
ModelState
из-за возникновения проблем преобразования, проверка достоверности добавляет ошибки в ModelState
на основе бизнес-правил. Примерами бизнес-правил могут быть обязательные поля, строки с максимально разрешенной длиной или даты с заданным допустимым диапазоном.
Правила проверки достоверности устанавливаются через атрибуты проверки достоверности, встроенные или специальные. В табл. 29.5 кратко описаны наиболее часто используемые встроенные атрибуты проверки достоверности. Обратите внимание, что некоторые их них также служат аннотациями данных для формирования сущностей EF Core.
Можно также разработать специальные атрибуты проверки достоверности, но в книге данная тема не рассматривается.
Маршрутизация — это способ, которым ASP.NET Core сопоставляет HTTP-запросы с контроллерами и действиями (исполняемые конечные точки) в вашем приложении взамен старого процесса отображения URL на структуру файлов проекта, принятого в Web Forms. Она также предлагает механизм для создания URL изнутри приложения на основе таких конечных точек. Конечная точка в приложении MVC или Web API состоит из контроллера, действия (только MVC), метода HTTP и необязательных значений (называемых значениями маршрута).
На заметку! Маршруты также применяются к страницам Razor, SignaIR, службам gRPC и т.д. В этой книге рассматриваются контроллеры стиля MVC и Web API.
Инфраструктура ASP.NET Core использует промежуточное ПО маршрутизации для сопоставления URL входящих запросов и для генерирования URL, отправляемых в ответах. Промежуточное ПО регистрируется в классе
Startup
, а конечные точки добавляются в классе Startup
или через атрибуты маршрутов, как будет показано позже в главе.
Конечные точки маршрутизации состоят из шаблонов URL, включающих в себя переменные-заполнители (называемые маркерами) и литералы, которые помещены в упорядоченную коллекцию, известную как таблица маршрутов. Каждая запись в ней определяет отличающийся шаблон URL, предназначенный для сопоставления. Заполнители могут быть специальными переменными или браться из заранее определенного списка. Зарезервированные маркеры маршрутов перечислены в табл. 29.6.
В дополнение к зарезервированным маркерам маршруты могут содержать специальные маркеры, которые отображаются (процессом привязки моделей) на параметры методов действий.
При определении маршрутов для служб ASP.NET метод действия не указывается. Вместо этого, как только контроллер обнаруживается, выполняемый метод действия базируется на методе HTTP запроса и назначениях методов HTTP методам действий. Детали будут приведены чуть позже.
При маршрутизации на основе соглашений (или традиционной маршрутизации) таблица маршрутов строится в методе
UseEndpoints()
класса Startup
. Метод MapControllerRoute()
добавляет конечную точку в таблицу маршрутов, указывая имя, шаблон URL и любые стандартные значения для переменных в шаблоне URL. В приведенном ниже примере кода заранее определенные заполнители {controller}
и {action}
ссылаются на контроллер и метод действия, содержащийся в данном контроллере. Заполнитель {id}
является специальным и транслируется в параметр (по имени id
) для метода действия. Добавление к маркеру маршрута знака вопроса указывает на его необязательность.
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
Запрашиваемый URL проверяется на соответствие с таблицей маршрутов. При наличии совпадения выполняется код, находящийся в этой конечной точке приложения. Примером URL, который мог бы обслуживаться таким маршрутом, является
Car/Delete/5
. В результате вызывается метод действия Delete()
класса контроллера CarController
с передачей значения 5
в параметре id
.
В параметре
default
указано, каким образом заполнять пустые фрагменты в URL, которые содержат не все определенные компоненты. С учетом предыдущего кода, если в URL ничего не задано (например, http://localhost:5001
), тогда механизм маршрутизации вызовет метод действия Index()
класса HomeController
без параметра id
. Параметру default
присуща поступательность, т.е. он допускает исключение справа налево. Однако пропускать части маршрута не разрешено. Ввод URL вида http://localhost:5001/Delete/5
не пройдет сопоставление с шаблоном {controller}/{action}/{id}
.
Механизм маршрутизации попытается отыскать первый маршрут на основе контроллера, действия, специальных маркеров и метода HTTP. Если механизм маршрутизации не может определить наилучший маршрут, тогда он сгенерирует исключение
AmbiguousMatchException
.
Обратите внимание, что шаблон маршрута не содержит протокол или имя хоста. Механизм маршрутизации автоматически добавляет в начало корректную информацию при создании маршрута и применяет метод HTTP, путь и параметры для определения соответствующей конечной точки приложения. Например, если ваш сайт запускается на
https://www.skimedic.com
, то протокол (HTTPS) и имя хоста (www.skimedic.com
) автоматически добавляются к маршруту при его создании (скажем, https://www.skimedic.com/Car/Delete/5
). Для входящего запроса механизм маршрутизации использует порцию Car/Delete/5
из URL.
Имена маршрутов могут применяться в качестве сокращения для генерации URL изнутри приложения. Выше конечной точке было назначено имя
default
.
При маршрутизации с помощью атрибутов маршруты определяются с использованием атрибутов C# в отношении контроллеров и их методов действий. Это может привести к более точной маршрутизации, но также увеличит объем конфигурации, поскольку для каждого контроллера и действия необходимо указать информацию маршрутизации.
Например, взгляните на приведенный ниже фрагмент кода. Четыре атрибута
Route
на методе действия Index()
эквивалентны маршруту, который был определен ранее. Метод действия Index()
является конечной точкой приложения для mysite.com
, mysite.com/Home
, mysite.com/Home/Index
или mysite.com/Home/Index/5
.
public class HomeController : Controller
{
[Route("/")]
[Route("/Home")]
[Route("/Home/Index")]
[Route("/Home/Index/{id?}")]
public IActionResult Index(int? id)
{
...
}
}
Основное различие между маршрутизацией на основе соглашений и маршрутизацией с помощью атрибутов заключается в том, что первая охватывает приложение, тогда как вторая — контроллер с атрибутом
Route
. Если маршрутизация на основе соглашений не применяется, то каждому контроллеру понадобится определить свой маршрут, иначе доступ к нему будет невозможен. Скажем, если в таблице маршрутов не определен стандартный маршрут, тогда следующий код обнаружить не удастся, т.к. маршрутизация для контроллера не сконфигурирована:
public class CarController : Controller
{
public IActionResult Delete(int id)
{
...
}
}
На заметку! Маршрутизацию на основе соглашений и маршрутизацию с помощью атрибутов можно использовать вместе. Если бы в методе
UseEndpoints()
был настроен стандартный маршрут контроллера (как в примере с маршрутизацией на основе соглашений), то предыдущий контроллер попал бы в таблицу маршрутов.
Когда маршруты добавляются на уровне контроллера, методы действий получают этот базовый маршрут. Например, следующий маршрут контроллера охватывает
и любые другие методы действий:Delete()
[Route("[controller]/[action]/{id?}")]
public class CarController : Controller
{
public IActionResult Delete(int id)
{
...
}
}
На заметку! При маршрутизации с помощью атрибутов встроенные маркеры помечаются квадратными скобками (
[]
), а не фигурными ({}
), как при маршрутизации на основе соглашений. Для специальных маркеров применяются все те же фигурные скобки.
Если методу действия необходимо перезапустить шаблон маршрута, тогда нужно предварить маршрут символом прямой косой черты (
/
). Скажем, если метод Delete()
должен следовать шаблону URL вида mysite.eom/Delete/Car/5
, то вот как понадобится сконфигурировать действие:
[Route("[controller]/[action]/{id?}")]
public class CarController : Controller
{
[Route("/[action]/[controller]/{id}")]
public IActionResult Delete(int id)
{
...
}
}
В маршрутах также можно жестко кодировать значения маршрутов вместо замены маркеров. Показанный ниже код даст тот же самый результат, как и предыдущий:
[Route("[controller]/[action]/{id?}")]
public class CarController : Controller
{
[Route("/Delete/Car/{id}")]
public IActionResult Delete(int id)
{
...
}
}
Маршрутам можно также назначать имена, что обеспечит сокращение для перенаправления по определенному маршруту с указанием только его имени. Например, следующий атрибут маршрута имеет имя
GetOrderDetails
:
[HttpGet("{orderId}", Name = "GetOrderDetails")]
Вы могли заметить, что ни в одном определении шаблона маршрута для методов не присутствует какой-нибудь метод HTTP. Причина в том, что механизм маршрутизации (в приложениях MVC и API) для выбора надлежащей конечной точки приложения использует шаблон маршрута и метод HTTP совместно.
Довольно часто при построении веб-приложений с применением паттерна MVC соответствовать определенному шаблону маршрута будут две конечные точки приложения. Средством различения в таких ситуациях является метод HTTP. Скажем, если
CarController
содержит два метода действий с именем Delete()
и они оба соответствуют шаблону маршрута, то выбор метода для выполнения основывается на методе HTTP, который используется в запросе. Первый метод Delete()
декорируется атрибутом HttpGet
и будет выполняться, когда входящим запросом является GET
. Второй метод Delete()
декорируется атрибутом HttpPost
и будет выполняться, когда входящим запросом является POST
:
[Route("[controller]/[action]/{id?}")]
public class CarController : Controller
{
[HttpGet]
public IActionResult Delete(int id)
{
...
}
[HttpPost]
public IActionResult Delete(int id, Car recordToDelete)
{
...
}
}
Маршруты можно модифицировать также с применением атрибутов методов HTTP, а не атрибута
Route
. Например, ниже показан необязательный маркер маршрута id
, добавленный в шаблон маршрута для обоих методов Delete()
:
[Route("[controller]/[action] ")]
public class CarController : Controller
{
[HttpGet("{id?}")]
public IActionResult Delete(int? id)
{
...
}
[HttpPost("{id}")]
public IActionResult Delete(int id, Car recordToDelete)
{
...
}
}
Маршруты можно перезапускать с использованием методов HTTP; понадобится просто предварить шаблон маршрута символом прямой косой черты (
/
), как демонстрируется в следующем примере:
[HttpGet("/[controller]/[action]/{makeId}/{makeName}")]
public IActionResult ByMake(int makeId, string makeName)
{
ViewBag.MakeName = makeName;
return View(_repo.GetAllBy(makeId));
}
На заметку! Если метод действия не декорирован каким-либо атрибутом метода HTTP, то по умолчанию принимается метод
GET
. Тем не менее, в веб-приложениях MVC непомеченные методы действий могут также реагировать на запросы POST
. По этой причине рекомендуется явно помечать все методы действий подходящим атрибутом метода HTTP.
Существенное различие между определениями маршрутов, которые применяются для приложений в стиле MVC, и определениями маршрутов, которые используются для служб REST, заключается в том, что в определениях маршрутов для служб не указываются методы действий. Методы действий выбираются на основе метода HTTP запроса (и необязательно типа содержимого), но не по имени. Ниже приведен код контроллера API с четырьмя методами, которые все соответствуют одному и тому же шаблону маршрута. Обратите внимание на атрибуты методов HTTP:
[Route("api/[controller]")]
[ApiController]
public class CarController : ControllerBase
{
[HttpGet("{id}")]
public IActionResult GetCarsById(int id)
{
...
}
[HttpPost]
public IActionResult CreateANewCar(Car entity)
{
...
}
[HttpPut("{id}")]
public IActionResult UpdateAnExistingCar(int id, Car entity)
{
...
}
[HttpDelete("{id}")]
public IActionResult DeleteACar(int id, Car entity)
{
...
}
}
Если метод действия не имеет атрибута метода HTTP, то он трактуется как конечная точка приложения для запросов
GET
. В случае если запрос соответствует маршруту, но метод действия с корректным атрибутом метода HTTP отсутствует, тогда сервер возвратит ошибку 404 (не найдено).
На заметку! Инфраструктура ASP.NET Web API позволяет не указывать метод HTTP для метода действия, если его имя начинается с
Get
, Put
, Delete
или Post
. Следование такому соглашению обычно считалось плохой идеей и в ASP.NET Core оно было удалено. Если для метода действия не указан метод HTTP, то он будет вызываться с применением НТТР-метода GET
.
Последним селектором конечных точек для контроллеров API является необязательный атрибут
Consumes
, который задает тип содержимого, принимаемый конечной точкой. В запросе должен использоваться соответствующий заголовок content-type
, иначе будет возвращена ошибка 415 Unsupported Media Туре (неподдерживаемый тип носителя). Следующие два примера конечных точек внутри одного и того же контроллера проводят различие между JSON и XML:
[HttpPost]
[Consumes("application/json")]
public IActionResult PostJson(IEnumerable values) =>
Ok(new { Consumes = "application/json", Values = values });
[HttpPost]
[Consumes("application/x-www-form-urlencoded")]
public IActionResult PostForm([FromForm] IEnumerable values) =>
Ok(new { Consumes = "application/x-www-form-urlencoded", Values = values });
Еще одно преимущество маршрутизации связано с тем, что вам больше не придется жестко кодировать URL для других страниц в своем сайте. Записи в таблице маршрутов применяются для сопоставления с входящими запросами, а также для построения URL. При построении URL схема, хост и порт добавляются на основе значений текущего запроса.
Фильтры в ASP.NET Core запускают код до или после специфической фазы конвейера обработки запросов. Существуют встроенные фильтры для авторизации и кеширования, а также возможность назначения специальных фильтров. В табл. 29.7 описаны типы фильтров, которые могут быть добавлены в конвейер, перечисленные в порядке их выполнения.
Фильтры авторизации работают с системой ASP.NET Core Identity, чтобы предотвратить доступ к контроллерам или действиям, использовать которые пользователь не имеет права. Создавать специальные фильтры авторизации не рекомендуется, поскольку встроенные классы
AuthorizeAttribute
и AllowAnonymousAttribute
обычно обеспечивают достаточный охват в случае применения ASP.NET Core Identity.
Код "перед" выполняется после фильтров авторизации и до любых других фильтров, а код "после" выполняется после всех остальных фильтров. Таким образом, фильтры ресурсов способны замкнуть накоротко целый конвейер запросов. Обычно фильтры ресурсов используются для кеширования. Если ответ находится в кеше, тогда фильтр может пропустить остаток конвейера.
Код "перед" выполняется немедленно перед выполнением метода действия, а код "после" выполняется сразу после выполнения метода действия. Фильтры действий могут замкнуть накоротко метод действия и любые фильтры, помещенные внутрь фильтров действий.
Фильтры исключений реализуют сквозную обработку ошибок в приложении. У них нет событий, возникающих до или после, но они обрабатывают любые необработанные исключения, сгенерированные при создании контроллеров, привязке моделей, запуске фильтров действий либо выполнении методов действий.
Фильтры результатов завершают выполнение экземпляра реализации
IActionResult
для метода действия. Распространенный сценарий применения фильтра результатов предусматривает добавление с его помощью информации заголовка в сообщение ответа HTTP.
Помимо поддержки базовой функциональности ASP.NET MVC и ASP.NET Web API разработчики ASP.NET Core сумели добавить множество новых средств и улучшений в сравнении с предшествующими инфраструктурами. В дополнение к унификации инфраструктур и контроллеров появились следующие усовершенствования и инновации:
• встроенное внедрение зависимостей:
• система конфигурации, основанная на среде и готовая к взаимодействию с облачными технологиями;
• легковесный, высокопроизводительный и модульный конвейер запросов HTTP.
• вся инфраструктура основана на мелкозернистых пакетах NuGet;
• интеграция современных инфраструктур и рабочих потоков разработки для клиентской стороны;
• введение вспомогательных функций дескрипторов;
• введение компонентов представлений;
• огромные улучшения в плане производительности.
Внедрение зависимостей (dependency injection — DI) представляет собой механизм для поддержки слабой связанности между объектами. Вместо создания зависимых объектов напрямую или передачи специфических реализаций в классы и/или методы параметры определяются как интерфейсы. Таким образом, классам или методам и классам могут передаваться любые реализации интерфейсов, что разительно увеличивает гибкость приложения.
Поддержка DI — один из главных принципов, заложенных в переписанную версию ASP.NET Core. Все службы конфигурации и промежуточного ПО через внедрение зависимостей получает не только класс
Startup
(рассматриваемый позже в главе); ваши специальные классы могут (и должны) быть добавлены в контейнер DI с целью внедрения в другие части приложения. При конфигурировании элемента в контейнере ASP.NET Core DI доступны три варианта времени существования, кратко описанные в табл. 29.8.
Элементы в контейнере DI могут быть внедрены внутрь конструкторов и методов классов, а также в представления Razor.
На заметку! Если вы хотите использовать другой контейнер DI, то имейте в виду, что инфраструктура ASP.NET Core проектировалась с учетом такой гибкости. Чтобы узнать, как подключить другой контейнер DI, обратитесь в документацию по ссылке
https://docs.microsoft.com/ru-ru/aspnet/core/fundamentals/dependency-injection
.
Осведомленность приложений ASP. NET Core об их среде выполнения включает переменные среды хоста и местоположения файлов через экземпляр реализации
IWebHostEnvironment
. В табл. 29.9 описаны свойства, доступные в этом интерфейсе.
Помимо доступа к важным файловым путям интерфейс
IWebHostEnvironment
применяется для выяснения среды времени выполнения.
Инфраструктура ASP.NET Core автоматически читает значение переменной среды по имени
ASPNETCORE_ENVIRONMENT
, чтобы установить среду времени выполнения. Если переменная ASPNETCORE_ENVIRONMENT
не установлена, тогда ASP.NET Core устанавливает ее значение в Production
(производственная среда). Установленное значение доступно через свойство EnvironmentName
интерфейса IWebHostEnvironment
.
Во время разработки приложений ASP.NET Core переменная
ASPNETCORE_ENVIRONMENT
обычно устанавливается с использованием файла настроек или командной строки. Последовательно идущие среды (подготовительная, производственная и т.д.), как правило, задействуют стандартные переменные среды операционной системы.
Вы можете применять для среды любое имя или одно из трех имен, которые предоставляются статическим классом
Environments
.
public static class Environments
{
public static readonly string Development = "Development"; // среда разработки
public static readonly string Staging = "Staging"; // подготовительная среда
public static readonly string Production = "Production"; // производственная среда
}
Класс
HostEnvironmentEnvExtensions
предлагает расширяющие методы на IHostEnvironment
для работы со свойством имени среды, которые описаны в табл. 29.10.
Ниже перечислены некоторые примеры использования настройки среды:
• выяснение, какие конфигурационные файлы загружать:
• установка параметров отладки, ошибок и ведения журнала:
• загрузка файлов JavaScript и CSS, специфичных для среды.
Вы увидите все это в действии при построении приложений
AutoLot.Api
и AutoLot.Mvc
в последующих двух главах.
В предшествующих версиях ASP.NET для конфигурирования служб и приложений применялся файл
web.config
, и разработчики получали доступ к конфигурационным настройкам через класс System.Configuration
. Разумеется, помещение в файл web.config
всех конфигурационных настроек для сайта, а не только специфичных для приложения, делало его (потенциально) запутанной смесью.
В ASP.NET Core была введена значительно более простая система конфигурации. По умолчанию она основывается на простых файлах JSON, которые хранят конфигурационные настройки в виде пар "имя-значение". Стандартный файл для конфигурации называется
appsettings.json
. Начальная версия файла appsettings.json
(созданная шаблонами для веб-приложения ASP.NET Core и службы API) просто содержит конфигурационную информацию для регистрации в журнале, а также настройку для ограничения хостов:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*"
}
Шаблон также создает файл
appsettings.Development.json
. Система конфигурации работает в сочетании с осведомленностью о среде времени выполнения, чтобы загружать дополнительные конфигурационные файлы на основе среды времени выполнения. Цель достигается инструктированием системы конфигурации о необходимости загрузки файла с именем appsettings.{имя_среды}.json
после файла appSettings.json
. В случае запуска приложения в среде разработки после файла начальных настроек загружается файл appsettings.Development.json
. Если запуск происходит в подготовительной среде, тогда загружается файл appsettings.Staging.json
. Важно отметить, что при загрузке более одного файла любые настройки, присутствующие в нескольких файлах, переопределяются настройками из последнего загруженного файла; они не являются аддитивными. Все конфигурационные настройки получаются через экземпляр реализации IConfiguration
, доступный посредством системы внедрения зависимостей ASP.NET Core.
После построения конфигурации к настройкам можно обращаться с использованием традиционного семейства методов
GetXXX()
, таких как GetSection()
, GetValue()
и т.д.:
Configuration.GetSection("Logging")
Также доступно сокращение для получения строк подключения:
Configuration.GetConnectionString("AutoLot")
Дополнительные возможности конфигурации будут повсеместно применяться в оставшемся материале книги.
Приложения ASP.NET предшествующих версий могли развертываться только на серверах Windows с использованием IIS. Инфраструктуру ASP.NET Core можно разворачивать под управлением многочисленных операционных систем многими способами, в том числе и вне веб-сервера. Ниже перечислены высокоуровневые варианты:
• на сервере Windows (включая Azure) с применением IIS;
• на сервере Windows (включая службы приложений Azure) вне IIS;
• на сервере Linux с использованием Apache или NGINX;
• под управлением Windows или Linux в контейнере Docker.
Подобная гибкость позволяет организациям выбирать платформу развертывания, которая имеет набольший смысл для организации, включая популярные модели развертывания на основе контейнеров (скажем, Docker), и не ограничиваться серверами Windows.
Следуя принципам .NET Core, все в ASP.NET Core происходит по подписке. По умолчанию в приложение ничего не загружается. Такой подход позволяет приложениям быть насколько возможно легковесными, улучшая производительность и сводя к минимуму объем их кода и потенциальный риск.
Теперь, когда у вас есть опыт работы с рядом основных концепций ASP.NET Core, самое время приступить к построению приложений ASP.NET Core. Проекты ASP.NET Core можно создавать с применением либо Visual Studio, либо командной строки. Оба варианта будут раскрыты в последующих двух разделах.
Преимущество Visual Studio связано с наличием графического пользовательского интерфейса, который поможет вам пройти через процесс создания решения и проектов, добавления пакетов NuGet и создания ссылок между проектами.
Начните с создания нового проекта в Visual Studio. Выберите в диалоговом окне Create a new project (Создание нового проекта) шаблон C# под названием ASP.NET Core Web Application (Веб-приложение ASP.NET Core). В диалоговом окне Configure your new project (Конфигурирование нового проекта) введите
AutoLot.Api
в качестве имени проекта и AutoLot
для имени решения (рис. 29.1).
На следующем экране выберите шаблон ASP.NET Core Web API, а выше в раскрывающихся списках — .NET Core и ASP.NET Core 5.0. Оставьте флажки внутри области Advanced (Дополнительно) в их стандартном состоянии (рис. 29.2).
Добавьте в решение еще один проект ASP.NET Core Web Application, выбрав шаблон ASP.NET Core Web Арр (Model-View-Controller) (Веб-приложение ASP.NET Core (модель-представление-контроллер)). Удостоверьтесь в том, что в раскрывающихся списках вверху выбраны варианты .NET Core и ASP.NET Core 5.0; оставьте флажки внутри области Advanced в их стандартном состоянии.
Наконец, добавьте в решение проект C# Class Library (.NET Core) (Библиотека классов C# (.NET Core)) и назначьте ему имя
AutoLot.Services
. Отредактируйте файл проекта, чтобы установить TargetFramework
в net 5.0
:
net5.0
Решение требует завершенного уровня доступа к данным из главы 23. Вы можете либо скопировать файлы в каталог текущего решения, либо оставить их на месте. В любом случае вам нужно щелкнуть правой кнопкой мыши на имени решения в окне Solution Explorer, выбрать в контекстном меню пункт Add►Existing Project (Добавить►Существующий проект), перейти к файлу
AutoLot.Models.csproj
и выбрать его. Повторите такие же действия для проекта AutoLot.Dal
.
На заметку! Хотя порядок добавления проектов в решение формально не имеет значения, среда Visual Studio сохранит ссылки между
AutoLot.Models
и AutoLot.Dal
, если проект AutoLot.Models
добавляется первым.
Добавьте указанные ниже ссылки на проекты, щелкнув правой кнопкой на имени проекта в окне Solution Explorer и выбрав в контекстном меню пункт Add►Project Reference (Добавить►Ссылка на проект) для каждого проекта.
Проекты
AutoLot.Api
и AutoLot.Mvc
ссылаются на:
•
AutoLot.Models
•
AutoLot.Dal
•
AutoLot.Services
Проект
AutoLot.Services
ссылается на:
•
AutoLot.Models
•
AutoLot.Dal
Для приложения необходимы дополнительные пакеты
NuGet
.
Добавьте перечисленные ниже пакеты в проект
AutoLot.Api
:
•
AutoMapper
•
System.Text.Json
•
Swashbuckle.AspNetCore.Annotations
•
Swashbuckle.AspNetCore.Swagger
•
Swashbuckle.AspNetCore.SwaggerGen
•
Swashbuckle.AspNetCore.SwaggerUI
•
Microsoft.VisualStudio.Web.CodeGeneration.Design
•
Microsoft.EntityFrameworkCore.SqlServer
На заметку! Благодаря шаблонам ASP.NET Core 5.0 API ссылка на
Swashbuckle.AspNetCore
уже присутствует. Указанные здесь пакеты Swashbuckle
добавляют возможности за рамками базовой реализации.
Добавьте следующие пакеты в проект
AutoLot.Mvc
:
•
AutoMapper
•
System.Text.Json
•
LigerShark.WebOptimizer.Core
•
Microsoft.Web.LibraryManager.Build
•
Microsoft.VisualStudio.Web.CodeGeneration.Design
•
Microsoft.EntityFrameworkCore.SqlServer
Добавьте указанные ниже пакеты в проект
AutoLot.Services
:
•
Microsoft.Extensions.Hosting.Abstractions
•
Microsoft.Extensions.Options
•
Serilog.AspNetCore
•
Serilog.Enrichers.Environment
•
Serilog.Settings.Configuration
•
Serlog.Sinks.Console
•
Serilog.Sinks.File
•
Serilog.Sinks.MSSqlServer
•
System.Text.Json
Как было показано ранее в книге, проекты и решения .NET Core можно создавать с применением командной строки. Откройте окно командной строки и перейдите в каталог, куда вы хотите поместить решение.
На заметку! В приводимых далее командах используется разделитель каталогов Windows. Если вы работаете не в среде Windows, тогда должным образом скорректируйте разделитель.
Создайте решение
AutoLot
и добавьте в него существующие проекты AutoLot.Models
и AutoLot.Dal
:
rem Создать решение
dotnet new sln -n AutoLot
rem Добавить в решение проекты
dotnet sln AutoLot.sln add ..\Chapter_23\AutoLot.Models
dotnet sln AutoLot.sln add ..\Chapter_23\AutoLot.Dal
Создайте проект
AutoLot.Services
, добавьте его в решение, добавьте пакеты NuGet и добавьте ссылки на проекты:
rem Создать библиотеку классов для служб приложения и добавить ее в решение
dotnet new classlib -lang c# -n AutoLot.Services -o .\AutoLot.Services -f net5.0
dotnet sln AutoLot.sln add AutoLot.Services
rem Добавить пакеты
dotnet add AutoLot.Services package Microsoft.Extensions.Hosting.Abstractions
dotnet add AutoLot.Services package Microsoft.Extensions.Options
dotnet add AutoLot.Services package Serilog.AspNetCore
dotnet add AutoLot.Services package Serilog.Enrichers.Environment
dotnet add AutoLot.Services package Serilog.Settings.Configuration
dotnet add AutoLot.Services package Serilog.Sinks.Console
dotnet add AutoLot.Services package Serilog.Sinks.File
dotnet add AutoLot.Services package Serilog.Sinks.MSSqlServer
dotnet add AutoLot.Services package System.Text.Json
rem Добавить ссылки на проекты
dotnet add AutoLot.Services reference ..\Chapter_23\AutoLot.Models
dotnet add AutoLot.Services reference ..\Chapter_23\AutoLot.Dal
Создайте проект
AutoLot.Api
, добавьте его в решение, добавьте пакеты NuGet и добавьте ссылки на проекты:
dotnet new webapi -lang c# -n AutoLot.Api -au none -o .\AutoLot.Api -f net5.0
dotnet sln AutoLot.sln add AutoLot.Api
rem Добавить пакеты
dotnet add AutoLot.Api package AutoMapper
dotnet add AutoLot.Api package Swashbuckle.AspNetCore
dotnet add AutoLot.Api package Swashbuckle.AspNetCore.Annotations
dotnet add AutoLot.Api package Swashbuckle.AspNetCore.Swagger
dotnet add AutoLot.Api package Swashbuckle.AspNetCore.SwaggerGen
dotnet add AutoLot.Api package Swashbuckle.AspNetCore.SwaggerUI
dotnet add AutoLot.Api package Microsoft.VisualStudio.Web.CodeGeneration.Design
dotnet add AutoLot.Api package Microsoft.EntityFrameworkCore.SqlServer
dotnet add AutoLot.Api package System.Text.Json
rem Добавить ссылки на проекты
dotnet add AutoLot.Api reference ..\Chapter_23\AutoLot.Dal
dotnet add AutoLot.Api reference ..\Chapter_23\AutoLot.Models
dotnet add AutoLot.Api reference AutoLot.Services
Создайте проект
AutoLot.Mvc
, добавьте его в решение, добавьте пакеты NuGet и добавьте ссылки на проекты:
dotnet new mvc -lang c# -n AutoLot.Mvc -au none -o .\AutoLot.Mvc -f net5.0
dotnet sln AutoLot.sln add AutoLot.Mvc
rem Добавить ссылки на проекты
dotnet add AutoLot.Mvc reference ..\Chapter_23\AutoLot.Models
dotnet add AutoLot.Mvc reference ..\Chapter_23\AutoLot.Dal
dotnet add AutoLot.Mvc reference AutoLot.Services
rem Добавить пакеты
dotnet add AutoLot.Mvc package AutoMapper
dotnet add AutoLot.Mvc package System.Text.Json
dotnet add AutoLot.Mvc package LigerShark.WebOptimizer.Core
dotnet add AutoLot.Mvc package Microsoft.Web.LibraryManager.Build
dotnet add AutoLot.Mvc package Microsoft.EntityFrameworkCore.SqlServer
dotnet add AutoLot.Mvc package Microsoft.VisualStudio.Web.CodeGeneration.Design
На этом настройка с применением командной строки завершена. Она намного эффективнее, если вы не нуждаетесь в графическом пользовательском интерфейсе Visual Studio.
Веб-приложения предшествующих версий ASP.NET всегда запускались с использованием IIS (или IIS Express). В ASP.NET Core приложения обычно запускаются с применением веб-сервера Kestrel, но существует вариант использования IIS, Apache, Nginx и т.д. через обратный прокси-сервер между Kestrel и другим веб-сервером. В результате происходит не только отход в сторону от строгого применения IIS, чтобы сменить модель развертывания, но и изменение возможностей разработки.
Во время разработки приложения теперь можно запускать следующим образом:
• из Visual Studio с использованием IIS Express;
• из Visual Studio с применением Kestrel;
• из командной строки .NET CLI с использованием Kestrel;
• из Visual Studio Code (VS Code) через меню Run (Запуск) с применением Kestrel;
• из VS Code через окно терминала с использованием .NET CLI и Kestrel.
Файл
launchsettings.json
(расположенный в узле Properties (Свойства) окна Solution Explorer) конфигурирует способ запуска приложения во время разработки под управлением Kestrel и IIS Express. Ниже приведено его содержимое в справочных целях (ваши порты IIS Express будут другими):
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:42788",
"sslPort": 44375
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"AutoLot.Api": {
"commandName": "Project",
"dotnetRunMessages": "true",
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "https://localhost:5001;http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
В разделе
iisSettings
определены настройки запуска приложения с применением IIS Express в качестве веб-сервера. Наиболее важными настройками, на которые следует обратить внимание, являются настройка applicationUrl
, определяющая порт, и блок environmentVariables
, где определяется среда времени выполнения. При запуске приложения в режиме отладки эта настройка заменяет собой любую настройку среды машины. Второй профиль (AutoLot.Mvc
или AutoLot.Api
в зависимости от того, какой проект используется) определяет настройки для ситуации, когда приложение запускается с применением Kestrel в качестве веб-сервера. Профиль определяет applicationUrl
и порты плюс среду.
Меню Run в Visual Studio позволяет выбрать либо IIS Express, либо Kestrel, как показано на рис. 29.3. После выбора профиля проект можно запустить, нажав <F5> (режим отладки), нажав <Ctrl+F5> (эквивалентно выбору пункта Start Without Debugging (Запустить без отладки) в меню Debug (Отладка)) или щелкнув на кнопке запуска с зеленой стрелкой (эквивалентно выбору пункта Start Debugging (Запустить с отладкой) в меню Debug).
На заметку! В случае запуска приложения из Visual Studio средства редактирования и продолжения больше не поддерживаются.
Чтобы запустить приложение из командной строки или окна терминала VS Code, перейдите в каталог, где находится файл
.csproj
для вашего приложения. Введите следующую команду для запуска приложения под управлением веб-сервера Kestrel:
dotnet run
Для завершения процесса нажмите <Ctrl+C>.
При запуске из командной строки код можно изменять, но изменения никак не будут отражаться в выполняющемся приложении. Чтобы изменения отражались в выполняющемся приложении, введите такую команду:
dotnet watch run
Обновленная команда вместе с вашим приложением запускает средство наблюдения за файлами. Когда в файлах любого проекта (или проекта, на который имеется ссылка) обнаруживаются изменения, приложение автоматически останавливается и затем снова запускается. Нововведением в версии ASP.NET Core 5 является перезагрузка любых подключенных окон браузера. Хотя в итоге средства редактирования и продолжения в точности не воспроизводятся, это немалое удобство при разработке.
Чтобы запустить проекты в VS Code, откройте каталог, где находится решение. После нажатия <F5> (или щелчка на кнопке запуска) среда VS Code предложит выбрать проект для запуска (
AutoLot.Api
или AutoLot.Mvc
), создаст конфигурацию запуска и поместит ее в файл по имени launch.json
. Кроме того, среда VS Code использует файл launchsettings.json
для чтения конфигурации портов.
В случае запуска приложения из VS Code код можно изменять, но изменения никак не будут отражаться в выполняющемся приложении. Чтобы изменения отражались в выполняющемся приложении, введите в окне терминала команду
dotnet watch run
.
При запуске приложения из Visual Studio или VS Code отладка работает вполне ожидаемым образом. Но при запуске из командной строки вам необходимо присоединиться к выполняющемуся процессу, прежде чем вы сможете отлаживать свое приложение. В Visual Studio и VS Code это делается легко.
После запуска приложения (посредством команды
dotnet run
или dotnet watch run
) выберите пункт меню Debug►Attach to Process (Отладкам►Присоединиться к процессу) в Visual Studio. В открывшемся диалоговом окне Attach to Process (Присоединение к процессу) отыщите процесс по имени вашего приложения (рис. 29.4).
После присоединения к выполняющемуся процессу вы можете устанавливать в Visual Studio точки останова, и отладка будет работать так, как ожидалось. Редактировать и продолжать выполнение не удастся; чтобы изменения отразились в функционирующем приложении, придется отсоединиться от процесса.
После запуска приложения (командой
dotnet run
или dotnet watch run
) щелкните на кнопке запуска с зеленой стрелкой и выберите .NET Core Attach (Присоединение .NET Core) вместо .NET Core Launch (web) (Запуск .NET Core (веб)), как показано на рис. 29.5.
Когда вы щелкнете на кнопке запуска, вам будет предложено выбрать процесс для присоединения к нему. Выберите свое приложение. Затем можете устанавливать точки останова обычным образом.
Преимущество использования среды VS Code заключается в том, что после ее присоединения (и применения команды
dotnet watch run
) вы можете обновлять свой код во время выполнения (без необходимости в отсоединении) и вносимые изменения будут отражаться в функционирующем приложении.
Вы могли заметить, что приложения
AutoLot.Api
и AutoLot.Mvc
имеют разные порты, указанные для их профилей IIS Express, но для обоих приложений порты Kestrel установлены в 5000 (HTTP) и 5001 (HTTPS), что вызовет проблемы, когда вы попытаетесь запустить приложения вместе. Измените порты для AutoLot.Api
на 5020 (HTTP) и 5021 (HTTPS), например:
"AutoLot.Api": {
"commandName": "Project",
"launchBrowser": true,
"launchUrl": "api/values",
"applicationUrl": "https://localhost:5021;http://localhost:5020",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
В отличие от классических приложений ASP.NET MVC или ASP.NET Web API приложения ASP.NET Core — это просто консольные приложения .NET Core, которые создают и конфигурируют экземпляр
WebHost
. Создание экземпляра WebHost
и его последующее конфигурирование обеспечивают настройку приложения на прослушивание (и реагирование) на запросы HTTP. Экземпляр WebHost
создается в методе Main()
внутри файла Program.cs
. Затем экземпляр WebHost
конфигурируется для вашего приложения в файле Startup.cs
.
Откройте файл класса
Program.cs
в приложении AutoLot.Api
и просмотрите его содержимое, которое для справки приведено ниже:
namespace AutoLot.Api
{
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup();
});
}
}
Метод
CreateDefaultBuilder()
ужимает наиболее типовую настройку приложения в один вызов. Он конфигурирует приложение (используя переменные среды и JSON-файлы appsettings
), настраивает стандартный поставщик ведения журнала и устанавливает контейнер DI. Такая настройка обеспечивается шаблонами ASP.NET Core для приложений API и MVC.
Следующий метод,
ConfigureWebHostDefaults()
, тоже является мета-методом, который добавляет поддержку для Kestrel, IIS и дополнительные настройки. Финальный шаг представляет собой установку класса конфигурации, специфичной для приложения, который в данном примере (и по соглашению) называется Startup
. Наконец, вызывается метод Run()
для активизации экземпляра WebHost
.
Помимо экземпляра
WebHost
в предыдущем коде создается экземпляр реализации IConfiguration
, который добавляется внутрь контейнера DI.
Класс
Startup
конфигурирует то, как приложение будет обрабатывать запросы и ответы HTTP, настраивает необходимые службы и добавляет службы в контейнер DI. Имя класса может быть любым, если оно соответствует строке UseStartup()
в конфигурации метода CreateHostBuilder()
, но по соглашению класс именуется как Startup
.
Процессу запуска требуется доступ к инфраструктуре, а также к службам и настройкам среды, которые внедряются в класс инфраструктурой. Классу
Startup
доступно пять служб для конфигурирования приложения, которые кратко описаны в табл. 29.11.
Конструктор принимает экземпляр реализации
IConfiguration
и необязательный экземпляр реализации IWebHostEnvironment/IHostEnvironment
. Метод ConfigureServices()
запускается до того, как метод Configure()
получает экземпляр реализации IServiceCollection
. Метод Configure()
должен принимать экземпляр реализации IApplicationBuilder
, но может принимать экземпляры реализаций IWebHostEnvironment/IHostEnvironment
, ILoggerFactory
и любых интерфейсов, которые были добавлены внутрь контейнера DI в методе ConfigureServices()
. Все перечисленные компоненты обсуждаются в последующих разделах.
Конструктор принимает экземпляр реализации интерфейса
IConfiguration
, который был создан методом Host.CreateDefaultBuilder
в файле класса Program.cs
, и присваивает его свойству Configuration
для использования где-то в другом месте внутри класса. Конструктор также может принимать экземпляр реализации IWebHostEnvironment
и/или ILoggerFactory
, хотя он не добавляется в стандартном шаблоне.
Добавьте в конструктор параметр для
IWebHostEnvironment
и присвойте его локальной переменной уровня класса. Это понадобится в методе ConfigureServices()
. Проделайте такую же работу для приложений AutoLot.Api
и AutoLot.Mvc
.
private readonly IWebHostEnvironment _env;
public Startup(
IConfiguration configuration, IWebHostEnvironment env)
{
_env = env;
Configuration = configuration;
}
Метод
ConfigureServices()
применяется для конфигурирования любых служб, необходимых приложению, и вставки их в контейнер DI. Сюда входят службы, требуемые для поддержки приложений MVC и служб API.
Метод
ConfigureServices()
для API-интерфейса AutoLot
по умолчанию конфигурируется с только одной службой, которая добавляет поддержку контроллеров. Благодаря этому мета-методу добавляется множество дополнительных служб, в том числе маршрутизация, авторизация, привязка моделей и все элементы, не относящиеся к пользовательскому интерфейсу, которые уже обсуждались в настоящей главе.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
}
Метод
AddControllers()
может быть расширен, например, для настройки обработки JSON. По умолчанию для ASP.NET Core используется "верблюжий" стиль при обработке JSON (первая буква в нижнем регистре, а каждое последующее слово начинается с буквы верхнего регистра, скажем, carRepo
). Это соответствует большинству инфраструктур производства не Microsoft, которые применяются для разработки веб-приложений. Однако в предшествующих версиях ASP.NET использовался стиль Pascal (например, CarRepo
). Переход на "верблюжий" стиль был критическим изменением для многих приложений, которые ожидали стиля Pascal. Чтобы вернуть стиль Pascal при обработке JSON приложением (и улучшить форматирование разметки JSON), модифицируйте метод AddControllers()
следующим образом:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers()
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.PropertyNamingPolicy = null;
options.JsonSerializerOptions.WriteIndented = true;
});
}
Добавьте в файл
Startup.cs
перечисленные ниже операторы using
:
using AutoLot.Dal.EfStructures;
using AutoLot.Dal.Initialization;
using AutoLot.Dal.Repos;
using AutoLot.Dal.Repos.Interfaces;
using Microsoft.EntityFrameworkCore;
Службам API необходим доступ к
ApplicationDbContext
и хранилищам внутри уровня доступа к данным. Существует встроенная поддержка для добавления EF Core в приложения ASP.NET Core. Добавьте следующий код в метод ConfigureServices()
класса Startup
:
var connectionString = Configuration.GetConnectionString("AutoLot");
services.AddDbContextPool(
options => options.UseSqlServer(connectionString,
sqlOptions => sqlOptions.EnableRetryOnFailure()));
Первая строка кода получает строку подключения из файла настроек (более подробно рассматривается позже). Следующая строка добавляет в контейнер DI пул экземпляров
ApplicationDbContext
. Во многом подобно пулу подключений пул ApplicationDbContext
может улучшить показатели производительности за счет наличия заранее установленных экземпляров, ожидающих потребления. Когда нужен контекст, он загружается из пула. По окончании его использования он очищается от любых следов применения и возвращается в пул.
Теперь необходимо добавить хранилища в контейнер DI. Вставьте в метод
ConfigureServices()
приведенный далее код после кода для конфигурирования ApplicationDbContext
:
services.AddScoped();
services.AddScoped();
services.AddScoped();
services.AddScoped();
services.AddScoped();
Модифицируйте файл
appsettings.development.json
, как показано ниже, добавив строку подключения к базе данных. Обязательно включите запятую, отделяющую разделы, и приведите строку подключения в соответствие со своей средой.
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"ConnectionStrings": {
"AutoLot": "Server=.,5433;Database=AutoLotFinal;
User ID=sa;Password=P@ssw0rd;"
}
}
Как обсуждалось ранее, каждый конфигурационный файл именуется согласно среде, что позволяет разносить значения, специфичные к среде, по разным файлам. Добавьте в проект новый файл по имени
appsettings.production.json
и обновите его следующим образом:
{
"ConnectionStrings": {
"AutoLot": "ITSASECRET"
}
}
Это предохраняет реальную строку подключения от системы управления версиями и делает возможным замену маркера (
ITSASECRET
) в течение процесса разработки.
Метод
ConfigureServices()
для веб-приложений MVC добавляет базовые службы для приложений API и поддержку визуализации представлений. Вместо вызова AddControllers()
в приложениях MVC вызывается AddControllersWithViews()
:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
}
Добавьте в файл Startup.es показанные ниже операторы using:
using AutoLot.Dal.EfStructures;
using AutoLot.Dal.Initialization;
using AutoLot.Dal.Repos;
using AutoLot.Dal.Repos.Interfaces;
using Microsoft.EntityFrameworkCore;
Веб-приложение также должно использовать уровень доступа к данным. Добавьте в метод
ConfigureServices()
класса Startup
следующий код:
var connectionString = Configuration.GetConnectionString("AutoLot");
services.AddDbContextPool(
options => options.UseSqlServer(connectionString,
sqlOptions => sqlOptions.EnableRetryOnFailure()));
services.AddScoped();
services.AddScoped();
services.AddScoped();
services.AddScoped();
services.AddScoped();
На заметку! Веб-приложение MVC будет работать как с уровнем доступа к данным, так и с API-интерфейсом для взаимодействия с данными, чтобы продемонстрировать оба механизма.
Модифицируйте файл
appsettings.development.json
, как показано ниже:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"ConnectionStrings": {
"AutoLot": "Server=.,5433;Database=AutoLotFinal;
User ID=sa;Password=P@ssw0rd;"
}
}
Метод
Configure()
применяется для настройки приложения на реагирование на запросы HTTP. Данный метод выполняется после метода ConfigureServices()
, т.е. все, что добавлено в контейнер DI, также может быть внедрено в Configure()
. Существуют различия в том, как приложения API и MVC конфигурируются для обработки запросов и ответов HTTP в конвейере.
Внутри стандартного шаблона выполняется проверка среды, и если она установлена в
Development
(среда разработки), тогда в конвейер обработки добавляется промежуточное ПО UseDeveloperExceptionPage()
, предоставляющее отладочную информацию, которую вы вряд ли захотите отображать в производственной среде. Далее производится вызов UseHttpsRedirection()
для перенаправления всего трафика на HTTPS (вместо HTTP). Затем добавляются вызовы арр.UseRouting()
, арр.UseAuthorization()
и арр.UseEndpoints()
. Вот полный код метода:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
// Если среда разработки, тогда отображать отладочную информацию.
app.UseDeveloperExceptionPage();
// Первоначальный код.
app.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json",
"AutoLot.Api v1"));
}
// Перенаправить трафик HTTP на HTTPS.
app.UseHttpsRedirection();
// Включить маршрутизацию.
app.UseRouting();
// Включить проверки авторизации.
app.UseAuthorization();
// Включить маршрутизацию с использованием конечных точек.
// Использовать для контроллеров маршрутизацию с помощью атрибутов.
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
Кроме того, когда приложение запускается в среде разработки, необходимо инициализировать базу данных. Добавьте в метод
Configure()
параметр типа ApplicationDbContext
и вызовите метод InitializeData()
из AutoLot.Dal
.
Ниже показан модифицированный код:
public void Configure(
IApplicationBuilder app,
IWebHostEnvironment env,
ApplicationDbContext context)
{
if (env.IsDevelopment())
{
// Если среда разработки, тогда отображать отладочную информацию.
app.UseDeveloperExceptionPage();
// Инициализировать базу данных.
if (Configuration.GetValue("RebuildDataBase"))
{
SampleDataInitializer.InitializeData(context);
}
}
...
}
Обновите файл
appsettings.development.json
с учетом свойства RebuildDataBase
(пока что установив его в false
):
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"RebuildDataBase": false,
"ConnectionStrings": {
"AutoLot": "Server=db;Database=AutoLotPresentation;
User ID=sa;Password=P@ssw0rd;"
}
}
Метод
Configure()
для веб-приложений немного сложнее, чем его аналог для API. Ниже приведен полный код метода с последующим обсуждением:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
Метод
Configure()
также проверяет среду, и если она установлена в Development
(среда разработки), тогда в конвейер обработки добавляется промежуточное ПО UseDeveloperExceptionPage()
. Для любой другой среды в конвейер обработки добавляется универсальное промежуточное ПО UseExceptionHandler()
и поддержка протокола строгой транспортной безопасности HTTP (HTTP Strict Transport Security — HSTS). Как и в аналоге для API, добавляется вызов app.UseHttpsRedirection()
. Следующим шагом является добавление поддержки статических файлов с помощью вызова app.UseStaticFiles()
. Поддержка статических файлов включается как мера по усилению безопасности. Если ваше приложение в ней не нуждается (подобно API-интерфейсам), тогда не добавляйте такую поддержку. Затем добавляется промежуточное ПО для маршрутизации, авторизации и конечных точек.
Добавьте в метод параметр типа
АрplicationDbContext
и вызовите InitializeData()
из AutoLot.Dal
. Вот модифицированный код:
public void Configure(
IApplicationBuilder app,
IWebHostEnvironment env,
ApplicationDbContext context)
{
if (env.IsDevelopment())
{
// Если среда разработки, тогда отображать отладочную информацию.
app.UseDeveloperExceptionPage();
// Инициализировать базу данных.
if (Configuration.GetValue("RebuildDataBase"))
{
SampleDataInitializer.InitializeData(context);
}
}
...
}
Обновите файл
appsettings.development.json
с учетом свойства RebuildDataBase
(пока что установив его в false
):
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"RebuildDataBase": false,
"ConnectionStrings": {
"AutoLot": "Server=db;Database=AutoLotPresentation;
User ID=sa;Password=P@ssw0rd;"
}
}
Стандартный шаблон настраивает в методе
UseEndpoints()
маршрутизацию на основе соглашений. Ее понадобится отключить и повсюду в приложении применять маршрутизацию с помощью атрибутов. Закомментируйте (или удалите) вызов MapControllerRoute()
и замените его вызовом MapControllers()
:
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
Далее добавьте атрибуты маршрутов к
HomeController
в приложении AutoLot.Mvc
. Первым делом добавьте шаблон контроллер/действие к самому контроллеру:
[Route("[controller]/[action]")]
public class HomeController : Controller
{
...
}
Затем добавьте три маршрута к методу
Index()
, так что он будет стандартным действием, когда не указано действие либо когда не указан контроллер или действие. Кроме того, снабдите метод атрибутом HttpGet
, чтобы явно объявить его действием GET
:
[Route("/")]
[Route("/[controller]")]
[Route("/[controller]/[action]")]
[HttpGet]
public IActionResult Index()
{
return View();
}
Базовая инфраструктура ведения журнала добавляется в контейнер DI как часть процесса запуска и конфигурирования. Инфраструктура ведения журнала использует довольно простой интерфейс
ILogger
. Основополагающим компонентом ведения журнала является класс LoggerExtensions
, определения методов которого показаны ниже:
public static class LoggerExtensions
{
public static void LogDebug(this ILogger logger, EventId eventId,
Exception exception, string message, params object[] args)
public static void LogDebug(this ILogger logger, EventId eventId,
string message, params
object[] args)
public static void LogDebug(this ILogger logger, Exception exception,
string message,
params object[] args)
public static void LogDebug(this ILogger logger,
string message, params object[] args)
public static void LogTrace(this ILogger logger, EventId eventId,
Exception exception, string message, params object[] args)
public static void LogTrace(this ILogger logger, EventId eventId,
string message, params
object[] args)
public static void LogTrace(this ILogger logger, Exception exception,
string message,
params object[] args)
public static void LogTrace(this ILogger logger,
string message, params object[] args)
Exception exception, string message, params object[] args)
public static void LogInformation(this ILogger logger, EventId eventId,
string message,
params object[] args)
public static void LogInformation(this ILogger logger, Exception exception,
string
message, params object[] args)
public static void LogInformation(this ILogger logger,
string message, params object[] args)
public static void LogWarning(this ILogger logger, EventId eventId,
Exception exception, string message, params object[] args)
public static void LogWarning(this ILogger logger, EventId eventId,
string message, params
object[] args)
public static void LogWarning(this ILogger logger, Exception exception,
string message,
params object[] args)
public static void LogWarning(this ILogger logger,
string message, params object[] args)
public static void LogError(this ILogger logger, EventId eventId,
Exception exception, string message, params object[] args)
public static void LogError(this ILogger logger, EventId eventId,
string message, params
object[] args)
public static void LogError(this ILogger logger, Exception exception,
string message,
params object[] args)
public static void LogError(this ILogger logger,
string message, params object[] args)
public static void LogCritical(this ILogger logger, EventId eventId,
Exception exception, string message, params object[] args)
public static void LogCritical(this ILogger logger, EventId eventId,
string message,
params object[] args)
public static void LogCritical(this ILogger logger, Exception exception,
string message,
params object[] args)
public static void LogCritical(this ILogger logger,
string message, params object[] args)
public static void Log(this ILogger logger, LogLevel logLevel,
string message, params
object[] args)
public static void Log(this ILogger logger, LogLevel logLevel, EventId eventId,
string
message, params object[] args)
public static void Log(this ILogger logger, LogLevel logLevel,
Exception exception, string message, params object[] args)
public static void Log(this ILogger logger, LogLevel logLevel, EventId eventId,
Exception exception, string message, params object[] args)
}
Яркая характеристика ASP.NET Core связана с расширяемостью конвейера в целом и с ведением журнала в частности. Стандартное средство ведения журнала может быть заменено другой инфраструктурой ведения журнала при условии, что новая инфраструктура способна интегрироваться с установленным шаблоном ведения журнала. Serilog — одна из инфраструктур, которая хорошо интегрируется с ASP.NET Core. В последующих разделах демонстрируется создание инфраструктуры ведения журнала, основанной на Serilog, и конфигурирование приложений ASP.NET Core для использования нового кода регистрации в журнале.
Начните с добавления в проект
AutoLot.Services
нового каталога по имени Logging
. Добавьте в этот каталог новый файл под названием IAppLogging.cs
для интерфейса IAppLogging
. Приведите содержимое файла IAppLogging.cs
к следующему виду:
using System;
using System.Runtime.CompilerServices;
namespace AutoLot.Services.Logging
{
public interface IAppLogging
{
void LogAppError(Exception exception, string message,
[CallerMemberName] string memberName = "",
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0);
void LogAppError(string message,
[CallerMemberName] string memberName = "",
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0);
void LogAppCritical(Exception exception, string message,
[CallerMemberName] string memberName = "",
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0);
void LogAppCritical(string message,
[CallerMemberName] string memberName = "",
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0);
void LogAppDebug(string message,
[CallerMemberName] string memberName = "",
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0);
void LogAppTrace(string message,
[CallerMemberName] string memberName = "",
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0);
void LogAppInformation(string message,
[CallerMemberName] string memberName = "",
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0);
void LogAppWarning(string message,
[CallerMemberName] string memberName = "",
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0);
}
}
Атрибуты
CallerMemberName
, CallerFilePath
и CallerLineNumber
инспектируют стек вызовов для получения имени члена, пути к файлу и номера строки в вызывающем коде. Например, если строка, в которой вызывается LogAppWarning()
, находится в функции DoWork()
внутри файла по имени MyClassFile.cs
, а номер этой строки 36, тогда вызов:
_appLogger.LogAppWarning("A warning");
преобразуется в следующий эквивалент:
_appLogger.LogAppWarning ("A warning","DoWork","c:/myfilepath/MyClassFile.cs",36);
Если методу при вызове передаются значения, тогда переданные значения используются вместо значений из атрибутов.
Класс
AppLogging
реализует интерфейс IAppLogging
. Добавьте новый класс по имени AppLogging
и модифицируйте операторы using
, как показано ниже:
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Serilog.Context;
Сделайте класс открытым и реализующим интерфейс
IAppLogging
. Добавьте конструктор, который принимает экземпляр реализации ILogger
(интерфейс, поддерживаемый напрямую ASP.NET Core) и экземпляр реализации IConfiguration
. Внутри конструктора получите доступ к конфигурации, чтобы извлечь имя приложения из файла настроек. Все три элемента (ILogger
, IConfiguration
и имя приложения) необходимо сохранить в переменных уровня класса.
namespace AutoLot.Services.Logging
{
public class AppLogging : IAppLogging
{
private readonly ILogger _logger;
private readonly IConfiguration _config;
private readonly string _applicationName;
public AppLogging(ILogger logger, IConfiguration config)
{
_logger = logger;
_config = config;
_applicationName = config.GetValue("ApplicationName");
}
}
}
Инфраструктура Serilog позволяет добавлять свойства в стандартный процесс ведения журнала, заталкивая их внутрь
LogContext
. Добавьте внутренний метод для заталкивания свойств MemberName
, FilePath
, LineNumber
и ApplicationName
:
internal List PushProperties(
string memberName,
string sourceFilePath,
int sourceLineNumber)
{
List list = new List
{
LogContext.PushProperty("MemberName", memberName),
LogContext.PushProperty("FilePath", sourceFilePath),
LogContext.PushProperty("LineNumber", sourceLineNumber),
LogContext.PushProperty("ApplicationName", _applicationName)
};
return list;
}
Каждая реализация метода следует одному и тому же процессу. На первом шаге вызывается метод
PushProperties()
для добавления дополнительных свойств и затем соответствующий метод регистрации в журнале, предоставляемый LoggerExtensions
в ILogger
. Ниже приведены все реализованные методы интерфейса:
public void LogAppError(Exception exception, string message,
[CallerMemberName] string memberName = "",
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0)
{
var list = PushProperties(memberName, sourceFilePath, sourceLineNumber);
_logger.LogError(exception, message);
foreach (var item in list)
{
item.Dispose();
}
}
public void LogAppError(string message,
[CallerMemberName] string memberName = "",
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0)
{
var list = PushProperties(memberName, sourceFilePath, sourceLineNumber);
_logger.LogError(message);
foreach (var item in list)
{
item.Dispose();
}
}
public void LogAppCritical(Exception exception, string message,
[CallerMemberName] string memberName = "",
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0)
{
var list = PushProperties(memberName, sourceFilePath, sourceLineNumber);
_logger.LogCritical(exception, message);
foreach (var item in list)
{
item.Dispose();
}
}
public void LogAppCritical(string message,
[CallerMemberName] string memberName = "",
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0)
{
var list = PushProperties(memberName, sourceFilePath, sourceLineNumber);
_logger.LogCritical(message);
foreach (var item in list)
{
item.Dispose();
}
}
public void LogAppDebug(string message,
[CallerMemberName] string memberName = "",
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0)
{
var list = PushProperties(memberName, sourceFilePath, sourceLineNumber);
_logger.LogDebug(message);
foreach (var item in list)
{
item.Dispose();
}
}
public void LogAppTrace(string message,
[CallerMemberName] string memberName = "",
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0)
{
var list = PushProperties(memberName, sourceFilePath, sourceLineNumber);
_logger.LogTrace(message);
foreach (var item in list)
{
item.Dispose();
}
}
public void LogAppInformation(string message,
[CallerMemberName] string memberName = "",
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0)
{
var list = PushProperties(memberName, sourceFilePath, sourceLineNumber);
_logger.LogInformation(message);
foreach (var item in list)
{
item.Dispose();
}
}
public void LogAppWarning(string message,
[CallerMemberName] string memberName = "",
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0)
{
var list = PushProperties(memberName, sourceFilePath, sourceLineNumber);
_logger.LogWarning(message);
foreach (var item in list)
{
item.Dispose();
}
}
Начните с замены стандартного средства ведения журнала инфраструктурой Serilog, добавив новый класс по имени
LoggingConfiguration
в каталог Logging
проекта AutoLot.Services
. Модифицируйте операторы using
и сделайте класс открытым и статическим:
using System;
using System.Collections.Generic;
using System.Data;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Serilog;
using Serilog.Events;
using Serilog.Sinks.MSSqlServer;
namespace AutoLot.Services.Logging
{
public static class LoggingConfiguration
{
}
}
Для записи в различные целевые объекты для ведения журналов инфраструктура Serilog использует приемники (sink). Целевыми объектами, которые будут применяться для ведения журнала в приложениях ASP.NET Core, являются текстовый файл, база данных и консоль. Приемники типа текстового файла и базы данных требуют конфигурации — выходного шаблона для текстового файла и списка полей для базы данных. Чтобы настроить выходной шаблон, создайте следующее статическое строковое поле, допускающее только чтение:
private static readonly string OutputTemplate =
@"[{TimeStamp:yy-MM-dd HH:mm:ss} {Level}]{ApplicationName}:
{SourceContext}{NewLine}
Message:{Message}{NewLine}in method
{MemberName} at {FilePath}:{LineNumber}{NewLine}
{Exception}{NewLine}";
Приемник SQL Server нуждается в списке столбцов, идентифицированных с использованием типа
SqlColumn
. Добавьте показанный далее код для конфигурирования столбцов базы данных:
private static readonly ColumnOptions ColumnOptions = new ColumnOptions
{
AdditionalColumns = new List
{
new SqlColumn {DataType = SqlDbType.VarChar, ColumnName = "ApplicationName"},
new SqlColumn {DataType = SqlDbType.VarChar, ColumnName = "MachineName"},
new SqlColumn {DataType = SqlDbType.VarChar, ColumnName = "MemberName"},
new SqlColumn {DataType = SqlDbType.VarChar, ColumnName = "FilePath"},
new SqlColumn {DataType = SqlDbType.Int, ColumnName = "LineNumber"},
new SqlColumn {DataType = SqlDbType.VarChar, ColumnName = "SourceContext"},
new SqlColumn {DataType = SqlDbType.VarChar, ColumnName = "RequestPath"},
new SqlColumn {DataType = SqlDbType.VarChar, ColumnName = "ActionName"},
}
};
Замена стандартного средства ведения журнала вариантом Serilog представляет собой процесс из трех шагов. Первый шаг — очистка существующего поставщика, второй — добавление Serilog в
HostBuildern
третий — завершение конфигурирования Serilog. Добавьте новый метод по имени ConfigureSerilog()
, который является расширяющим методом для IHostBuilder
:
public static IHostBuilder ConfigureSerilog(this IHostBuilder builder)
{
builder
.ConfigureLogging((context, logging) => { logging.ClearProviders(); })
.UseSerilog((hostingContext, loggerConfiguration) =>
{
var config = hostingContext.Configuration;
var connectionString = config.GetConnectionString("AutoLot").ToString();
var tableName = config["Logging:MSSqlServer:tableName"].ToString();
var schema = config["Logging:MSSqlServer:schema"].ToString();
string restrictedToMinimumLevel =
config["Logging:MSSqlServer:restrictedToMinimumLevel"].ToString();
if (!Enum.TryParse(restrictedToMinimumLevel, out var logLevel))
{
logLevel = LogEventLevel.Debug;
}
LogEventLevel level = (LogEventLevel)Enum.Parse(typeof(LogEventLevel),
restrictedToMinimumLevel);
var sqlOptions = new MSSqlServerSinkOptions
{
AutoCreateSqlTable = false,
SchemaName = schema,
TableName = tableName,
};
if (hostingContext.HostingEnvironment.IsDevelopment())
{
sqlOptions.BatchPeriod = new TimeSpan(0, 0, 0, 1);
sqlOptions.BatchPostingLimit = 1;
}
loggerConfiguration
.Enrich.FromLogContext()
.Enrich.WithMachineName()
.WriteTo.File(
path: "ErrorLog.txt",
rollingInterval: RollingInterval.Day,
restrictedToMinimumLevel: logLevel,
outputTemplate: OutputTemplate)
.WriteTo.Console(restrictedToMinimumLevel: logLevel)
.WriteTo.MSSqlServer(
connectionString: connectionString,
sqlOptions,
restrictedToMinimumLevel: level,
columnOptions: ColumnOptions);
});
return builder;
}
Теперь, когда все готово, пора заменить стандартное средство ведения журнала на Serilog.
Раздел
Logging
во всех файлах настроек приложения (appsettings.json
, appsettings.development.json
и appsettings.production
) для проектов AutoLot.Api
и AutoLot.Dal
потребуется модифицировать с учетом новой информации о ведении журнала и добавить имя приложения.
Откройте файлы
appsettings.json
и обновите размертку JSON, как показано ниже; удостоверьтесь в том, что применяете корректное имя проекта в узле ApplicationName
и указываете строку подключения, соответствующую вашей системе:
// appsettings.json
{
"Logging": {
"MSSqlServer": {
"schema": "Logging",
"tableName": "SeriLogs",
"restrictedToMinimumLevel": "Warning"
}
},
"ApplicationName": "AutoLot.Api",
"AllowedHosts": "*"
}
// appsettings.development.json
{
"Logging": {
"MSSqlServer": {
"schema": "Logging",
"tableName": "SeriLogs",
"restrictedToMinimumLevel": "Warning"
}
},
"RebuildDataBase": false,
"ApplicationName": "AutoLot.Api - Dev",
"ConnectionStrings": {
"AutoLot": "Server=.,5433;Database=AutoLot;User ID=sa;Password=P@ssw0rd;"
}
}
// appsettings.production.json
{
"Logging": {
"MSSqlServer": {
"schema": "Logging",
"tableName": "SeriLogs",
"restrictedToMinimumLevel": "Warning"
}
},
"RebuildDataBase": false,
"ApplicationName": "AutoLot.Api - Prod",
"ConnectionStrings": {
"AutoLot": "It's a secret"
}
}
Добавьте в файлы Program.cs в проектах
AutoLot.Api
и AutoLot.Mvc
следующий оператор using
:
using AutoLot.Services.Logging;
Модифицируйте метод
CreateHostBuilder()
в обоих проектах, как показано ниже:
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup();
}).ConfigureSerilog();
Добавьте в файлы
Startup.cs
в проектах AutoLot.Api
и AutoLot.Mvc
следующий оператор using
:
using AutoLot.Services.Logging;
Затем необходимо поместить новые интерфейсы ведения журнала в контейнер DI. Добавьте в метод
ConfigureServices()
в обоих проектах такой код:
services.AddScoped(typeof(IAppLogging<>), typeof(AppLogging<>));
Следующее обновление связано с заменой ссылок на
ILogger
ссылками на IAppLogging
. Начните с класса WeatherForecastController
в проекте AutoLot.Api
. Добавьте в класс следующий оператор using
:
using AutoLot.Services.Logging;
Далее измените
ILogger
на IAppLogging
:
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
...
private readonly IAppLogging _logger;
public WeatherForecastController(IAppLogging logger)
{
_logger = logger;
}
...
}
Теперь модифицируйте
HomeController
в проекте AutoLot.Mvc
. Добавьте в класс следующий оператор using
:
using AutoLot.Services.Logging;
Измените
ILogger
на IAppLogging
:
[Route("[controller]/[action]")]
public class HomeController : Controller
{
private readonly IAppLogging _logger;
public HomeController(IAppLogging logger)
{
_logger = logger;
}
...
}
После этого регистрация в журнале выполняется в каждом контроллере простым обращением к средству ведения журнала, например:
// WeatherForecastController.cs (AutoLot.Api)
[HttpGet]
public IEnumerable Get()
{
_logger.LogAppWarning("This is a test");
...
}
// HomeController.cs (AutoLot.Mvc)
[Route("/")]
[Route("/[controller]")]
[Route("/[controller]/[action]")]
[HttpGet]
public IActionResult Index()
{
_logger.LogAppWarning("This is a test");
return View();
}
Имея установленную инфраструктуру Serilog, самое время протестировать ведение журналов для приложений. Если вы используете Visual Studio, тогда укажите
AutoLot.Mvc
в качестве стартового проекта (щелкните правой кнопкой мыши на имени проекта в окне Solution Explorer, выберите в контекстном меню пункт Set as Startup Project (Установить как стартовый проект) и щелкните на кнопке запуска с зеленой стрелкой или нажмите <F5>). В случае работы в VS Code откройте окно терминала (<Ctrl+'>), перейдите в каталог AutoLot.Mvc
и введите команду dotnet run
.
В Visual Studio автоматически запустится браузер с представлением
Home/Index
. Если вы применяете VS Code, то вам понадобится открыть браузер и перейти по ссылке https://localhost:5001
. После загрузки вы можете закрыть браузер, поскольку обращение к средству ведения журнала произошло при загрузке домашней страницы. Закрытие браузера в случае использования Visual Studio останавливает отладку. Чтобы остановить отладку в VS Code, нажмите <Ctrl+C> в окне терминала.
В каталоге проекта вы увидите файл по имени
ErrorLogГГГMMДД.txt
, в котором обнаружите запись, похожую на показанную ниже:
[ГГ-ММ-ДД чч:мм:сс Warning]AutoLot.Mvc -
Dev:AutoLot.Mvc.Controllers.HomeController
Message:This is a test
in method Index at
D:\Projects\Books\csharp9-wf\Code\New\Chapter_29\AutoLot.Mvc\Controllers\
HomeController.cs:30
Для тестирования кода регистрации в журнале в проекте
AutoLot.Api
установите этот проект в качестве стартового (Visual Studio) или перейдите в каталог AutoLot.Api
в окне терминала (VS Code). Нажмите <F5> или введите dotnet run
и перейдите по ссылке https://localhost:44375/swagger/index.html
. В итоге загрузится страница Swagger для приложения API (рис. 29.6).
Щелкните на кнопке GET для записи
WeatherForecast
. В результате откроется экран с деталями для этого метода действия, включая возможность Try it out (Опробовать), как видно на рис. 29.7.
После щелчка на кнопке Try it out щелкните на кнопке Execute (Выполнить), которая обеспечивает обращение к конечной точке (рис. 29.8).
В каталоге проекта
AutoLot.Api
вы снова увидите файл по имени ErrorLogГГГГММДД.txt
и найдете в нем примерно такую запись:
[ГГ-ММ-ДД чч:мм:сс Warning]AutoLot.Api -
Dev:AutoLot.Api.Controllers.
WeatherForecastController
Message:This is a test
in method Get at
D:\Projects\Books\csharp9-wf\Code\New\Chapter_29\AutoLot.Api\Controllers\
WeatherForecastController.cs:30
На заметку! Нововведением в версии ASP.NET Core 5 является то, что Swagger по умолчанию включается в шаблон API. Инструменты Swagger будут подробно исследованы в следующей главе.
В главе была представлена инфраструктура ASP.NET Core. Глава начиналась с краткого обзора истории появления ASP.NET, после чего были рассмотрены функциональные средства из классических инфраструктур ASP.NET MVC и ASP.NET Web API, которые присутствуют в ASP.NET Core.
Далее вы узнали о новых средствах ASP.NET Core и о том, как они работают. После изучения различных способов запуска и отладки приложений ASP.NET Core вы создали решение с двумя проектами ASP.NET Core — для общей библиотеки прикладных служб и для уровня доступа к данным AutoLot (из главы 23). Наконец, вы заменили в обоих проектах стандартное средство ведения журнала ASP.NET Core инфраструктурой Serilog.
В следующей главе приложение
AutoLot.Api
будет завершено.
В предыдущей главе была представлена инфраструктура ASP.NET Core, обсуждались ее новые возможности, были созданы проекты, а также обновлен код в AutoLot.Mvc и
AutoLot.Api
для включения AutoLot.Dal
и ведения журнала Serilog.
Внимание в текущей главе будет сосредоточено на завершении работы над REST-службой
AutoLot.Api
.
На заметку! Исходный код, рассматриваемый в этой главе, находится в папке
Chapter_30
внутри хранилища GitHub для настоящей книги. Вы также можете продолжить работу с решением, начатым в главе 29.
Инфраструктура ASP.NET MVC начала набирать обороты почти сразу после своего выхода, а в составе версий ASP.NET MVC 4 и Visual Studio 2012 компания Microsoft выпустила ASP.NET Web API. Версия ASP.NET Web API 2 вышла вместе c Visual Studio 2013 и затем с выходом Visual Studio 2013 Update 1 была модернизирована до версии 2.2.
Продукт ASP.NETWeb API с самого начала разрабатывался как основанная на службах инфраструктура для построения служб REST (REpresentational State Transfer — передача состояния представления), которая базируется на инфраструктуре MVC минус "V" (представление) с рядом оптимизаций, направленных на создание автономных служб. Такие службы могут вызываться с применением любой технологии, а не только тех, которые производит Microsoft. Обращения к службе Web API основаны на базовых HTTP-методах (
GET
, PUT
, POST
, DELETE
) осуществляются через универсальный идентификатор ресурса (uniform resource identifier — URI), например:
http://www.skimedic.com:5001/api/cars
Он похож на унифицированный указатель ресурса (uniform resource locator — URL), поскольку таковым и является! Указатель URL — это просто идентификатор URI, который указывает на физический ресурс в сети.
При вызове служб Web API используется схема HTTP (Hypertext Transfer Protocol — протокол передачи гипертекста) на конкретном хосте (в приведенном выше примере
www.skimedic.com
) и специфическом порте (5001), за которыми указывается путь (api/cars
), а также необязательные запрос и фрагмент (в примере отсутствуют). Обращение к службе Web API может также содержать текст в теле сообщения, как вы увидите далее в этой главе. Из предыдущей главы вы узнали, что ASP.NET Core объединяет Web API и MVC в одну инфраструктуру.
Вспомните, что действия возвращают тип
IActionResult
(или Task
для асинхронных операций). Кроме вспомогательных методов в ControllerBase
, возвращающих специфические коды состояния HTTP методы действий способны возвращать содержимое как ответы в формате JSON (JavaScript Object Notation — запись объектов JavaScript).
На заметку! Строго говоря, методы действий могут возвращать широкий диапазон форматов. Формат JSON рассматривается в книге из-за своей популярности.
Большинство служб REST получают и отправляют данные клиентам с применением формата JSON. Ниже приведен простой пример данных JSON, состоящих из двух значений:
[
"value1",
"value2"
]
На заметку! Сериализация JSON с использованием
System.Text.Json
подробно обсуждалась в главе 20.
Службы API также применяют коды состояния HTTP для сообщения об успехе или неудаче. Некоторые вспомогательные методы для возвращения кодов состояния HTTP, доступные в классе
ControllerBase
, были перечислены в табл. 29.3. Успешные запросы возвращают коды состояния в диапазоне до 200, причем 200 (ОК) является самым распространенным кодом успеха. В действительности он настолько распространен, что вам не придется возвращать его явно. Если никаких исключений не возникало, а код состояния не был указан, тогда клиенту будет возвращен код 200 вместе с любыми данными.
Чтобы подготовиться к последующим примерам, создайте в проекте
AutoLot.Api
новый контроллер, добавив в каталог Controllers
новый файл по имени ValuesController.cs
с показанным ниже кодом:
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
}
На заметку! В среде Visual Studio для контроллеров предусмотрены шаблоны. Чтобы получить к ним доступ, щелкните правой кнопкой мыши на имени каталога
Controllers
в проекте AutoLot.Api
, выберите в контекстном меню пункт Add►Controller (Добавить►Контроллер) и укажите шаблон MVC Controller — Empty (Контроллер MVC — Пустой).
В коде устанавливается маршрут для контроллера с использованием значения (
api
) и маркера ([controller]
). Такой шаблон маршрута будет соответствовать URL наподобие www.skimedic.com/api/values
. Атрибут ApiController
выбирает несколько специфичных для API средств (раскрываются в следующем разделе). Наконец, класс контроллера наследуется от ControllerBase
. Как обсуждалось в главе 29, в инфраструктуре ASP.NET Core все типы контроллеров, доступные в классической версии ASP.NET, были объединены в один класс по имени Controller
с базовым классом ControllerBase
. Класс Controller
обеспечивает функциональность, специфичную для представлений ("V" в MVC), тогда как ControllerBase
предлагает оставшуюся базовую функциональность для приложений в стиле MVC.
Существует несколько способов возвращения содержимого в формате JSON из метода действия. Все приведенные далее примеры возвращают те же самые данные JSON с кодом состояния 200. Различия практически полностью стилистические. Добавьте в свой класс
ValuesController
следующий код:
[HttpGet]
public IActionResult Get()
{
return Ok(new string[] { "value1", "value2" });
}
[HttpGet("one")]
public IEnumerable Get1()
{
return new string[] { "value1", "value2" };
}
[HttpGet("two")]
public ActionResult> Get2()
{
return new string[] { "value1", "value2" };
}
[HttpGet("three")]
public string[] Get3()
{
return new string[] { "value1", "value2" };
}
[HttpGet("four")]
public IActionResult Get4()
{
return new JsonResult(new string[] { "value1", "value2" });
}
Чтобы протестировать код, запустите приложение
AutoLot.Api
; вы увидите список всех методов из ValuesController
в пользовательском интерфейсе (рис. 30.1).
Вспомните, что при определении маршрутов суффикс
Controller
отбрасывается из имен маршрутов, поэтому конечные точки в ValuesController
сопоставляются с Values
, а не с ValuesController
.
Для выполнения одного из методов щелкните на кнопке GET, на кнопке Try it out (Опробовать) и на кнопке Execute (Выполнить). После выполнения метода пользовательский интерфейс обновится, чтобы отобразить результаты; наиболее важная часть пользовательского интерфейса Swagger показана на рис. 30.2.
Вы увидите, что выполнение каждого метода приводит к получению тех же самых результатов JSON.
Атрибут
ApiController
, появившийся в версии ASP.NET Core 2.1, в сочетании с классом ControllerBase
обеспечивает правила, соглашения и линии поведения, специфичные для REST. Соглашения и линии поведения рассматриваются в последующих разделах.
При наличии атрибута
ApiController
контроллер обязан использовать маршрутизацию с помощью атрибутов. Это просто принудительное применение того, что многие расценивают как установившуюся практику.
Если есть проблема с привязкой модели, то действие будет автоматически возвращать код состояния HTTP 400 (Bad Request), что заменяет следующий код:
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
Для выполнения показанной выше проверки инфраструктура ASP.NET Core использует фильтр действий
ModelStatelnvalidFilter
. При наличии ошибок привязки или проверки достоверности ответ HTTP 400 в своем теле содержит детальные сведения об ошибках. Вот пример:
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"traceId": "|7fb5e16a-4c8f23bbfc974667.",
"errors": {
"": [
"A non-empty request body is required."
]
}
}
Такое поведение можно отключить через конфигурацию в методе
ConfigureServices()
класса Startup
:
services.AddControllers()
.ConfigureApiBehaviorOptions(options =>
{
options.SuppressModelStateInvalidFilter = true;
});
Механизм привязки моделей будет логически выводить источники извлечения значений на основе соглашений, описанных в табл. 30.1.
Такое поведение можно отключить через конфигурацию в методе
Configure Services()
класса Startup
:
services.AddControllers().ConfigureApiBehaviorOptions(options =>
{
// Подавить все выведение источников для привязки.
options.SuppressInferBindingSourcesForParameters= true;
// Подавить выведение типа содержимого multipart/form-data.
options. SuppressConsumesConstraintForFormFileParameters = true;
});
ASP.NET Core трансформирует результат ошибки (состояние 400 или выше) в результат с помощью типа
ProblemDetails
, который показан ниже:
public class ProblemDetails
{
public string Type { get; set; }
public string Title { get; set; }
public int? Status { get; set; }
public string Detail { get; set; }
public string Instance { get; set; }
public IDictionary Extensions { get; }
= new Dictionary(StringComparer.Ordinal);
}
Чтобы протестировать это поведение, добавьте в
ValuesController
еще один метод:
[HttpGet("error")]
public IActionResult Error()
{
return NotFound();
}
Запустите приложение и посредством пользовательского интерфейса Swagger выполните новую конечную точку
error
. Результатом по-прежнему будет код состояния 404 (Not Found), но в теле ответа возвратится дополнительная информация. Ниже приведен пример ответа (ваше значение traceId
будет другим):
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.4",
"title": "Not Found",
"status": 404,
"traceId": "00-9a609e7e05f46d4d82d5f897b90da624-a6484fb34a7d3a44-00"
}
Такое поведение можно отключить через конфигурацию в методе
ConfigureServices()
класса Startup
:
services.AddControllers()
.ConfigureApiBehaviorOptions(options =>
{
options.SuppressMapClientErrors = true;
});
Когда поведение отключено, вызов конечной точки error возвращает код состояния 404 без какой-либо дополнительной информации.
Продукт Swagger (также известный как OpenAPI) является стандартом с открытым кодом для документирования служб REST, основанных на API. Два главных варианта для добавления Swagger к API-интерфейсам ASP.NET Core — Swashbuckle и NSwag. Версия ASP.NET Core 5 теперь включает Swashbuckle в виде части шаблона нового проекта. Документация
swagger.json
, сгенерированная для AutoLot.Api
, содержит информацию по сайту, по каждой конечной точке и по любым объектам, задействованным в конечных точках.
Пользовательский интерфейс Swagger базируется на веб-интерфейсе и позволяет интерактивным образом исследовать конечные точки приложения, а также тестировать их (как делалось ранее в этой главе). Его можно расширить, добавляя документацию в сгенерированный файл
swagger.json
.
Стандартный шаблон проекта API добавляет код для генерирования файла
swagger.json
в метод ConfigureService()
класса Startup
:
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "AutoLot.Api", Version = "v1" });
});
Первое изменение стандартного кода предусматривает добавление метаданных к
OpenApiInfo
. Модифицируйте вызов AddSwaggerGen()
следующим образом, чтобы обновить заголовок и добавить описание и сведения о лицензии:
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1",
new OpenApiInfo
{
Title = "AutoLot Service",
Version = "v1",
Description = "Service to support the AutoLot dealer site",
License = new OpenApiLicense
{
Name = "Skimedic Inc",
Url = new Uri("http://www.skimedic.com")
}
});
});
Следующий шаг связан с переносом вызовов
UseSwagger()
и UseSwaggerUI()
из блока, предназначенного только для среды разработки, в главный путь выполнения внутри метода Configure()
. Кроме того, поменяйте заголовок "AutoLot.Api vl"
на "AutoLot Service vl"
.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env,
ApplicationDbContext context)
{
if (env.IsDevelopment())
{
// Если среда разработки, тогда отображать отладочную информацию.
app.UseDeveloperExceptionPage();
// Первоначальный код:
// app.UseSwagger();
// app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json",
// "AutoLot.Api v1"));
// Инициализировать базу данных.
if (Configuration.GetValue("RebuildDataBase"))
{
SampleDataInitializer.ClearAndReseedDatabase(context);
}
}
// Включить промежуточное ПО для обслуживания сгенерированного
// файла Swagger как конечной точки JSON.
app.UseSwagger();
// Включить промежуточное ПО для обслуживания пользовательского
// интерфейса Swagger (HTML, JS, CSS и т.д.), указывая конечную
// точку JSON для Swagger
app.UseSwaggerUI(c => { c.SwaggerEndpoint("/swagger/v1/swagger.json",
"AutoLot Service
v1"); });
...
}
В предыдущем коде используется
Swagger(app.UseSwagger())
и пользовательский интерфейс Swagger(app.useSwaggerUI()
). В нем также конфигурируется конечная точка для файла swagger.json
.
Инфраструктура .NET Core способна генерировать файл XML-документации для вашего проекта, исследуя методы на предмет наличия комментариев с тремя символами прямой косой черты (
///
). Чтобы включить такую функциональность в Visual Studio, щелкните правой кнопкой мыши на имени проекта AutoLot.Api
и в контекстном меню выберите пункт Properties (Свойства). В открывшемся диалоговом окне Properties (Свойства) перейдите на вкладку Build (Сборка), отметьте флажок XML documentation file (Файл XML-документации) и укажите в качестве имени файла AutoLot.Api.xml
. Кроме того, введите 1591 в текстовом поле Suppress warnings (Подавлять предупреждения), как показано на рис. 30.3.
Те же настройки можно вводить прямо в файле проекта. Ниже показан раздел
PropertyGroup
, который понадобится добавить:
AutoLot.Api.xml
1701;1702;1591;
Настройка
NoWarn
с указанием 1591
отключает выдачу предупреждений компилятором для методов, которые не имеют XML-комментариев.
На заметку! Предупреждения 1701 и 1702 являются пережитками ранних дней классической платформы .NET, которые обнажают компиляторы .NET Core. Чтобы взглянуть на процесс в действии, модифицируйте метод
Get()
класса ValuesController
следующим образом:
///
/// This is an example Get method returning JSON
///
/// This is one of several examples for returning JSON:
///
/// [
/// "value1",
/// "value2"
/// ]
///
///
/// List of strings
[HttpGet]
public IActionResult Get()
{
return Ok(new string[] { "value1", "value2" });
}
Когда вы скомпилируете проект, в корневом каталоге проекта появится новый файл по имени
AutoLot.Api.xml
. Открыв его, вы увидите только что добавленные комментарии:
AutoLot.Api
This is an example Get method returning JSON
This is one of several examples for returning JSON:
[
"value1",
"value2"
]
List of strings
На заметку! Если вы вводите три символа прямой косой черты перед определением класса или метода в Visual Studio, то среда создает начальную заглушку для XML-комментариев.
Далее необходимо объединить XML-комментарии со сгенерированным файлом
swagger.json
.
Сгенерированные XML-комментарии должны быть добавлены в процесс генерации
swagger.json
. Начните с добавления следующих операторов using
в файл класса Startup.cs
:
using System.IO;
using System.Reflection;
Файл XML-документации добавляется в Swagger вызовом метода
IncludeXmlComments()
внутри метода AddSwaggerGen()
. Перейдите к методу ConfigureServices()
класса Startup
и модифицируйте метод AddSwaggerGen()
, добавив файл XML-документации:
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1",
new OpenApiInfo
{
Title = "AutoLot Service",
Version = "v1",
Description = "Service to support the AutoLot dealer site",
License = new OpenApiLicense
{
Name = "Skimedic Inc",
Url = new Uri("http://www.skimedic.com")
}
});
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
c.IncludeXmlComments(xmlPath);
});
Запустите приложение и загляните в пользовательский интерфейс Swagger. Обратите внимание на XML-комментарии, интегрированные в пользовательский интерфейс Swagger (рис. 30.4).
Помимо XML-документации документирование может быть улучшено дополнительной конфигурацией конечных точек приложения.
Существуют дополнительные атрибуты, которые дополняют документацию Swagger. Чтобы применить их, начните с добавления показанных далее операторов
using
в файл ValuesController.cs
:
using Microsoft.AspNetCore.Http;
using Swashbuckle.AspNetCore.Annotations;
Атрибут
Produces
задает тип содержимого для конечной точки. Атрибут ProducesResponseType
использует перечисление StatusCodes
для указания возможного кода возврата для конечной точки. Модифицируйте метод Get()
класса ValuesController
, чтобы установить application/json
в качестве возвращаемого типа и сообщить о том, что результатом действия будет либо 200 (ОК), либо 400 (Bad Request):
[HttpGet]
[Produces("application/json")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public ActionResult> Get()
{
return new string[] {"value1", "value2"};
}
Хотя атрибут
ProducesResponseType
добавляет в документацию коды ответов, настроить эту информацию невозможно. К счастью, Swashbuckle добавляет атрибут SwaggerResponse
, предназначенный как раз для такой цели. Приведите код метода Get()
к следующему виду:
[HttpGet]
[Produces("application/json")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[SwaggerResponse(200, "The execution was successful")]
[SwaggerResponse(400, "The request was invalid")]
public ActionResult> Get()
{
return new string[] {"value1", "value2"};
}
Прежде чем аннотации Swagger будут приняты и добавлены в сгенерированную документацию, их потребуется включить. Откройте файл
Startup.cs
и перейдите к методу Configure()
. Обновите вызов AddSwaggerGen()
, как показано ниже:
services.AddSwaggerGen(c =>
{
c.EnableAnnotations();
...
});
Теперь, просматривая раздел ответов в пользовательском интерфейсе Swagger, вы будете видеть настроенный обмен сообщениями (рис. 30.5).
На заметку! В Swashbuckle поддерживается большой объем дополнительной настройки, за сведениями о которой обращайтесь в документацию по ссылке
https://github.com/domaindrivendev/Swashbuckle.AspNetCore
.
Большинство функциональных средств приложения
AutoLot.Api
можно отнести к одному из перечисленных далее методов:
•
GetOne()
•
GetAll()
•
UpdateOne()
•
AddOnе()
•
DeleteOne()
Основные методы API будут реализованы в обобщенном базовом контроллере API. Начните с создания нового каталога под названием
Base
в каталоге Controllers
проекта AutoLot.Api
. Добавьте в этот каталог новый файл класса по имени BaseCrudController.cs
. Модифицируйте операторы using
и определение класса, как демонстрируется ниже:
using System;
using System.Collections.Generic;
using AutoLot.Dal.Exceptions;
using AutoLot.Models.Entities.Base;
using AutoLot.Dal.Repos.Base;
using AutoLot.Services.Logging;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Swashbuckle.AspNetCore.Annotations;
namespace AutoLot.Api.Controllers.Base
{
[ApiController]
public abstract class BaseCrudController : ControllerBase
where T : BaseEntity, new()
where TController : BaseCrudController
{
}
}
Класс является открытым и абстрактным, а также унаследованным от
ControllerBase
. Он принимает два обобщенных параметра типа. Первый тип ограничивается так, чтобы быть производным от BaseEntity
и иметь стандартный конструктор, а второй — быть производным от BaseCrudController
(для представления производных контроллеров). Когда к базовому классу добавляется атрибут ApiController
, производные контроллеры получают функциональность, обеспечиваемую атрибутом.
На заметку! Для этого класса не определен маршрут. Он будет установлен с использованием производных классов.
На следующем шаге добавляются две защищенные переменные уровня класса: одна для хранения реализации интерфейса
IRepo
и еще одна для хранения реализации интерфейса IAppLogging
. Обе они должны устанавливаться с применением конструктора.
protected readonly IRepo MainRepo;
protected readonly IAppLogging Logger;
protected BaseCrudController(IRepo repo, IAppLogging logger)
{
MainRepo = repo;
Logger = logger;
}
Есть два HTTP-метода
GET
, GetOne()
и GetAll()
. Оба они используют хранилище, переданное контроллеру. Первым делом добавьте метод Getll()
, который служит в качестве конечной точки для шаблона маршрута контроллера:
///
/// Gets all records
///
/// All records
/// Returns all items
[Produces("application/json")]
[ProducesResponseType(StatusCodes.Status200OK)]
[SwaggerResponse(200, "The execution was successful")]
[SwaggerResponse(400, "The request was invalid")]
[HttpGet]
public ActionResult> GetAll()
{
return Ok(MainRepo.GetAllIgnoreQueryFilters());
}
Следующий метод получает одиночную запись на основе параметра
id
, который передается как обязательный параметр маршрута и добавляется к маршруту производного контроллера:
///
/// Gets a single record
///
/// Primary key of the record
/// Single record
/// Found the record
/// No content
[Produces("application/json")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[SwaggerResponse(200, "The execution was successful")]
[SwaggerResponse(204, "No content")]
[HttpGet("{id}")]
public ActionResult GetOne(int id)
{
var entity = MainRepo.Find(id);
if (entity == null)
{
return NotFound();
}
return Ok(entity);
}
Значение маршрута автоматически присваивается параметру
id
(неявно из [FromRoute]
).
Обновление записи делается с применением HTTP-метода
PUT
. Ниже приведен код метода UpdateOne()
:
///
/// Updates a single record
///
///
/// Sample body:
///
/// {
/// "Id": 1,
/// "TimeStamp": "AAAAAAAAB+E="
/// "MakeId": 1,
/// "Color": "Black",
/// "PetName": "Zippy",
/// "MakeColor": "VW (Black)",
/// }
///
///
/// Primary key of the record to update
/// Single record
/// Found and updated the record
/// Bad request
[Produces("application/json")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[SwaggerResponse(200, "The execution was successful")]
[SwaggerResponse(400, "The request was invalid")]
[HttpPut("{id}")]
public IActionResult UpdateOne(int id,T entity)
{
if (id != entity.Id)
{
return BadRequest();
}
try
{
MainRepo.Update(entity);
}
catch (CustomException ex)
{
// Пример специального исключения.
// Должно обрабатываться более элегантно.
return BadRequest(ex);
}
catch (Exception ex)
{
// Должно обрабатываться более элегантно.
return BadRequest(ex);
}
return Ok(entity);
}
Метод начинается с установки маршрута как запроса
HttpPut
на основе маршрута производного контроллера с обязательным параметром маршрута id
. Значение маршрута присваивается параметру id
(неявно из [FromRoute]
), а сущность (entity
) извлекается из тела запроса (неявно из [FromBody]
).Также обратите внимание, что проверка достоверности модели отсутствует, поскольку делается автоматически атрибутом ApiController
. Если состояние модели укажет на наличие ошибок, тогда клиенту будет возвращен код 400 (Bad Request).
Метод проверяет, совпадает ли значение маршрута (
id
) со значением id
в теле запроса. Если не совпадает, то возвращается код 400 (Bad Request). Если совпадает, тогда используется хранилище для обновления записи. Если обновление терпит неудачу с генерацией исключения, то клиенту возвращается код 400 (Bad Request). Если операция обновления проходит успешно, тогда клиенту возвращается код 200 (ОК) и обновленная запись в качестве тела запроса.
На заметку! Обработка исключений в этом примере (а также в остальных примерах) абсолютно неадекватна. В производственных приложениях вы должны задействовать все знания, полученные к настоящему времени, чтобы элегантно обрабатывать возникающие проблемы в соответствии с имеющимися требованиями.
Вставка записи делается с применением HTTP-метода
POST
. Ниже приведен код метода AddOne()
:
///
/// Adds a single record
///
///
/// Sample body:
///
/// {
/// "Id": 1,
/// "TimeStamp": "AAAAAAAAB+E="
/// "MakeId": 1,
/// "Color": "Black",
/// "PetName": "Zippy",
/// "MakeColor": "VW (Black)",
/// }
///
///
/// Added record
/// Found and updated the record
/// Bad request
[Produces("application/json")]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[SwaggerResponse(201, "The execution was successful")]
[SwaggerResponse(400, "The request was invalid")]
[HttpPost]
public ActionResult AddOne(T entity)
{
try
{
MainRepo.Add(entity);
}
catch (Exception ex)
{
return BadRequest(ex);
}
return CreatedAtAction(nameof(GetOne), new {id = entity.Id}, entity);
}
Метод начинается с определения маршрута как запроса
HttpPost
. Параметр маршрута отсутствует, потому что создается новая запись. Если хранилище успешно добавит запись, то ответом будет результат вызова метода CreatedAtAction()
, который возвращает клиенту код 201 вместе с URL для вновь созданной сущности в виде значения заголовка Location
. Вновь созданная сущность в формате JSON помещается внутрь тела ответа.
Удаление записи делается с применением HTTP-метода
DELETE
. Ниже приведен код метода DeleteOne()
:
///
/// Deletes a single record
///
///
/// Sample body:
///
/// {
/// "Id": 1,
/// "TimeStamp": "AAAAAAAAB+E="
/// }
///
///
/// Nothing
/// Found and deleted the record
/// Bad request
[Produces("application/json")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[SwaggerResponse(200, "The execution was successful")]
[SwaggerResponse(400, "The request was invalid")]
[HttpDelete("{id}")]
public ActionResult DeleteOne(int id, T entity)
{
if (id != entity.Id)
{
return BadRequest();
}
try
{
MainRepo.Delete(entity);
}
catch (Exception ex)
{
// Должно обрабатываться более элегантно.
return new BadRequestObjectResult(ex.GetBaseException()?.Message);
}
return Ok();
}
Метод начинается с определения маршрута как запроса
HttpDelete
с обязательным параметром маршрута id
. Значение id
в маршруте сравнивается со значением id
, отправленным с остальной частью сущности в теле запроса, и если они не совпадают, то возвращается код 400 (Bad Request). Если хранилище успешно удаляет запись, тогда клиенту возвращается код 200 (ОК), а если возникла какая-то ошибка, то клиент получает код 400 (Bad Request).
На этом создание базового контроллера завершено.
Приложению
AutoLot.Api
необходим дополнительный метод HttpGet
для получения записей Car
на основе значения Make
. Он будет создан в новом классе по имени CarsController
. Создайте в каталоге Controllers
новый пустой контроллер API под названием CarsController
. Модифицируйте операторы using
следующим образом:
using System.Collections.Generic;
using AutoLot.Api.Controllers.Base;
using Microsoft.AspNetCore.Mvc;
using AutoLot.Models.Entities;
using AutoLot.Dal.Repos.Interfaces;
using AutoLot.Services.Logging;
using Microsoft.AspNetCore.Http;
using Swashbuckle.AspNetCore.Annotations;
Класс
CarsController
является производным от класса BaseCrudController
и определяет маршрут на уровне контроллера. Конструктор принимает специфичное для сущности хранилище и средство ведения журнала. Вот первоначальный код контроллера:
namespace AutoLot.Api.Controllers
{
[Route("api/[controller]")]
public class CarsController : BaseCrudController
{
public CarsController(ICarRepo carRepo, IAppLogging logger) :
base(carRepo, logger)
{
}
}
}
Класс
CarsController
расширяет базовый класс еще одним методом действия, который получает все записи об автомобилях конкретного производителя. Добавьте показанный ниже код:
///
/// Gets all cars by make
///
/// All cars for a make
/// Primary key of the make
/// Returns all cars by make
[Produces("application/json")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[SwaggerResponse(200, "The execution was successful")]
[SwaggerResponse(204, "No content")]
[HttpGet("bymake/{id?}")]
public ActionResult> GetCarsByMake(int? id)
{
if (id.HasValue && id.Value>0)
{
return Ok(((ICarRepo)MainRepo).GetAllBy(id.Value));
}
return Ok(MainRepo.GetAllIgnoreQueryFilters());
}
Атрибут
HttpGet
расширяет маршрут константой bymake
и необязательным идентификатором производителя для фильтрации, например:
https://localhost:5021/api/cars/bymake/5
Сначала в методе проверяется, было ли передано значение для
id
. Если нет, то получаются все автомобили. Если значение было передано, тогда с использованием метода GetAllBy()
класса CarRepo
получаются автомобили по производителю. Поскольку защищенное свойство MainRepo
базового класса определено с типом IRepo
, его потребуется привести к типу ICarRepo
.
Все оставшиеся контроллеры, специфичные для сущностей, будут производными от класса
BaseCrudController
, но без добавления дополнительной функциональности. Добавьте в каталог Controllers
еще четыре пустых контроллера API с именами CreditRisksController
, CustomersController
, MakesController
и OrdersController
.
Вот код оставшихся контроллеров:
// CreditRisksController.cs
using AutoLot.Api.Controllers.Base;
using AutoLot.Models.Entities;
using AutoLot.Dal.Repos.Interfaces;
using AutoLot.Services.Logging;
using Microsoft.AspNetCore.Mvc;
namespace AutoLot.Api.Controllers
{
[Route("api/[controller]")]
public class CreditRisksController
: BaseCrudController
{
public CreditRisksController(
ICreditRiskRepo creditRiskRepo, IAppLogging logger)
: base(creditRiskRepo, logger)
{
}
}
}
// CustomersController.cs
using AutoLot.Api.Controllers.Base;
using AutoLot.Models.Entities;
using AutoLot.Dal.Repos.Interfaces;
using AutoLot.Services.Logging;
using Microsoft.AspNetCore.Mvc;
namespace AutoLot.Api.Controllers
{
[Route("api/[controller]")]
public class CustomersController : BaseCrudController
{
public CustomersController(
ICustomerRepo customerRepo, IAppLogging logger)
: base(customerRepo, logger)
{
}
}
}
// MakesController.cs
using AutoLot.Api.Controllers.Base;
using AutoLot.Models.Entities;
using Microsoft.AspNetCore.Mvc;
using AutoLot.Dal.Repos.Interfaces;
using AutoLot.Services.Logging;
namespace AutoLot.Api.Controllers
{
[Route("api/[controller]")]
public class MakesController : BaseCrudController
{
public MakesController(IMakeRepo makeRepo, IAppLogging logger)
: base(makeRepo, logger)
{
}
}
}
// OrdersController.cs
using AutoLot.Api.Controllers.Base;
using AutoLot.Dal.Repos.Interfaces;
using AutoLot.Models.Entities;
using AutoLot.Services.Logging;
using Microsoft.AspNetCore.Mvc;
namespace AutoLot.Api.Controllers
{
[Route("api/[controller]")]
public class OrdersController : BaseCrudController
{
public OrdersController(IOrderRepo orderRepo,
IAppLogging logger) :
base(orderRepo, logger)
{
}
}
}
Итак, все контроллеры готовы и вы можете с помощью пользовательского интерфейса Swagger протестировать полную функциональность. Если вы собираетесь добавлять/обновлять/удалять записи, тогда измените значение
RebuildDataBase
на true
в файле appsettings.development.json
:
{
...
"RebuildDataBase": true,
...
}
Когда в приложении Web API возникает исключение, никакая страница со сведениями об ошибке не отображается, т.к. пользователем обычно является другое приложение, а не человек. Информация об ошибке должна быть отправлена в формате JSON наряду с кодом состояния HTTP. Как обсуждалось в главе 29, инфраструктура ASP.NET Core позволяет создавать фильтры, которые запускаются при появлении необработанных исключений. Фильтры можно применять глобально, на уровне контроллера или на уровне действия. Для текущего приложения вы построите фильтр исключений для отправки данных JSON (вместе с кодом HTTP 500) и включения трассировки стека, если сайт функционирует в режиме отладки.
На заметку! Фильтры — крайне мощное средство ASP.NET Core. В этой главе вы ознакомитесь только с фильтрами исключений, но с их помощью можно создавать очень многое, что значительно экономит время при построении приложений ASP.NET Core. Полную информацию о фильтрах ищите в документации по ссылке
https://docs.microsoft.com/ru-ru/aspnet/core/mvc/controllers/filters
.
Создайте новый каталог под названием
Filters
и добавьте в него новый файл класса по имени CustomExceptionFilterAttribute.cs
. Приведите операторы using
к следующему виду:
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Hosting;
Сделайте класс открытым и унаследованным от
ЕхсерtionFiIterAttribute
. Переопределите метод OnException()
, как показано ниже:
namespace AutoLot.Api.Filters
{
public class CustomExceptionFilterAttribute : ExceptionFilterAttribute
{
public override void OnException(ExceptionContext context)
{
}
}
}
В отличие от большинства фильтров в ASP.NET Core, которые имеют обработчик событий "перед" и "после", фильтры исключений располагают только одним обработчиком:
OnException()
(или OnExceptionAsync()
). Обработчик принимает один параметр, ExceptionContext
, который предоставляет доступ к ActionContext
, а также к сгенерированному исключению.
Кроме того, фильтры принимают участие во внедрении зависимостей, позволяя получить доступ в коде к любому элементу внутри контейнера. В рассматриваемом примере вам необходим экземпляр реализации
IWebHostEnvironment
, внедренный в фильтр, который будет использоваться для выяснения среды времени выполнения. Если средой является Development
, тогда ответ должен также включать трассировку стека. Добавьте переменную уровня класса для хранения экземпляра реализации IWebHostEnvironment
и конструктор:
private readonly IWebHostEnvironment _hostEnvironment;
public CustomExceptionFilterAttribute(IWebHostEnvironment hostEnvironment)
{
_hostEnvironment = hostEnvironment;
}
Код в обработчике
OnException()
проверяет тип сгенерированного исключения и строит соответствующий ответ. В случае среды Development
в сообщение ответа включается трассировка стека. Затем создается динамический объект, который содержит значения для отправки вызывающему запросу, и возвращается в IActionResult
. Вот модифицированный код метода:
public override void OnException(ExceptionContext context)
{
var ex = context.Exception;
string stackTrace = _hostEnvironment.IsDevelopment()
? context.Exception.StackTrace :
string.Empty;
string message = ex.Message;
string error;
IActionResult actionResult;
switch (ex)
{
case DbUpdateConcurrencyException ce:
// Возвращается код HTTP 400.
error = "Concurrency Issue.";
actionResult = new BadRequestObjectResult(
new {Error = error, Message = message, StackTrace = stackTrace});
break;
default:
error = "General Error.";
actionResult = new ObjectResult(
new {Error = error, Message = message, StackTrace = stackTrace})
{
StatusCode = 500
};
break;
}
//context.ExceptionHandled = true; // Если убрать здесь комментарий,
// то исключение поглощается
context.Result = actionResult;
}
Если вы хотите, чтобы фильтр исключений поглотил исключение и установил код состояния в 200 (скажем, для регистрации ошибки в журнале, не возвращая ее клиенту), тогда поместите следующую строку перед установкой
Result
(в предыдущем примере кода просто уберите комментарий):
context.ExceptionHandled = true;
Фильтры можно применять к методам действий, контроллерам или глобально к приложению. Код "перед" фильтров выполняется снаружи вовнутрь (глобальный, контроллер, метод действия), в то время как код "после" фильтров выполняется изнутри наружу (метод действия, контроллер, глобальный).
На уровне приложения фильтры добавляются в методе
ConfigureServices()
класса Startup
. Откройте файл класса Startup.cs
и поместите в начало файла следующий оператор using
:
using AutoLot.Api.Filters;
Модифицируйте метод
AddControllers()
, добавив специальный фильтр:
services
.AddControllers(config => config.Filters.Add(
new CustomExceptionFilterAttribute(_env)))
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.PropertyNamingPolicy = null;
options.JsonSerializerOptions.WriteIndented = true;
})
.ConfigureApiBehaviorOptions(options =>
{
...
});
Чтобы протестировать фильтр исключений, откройте файл
WeatherForecastController.cs
и обновите метод действия Get()
показанным ниже кодом:
[HttpGet]
public IEnumerable Get()
{
_logger.LogAppWarning("This is a test");
throw new Exception("Test Exception");
...
}
Запустите приложение и испытайте метод с использованием Swagger. Результаты, отображенные в пользовательском интерфейсе Swagger должны соответствовать следующему выводу (трассировка стека приведена с сокращениями):
{
"Error": "General Error.",
"Message": "Test Exception",
"StackTrace": " at AutoLot.Api.Controllers.WeatherForecastController.Get() in
D:\\Projects\\Books\\csharp9-wf\\Code\\New\\Chapter_30\\AutoLot.Api\\Controllers\\
WeatherForecastController.cs:line 31\r\n "
}
Приложения API должны иметь политики, которые разрешают или запрещают взаимодействовать с ними клиентам, обращающимся из другого сервера. Такие типы запросов называются запросами между источниками (cross-origin requests — CORS). Хотя в этом нет необходимости при работе локально на своей машине для всего мира ASP.NET Core, поддержка CORS нужна фреймворкам JavaScript, которые желают взаимодействовать с вашим приложением API, даже когда они все вместе функционируют локально.
На заметку! Дополнительные сведения о поддержке CORS ищите в документации по ссылке
https://docs.microsoft.com/ru-ru/aspnet/core/security/cors
.
Инфраструктура ASP.NET Core располагает развитой поддержкой конфигурирования CORS, включая методы для разрешения/запрещения заголовков, методов, источников, учетных данных и многого другого. В этом примере все будет оставлено максимально открытым.
Конфигурирование начинается с создания политики CORS и добавления ее в коллекцию служб. Политика имеет имя (оно будет использоваться в методе
Configure()
), за которым следуют правила. Далее будет сознана политика по имени AllowAll
, разрешающая все. Добавьте в метод ConfigureServices()
класса Startup
следующий код:
services.AddCors(options =>
{
options.AddPolicy("AllowAll", builder =>
{
builder
.AllowAnyHeader()
.AllowAnyMethod()
.AllowAnyOrigin();
});
});
Наконец, политику CORS необходимо добавить в конвейер обработки HTTP. Поместите между вызовами
арр. UseRouting()
и арр.UseEndpoints()
в методе Configure()
класса Startup
показанную ниже строку (выделенную полужирным):
public void Configure(
IApplicationBuilder app,
IWebHostEnvironment env,
ApplicationDbContext context)
{
...
// Включить маршрутизацию.
app.UseRouting();
// Добавить политику CORS.
app.UseCors("AllowAll");
// Включить проверки авторизации.
app.UseAuthorization();
...
}
В главе вы продолжили изучение ASP.NET Core. Сначала вы узнали о возвращении данных JSON из методов действий, после чего взглянули на атрибут
ApiController
и его влияние на контроллеры API. Затем вы обновили общую реализацию Swashbuckle, чтобы включить XML-документацию приложения и информацию из атрибутов методов действий.
Далее был построен базовый контроллер, содержащий большинство функциональности приложения. После этого в проект были добавлены производные контроллеры, специфичные для сущностей. В заключение был добавлен фильтр исключений уровня приложения и поддержка запросов между источниками.
В следующей главе вы завершите построение веб-приложения ASP.NET Core, т.е.
AutoLot.Mvc
.
В главе 29 была заложена основа ASP.NET Core, а в главе 30 вы построили службу REST. В этой главе вы будете создавать веб-приложение с использованием паттерна МУС. Все начинается с помещения "V" обратно в "МУС".
На заметку! Исходный код, рассматриваемый в этой главе, находится в папке
Chapter_31
внутри хранилища GitHub для настоящей книги. Вы также можете продолжить работу с решением, начатым в главе 29 и обновленным в главе 30.
При построении служб ASP.NET Core были задействованы только части "М " (модели) и "С" (контроллеры ) паттерна МУС. Пользовательский интерфейс создается с применением части "V", т.е. представлений паттерна МУС. Представления строятся с использованием кода HTML, JavaScript, CSS и Razor. Они необязательно имеют страницу базовой компоновки и визуализируются из метода действия контроллера или компонента представления. Если вы имели дело с классической инфраструктурой ASP.NET МУС, то все должно выглядеть знакомым.
Как кратко упоминалось в главе 29, объекты результатов
ViewResult
и PartialView
являются экземплярами класса ActionResult
, которые возвращаются из методов действий с применением вспомогательных методов класса Controller
. Класс PartialViewResult
спроектирован для визуализации внутри другого представления и не использует страницу компоновки, тогда как класс ViewResult
обычно визуализируется в сочетании со страницей компоновки.
По соглашению, принятому в ASP.NET Core (что было и в ASP.NET МУС), экземпляр View или PartialView визуализирует файл
*.cshtml
с таким же именем, как у метода. Представление должно находиться либо в каталоге с именем контроллера (без суффикса Controller
), либо в каталоге Shared
(оба расположены внутри родительского каталога Views
).
Например, следующий код будет визуализировать представление
SampleAction.cshtml
, находящееся в каталоге Views\Sample
или Views\Shared
:
[Route("[controller]/[action]")]
public class SampleController: Controller
{
public ActionResult SampleAction()
{
return View();
}
}
На заметку! Первым производится поиск в каталоге с именем контроллера. Если представление там не обнаружено, то поиск выполняется в каталоге
Shared
. Если оно по-прежнему не найдено, тогда генерируется исключение.
Чтобы визуализировать представление с именем, которое отличается от имени метода действия, передавайте имя файла (без расширения
cshtml
). Показанный ниже код будет визуализировать представление CustomViewName.cshtml
:
public ActionResult SampleAction()
{
return View("CustomViewName");
}
Последние две перегруженные версии предназначены для передачи объекта данных, который становится моделью для представления. В первом примере применяется стандартное имя представления, а во втором указывается другое имя представления:
public ActionResult SampleAction()
{
var sampleModel = new SampleActionViewModel();
return View(sampleModel);
}
public ActionResult SampleAction()
{
var sampleModel = new SampleActionViewModel();
return View("CustomViewName",sampleModel);
}
В следующем разделе подробно рассматривается механизм визуализации Razor с использованием представления, которое визуализируется из метода действия по имени
RazorSyntax()
класса HomeController
. Метод действия будет получать запись Car
из экземпляра класса CarRepo
, внедряемого в метод, и передавать экземпляр Car
в качестве модели представлению.
Откройте
HomeController
в каталоге Controllers
приложения AutoLot.Mvc
и добавьте следующий оператор using
:
using AutoLot.Dal.Repos.Interfaces;
Затем добавьте в контроллер метод
Razorsyntax()
:
[HttpGet]
public IActionResult RazorSyntax([FromServices] ICarRepo carRepo)
{
var car = carRepo.Find(1);
return View(car);
}
Метод действия декорируется атрибутом
HTTPGet
с целью установки этого метода в качестве конечной точки приложения для /Home/RazorSyntax
при условии, что поступивший запрос является HTTP-запросом GET
. Атрибут FromServices
на параметре ICarRepo
информирует ASP.NET Core о том, что параметр не должен привязываться к каким-либо входящим данным, а взамен метод получает экземпляр реализации ICarRepo
из контейнера DI (dependency injection — внедрение зависимостей). Метод получает экземпляр Car
и возвращает экземпляр ViewResuit
с применением метода View()
. Поскольку имя представления не было указано, ASP.NET Core будет искать представление с именем RazorSyntax.cshtml
в каталоге Views\Home
или Views\Shared
. Если ни в одном местоположении представление не найдено, тогда клиенту (браузеру) возвратится исключение.
Запустите приложение и перейдите в браузере по ссылке
https://localhost:5001/Home/RazorSyntax
(в случае использования Visual Studio и IIS вам понадобится изменить номер порта). Так как в проекте отсутствует представление, которое может удовлетворить запрос, в браузер возвращается исключение. Вспомните из главы 29, что внутри метода Configure()
класса Startup
в конвейер HTTP добавляется вызов UseDeveloperExceptionPage()
, если средой является Development
. Результаты работы этого метода показаны на рис. 31.1.
Страница исключений для разработчиков предоставляет обширную информацию для отладки приложения, в числе которой низкоуровневые детали исключения, укомплектованные трассировкой стека. Теперь закомментируйте приведенную ниже строку в методе
Configure()
и замените ее "стандартным" обработчиком ошибок:
if (env.IsDevelopment())
{
// app.UseDeveloperExceptionPage();
app.UseExceptionHandler("/Home/Error");
...
}
Снова запустив приложение и перейдя по ссылке
http://localhost:5001/Home/RazorSyntax
, вы завидите стандартную страницу ошибок, которая показана на рис. 31.2.
На заметку! Во всех примерах URL в этой главе применяется веб-сервер Kestrel и порт 5001. Если вы имеете дело с Visual Studio и веб-сервером IIS Express, тогда используйте URL из профиля для IIS в файле
launchsettings.json
.
Стандартный обработчик ошибок выполняет перенаправление ошибок методу действия
Error
класса HomeController
. Не забудьте восстановить применение страницы исключений для разработчиков в методе Configure()
:
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
...
}
Дополнительные сведения о настройке обработки ошибок и доступных вариантах ищите в документации по ссылке
https://docs.microsoft.com/ru-ru/aspnet/core/fundamentals/error-handling
.
Механизм визуализации Razor задумывался как усовершенствование механизма визуализации Web Forms и использует Razor в качестве основного языка. Razor — это код серверной стороны, который встраивается в представление, базируется на C# и избавляет от многих неудобств, присущих механизму визуализации Web Forms. Встраивание Razor в HTML и CSS приводит к тому, что код становится намного чище и лучше для восприятия, чем в случае, когда применяется синтаксис механизма визуализации Web Forms.
Первым делом добавьте новое представление, щелкнув правой кнопкой мыши на имени каталога
Views\Home
в проекте AutoLot.Mvc
и выбрав в контекстном меню пункт Add►New Item (Добавить►Новый элемент). В открывшемся диалоговом окне Add New Item — AutoLot.Mvc
(Добавить новый элемент — AutoLot.Mvc
) выберите шаблон Razor View — Empty (Представление Razor — Пустое) и назначьте представлению имя RazorSyntax.cshtml
.
На заметку! Контекстное меню, открывшееся в результате щелчка правой кнопкой мыши на
Views\Home
, содержит также пункт Add►View (Добавить►Представление). Тем не менее, его выбор приводит к переходу в то же самое диалоговое окно Add New Item.
Представления Razor, как правило, строго типизированы с использованием директивы
@model
(обратите внимание на букву m
в нижнем регистре). Измените тип нового представления на сущность Car
, добавив в начало файла представления такой код:
@model AutoLot.Models.Entities.Car
Поместите в верхнюю часть страницы дескриптор <
hl
>. Он не имеет ничего общего с Razor, а просто добавляет заголовок к странице:
Razor Syntax
Блоки операторов Razor открываются с помощью символа
@
и являются либо самостоятельными операторами (вроде foreach
), либо заключаются в фигурные скобки, как демонстрируется в следующих примерах:
@for (var i = 0; i < 15; i++)
{
// Делать что-то.
}
@{
// Блок кода.
var foo = "Foo";
var bar = "Bar";
var htmlString = "- one
- two
";
}
Чтобы вывести значение переменной в представление, просто укажите символ
@
с именем переменной, что эквивалентно вызову Response.Write()
. Как видите, при выводе напрямую в браузер после оператора нет точки с запятой:
@foo
@htmlString
@foo.@bar
В предыдущем примере две переменные комбинируются посредством точки между ними (
@foo.@bar
). Это не обычная "точечная" запись в языке С#, предназначенная для навигации по цепочке свойств. Здесь просто значения двух переменных выводятся в поток ответа с физической точкой между ними. Если вас интересует "точечная" запись в отношении переменной, тогда примените @
к переменной и записывайте свой код стандартным образом:
@foo.ToUpper()
Если вы хотите вывести низкоуровневую HTML-разметку, тогда используйте так называемые вспомогательные функции HTML (HTML helper), которые встроены в механизм визуализации Razor. Следующая строка выводит низкоуровневую HTML-разметку:
@Html.Raw(htmlString)
В блоках кода можно смешивать разметку и код. Строки, начинающиеся с разметки, интерпретируются как HTML, а остальные строки — как код. Если строка начинается с текста, который не является кодом, вы должны применять указатель содержимого (
@:
) или указатель блока содержимого (
). Обратите внимание, что строки могут меняться с одного вида на другой и наоборот. Ниже приведен пример:
@{
@:Straight Text
Value:@Model.Id
Lines without HTML tag
}
При желании отменить символ
@
используйте удвоенный @
. Кроме того, механизм Razor достаточно интеллектуален, чтобы обрабатывать адреса электронной почты, поэтому отменять символ @
в них не нужно. Если необходимо заставить Razor трактовать символ @
подобно маркеру Razor, тогда добавьте круглые скобки:
foo@foo.com
@@foo
test@foo
test@(foo)
Предыдущий код выводит
foo@foo.com
, @foo
, test@foo
и testFoo
.
Комментарии Razor открываются с помощью
@*
и закрываются посредством *@
:
@*
Multiline Comments
Hi.
*@
В Razor также поддерживаются внутристрочные функции. Например, следующая функция сортирует список строк:
@functions {
public static IList SortList(IList strings) {
var list = from s in strings orderby s select s;
return list.ToList();
}
}
Приведенный далее код создает список строк, сортирует их с применением функции
SortList()
и выводит отсортированный список в браузер:
@{
var myList = new List {"C", "A", "Z", "F"};
var sortedList = SortList(myList);
}
@foreach (string s in sortedList)
{
@s@:
}
Вот еще один пример, где создается делегат, который можно использовать, чтобы установить для строки полужирное начертание:
@{
Func b = @@item;
}
This will be bold: @b("Foo")
Кроме того, Razor содержит вспомогательные методы HTML, которые предоставляются инфраструктурой ASP.NET Core, например,
DisplayForModel()
и EditorForModel()
. Первый применяет рефлексию к модели представления для отображения на веб-странице. Второй тоже использует рефлексию, чтобы создать HTML-разметку для формы редактирования (имейте в виду, что он не поставляет дескрипторы Form, а только разметку для модели). Вспомогательные методы HTML подробно рассматриваются позже в главе.
Наконец, в версии ASP.NET Core появились вспомогательные функции дескрипторов (tag helper), которые объединяют разметку и код; они будут обсуждаться далее в главе.
Представления — это специальные файлы кода с расширением
cshtml
, содержащие сочетание разметки HTML, стилей CSS, кода JavaScript и кода Razor.
Внутри каталога Views хранятся представления в проектах ASP.NET Core, использующих паттерн MVC. В самом каталоге Views находятся два файла:
_iewStart.cshtml
и _ViewImports.cshtml
.
Код в файле
_ViewStart.cshtml
выполняется перед визуализацией любого другого представления (за исключением частичных представлений и компоновок). Файл _ViewStart.cshtml
обычно применяется с целью установки стандартной компоновки для представлений, в которых она не указана. Компоновки подробно рассматриваются в разделе "Компоновки" позже в главе. Вот как выглядит содержимое файла _ViewStart.cshtml
:
@{
Layout = "_Layout";
}
Файл
_ViewImports.cshtml
служит для импортирования совместно используемых директив, таких как операторы using
. Содержимое применяется ко всем представлениям в том же каталоге или подкаталоге, где находится файл _ViewImports
. Добавьте оператор using
для AutoLot.Models.Entities
:
@using AutoLot.Mvc
@using AutoLot.Mvc.Models
@using AutoLot.Models.Entities
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
Строка
@addTegHelper
будет раскрыта вместе со вспомогательными функциями дескрипторов.
На заметку! А для чего служит ведущий символ подчеркивания в
_ViewStart.html
, _ViewImports.cshtml
и _Layout.cshtml
? Механизм визуализации Razor изначально создавался для платформы WebMatrix, где не разрешалось напрямую визуализировать файлы, имена которых начинались с символа подчеркивания. Все ключевые файлы (вроде компоновки и конфигурации) имеют имена, начинающиеся с символа подчеркивания. Это не соглашение MVC, поскольку здесь отсутствует проблема, которая была в WebMatrix, но наследие символа подчеркивания продолжает существовать.
Как упоминалось ранее, каждый контроллер получает собственный каталог внутри каталога Views, в котором хранятся его специфичные представления. Имя такого каталога совпадает с именем контроллера (без суффикса
Controller
). Скажем, в каталоге Views\Cars
содержатся все представления для CarsController
. Представления обычно именуются согласно методам действий, которые их визуализируют, хотя их имена можно изменять, как уже было показано.
Внутри каталога
Views
есть специальный каталог по имени Shared
, в котором хранятся представления, доступные всем контроллерам и действиям. Как уже упоминалось, если запрошенный файл представления не удалось найти в каталоге, специфичном для контроллера, тогда поиск производится в каталоге Shared
.
В каталоге
DisplayTemplates
хранятся специальные шаблоны, которые управляют визуализацией типов, а также содействуют многократному использованию кода и согласованности отображения. Когда вызываются методы DisplayFor()/DisplayForModel()
, механизм визуализации Razor ищет шаблон, имя которого совпадает с именем визуализируемого типа, например, Car.cshtml
для класса Car
. Если специальный шаблон найти не удалось, тогда разметка визуализируется с применением рефлексии. Поиск начинается с каталога Views\{CurrentControllerName}\DisplayTemplates
и в случае неудачи продолжается в каталоге Views\Shared\DisplayTemplates
. Методы DisplayFor()/DisplayForModel()
принимают необязательный параметр, указывающий имя шаблона.
Создайте внутри каталога
Views\Shared
новый каталог под названием DisplayTemplates
и добавьте в него новое представление по имени DateTime.cshtml
. Удалите сгенерированный код вместе с комментариями и замените его следующим кодом:
@model DateTime?
@if (Model == null)
{
@:Unknown
}
else
{
if (ViewData.ModelMetadata.IsNullableValueType)
{
@:@(Model.Value.ToString("d"))
}
else
{
@:@(((DateTime)Model).ToString("d"))
}
}
Обратите внимание, что в директиве
@model
, строго типизирующей представление, используется буква m нижнего регистра. При ссылке на присвоенное значение модели в Razor применяется буква М
верхнего регистра. В этом примере определение модели допускает значения null
. Если переданное представлению значение для модели равно null
, то шаблон отображает слово Unknown
(неизвестно). В противном случае шаблон отображает дату в сокращенном формате, используя свойство Value
допускающего null
типа или саму модель.
Создайте внутри каталога
Views
новый каталог по имени Cars
, а внутри него — каталог под названием DisplayTemplates
. Добавьте в каталог DisplayTemplates
новое представление по имени Car.cshtml
. Удалите сгенерированный код вместе с комментариями и замените его показанным ниже кодом, который отображает сущность Car
:
@model AutoLot.Models.Entities.Car
@Html.DisplayNameFor(model => model.MakeId)
@Html.DisplayFor(model => model.MakeNavigation.Name)
@Html.DisplayNameFor(model => model.Color)
@Html.DisplayFor(model => model.Color)
@Html.DisplayNameFor(model => model.PetName)
@Html.DisplayFor(model => model.PetName)
Вспомогательная функция HTML под названием
DisplayNameFor()
отображает имя свойства, если только свойство не декорировано или атрибутом Display(Name="")
, или атрибутом DisplayName("")
, и тогда применяется отображаемое значение. Метод DisplayFor()
отображает значение для свойства модели, указанное в выражении. Обратите внимание, что для получения названия производителя используется навигационное свойство MakeNavigation
.
Запустив приложение и перейдя на страницу
RazorSyntax
, вы можете быть удивлены тем, что шаблон отображения Car
не применяется. Причина в том, что шаблон находится в каталоге представления Cars
, а метод действия RazorSyntax
и представление вызываются из HomeController
. Методы действий в HomeController
будут осуществлять поиск представлений в каталогах Home
и Shared
и потому не найдут шаблон отображения Car
.
Если вы переместите файл
Car.cshtml
в каталог Shared\DisplayTemplates
, тогда представление RazorSyntax
будет использовать шаблон отображения Car
.
Шаблон
CarWithColor
похож на шаблон Car
. Разница в том, что этот шаблон изменяет цвет текста Color (Цвет) на основе значения свойства Color
модели. Добавьте в каталог Cars\DisplayTemplates
новый шаблон по имени CarWithColors.cshtml
и приведите разметку к следующему виду:
@model Car
@Html.DisplayNameFor(model => model.PetName)
@Html.DisplayFor(model => model.PetName)
@Html.DisplayNameFor(model => model.MakeNavigation)
@Html.DisplayFor(model => model.MakeNavigation.Name)
@Html.DisplayNameFor(model => model.Color)
@Html.DisplayFor(model => model.Color)
Чтобы применить шаблон
CarWithColors.cshtml
вместо Car.cshtml
, вызовите DisplayForModel()
с именем шаблона (обратите внимание, что правила местоположения по-прежнему актуальны):
@Html.DisplayForModel("CarWithColors")
Каталог
EditorTemplates
работает аналогично каталогу DisplayTemplates
, но находящиеся в нем шаблоны используются для редактирования.
Создайте внутри каталога
Views\Cars
новый каталог под названием EditorTemplates
и добавьте в него новое представление по имени Car.cshtml
. Удалите сгенерированный код вместе с комментариями и замените его показанным ниже кодом, который является разметкой для редактирования сущности Car
:
@model Car
В шаблоне редактирования задействовано несколько вспомогательных функций дескрипторов (
asp-for
, asp-items
, asp-validation-for
и asp-validation-summary
), которые рассматриваются позже в главе.
Шаблон редактирования
Car
вызывается с помощью вспомогательных функций HTML, которые называются EditorFor()
и EditorForModel()
. Подобно шаблонам отображения упомянутые функции будут искать представление с именем Car.cshtml
или с таким же именем, как у метода.
По аналогии с мастер-страницами Web Forms в MVC поддерживаются компоновки, которые совместно используются представлениями, чтобы обеспечить согласованный внешний вид страниц сайта. Перейдите в каталог
Views\Shared
и откройте файл _Layout.cshtml
. Это полноценный HTML-файл с дескрипторами
и
.
Файл
_Layout.cshtml
является основой, в которую визуализируются другие представления. Кроме того, поскольку большая часть страницы (такая как разметка для навигации и верхнего и/или нижнего колонтитула) поддерживается страницей компоновки, страницы представлений сохраняются небольшими и простыми. Найдите в файле _Layout.cshtml
следующую строку кода Razor:
@RenderBody()
Эта строка указывает странице компоновки, где визуализировать представление. Теперь перейдите к строке, расположенной прямо перед закрывающим дескриптором
, которая создает новый раздел для компоновки и объявляет его необязательным:
@await RenderSectionAsync("scripts", required: false)
Разделы также могут помечаться как обязательные путем передачи для второго параметра (
required
) значения true
. Вдобавок они могут визуализироваться синхронным образом:
@RenderSection("Header",true)
Любой код и/или разметка в блоке @ section файла представления будет визуализироваться не там, где вызывается
@RenderBody()
, а в месте определения раздела, присутствующего в компоновке. Например, пусть у вас есть представление со следующей реализацией раздела:
@section Scripts {
}
Код из представления визуализируется в компоновке на месте определения раздела. Если компоновка содержит показанное ниже определение:
@await RenderSectionAsync("Scripts", required: false)
тогда будет добавлен раздел представления, приводя в результате к отправке браузеру следующей разметки:
В ASP.NET Core появились два новых метода:
IgnoreBody()
и IgnoreSection()
. В случае помещения внутрь компоновки эти методы отменяют визуализацию тела представления или указанного раздела соответственно. Они позволяют включать или отключать функции представления в компоновке на основе условной логики, такой как уровни безопасности.
Как упоминалось ранее, стандартная страница компоновки определяется в файле
_ViewStart.cshtml
. Любое представление, где не указана компоновка, будет использовать компоновку, определенную в первом файле _ViewStart.cshtml
, который обнаруживается в каталоге представления или выше него в структуре каталогов.
Частичные представления концептуально похожи на пользовательские элементы управления в Web Forms. Частичные представления удобны для инкапсуляции пользовательского интерфейса, что помогает сократить объем повторяющегося кода и/или разметки. Частичное представление не задействует компоновку и внедряется внутрь другого представления или визуализируется с помощью компонента представления (рассматривается позже в главе).
Временами файлы могут становиться большими и громоздкими. Один из способов справиться с такой проблемой предусматривает разбиение компоновки на набор специализированных частичных представлений.
Создайте внутри каталога Shared новый каталог подназванием
Partials
и добавьте в него три пустых представления с именами _Head.cshtml
, _JavaScriptFiles.cshtml
и _Menu.cshtml
.
Вырежьте содержимое между дескрипторами
в компоновке и вставьте его в файл _Head.cshtml
:
@ViewData["Title"] - AutoLot.Mvc
Замените разметку, удаленную из файла
_Layout.cshtml
, вызовом для визуализации нового частичного представления:
Дескриптор
— это еще один пример вспомогательной функции дескриптора. В атрибуте name указывается имя частичного представления с путем, начинающимся с текущего каталога представления, которым в данном случае является Views\Shared
.
Для частичного представления
Menu
вырежьте всю разметку между дескрипторами
(не
) и вставьте ее в файл Menu.cshtml
. Модифицируйте файл Layout.cshtml
, чтобы визуализировать частичное представление Menu
:
Наконец, вырежьте дескрипторы
для файлов JavaScript и вставьте их в частичное представление JavaScriptFiles
. Удостоверьтесь в том, что оставили дескриптор RenderSection
на своем месте. Вот частичное представление JavaScriptFiles
:
Ниже приведена текущая разметка в файле
_Layout.cshtml
:
@RenderBody()
@await RenderSectionAsync("Scripts", required: false)
Существует несколько способов отправки данных представлению. В случае строго типизированных представлений данные можно отправлять, когда представления визуализируются (либо из метода действия, либо через вспомогательную функцию дескриптора
).
При передаче методу
View()
модели или модели представления значение присваивается свойству @model
строго типизированного представления (обратите внимание на букву m
в нижнем регистре):
@model IEnumerable
Свойство
@model
устанавливает тип для представления, к которому затем можно получать доступ с использованием Razor-команды @Model
(обратите внимание на букву М
в верхнем регистре):
@foreach (var item in Model)
{
// Делать что-то.
}
В методе действия
RazorViewSyntax()
демонстрируется представление, получающее данные из этого метода действия:
[HttpGet]
public IActionResult RazorSyntax([FromServices] ICarRepo carRepo)
{
var car = carRepo.Find(1);
return View(car);
}
Значение модели может быть передано и в
, как показано ниже:
Объекты
ViewBag
, ViewData
и TempData
являются механизмами для отправки представлению данных небольшого объема. В табл. 31.1 описаны три механизма передачи данных из контроллера в представление (помимо свойства Model
) либо из контроллера в контроллер.
И
ViewBag
, и ViewData
указывают на тот же самый объект; они просто предлагают разные способы доступа к данным. Еще раз взгляните на созданный ранее файл _HeadPartial.cshtml
(важная строка выделена полужирным):
@ViewData["Title"] - AutoLot.Mvc
Вы заметите, что в атрибуте
для установки значения применяется объект ViewData
. Поскольку ViewData
— конструкция Razor, она предваряется символом @
. Чтобы увидеть результаты, модифицируйте представление RazorSyntax.cshtml
следующим образом:
@model AutoLot.Models.Entities.Car
@{
ViewData["Title"] = "RazorSyntax";
}
Razor Syntax
...
Теперь после запуска приложенияи перехода поссылке
https://localhost:5001/Home/RazorSyntax
вы увидите на вкладке браузера заголовок Razor Syntax — AutoLot.Mvc
(Синтаксис Razor — AutoLot.Mvc
).
Вспомогательные функции дескрипторов являются новым средством, введенным в версии ASP.NET Core. Вспомогательная функция дескриптора (tag helper) — это разметка (специальный дескриптор или атрибут в стандартном дескрипторе), представляющий код серверной стороны, который затем помогает сформировать выпускаемую HTML-разметку Они значительно совершенствуют процесс разработки и улучшают читабельность представлений MVC.
В отличие от вспомогательных функций HTML, которые вызываются как методы Razor, вспомогательные функции дескрипторов представляют собой атрибуты, добавляемые к стандартным HTML-элементам или автономным специальным дескрипторам. В случае использования для разработки среды Visual Studio появляется дополнительное преимущество в виде средства IntelliSense, которое отображает подсказки по встроенным вспомогательным функциям дескрипторов.
Например, показанная ниже вспомогательная функция HTML создает метку для свойства
FullName
заказчика:
@Html.Label("FullName","Full Name:",new {@class="customer"})
В итоге генерируется следующая HTML-разметка:
По всей видимости, синтаксис вспомогательных функций HTML хорошо понятен разработчикам на языке С#, применяющим ASP.NET МУС и Razor. Но его нельзя считать интуитивно понятным, особенно для тех, кто имеет дело с HTML/CSS/JavaScript, но не с языком С#.
Версия в виде вспомогательной функции дескриптора выглядит так:
Она производит тот же самый вывод, но вспомогательные функции дескрипторов благодаря своей интеграции с дескрипторами HTML удерживают разработчика "в рамках разметки".
Существует множество встроенных вспомогательных функций дескрипторов, которые предназначены для применения вместо соответствующих им вспомогательных функций HTML. Однако не все вспомогательные функции HTML имеют ассоциированные вспомогательные функции дескрипторов. В табл. 31.2 перечислены самые распространенные вспомогательные функции дескрипторов, соответствующие им вспомогательные функции HTML и доступные атрибуты. Они будут раскрыты более подробно в оставшейся части главы.
Вспомогательные функции дескрипторов потребуется сделать видимыми любому коду, где их желательно использовать. Файл
_ViewImports.html
из стандартного шаблона уже содержит следующую строку:
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
Строка делает все вспомогательные функции дескрипторов из сборки
Microsoft.AspNetCore.Mvc.TagHelpers
(содержащей все встроенные вспомогательные функции дескрипторов) доступными всем представлениям на уровне каталога с файлом _ViewImports.cshtml
и ниже него в иерархии каталогов.
Вспомогательная функция дескриптора для формы (
) заменяет вспомогательные функции HTML с именами Html.BeginForm()
и Html.BeginRouteForm()
. Скажем, чтобы создать форму, которая отправляет версию действия Edit
для НТТР-метода POST контроллера CarsController
с одним параметром (Id
), потребуется следующий код и разметка:
asp-route-id="@Model.Id" >
С точки зрения строгой HTML-разметки дескриптор
будет работать без атрибутов вспомогательной функции дескриптора для формы. Если атрибуты отсутствуют, тогда это просто обычная HTML-форма, к которой понадобится вручную добавить маркер защиты от подделки. Тем не менее, после добавления одного из атрибутов asp-*
к форме добавляется и маркер защиты от подделки, который можно отключить, добавив к дескриптору
атрибут asp-antiforgery="false"
. Маркер защиты от подделки рассматривается позже в главе.
Форма создания для сущности
Car
отправляется методу действия Create()
класса CarsController
. Добавьте в каталог Views\Cars
новое пустое представление Razor по имени Create.cshtml
со следующим содержимым:
@model Car
@{
ViewData["Title"] = "Create";
}
Create a New Car
Хотя представление не полное, его достаточно для демонстрации того, что было раскрыто до сих пор, а также вспомогательной функции дескриптора для формы. Первая строка строго типизирует представление сущностным классом
Car
. Блок кода Razor устанавливает специфичный к представлению заголовок для страницы HTML-дескриптор
имеет атрибуты asp-controller
и asp-action
, которые выполняются на серверной стороне для формирования дескриптора, а также добавления маркера защиты от подделки. Чтобы визуализировать это представление, добавьте в каталог Controllers
новый контроллер по имени CarsController
. Модифицируйте код, как показано ниже (позже в главе он будет обновлен):
using Microsoft.AspNetCore.Mvc;
namespace AutoLot.Mvc.Controllers
{
[Route("[controller]/[action]")]
public class CarsController : Controller
{
public IActionResult Create()
{
return View();
}
}
}
Теперь запустите приложение и перейдите по ссылке
http://localhost:5001/Cars/Create
. Инспектирование источника покажет, что форма имеет атрибут действия (action
), основанный на asp-controller
и asp-action
, метод (method
), установленный в post
, и добавленный скрытый элемент
с именем __RequestVerificationToken
:
value="CfDJ8Hqg5HsrvCtOkkLRHY4ukxwv
ix0vkQ3vOvezvtJWdl0P5lwbI5-
FFWXh8KCFZo7eKxveCuK8NRJywj8Jz23pP2nV37fIGqqcITRyISGgq7tRYZDuPv8N
MIYz2nCWRiDbxOvlkg61DTDW9BrJxr8H63Y">
Далее в главе представление
Create
будет неоднократно обновляться.
Вспомогательная функция дескриптора для действия формы используется в элементах кнопок и изображений с целью изменения действия содержащей их формы. Например, следующая кнопка, добавленная к форме редактирования, вызовет передачу запроса
POST
конечной точке Create
:
Вспомогательная функция дескриптора для якоря (
<а>
) заменяет вспомогательную функцию HTML с именем Html.ActionLink()
. Скажем, чтобы создать ссылку на представление RazorSyntax
, применяйте такой код:
asp-action="RazorSyntax">
Razor Syntax
Для добавления страницы синтаксиса Razor в меню модифицируйте
_Menu.cshtml
, как показано ниже, добавив новый элемент меню между элементами Home (Домой) и Privacy (Секретность) (дескрипторы
, окружающие дескрипторы якорей, предназначены для меню Bootstrap):
...
asp-action="Index">Home
asp-action="RazorSyntax">Razor Syntax
asp-action="Privacy">Privacy
Вспомогательная функция дескриптора для элемента ввода (
) является одной из наиболее универсальных. В дополнение к автоматической генерации атрибутов id
и name
стандарта HTML, а также любых атрибутов data-val
стандарта HTML5, вспомогательная функция дескриптора строит надлежащую HTML-разметку, основываясь на типе данных целевого свойства. В табл. 31.3 перечислены типы HTML, которые создаются на базе типов .NET Core свойств.
Кроме того, вспомогательная функция дескриптора для элемента ввода добавит атрибуты
type
из HTML5, основываясь на аннотациях данных. В табл. 31.4 перечислены некоторые распространенные аннотации и генерируемые атрибуты type
из HTML5.
Шаблон редактирования
Car.cshtml
содержит дескрипторы
для свойств PetName
и Color
. В качестве напоминания ниже приведены только эти дескрипторы:
Вспомогательная функция дескриптора для элемента ввода добавляет к визуализируемому дескриптору атрибуты
name
и id
, существующее значение для свойства (если оно есть) и атрибуты проверки достоверности HTML5. Оба поля являются обязательными и имеют ограничение на длину строки в 50 символов. Вот визуализированная разметка для указанных двух свойств:
data-val-length="The field Pet
Name must be a string with a
maximum length of 50." data-val-length-max="50"
data-val-
required="The Pet Name field is required."
id="PetName" maxlength="50" name="PetName"
value="Zippy">
data-val-length="The field
Color must be a string with a
maximum length of 50."
data-val-length-max="50"
data-val-
required="The Color field is required."
id="Color" maxlength="50" name="Color" value="Black"
aria-describedby="Color-error" aria-invalid="false">
Вспомогательная функция дескриптора для текстовой области (
) автоматически добавляет атрибуты id
и name
и любые атрибуты проверки достоверности HTML5, определенные для свойства. Например, следующая строка создает дескриптор
для свойства Description
:
Вспомогательная функция дескриптора для элемента выбора (
) создает дескрипторы ввода с выбором из свойства модели и коллекции. Как и в других вспомогательных функциях дескрипторов для элементов ввода, к разметке автоматически добавляются атрибуты id
и name
, а также любые атрибуты data-val
из HTML5. Если значение свойства модели совпадает с одним из значений в списке, тогда для этого варианта в разметку добавляется атрибут selected
.
Например, пусть имеется модель со свойством по имени
Country
и список SelectList
по имени Countries
с таким определением:
public List Countries { get; } = new List
{
new SelectListItem { Value = "MX", Text = "Mexico" },
new SelectListItem { Value = "CA", Text = "Canada" },
new SelectListItem { Value = "US", Text = "USA" },
};
Следующая разметка будет визуализировать дескриптор
с надлежащими дескрипторами
:
Если значением свойства
Country
является CA
, тогда в представление будет выведена показанная ниже разметка:
Вспомогательные функции дескрипторов для сообщения проверки достоверности и для сводки по проверке достоверности в точности отражают вспомогательные функции HTML с именами
Html.ValidationMessageFor()
и Html.ValidationSummaryFor()
. Первая применяется к HTML-дескриптору
для отдельного свойства модели, а вторая — к HTML-дескриптору для целой модели. Сводка по проверке достоверности поддерживает варианты Аll
(все ошибки), ModelOnly
(ошибки только модели, но не свойств модели) и None
(никаких ошибок).
Вспомните вспомогательные функции дескрипторов для проверки достоверности из
EditorTemplate
в файле Car.cshtml
(выделены полужирным):
Эти вспомогательные функции дескрипторов будут отображать ошибки модели, возникшие во время привязки и проверки достоверности, как показано на рис. 31.3.
Вспомогательная функция дескриптора для среды
Вспомогательная функция дескриптора для среды (
) обычно используется для условной загрузки файлов JavaScript и CSS (или подходящей разметки) на основе среды, в которой запущен сайт. Откройте частичное представление _Head.cshtml
и модифицируйте разметку следующим образом:
@ViewData["Title"] - AutoLot.Mvc
В первой вспомогательной функции дескриптора для среды применяется атрибут
include="Development"
, чтобы включить содержащиеся файлы, когда среда установлена в Development
. В таком случае загружается неминифицированная версия Bootstrap. Во второй вспомогательной функции дескриптора для среды используется атрибут exclude="Development"
, чтобы задействовать содержащиеся файлы, когда среда отличается от Development
. В таком случае загружается минифицированная версия Bootstrap. Файл site.css
остается тем же самым в среде Development
и других средах, поэтому он загружается за пределами вспомогательной функции дескриптора для среды.
Теперь модифицируйте частичное представление
_JavaScriptFiles.cshtml
, как показано ниже (обратите внимание, что файлы в разделе Development
больше не имеют расширения .min
):
Вспомогательная функция дескриптора для ссылки
Вспомогательная функция дескриптора для ссылки (
) имеет атрибуты, применяемые с локальными и удаленными файлами. Атрибут asp-append-version
, используемый с локальными файлами, добавляет хеш-значение для файла как параметр строки запроса в URL, который отправляется браузеру. При изменении файла изменяется и хеш-значение, обновляя посылаемый браузеру URL. Поскольку ссылка изменилась, браузер очищает кеш от этого файла и перезагружает его. Модифицируйте дескрипторы ссылок для bootstrap.css
и site.css
в файле _Head.cshtml
следующим образом:
asp-append-
version="true"/>
asp-append-version="true"/>
Ссылка, отправляемая браузеру для файла
site.css
, теперь выглядит так (ваше хеш-значение будет другим):
rel="stylesheet">
При загрузке файлов CSS из сети доставки содержимого вспомогательные функции дескрипторов предоставляют механизм тестирования, позволяющий удостовериться в том, что файл был загружен надлежащим образом. Тест ищет конкретное значение для свойства в определенном классе CSS, и если свойство не дает совпадения, то вспомогательная функция дескриптора загрузит запасной файл. Модифицируйте раздел
в файле _Head.cshtml
, как показано ниже:
href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/
bootstrap.min.css"
asp-fallback-href="~/lib/bootstrap/dist/css/bootstrap.css"
asp-fallback-test-class="sr-only"
asp-fallback-test-property="position"
asp-fallback-
test-value="absolute"
crossorigin="anonymous"
integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/
iJTQUOhcWr7x9JvoRxT2MZw1T"/>
Вспомогательная функция дескриптора для сценария
Вспомогательная функция дескриптора для сценария (
) похожа на вспомогательную функцию дескриптора для ссылки с настройками очистки кеша и перехода на запасной вариант загрузки из сети доставки содержимого. Атрибут asp-append-version
работает для сценариев точно так же, как для ссылок на таблицы стилей. Атрибуты asp-fallback-*
также применяются с источниками файлов в сети доставки содержимого. Атрибут asp-fallback-test
просто проверяет достоверность кода JavaScript и в случае неудачи загружает файл из запасного источника.
Обновите частичное представление
_JavaScriptFiles.cshtml
, чтобы использовать очистку кеша и переход на запасной вариант загрузки из сети доставки содержимого (обратите внимание, что шаблон MVC уже содержит атрибут asp-append-version
в дескрипторе
для site.js
):
asp-append-version="true">
asp-append-version="true">
asp-fallback-src="~/lib/jquery/dist/jquery.min.js"
asp-fallback-test="window.jQuery"
crossorigin="anonymous"
integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=">
asp-fallback-src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"
asp-fallback-test="window.jQuery && window.jQuery.fn && window.jQuery.fn.modal"
crossorigin="anonymous"
integrity="sha384-xrRywqdh3PHs8keKZN+8zzc5TX0GRTLCcmivcbNJWm2rs5C
8PRhcEn3czEjhAO9o">
Частичное представление
_ValidationScriptsPartial.cshtml
необходимо обновить с применением вспомогательных функций дескрипторов для среды и сценариев:
asp-append-version="true">
script>
asp-
append-version="true">
asp-fallback-src="~/lib/jquery-validation/dist/jquery.validate.min.js"
asp-fallback-test="window.jQuery && window.jQuery.validator"
crossorigin="anonymous"
integrity="sha256-F6h55Qw6sweK+t7SiOJX+2bpSAa3b/fnlrVCJvmEj1A=">
asp-fallback-src="~/lib/jquery-validation-unobtrusive/
jquery.validate.unobtrusive.min.js"
asp-fallback-test="window.jQuery && window.jQuery.validator &&
window.jQuery.validator.
unobtrusive"
crossorigin="anonymous"
integrity="sha256-9GycpJnliUjJDVDqP0UEu/bsm9U+3dnQUH8+3W10vkY=">
Вспомогательная функция дескриптора для изображения
Вспомогательная функция дескриптора для изображения (
) предоставляет атрибут asp-append-version
, который работает точно так же, как во вспомогательных функциях дескрипторов для ссылки и сценария.
Специальные вспомогательные функции дескрипторов
Специальные вспомогательные функции дескрипторов могут помочь избавиться от повторяющегося кода. В проекте
AutoLot.Mvc
специальные вспомогательные функции дескрипторов заменят HTML-разметку, используемую для навигации между экранами CRUD для Car
.
Подготовительные шаги
Специальные вспомогательные функции дескрипторов задействуют
UrlHelperFactory
и IActionContextAccessor
для ссылок на основе маршрутизации. Кроме того, будет добавлен расширяющий метод для типа string, чтобы удалять суффикс Controller
из имен контроллеров.
Обновление Startup.cs
Для создания экземпляра
UrlFactory
класса, производного не от класса Controller
, в коллекцию служб потребуется добавить IActionContextAccessor
. Начните с добавления в файл Startup.cs
следующих пространств имен:
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.DependencyInjection.Extensions;
Затем добавьте в метод
ConfigureServices()
такую строку:
services.TryAddSingleton();
Создание расширяющего метода для типа string
При обращении к именам контроллеров в коде инфраструктуре ASP.NET Core довольно часто требуется низкоуровневое строковое значение, не содержащее суффикс
Controller
, что препятствует использованию метода nameof()
без последующего вызова string.Replace()
. Со временем задача становится утомительной, поэтому для ее решения будет создан расширяющий метод для типа string
.
Создайте в проекте
AutoLot.Services
новый каталог по имени Utilities
и добавьте в него файл StringExtensions.cs
со статическим классом StringExtensions
. Модифицируйте код, добавив расширяющий метод RemoveController()
:
using System;
namespace AutoLot.Mvc.Extensions
{
public static class StringExtensions
{
public static string RemoveController(this string original)
=> original.Replace("Controller", "", StringComparison.OrdinalIgnoreCase);
}
}
Создание базового класса
Создайте в проекте
AutoLot.Mvc
новый каталог по имени TagHelpers
и внутри него каталог Base
. Добавьте в каталог Base
файл класса ItemLinkTagHelperBase.cs
, сделайте класс ItemLinkTagHelperBase
открытым и абстрактным, а также унаследованным от класса TagHelper
. Приведите код класса к следующему виду:
using AutoLot.Mvc.Controllers;
using AutoLot.Services.Utilities;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Razor.TagHelpers;
namespace AutoLot.Mvc.TagHelpers.Base
{
public abstract class ItemLinkTagHelperBase : TagHelper
{
}
}
Добавьте конструктор, который принимает экземпляры реализаций
IActionContextAccessor
и IUrlHelperFactory
. Используйте UrlHelperFactory
с ActionContextAccessor
, чтобы создать экземпляр реализации IUrlHelper
, и сохраните его в переменной уровня класса. Вот необходимый код:
protected readonly IUrlHelper UrlHelper;
protected ItemLinkTagHelperBase(IActionContextAccessor contextAccessor,
IUrlHelperFactory
urlHelperFactory)
{
UrlHelper = urlHelperFactory.GetUrlHelper(contextAccessor.ActionContext);
}
Добавьте открытое свойство для хранения
Id
элемента:
public int? ItemId { get; set; }
При вызове вспомогательной функции дескриптора вызывается метод
Process()
, принимающий два параметра, TagHelperContext
и TagHelperOutput
. Параметр TagHelperContext
применяется для получения остальных атрибутов дескриптора и словаря объектов, которые используются с целью взаимодействия с другими вспомогательными функциями дескрипторов, нацеленными на дочерние элементы. Параметр TagHelperOutput
применяется для создания визуализированного вывода. Поскольку это базовый класс, создайте метод по имени BuildContent()
, который производные классы смогут вызывать из метода Process()
. Добавьте следующий код:
protected void BuildContent(TagHelperOutput output,
string actionName, string className, string displayText, string fontAwesomeName)
{
output.TagName = "a"; // Заменить дескриптором .
var target = (ItemId.HasValue)
? UrlHelper.Action(actionName,
nameof(CarsController).RemoveController(),
new {id = ItemId})
: UrlHelper.Action(actionName, nameof(CarsController).RemoveController());
output.Attributes.SetAttribute("href", target);
output.Attributes.Add("class",className);
output.Content.AppendHtml($@"{displayText}
");
}
В предыдущем код присутствует ссылка на набор инструментов для значков и шрифтов Font Awesome, который будет добавлен в проект позже в главе.
Вспомогательная функция дескриптора для вывода сведений об элементе
Создайте в каталоге
TagHelpers
новый файл класса по имени ItemDetailsTagHelper.cs
. Сделайте класс ItemDetailsTagHelper
открытым и унаследованным от класса ItemLinkTagHelperBase
. Добавьте в новый файл показанный ниже код:
using AutoLot.Mvc.Controllers;
using AutoLot.Mvc.TagHelpers.Base;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Razor.TagHelpers;
namespace AutoLot.Mvc.TagHelpers
{
public class ItemDetailsTagHelper : ItemLinkTagHelperBase
{
}
}
Добавьте открытый конструктор, который принимает обязательные экземпляры и передает их конструктору базового класса:
public ItemDetailsTagHelper(
IActionContextAccessor contextAccessor,
IUrlHelperFactory urlHelperFactory)
: base(contextAccessor, urlHelperFactory) {}
Переопределите метод
Process()
, чтобы вызывать метод BuildContent()
базового класса:
public override void Process(TagHelperContext context, TagHelperOutput output)
{
BuildContent(output,nameof(CarsController.Details),
"text-info","Details","info-circle");
}
Код создает ссылку Details (Детали) с изображением значка информации из Font Awesome. Чтобы не возникали ошибки при компиляции, добавьте в
CarsController
базовый метод Details()
:
public IActionResult Details()
{
return View();
}
Вспомогательная функция дескриптора для удаления элемента
Создайте в каталоге
TagHelpers
новый файл класса по имени ItemDeleteTagHelper.cs
. Сделайте класс ItemDeleteTagHelper
открытым и унаследованным от класса ItemLinkTagHelperBase
. Добавьте в новый файл следующий код:
using AutoLot.Mvc.Controllers;
using AutoLot.Mvc.TagHelpers.Base;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Razor.TagHelpers;
namespace AutoLot.Mvc.TagHelpers
{
public class ItemDeleteTagHelper : ItemLinkTagHelperBase
{
}
}
Добавьте открытый конструктор, который принимает обязательные экземпляры и передает их конструктору базового класса:
public ItemDeleteTagHelper(
IActionContextAccessor contextAccessor,
IUrlHelperFactory urlHelperFactory)
: base(contextAccessor, urlHelperFactory) {}
Переопределите метод
Process()
, чтобы вызывать метод BuildContent()
базового класса:
public override void Process(TagHelperContext context, TagHelperOutput output)
{
BuildContent(output,nameof(CarsController.Delete),"text-danger","Delete","trash");
}
Код создает ссылку Delete (Удалить) с изображением значка мусорного ящика из Font Awesome. Чтобы не возникали ошибки при компиляции, добавьте в
CarsController
базовый метод Delete()
:
public IActionResult Delete()
{
return View();
}
Вспомогательная функция дескриптора для редактирования сведений об элементе
Создайте в каталоге
TagHelpers
новый файл класса по имени ItemEditTagHelper.cs
. Сделайте класс ItemEditTagHelper
открытым и унаследованным от класса ItemLinkTagHelperBase
. Добавьте в новый файл показанный ниже код:
using AutoLot.Mvc.Controllers;
using AutoLot.Mvc.TagHelpers.Base;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Razor.TagHelpers;
namespace AutoLot.Mvc.TagHelpers
{
public class ItemEditTagHelper : ItemLinkTagHelperBase
{
}
}
Добавьте открытый конструктор, который принимает обязательные экземпляры и передает их конструктору базового класса:
public ItemEditTagHelper(
IActionContextAccessor contextAccessor,
IUrlHelperFactory urlHelperFactory)
: base(contextAccessor, urlHelperFactory) {}
Переопределите метод
Process()
, чтобы вызывать метод BuildContent()
базового класса:
public override void Process(TagHelperContext context, TagHelperOutput output)
{
BuildContent(output,nameof(CarsController.Edit),"text-warning","Edit","edit");
}
Код создает ссылку Edit (Редактировать) с изображением значка карандаша из Font Awesome. Чтобы не возникали ошибки при компиляции, добавьте в
CarsController
базовый метод Edit()
:
public IActionResult Edit()
{
return View();
}
Вспомогательная функция дескриптора для создания элемента
Создайте в каталоге
TagHelpers
новый файл класса по имени itemCreateTagHelper.cs
. Сделайте класс ItemCreateTagHelper
открытым и унаследованным от класса ItemLinkTagHelperBase
. Добавьте в новый файл следующий код:
using AutoLot.Mvc.Controllers;
using AutoLot.Mvc.TagHelpers.Base;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Razor.TagHelpers;
namespace AutoLot.Mvc.TagHelpers
{
public class ItemCreateTagHelper : ItemLinkTagHelperBase
{
}
}
Добавьте открытый конструктор, который принимает обязательные экземпляры и передает их конструктору базового класса:
public ItemCreateTagHelper(
IActionContextAccessor contextAccessor,
IUrlHelperFactory urlHelperFactory)
: base(contextAccessor, urlHelperFactory) {}
Переопределите метод
Process()
, чтобы вызывать метод BuildContent()
базового класса:
public override void Process(TagHelperContext context, TagHelperOutput output)
{
BuildContent(output,nameof(CarsController.Create),"text-success","Create new","plus");
}
Код создает ссылку Create new (Создать) с изображением значка плюса из Font Awesome.
Вспомогательная функция дескриптора для вывода списка элементов
Создайте в каталоге
TagHelpers
новый файл класса по имени ItemListTagHelper.cs
. Сделайте класс ItemListTagHelper
открытым и унаследованным от класса ItemLinkTagHelperBase
. Добавьте в новый файл показанный ниже код:
using AutoLot.Mvc.Controllers;
using AutoLot.Mvc.TagHelpers.Base;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Razor.TagHelpers;
namespace AutoLot.Mvc.TagHelpers
{
public class ItemListTagHelper : ItemLinkTagHelperBase
{
}
}
Добавьте открытый конструктор, который принимает обязательные экземпляры и передает их конструктору базового класса:
public ItemListTagHelper(
IActionContextAccessor contextAccessor,
IUrlHelperFactory urlHelperFactory)
: base(contextAccessor, urlHelperFactory) {}
Переопределите метод
Process()
, чтобы вызывать метод BuildContent()
базового класса:
public override void Process(TagHelperContext context, TagHelperOutput output)
{
BuildContent(output,nameof(CarsController.Index),
"text-default","Back to List","list");
}
Код создает ссылку Back to List (Список) с изображением значка списка из Font Awesome. Чтобы не возникали ошибки при компиляции, добавьте в
CarsController
базовый метод Index()
:
public IActionResult Index()
{
return View();
}
Обеспечение видимости специальных вспомогательных функций дескрипторов
Чтобы сделать специальные вспомогательные функции дескрипторов видимыми, потребуется выполнить команду
@addTagHelper
для представлений, которые используют эти вспомогательные функции дескрипторов, или поместить ее в файл _ViewImports.cshtml
. Откройте файл _ViewImports.cshtml
в каталоге Views и добавьте в него следующую строку:
@addTagHelper *, AutoLot.Mvc
Вспомогательные функции HTML
Вспомогательные функции HTML из ASP.NET MVC по-прежнему поддерживаются, а некоторые из них применяются довольно широко и перечислены в табл. 31.5.
Вспомогательная функция DisplayFor()
Вспомогательная функция
DisplayFor()
отображает объект, определяемый выражением. Если для отображаемого типа существует шаблон отображения, тогда он будет применяться при создании HTML-разметки, представляющей элемент. Например, если моделью представления является сущность Car
, то информацию о производителе автомобиля можно отобразить следующим образом:
@Html.DisplayFor(x=>x.MakeNavigation);
Если в каталоге
DisplayTemplates
присутствует представление по имени Make.cshtml
, тогда оно будет использоваться для визуализации значений (вспомните, что поиск имени шаблона базируется на типе объекта, а не на имени его свойства). Если представление по имени ShowMake.cshtml
(например) существует, то оно будет применяться для визуализации объекта с помощью приведенного ниже вызова:
@Html.DisplayFor(x=>x.MakeNavigation, "ShowMake");
В случае если шаблон не указан и отсутствует представление с именем класса, тогда для создания HTML-разметки, подлежащей отображению, используется рефлексия.
Вспомогательная функция DisplayForModel()
Вспомогательная функция
DisplayForModel()
отображает модель для представления. Если для отображаемого типа существует шаблон отображения, то он будет применяться при создании HTML-разметки, представляющей элемент. Продолжая предыдущий пример представления с сущностью Car
в качестве модели, полную информацию Car
можно отобразить следующим образом:
@Html.DisplayForModel();
Как и в случае со вспомогательной функцией
DisplayFor()
, если существует шаблон отображения, имеющий имя типа, тогда он будет использоваться. Можно также применять именованные шаблоны. Скажем, для отображения сущности Car
с помощью шаблона отображения CarWithColors.html
необходимо использовать такой вызов:
@Html.DisplayForModel("CarWithColors");
Если шаблон не указан и отсутствует представление с именем класса, то для создания HTML-разметки, подлежащей отображению, используется рефлексия.
Вспомогательные функции EditorFor() и EditorForModel()
Вспомогательные функции
EditorFor()
и EditorForModel()
работают аналогично соответствующим вспомогательным функциям для отображения, но с тем отличием, что шаблоны ищутся в каталоге EditorTemplates
и вместо представления объекта, предназначенного только для чтения, отображаются HTML-формы редакторов.
Управление библиотеками клиентской стороны
До завершения представлений нужно обновить библиотеки клиентской стороны (CSS и JavaScript). Проект диспетчера библиотек LibraryManager (первоначально разрабатываемый Мэдсом Кристенсеном) теперь является частью Visual Studio (VS2019) и также доступен в виде глобального инструмента .NET Core. Для извлечения инструментов CSS и JavaScript из
CDNJS.com
, UNPKG.com
, jsDelivr.com
или файловой системы в LibraryManager
используется простой файл JSON.
Установка диспетчера библиотек как глобального инструмента .NET Core
Диспетчер библиотек встроен в Visual Studio. Чтобы установить его как глобальный инструмент .NET Core, введите следующую команду:
dotnet tool install --global Microsoft.Web.LibraryManager.Cli --version 2.1.113
Текущая версия диспетчера библиотек доступна по ссылке
https://www.nuget.org/packages/Microsoft.Web.LibraryManager.Cli/
.
Добавление в проект AutoLot.Mvc библиотек клиентской стороны
При создании проекта
AutoLot.Mvc
(с помощью Visual Studio или командной строки .NET Core CLI) в каталог wwwroot\lib
было установлено несколько файлов JavaScript и CSS. Удалите каталог lib
вместе со всеми содержащимися в нем файлами, т.к. все они будут заменены диспетчером библиотек.
Добавление файла libman.json
Файл
libman.json
управляет тем, что именно устанавливается, из каких источников и куда попадают установленные файлы.
Visual Studio
Если вы работаете в Visual Studio, тогда щелкните правой кнопкой мыши на имени проекта
AutoLot.Mvc
и выберите в контекстном меню пункт Manage Client-Side Libraries (Управлять библиотеками клиентской стороны), в результате чего в корневой каталог проекта добавится файл libman.json
. В Visual Studio также есть возможность связать диспетчер библиотек с процессом MSBuild. Щелкните правой кнопкой мыши на имени файла libman.json
и выберите в контекстном меню пункт Enable restore on build (Включить восстановление при сборке). Вам будет предложено разрешить другому пакету NuGet (Microsoft.Web.LibraryManager.Build
) восстановиться в проекте. Разрешите установку пакета.
Командная строка
Создайте новый файл
libman.json
посредством следующей команды (она устанавливает CDNJS.com
в качестве стандартного поставщика):
libman init --default-provider cdnjs
Обновление файла libman.json
Для поиска библиотек, подлежащих установке, сеть доставки содержимого
CDNJS.com
предлагает удобный для человека API-интерфейс. Список всех доступных библиотек можно просмотреть по следующему URL:
https://api.cdnjs.com/libraries?output=human
Найдя библиотеку, которую вы хотите установить, модифицируйте URL, указав имя библиотеки из списка, чтобы увидеть ее версии и файлы для каждой версии. Например, для просмотра всех доступных версий и файлов jQuery используйте такую ссылку:
https://api.cdnjs.com/libraries/jquery?output=human
После выбора версии и файлов для установки добавьте имя библиотеки (плюс версию), место назначения (обычно
wwwroot/lib/<ИмяБиблиотеки>
) и файлы, которые требуется загрузить. Скажем, чтобы загрузить jQuery, введите в массив JSON библиотеки следующий код:
{
"library": "jquery@3.5.1",
"destination": "wwwroot/lib/jquery",
"files": [ "jquery.js"]
}
Ниже приведено полное содержимое файла
libman.json
, где указаны все файлы, необходимые для разрабатываемого приложения:
{
"version": "1.0",
"defaultProvider": "cdnjs",
"defaultDestination": "wwwroot/lib",
"libraries": [
{
"library": "jquery@3.5.1",
"destination": "wwwroot/lib/jquery",
"files": [ "jquery.js", "jquery.min.js" ]
},
{
"library": "jquery-validate@1.19.2",
"destination": "wwwroot/lib/jquery-validation",
"files": [ "jquery.validate.js", "jquery.validate.min.js",
"additional-methods.js",
"additional-methods.min.js" ]
},
{
"library": "jquery-validation-unobtrusive@3.2.11",
"destination": "wwwroot/lib/jquery-validation-unobtrusive",
"files": [ "jquery.validate.unobtrusive.js",
"jquery.validate.unobtrusive.min.js" ]
},
{
"library": "twitter-bootstrap@4.5.3",
"destination": "wwwroot/lib/bootstrap",
"files": [
"css/bootstrap.css",
"js/bootstrap.bundle.js",
"js/bootstrap.js"
]
},
{
"library": "font-awesome@5.15.1",
"destination": "wwwroot/lib/font-awesome/",
"files": [
"js/all.js",
"css/all.css",
"sprites/brands.svg",
"sprites/regular.svg",
"sprites/solid.svg",
"webfonts/fa-brands-400.eot",
"webfonts/fa-brands-400.svg",
"webfonts/fa-brands-400.ttf",
"webfonts/fa-brands-400.woff",
"webfonts/fa-brands-400.woff2",
"webfonts/fa-regular-400.eot",
"webfonts/fa-regular-400.svg",
"webfonts/fa-regular-400.ttf",
"webfonts/fa-regular-400.woff",
"webfonts/fa-regular-400.woff2",
"webfonts/fa-solid-900.eot",
"webfonts/fa-solid-900.svg",
"webfonts/fa-solid-900.ttf",
"webfonts/fa-solid-900.woff",
"webfonts/fa-solid-900.woff2"
]
}
]
}
На заметку! Вскоре будет объяснена причина отсутствия в списке минифицированных файлов.
После сохранения
libman.json
(в Visual Studio) файлы будут загружены в каталог wwwroot\lib
проекта. Если же вы работаете в командной строке, тогда введите следующую команду, чтобы перезагрузить все файлы:
libman restore
Доступны дополнительные параметры командной строки, которые можно просмотреть с помощью команды
libman -h
.
Обновление ссылок на файлы JavaScript и CSS
С переходом на диспетчер библиотек местоположение многих файлов JavaScript и CSS изменилось. Файлы Bootstrap и jQuery были загружены в каталог
\dist
. Кроме того, в приложение был добавлен набор инструментов для значков и шрифтов Font Awesome.
Местоположение файлов Bootstrap необходимо изменить на
~/lib/boostrap/css
вместо ~/lib/boostrap/dist/css
. Добавьте Font Awesome в конец, прямо перед site.css
. Модифицируйте файл _Head.cshtml
, как показано ниже:
@ViewData["Title"] - AutoLot.Mvc
asp-append-
version="true"/>
asp-fallback-href="~/lib/bootstrap/css/bootstrap.css"
asp-fallback-test-class="sr-only"
asp-fallback-test-property="position"
asp-fallback-
test-value="absolute"
crossorigin="anonymous"
integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/
iJTQUOhcWr7x9JvoRxT2MZw1T"/>
asp-append-version="true"/>
asp-append-version="true"/>
Далее модифицируйте файл
JavaScriptFiles.cshtml
, удалив \dist
из местоположений jQuery и Bootstrap:
asp-append-version="true">
asp-fallback-src="~/lib/jquery/jquery.min.js"
asp-fallback-test="window.jQuery"
crossorigin="anonymous"
integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=">
asp-fallback-src="~/lib/bootstrap/js/bootstrap.bundle.min.js"
asp-fallback-test="window.jQuery && window.jQuery.fn && window.jQuery.fn.modal"
crossorigin="anonymous"
integrity="sha384-xrRywqdh3PHs8keKZN+8zzc5TX0GRTLCcmivcbNJWm2rs5C
8PRhcEn3czEjhAO9o">
Финальное изменение связано с обновлением местоположений
jquery.validate
в частичном представлении _ValidationScriptsPartial.cshtml
:
asp-append-version="true">
asp-
append-version="true">
asp-fallback-src="~/lib/jquery-validation/jquery.validate.min.js"
asp-fallback-test="window.jQuery && window.jQuery.validator"
crossorigin="anonymous"
integrity="sha256-F6h55Qw6sweK+t7SiOJX+2bpSAa3b/fnlrVCJvmEj1A=">
asp-fallback-src="~/lib/jquery-validation-unobtrusive/
jquery.validate.unobtrusive.min.js"
asp-fallback-test="window.jQuery && window.jQuery.validator &&
window.jQuery.validator.
unobtrusive"
crossorigin="anonymous"
integrity="sha256-9GycpJnliUjJDVDqP0UEu/bsm9U+3dnQUH8+3W10vkY=">
Завершение работы над представлениями CarsController и Cars
В этом разделе будет завершена работа над представлениями
CarsController
и Cars
. Если вы установите в true
флаг RebuildDatabase
внутри файла appsettings.development.json
, тогда любые изменения,внесенные вами во время тестирования этих представлений, будут сбрасываться при следующем запуске приложения.
Класс CarsController
Класс
CarsController
является центральной точкой приложения AutoLot.Mvc
, обладая возможностями создания, чтения, обновления и удаления. В этой версии CarsController
напрямую используется уровень доступа к данным. Позже в главе вы создадите еще одну версию CarsController
, в которой для доступа к данным будет применяться служба AutoLot.Api
.
Приведите операторы
using
в классе CarsController
к следующему виду:
using AutoLot.Dal.Repos.Interfaces;
using AutoLot.Models.Entities;
using AutoLot.Services.Logging;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
Ранее вы добавили класс контроллера с маршрутом. Теперь наступило время добавить экземпляры реализаций
ICarRepo
и IAppLogging
через внедрение зависимостей. Добавьте две переменные уровня класса для хранения этих экземпляров, а также конструктор, который будет внедрять оба экземпляра:
private readonly ICarRepo _repo;
private readonly IAppLogging _logging;
public CarsController(ICarRepo repo, IAppLogging logging)
{
_repo = repo;
_logging = logging;
}
Частичное представление списка автомобилей
Списковые представления (одно для целого реестра автомобилей и одно для списка автомобилей по производителям) совместно используют частичное представление. Создайте в каталоге
Views\Cars
новый каталог по имени Partials
и добавьте в него файл представления _CarListPartial.cshtml
, очистив его содержимое. Установите IEnumerable
в качестве типа (его ненужно указывать полностью, поскольку в файл _ViewImports.cshtml
добавлено пространство имен AutoLot.Models.Entities
):
@model IEnumerable< Car>
Далее добавьте блок кода Razor с набором булевских переменных, которые указывают, должны ли отображаться производители. Когда частичное представление
CarListPartial.cshtml
применяется полным реестром автомобилей, производители будут показаны, а когда отображаются автомобили только одного производителя, то поле Make
должно быть скрыто:
@{
var showMake = true;
if (bool.TryParse(ViewBag.ByMake?.ToString(), out bool byMake))
{
showMake = !byMake;
}
}
В следующей разметке
ItemCreateTagHelper
используется для создания ссылки на метод Create()
типа HttpGet
. В случае применения специальных вспомогательных функций дескрипторов имя указывается с использованием "шашлычного" стиля в нижнем регистре, т.е. суффикс TagHelper
отбрасывается, а каждое слово в стиле Pascal приводится к нижнему регистру и отделяется символом переноса (что похоже на шашлык):
Для настройки таблицы и ее заголовков применяется вспомогательная функция HTML, посредством которой получаются значения
DisplayName
, связанные с каждым свойством. Для DisplayName
будет выбираться значение атрибута Display
или DisplayName
, и если он не установлен, то будет использоваться имя свойства. В следующем разделе применяется блок кода Razor для отображения информации о производителе на основе ранее установленной переменной уровня представления:
@if (showMake)
{
@Html.DisplayNameFor(model => model.MakeId)
}
@Html.DisplayNameFor(model => model.Color)
@Html.DisplayNameFor(model => model.PetName)
В последнем разделе производится проход по записям и их отображение с использованием вспомогательной функции HTML по имени
DisplayFor()
. Эта вспомогательная функция HTML ищет шаблон отображения с именем, соответствующим типу свойства, и если шаблон не обнаруживается, то разметка создается стандартным образом. Для каждого свойства объекта также выполняется поиск шаблона отображения, который применяется при его наличии. Например, если Car
имеет свойство DateTime
, то для него будет использоваться показанный ранее в главе шаблон DisplayTemplate
.
В следующем блоке также задействованы специальные вспомогательные функции дескрипторов
item-edit
, item-details
и item-delete
, которые были добавлены ранее. Обратите внимание, что при передаче значений открытому свойству специальной вспомогательной функции имя свойства указывается с применением "шашлычного" стиля в нижнем регистре и добавляется к дескриптору в виде атрибута:
@foreach (var item in Model)
{
@if (showMake)
{
@Html.DisplayFor(modelItem => item.MakeNavigation.Name)
}
@Html.DisplayFor(modelItem => item.Color)
@Html.DisplayFor(modelItem => item.PetName)
|
|
}
Представление Index
При наличии частичного представления
_CarListPartial
представление Index
будет небольшим. Создайте в каталоге Views\Cars
новый файл представления по имени Index.cshtml
. Удалите весь сгенерированный код и добавьте следующую разметку:
@model IEnumerable
@{
ViewData["Title"] = "Index";
}
Vehicle Inventory
Частичное представление
_CarListPartial
вызывается со значением модели содержащего представления (IEnumerable
), которое передается с помощью атрибута model
. В итоге модель частичного представления устанавливается в объект, переданный вспомогательной функции дескриптора
.
Чтобы взглянуть на представление
Index
, модифицируйте метод Index()
класса CarsController
, как показано ниже:
[Route("/[controller]")]
[Route("/[controller]/[action]")]
public IActionResult Index()
=> View(_repo.GetAllIgnoreQueryFilters());
Запустив приложение и перейдя по ссылке
https://localhost:5001/Cars/Index
, вы увидите список автомобилей (рис. 31.4).
В правой части списка отображаются специальные вспомогательные функции дескрипторов.
Представление ВуMake
Представление
ВуMake
похоже на Index
, но настраивает частичное представление так, что информация о производителе отображается только в заголовке страницы. Создайте в каталоге Views\Cars
новый файл представления по имени ВуMake.cshtml
. Удалите весь сгенерированный код и добавьте следующую разметку:
@model IEnumerable
@{
ViewData["Title"] = "Index";
}
Vehicle Inventory for @ViewBag.MakeName
@{
var mode = new ViewDataDictionary(ViewData) {{"ByMake", true}};
}
Отличия заметить легко. Здесь создается экземпляр
ViewDataDictionary
, содержащий свойство ByMake
из ViewBag
, который затем вместе с моделью передается частичному представлению, что позволяет скрыть информацию о производителе. Метод действия для этого представления должен получить все автомобили с указанным значением MakeId
и установить ViewBag
в MakeName
с целью отображения в пользовательском интерфейсе. Оба значения берутся из маршрута. Добавьте в класс CarsController
новый метод действия по имени ByMake()
:
[HttpGet("/[controller]/[action]/{makeId}/{makeName}")]
public IActionResult ByMake(int makeId, string makeName)
{
ViewBag.MakeName = makeName;
return View(_repo.GetAllBy(makeId));
}
Запустив приложение и перейдя по ссылке
https://localhost:5001/Cars/l/VW
, вы увидите список, показанный на рис. 31.5.
Представление Details
Создайте в каталоге
Views\Cars
новый файл представления по имени Details.cshtml
. Удалите весь сгенерированный код и добавьте следующую разметку:
@model Car
@{
ViewData["Title"] = "Details";
}
Details for @Model.PetName
@Html.DisplayForModel()
Вспомогательная функция
@Html.DisplayForModel()
использует созданный ранее шаблон отображения (Car.cshtml
) для вывода детальной информации об автомобиле.
Прежде чем обновлять метод действия
Details()
, добавьте вспомогательный метод по имени GetOne()
, который будет извлекать одиночную запись Car
:
internal Car GetOneCar(int? id) => !id.HasValue ? null : _repo.Find(id.Value);
Модифицируйте метод действия
Details()
следующим образом:
[HttpGet("{id?}")]
public IActionResult Details(int? id)
{
if (!id.HasValue)
{
return BadRequest();
}
var car = GetOneCar(id);
if (car == null)
{
return NotFound();
}
return View(car);
}
Маршрут для метода действия
Details()
содержит необязательный параметр маршрута id
с идентификатором автомобиля, значение которого присваивается параметру id
метода. Обратите внимание, что у параметра маршрута есть вопросительный знак с маркером. Это указывает на необязательность параметра, почти как вопросительный знак в типе int?
делает переменную int
допускающей значение null
. Если параметр не был предоставлен или оболочка службы не может отыскать автомобиль с идентификатором, заданным в параметре маршрута, тогда метод возвращает ошибку NotFound
. В противном случае метод отправляет найденную запись Car
представлению Details
. Запустив приложение и перейдя по ссылке https://localhost:5001/Cars/Details/1
, вы увидите экран, показанный на рис. 31.6.
Представление Create
Представление
Create
было начато ранее. Вот его полная разметка:
@model Car
@{
ViewData["Title"] = "Create";
}
Create a New Car
@Html.EditorForModel()
class="btn btn-success">Create
|
@section Scripts {
}
Вспомогательная функция
@Html.EditorForModel()
использует созданный ранее шаблон отображения (Car.cshtml
) для отображения редактора сведений об автомобиле.
В разделе
Scripts
представления указано частичное представление _ValidationScriptsPartial
. Вспомните, что в компоновке этот раздел встречается после загрузки jQuery. Шаблон разделов помогает гарантировать загрузку надлежащих зависимостей до загрузки самого содержимого.
Методы действий Create()
В рамках процесса создания применяются два метода действий: первый (
HttpGet
) возвращает пустое представление для ввода новой записи, а второй (HttpPut
) отправляет значения новой записи.
Вспомогательный метод GetMakes()
Вспомогательный метод
GetMakes()
возвращает список записей Make
в виде экземпляра SelectList
и принимает в качестве параметра экземпляр реализации IMakeRepo
:
internal SelectList GetMakes(IMakeRepo makeRepo)
=> new SelectList(makeRepo.GetAll(), nameof(Make.Id), nameof(Make.Name));
Метод действия Create() для GET
Метод действия
Create()
для GET
помещает в словарь ViewData
список SelectList
с записями Make
и отправляет его представлению Create
:
[HttpGet]
public IActionResult Create([FromServices] IMakeRepo makeRepo)
{
ViewData["MakeId"] = GetMakes(makeRepo);
return View();
}
Форму создания можно просмотреть по ссылке
/Cars/Create
(рис. 31.7).
Метод действия Create() для POST
Метод действия
Create()
для POST
применяет неявную привязку модели для создания сущности Car
из значений формы. Вот его код:
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Create([FromServices] IMakeRepo makeRepo, Car car)
{
if (ModelState.IsValid)
{
_repo.Add(car);
return RedirectToAction(nameof(Details),new {id = car.Id});
}
ViewData["MakeId"] = GetMakes(makeRepo);
return View(car);
}
Атрибут
HttpPost
помечает метод как конечную точку приложения для маршрута Cars/Create
, когда запросом является POST
. Атрибут ValidateAntiForgeryToken
, использует значение скрытого элемента ввода для __RequestVerificationToken
чтобы сократить количество атак на сайт.
Экземпляр реализации
IMakeRepo
внедряется в метод из контейнера DI. Поскольку внедрение осуществляется в метод, применяется атрибут FromServices
. Как вы наверняка помните, атрибут FromServices
сообщает механизму привязки о том, чтобы он не пытался привязывать этот тип, и позволяет контейнеру DI узнать о необходимости создания экземпляра класса.
Сущность
Car
неявно привязывается к данным входящего запроса. Если состояние модели (ModelState
) допустимо, тогда сущность Car
добавляется в базу данных и пользователь перенаправляется на метод действия Details()
с использованием вновь созданного идентификатора Car
в качестве параметра маршрута. Такой шаблон называется "отправка-перенаправление-получение" (Post-Redirect-Get
). Пользователь выполняет отправку с помощью метода HttpPost(Create()
) и затем перенаправляется на метод HttpGet(Details()
), что предотвращает повторную отправку браузером запроса POST
, если пользователь решит обновить страницу.
Если состояние модели не является допустимым, то список
SelectList
с записями Make
добавляется в объект ViewData
и сущность, которая была отправлена, посылается обратно представлению Create
. Состояние модели тоже неявно отправляется представлению, так что могут быть отображены любые ошибки.
Представление Edit
Создайте в каталоге
Views\Cars
новый файл представления по имени Edit.cshtml
. Удалите весь сгенерированный код и добавьте следующую разметку:
@model Car
@{
ViewData["Title"] = "Edit";
}
Edit @Model.PetName
asp-route-id="@Model.Id">
@Html.EditorForModel()
Save
|
@section Scripts {
}
В представлении также применяется вспомогательная функция
@Html.EditorForModel()
и частичное представление _ValidationScriptsPartial
. Однако оно еще содержит два скрытых элемента ввода для Id
и TimeStamp
. Они будут отправляться вместе с остальными данными формы, но не должны редактироваться пользователями. Без значений Id
и TimeStamp
не удалось бы сохранять изменения.
Методы действий Edit()
В рамках процесса редактирования используются два метода действий: первый (
HttpGet
) возвращает сущность, подлежащую редактированию, а второй (HttpPut
) отправляет значения обновленной записи.
Метод действия Edit() для GET
Метод действия
Edit()
для GET
получает одиночную запись Car
с идентификатором Id
через оболочку службы и отправляет ее представлению Edit
:
[HttpGet("{id?}")]
public IActionResult Edit([FromServices] IMakeRepo makeRepo, int? id)
{
var car = GetOneCar(id);
if (car == null)
{
return NoContent();
}
ViewData["MakeId"] = GetMakes(makeRepo);
return View(car);
}
Маршрут имеет необязательный параметр
id
, значение которого передается методу с применением параметра id
. Экземпляр реализации IMakeRepo
внедряется в метод и используется для создания списка SelectList
записей Make
. Посредством вспомогательного метода GetOneCar()
получается запись Car
. Если запись Car
найти не удалось, тогда метод возвращает ошибку NoContent
. В противном случае он добавляет список SelectList
записей Make
в словарь ViewData
и визуализирует представление Edit
.
Форму редактирования можно просмотреть по ссылке
/Cars/Edit/1
(рис. 31.8).
Метод действия Edit() для POST
Метод действия
Edit()
для POST
аналогичен методу действия Create()
для POST
с отличиями, описанными после кода метода:
[HttpPost("{id}")]
[ValidateAntiForgeryToken]
public IActionResult Edit([FromServices] IMakeRepo makeRepo, int id, Car car)
{
if (id != car.Id)
{
return BadRequest();
}
if (ModelState.IsValid)
{
_repo.Update(car);
return RedirectToAction(nameof(Details),new {id = car.Id});
}
ViewData["MakeId"] = GetMakes(makeRepo);
return View(car);
}
Метод действия
Edit()
для POST
принимает один обязательный параметр маршрута id
. Если его значение не совпадает со значением Id
реконструированной сущности Car
, тогда клиенту отправляется ошибка BadRequest
. Если состояние модели допустимо, то сущность обновляется, после чего пользователь перенаправляется на метод действия Details()
с применением свойства Id
сущности Car
в качестве параметра маршрута. Здесь также используется шаблон "отправка-перенаправление-получение".
Если состояние модели не является допустимым, то список
SelectList
с записями Make
добавляется в объект ViewData
и сущность, которая была отправлена, посылается обратно представлению Edit
. Состояние модели тоже неявно отправляется представлению, так что могут быть отображены любые ошибки.
Представление Delete
Создайте в каталоге
Views\Cars
новый файл представления по имени Delete.cshtml
. Удалите весь сгенерированный код и добавьте следующую разметку:
@model Car
@{
ViewData["Title"] = "Delete";
}
Delete @Model.PetName
Are you sure you want to delete this car?
@Html.DisplayForModel()
Delete
|
В представлении
Delete
тоже применяется вспомогательная функция @Html.DisplayForModel()
и два скрытых элемента ввода для Id
и TimeStamp
. Это единственные поля, которые отправляются в виде данных формы.
Методы действий Delete()
В рамках процесса удаления используются два метода действий: первый (
HttpGet
) возвращает сущность, подлежащую удалению, а второй (HttpPut
) отправляет значения удаляемой записи.
Метод действия Delete() для GET
Метод действия
Delete()
для GET
функционирует точно так же, как метод действия Details()
:
[HttpGet("{id?}")]
public IActionResult Delete(int? id)
{
var car = GetOneCar(id);
if (car == null)
{
return NotFound();
}
return View(car);
}
Форму удаления можно просмотреть по ссылке
/Cars/Delete/1
(рис. 31.9).
Метод действия Delete() для POST
Метод действия
Delete()
для POST
просто отправляет значения Id
и TimeStamp
оболочке службы:
[HttpPost("{id}")]
[ValidateAntiForgeryToken]
public IActionResult Delete(int id, Car car)
{
if (id != car.Id)
{
return BadRequest();
}
_repo.Delete(car);
return RedirectToAction(nameof(Index));
}
Метод действия
Delete()
для POST
оптимизирован для отправки только значений, которые необходимы инфраструктуре EF Core для удаления записи.
На этом создание представлений и контроллера для сущности
Car
завершено.
Компоненты представлений
Компоненты представлений — еще одно новое функциональное средство, появившееся в ASP.NET Core. Они сочетают в себе преимущества частичных представлений и дочерних действий для визуализации частей пользовательского интерфейса. Как и частичные представления, компоненты представлений вызываются из другого представления,но в отличие от частичных представлений самих по себе компоненты представлений также имеют компонент серверной стороны. Благодаря такой комбинации они хорошо подходят для решения задач, подобных созданию динамических меню (как вскоре будет показано), панелей входа, содержимого боковой панели и всего того, что требует кода серверной стороны, но не может квалифицироваться как автономное представление.
На заметку! Дочерние действия в классической инфраструктуре ASP.NET MVC были методами действий контроллера, которые не могли служить конечными точками, видимыми клиенту. В ASP.NET Core они не существуют.
Для
AutoLot
компонент представления будет динамически создавать меню на основе производителей, которые присутствуют в базе данных. Меню отображается на каждой странице, поэтому вполне логичным местом для него является файл _Layout.cshtml
. Но _Layout.cshtml
не имеет компонента серверной стороны (в отличие от представлений), так что любое действие в приложении должно предоставлять данные компоновке _Layout.cshtml
. Это можно делать в обработчике события OnActionExecuting()
и в записях, помещаемых в объект ViewBag
, но сопровождать подобное не будет простой задачей. Смешивание возможностей серверной стороны и инкапсуляции пользовательского интерфейса превращает такой сценарий в идеальный вариант для использования компонентов представлений.
Код серверной стороны
Создайте в корневом каталоге проекта
AutoLot.Mvc
новый каталог по имени ViewComponents
и добавьте в него файл класса MenuViewComponent.cs
. Подобно контроллерам классы компонентов представлений по соглашению именуются с суффиксом ViewComponent
. И как у контроллеров, при обращении к компонентам представлений суффикс ViewComponent
отбрасывается.
Добавьте в начало файла следующие операторы
using
:
using System.Linq;
using AutoLot.Dal.Repos.Interfaces;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ViewComponents;
Сделайте класс общедоступным и унаследованным от
ViewComponent
. Компоненты представлений не обязательно наследовать от базового класса ViewComponent
, но аналогично ситуации с базовым классом Controller
наследование от ViewComponent
упрощает большую часть работы. Создайте конструктор, который принимает экземпляр реализации интерфейса IMakeRepo
и присваивает его переменной уровня класса. Пока что код выглядит так:
namespace AutoLot.Mvc.ViewComponents
{
public class MenuViewComponent : ViewComponent
{
private readonly IMakeRepo _makeRepo;
public MenuViewComponent(IMakeRepo makeRepo)
{
_makeRepo = makeRepo;
}
}
Компонентам представлений доступны два метода,
Invoke()
и InvokeAsync()
. Один из них должен быть реализован и поскольку MakeRepo
делает только синхронные вызовы, добавьте метод Invoke()
:
public async IViewComponentResult Invoke()
{
}
Когда компонент представления визуализируется из представления, вызывается открытый метод
Invoke()/InvokeAsync()
. Этот метод возвращает экземпляр реализации интерфейса IViewComponentResult
, который концептуально подобен PartialViewResult
, но сильно упрощен. В методе Invoke()
получается список производителей из хранилища и в случае успеха возвращается экземпляр ViewViewComponentResult
(в его имени нет опечатки), где в качестве модели представления применяется список производителей. Если вызов для получения записей Make
завершается неудачей, тогда производится возврат экземпляра ContentViewComponentResult
с сообщением об ошибке. Модифицируйте код метода, как показано ниже:
public IViewComponentResult Invoke()
{
var makes = _makeRepo.GetAll().ToList();
if (!makes.Any())
{
return new ContentViewComponentResult("Unable to get the makes");
}
return View("MenuView", makes);
}
Вспомогательный метод
View()
из базового класса ViewComponent
похож на вспомогательный метод с тем же именем из класса Controller
, но с парой ключевых отличий. Первое отличие заключается в том, что стандартным именем файла представления является Default.cshtml
, а не имя метода. Однако подобно вспомогательному методу View()
из класса Controller
имя представления может быть любым, когда оно передается вызову метода (без расширения .cshtml
). Второе отличие связано с тем, что представление обязано находиться в одном из следующих трех каталогов:
Views/< controller>/Components/<имя_компонента_представления>/
Views/Shared/Components/<имя_компонента_представления>/
Pages/Shared/Components/<имя_компонента_представления>/
На заметку! В версии ASP.NET Core 2.x появился еще один механизм для создания веб-приложений, который называется Razor Pages, но в этой книге он не рассматривается.
Класс C# может находиться где угодно (даже в другой сборке), но файл
<имя_представления>.cshtml
должен храниться в одном из ранее перечисленных каталогов.
Построение частичного представления
Частичное представление, визуализируемое классом
MenuViewComponent
, будет проходить по записям Make
, добавляя каждую в виде элемента списка, который предназначен для отображения в меню Bootstrap. Элемент меню All (Все) добавляется первым как жестко закодированное значение.
Создайте внутри каталога
Views\Shared
новый каталог по имени Components
, а в нем — еще один каталог под названием Menu
. Имя каталога должно совпадать с именем созданного ранее класса компонента представления минус суффикс ViewComponent
. Добавьте в каталог Menu
файл частичного представления по имени MenuView.cshtml
.
Удалите существующий код и поместите в файл показанную ниже разметку:
@model IEnumerable
Вызов компонентов представлений
Компоненты представлений обычно визуализируются из представления (хотя их можно визуализировать также из метода действия контроллера). Синтаксис довольно прямолинеен:
Component.Invoke(<имя_компонента_представления>)
или @await Component.InvokeAsync(<имя_компонента_представления>)
. Как и в случае с контроллерами, при вызове компонента представления суффикс ViewComponent
не должен указываться:
@await Component.InvokeAsync("Menu") // асинхронная версия
@Component.Invoke("Menu") // синхронная версия
Вызов компонентов представлений как специальных вспомогательных функций дескрипторов
Появившиеся в ASP.NET 1.1 компоненты представлений можно вызывать с использованием синтаксиса вспомогательных функций дескрипторов. Вместо применения
Component.InvokeAsync()/Component.Invoke()
просто вызывайте компонент представления следующим образом:
В приложении потребуется разрешить использование такого способа вызова компонентов представлений, что делается добавлением команды
@addTagHelper
с именем сборки, которая содержит нужный компонент представления. В файл _ViewImports.cshtml
необходимо добавить показанную ниже строку, которая уже была добавлена для специальных вспомогательных функций дескрипторов:
@addTagHelper *, AutoLot.Mvc
Обновление меню
Откройте частичное представление
_Menu.cshtml
и перейдите в место сразу после блока /
, который соответствует методу действия Home/Index
. Поместите в частичное представление следующую разметку:
data-toggle="dropdown">Inventory
class="fa fa-car">
Строка, выделенная полужирным, визуализирует
MenuViewComponent
внутри меню. Окружающая ее разметка реализует форматирование Bootstrap.
Запустив приложение, вы увидите меню Inventory (Реестр), содержащее производителей в качестве элементов подменю (рис. 31.10).
Пакетирование и минификация
При построении веб-приложений с применением библиотек клиентской стороны необходимо принять во внимание два дополнительных фактора, которые направлены на улучшение показателей производительности — пакетирование и минификацию.
Пакетирование
У веб-браузеров есть установленный предел на количество файлов, которые разрешено загружать параллельно из одной конечной точки. В случае использования с вашими файлами JavaScript и CSS приемов разработки SOLID, которые предусматривают разбиение связанного кода и стилей на более мелкие и управляемые файлы, могут возникать проблемы. Такой подход совершенствует процесс разработки, но становится причиной снижения производительности приложения из-за того, что файлы ожидают своей очереди на загрузку. Пакетирование — это просто объединение файлов с целью предотвращения их блокировки при достижении веб-браузером своего предела загрузки.
Минификация
Кроме того, для улучшения показателей производительности процесс минификации изменяет файлы CSS и JavaScript, уменьшая их размеры. Необязательные пробельные символы удаляются, а имена, не являющиеся ключевыми словами, делаются короче. Хотя файлы становятся практически нечитабельными для человека, функциональность не затрагивается, причем размеры файлов могут значительно сократиться. В свою очередь это ускоряет процесс загрузки, приводя к увеличению производительности приложения.
Решение WebOptimizer
Существует много инструментов разработки, которые позволяют пакетировать и минифицировать файлы как часть процесса сборки проекта. Безусловно, они эффективны, но могут стать проблематичными, если процессы перестают быть синхронизированными, поскольку на самом деле нет хорошего средства для сравнения исходных файлов с их пакетированными и минифицированными версиями.
WebOptimizer представляет собой пакет с открытым кодом, который обеспечивает пакетирование, минификацию и кеширование в качестве части конвейера ASP.NET Core. Он гарантирует, что пакетированные и минифицированные файлы соответствуют первоначальным файлам. Такие файлы не только точны, они еще и кешируются, значительно уменьшая количество операций дискового чтения для запросов страниц. Вы уже добавили пакет
Libershark.WebOptimizer.Core
при создании проектов в главе 29. Теперь пора им воспользоваться.
Обновление Startup.cs
Первый шаг предусматривает добавление WebOptimizer в конвейер. Откройте файл
Startup.cs
из проекта AutoLot.Mvc
, отыщите в нем метод Configure()
и добавьте в него следующую строку (сразу после вызова арр.UseStaticFiles()
):
app.UseWebOptimizer();
Следующим шагом будет конфигурирование того, что должно минифицироваться и пакетироваться. Обычно при разработке своего приложения вы хотите видеть непакетированные/неминифицированные версии файлов, но в подготовительной и производственной средах желательно применять пакетирование и минификацию. Добавьте показанный ниже блок кода в метод
ConfigureServices()
:
if (_env.IsDevelopment() || _env.IsEnvironment("Local"))
{
services.AddWebOptimizer(false,false);
}
else
{
services.AddWebOptimizer(options =>
{
options.MinifyCssFiles(); // Минифицировать все файлы CSS
//options.MinifyJsFiles(); // Минифицировать все файлы JavaScript
options.MinifyJsFiles("js/site.js");
options.MinifyJsFiles("lib/**/*.js");
});
}
В случае среды
Development
пакетирование и минификация отключаются. Для остальных сред минифицируются все файлы CSS, файл site.js
и все файлы JavaScript (с расширением .js
) в каталоге lib
и его подкаталогах. Обратите внимание, что все пути в проекте начинаются с каталога wwwroot
.
WebOptimizer также поддерживает пакетирование. В первом примере создается пакет с использованием универсализации файловых имен, а во втором — пакет, для которого приводится список конкретных имен:
options.AddJavaScriptBundle("js/validations/validationCode.js",
"js/validations/**/*.js");
options.AddJavaScriptBundle("js/validations/validationCode.js",
"js/validations/validators.
js", "js/validations/errorFormatting.js");
Важно отметить, что минифицированные и пакетированные файлы на самом деле не находятся на диске, а помещаются в кеш. Также важно отметить, что минифицированные файлы сохраняют то же самое имя (site.js и не имеют обычное расширение
.min
(site.min.js
).
На заметку! При обновлении своих представлений с целью добавления ссылок на пакетированные файлы среда Visual Studio сообщит о том, что они не существуют. Не переживайте, все будет визуализироваться из кеша.
Обновление _Viewlmports.cshtml
На финальном шаге в систему добавляются вспомогательные функции дескрипторов WebOptimizer. Они работают точно так же, как вспомогательные функции дескрипторов
asp-append-version
, описанные ранее в главе, но делают это автоматически для всех пакетированных и минифицированных файлов. Поместите в конец файла _ViewImports.cshtml
следующую строку:
@addTagHelper *, WebOptimizer.Core
Шаблон параметров в ASP.NET Core
Шаблон параметров обеспечивает доступ сконфигурированных классов настроек к другим классам через внедрение зависимостей. Конфигурационные классы могут быть внедрены в другой класс с применением одной их версий
IOptions
. В табл. 31.6 кратко описан ряд версий интерфейса IOptions
.
Добавление информации об автодилере
На автомобильном сайте должна отображаться информация об автодилере, которая обязана быть настраиваемой без необходимости в повторном развертывании всего сайта, чего можно достичь с использованием шаблона параметров. Начните с добавления информации об автодилере в файл
appsettings.json
:
{
"Logging": {
"MSSqlServer": {
"schema": "Logging",
"tableName": "SeriLogs",
"restrictedToMinimumLevel": "Warning"
}
},
"ApplicationName": "AutoLot.MVC",
"AllowedHosts": "*",
"DealerInfo": {
"DealerName": "Skimedic's Used Cars",
"City": "West Chester",
"State": "Ohio"
}
}
Далее понадобится создать модель представления для хранения информации об автодилере. Добавьте в каталог
Models
проекта AutoLot.Mvc
новый файл класса по имени DealerInfo.cs
со следующим содержимым:
namespace AutoLot.Mvc.Models
{
public class DealerInfo
{
public string DealerName { get; set; }
public string City { get; set; }
public string State { get; set; }
}
}
На заметку! Конфигурируемый класс должен иметь открытый конструктор без параметров и не быть абстрактным. Стандартные значения можно устанавливать в свойствах класса.
Метод
Configure()
интерфейса IServiceCollection
сопоставляет раздел конфигурационных файлов с конкретным типом. Затем этот тип может быть внедрен в классы и представления с применением шаблона параметров. Откройте файл Startup.cs
и добавьте в него показанный ниже оператор using
:
using AutoLot.Mvc.Models;
Перейдите к методу
ConfigureServices()
и поместите в него следующую строку кода:
services.Configure(Configuration.GetSection(nameof(DealerInfo)));
Откройте файл
HomeController.cs
и добавьте в него такой оператор using
:
using Microsoft.Extensions.Options;
Затем модифицируйте метод
Index()
, как продемонстрировано далее:
[Route("/")]
[Route("/[controller]")]
[Route("/[controller]/[action]")]
[HttpGet]
public IActionResult Index([FromServices] IOptionsMonitor dealerMonitor)
{
var vm = dealerMonitor.CurrentValue;
return View(vm);
}
Когда класс сконфигурирован в коллекции служб и добавлен в контейнер DI, его можно извлечь с использованием шаблона параметров. В рассматриваемом примере
OptionsMonitor
будет читать конфигурационный файл, чтобы создать экземпляр класса DealerInfo
. Свойство CurrentValue
получает экземпляр DealerInfo
, созданный из текущего файла настроек (даже если файл изменялся после запуска приложения). Затем экземпляр DealerInfo
передается представлению Index.cshtml
.
Обновите представление
Index.cshtml
, расположенное в каталоге Views\Home
, чтобы оно было строго типизированным для класса DealerInfo
и отображало свойства модели:
@model AutoLot.Mvc.Models.DealerInfo
@{
ViewData["Title"] = "Home Page";
}
Welcome to @Model.DealerName
Located in @Model.City, @Model.State
На заметку! За дополнительными сведениями о шаблоне параметров в ASP.NET Core обращайтесь в документацию по ссылке
https://docs.microsoft.com/ru-ru/aspnet/core/fundamentals/configuration/options
.
Создание оболочки службы
Вплоть до этого момента в приложении
AutoLot.Mvc
применялся уровень доступа к данным напрямую. Еще один подход предусматривает использование службы AutoLot.Api
, позволяя ей обрабатывать весь доступ к данным.
Обновление конфигурации приложения
Конечные точки приложения
AutoLot.Api
будут варьироваться на основе среды. Скажем, при разработке на вашей рабочей станции базовый URI выглядит как https://localhost:5021
. В промежуточной среде им может быть https://mytestserver.com
. Осведомленность о среде в сочетании с обновленной конфигурационной системой (представленной в главе 29) будут применяться для добавления разных значений.
Файл
appsettings.Development.json
добавит информацию о службе для локальной машины По мере того как код перемещается по разным средам, настройки будут обновляться в специфическом файле среды, чтобы соответствовать базовому URI и конечным точкам для этой среды. В рассматриваемом примере вы обновляете только настройки для среды Development
. Откройте файл appsettings.Development.json
и модифицируйте его следующим образом (изменения выделены полужирным):
{
"Logging": {
"MSSqlServer": {
"schema": "Logging",
"tableName": "SeriLogs",
"restrictedToMinimumLevel": "Warning"
}
},
"RebuildDataBase": false,
"ApplicationName": "AutoLot.Mvc - Dev",
"ConnectionStrings": {
"AutoLot": "Server=.,5433;Database=AutoLot;User ID=sa;Password=P@ssw0rd;"
},
"ApiServiceSettings": {
"Uri": "https://localhost:5021/",
"CarBaseUri": "api/Cars",
"MakeBaseUri": "api/Makes"
}
}
На заметку! Удостоверьтесь, что номер порта соответствует вашей конфигурации для
AutoLot.Api
.
За счет использования конфигурационной системы ASP.NET Core и обновления файлов, специфичных для среды (например,
appsettings.staging.json
и appsettings.production.json
), ваше приложение будет располагать надлежащими значениями без необходимости в изменении кода.
Создание класса ApiServiceSettings
Настройки службы будут заполняться из настроек таким же способом, как и информация об автодилере. Создайте в проекте
AutoLot.Services
новый каталог по имени ApiWrapper
и добавьте в него файл класса ApiServiceSettings.cs
. Имена свойств класса должны совпадать с именами свойств в разделе ApiServiceSettings
файла appsettings.Development.json
. Код класса показан ниже:
namespace AutoLot.Services.ApiWrapper
{
public class ApiServiceSettings
{
public ApiServiceSettings() { }
public string Uri { get; set; }
public string CarBaseUri { get; set; }
public string MakeBaseUri { get; set; }
}
}
Оболочка службы API
В версии ASP.NET Core 2.1 появился интерфейс
IHTTPClientFactory
, который позволяет конфигурировать строго типизированные классы для вызова внутри служб REST. Создание строго типизированного класса дает возможность инкапсулировать все обращения к API в одном месте. Это централизует взаимодействие со службой, конфигурацию клиента HTTP, обработку ошибок и т.д. Затем класс можно добавить в контейнер DI для дальнейшего применения в приложении. Контейнер DI и реализация IHTTPClientFactory
обрабатывают создание и освобождение HTTPClient
.
Интерфейс IApiServiceWrapper
Интерфейс оболочки службы
AutoLot
содержит методы для обращения к службе AutoLot.Api
. Создайте в каталоге ApiWrapper
новый файл интерфейса IApiServiceWrapper.cs
и приведите операторы using
к следующему виду:
using System.Collections.Generic;
using System.Threading.Tasks;
using AutoLot.Models.Entities;
Модифицируйте код интерфейса, как показано ниже:
namespace AutoLot.Services.ApiWrapper
{
public interface IApiServiceWrapper
{
Task> GetCarsAsync();
Task> GetCarsByMakeAsync(int id);
Task GetCarAsync(int id);
Task AddCarAsync(Car entity);
Task UpdateCarAsync(int id, Car entity);
Task DeleteCarAsync(int id, Car entity);
Task> GetMakesAsync();
}
}
Класс ApiServiceWrapper
Создайте в каталоге
ApiWrapper
проекта AutoLot.Services
новый файл класса по имени ApiServiceWrapper.cs
и модифицируйте его операторы using
следующим образом:
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using AutoLot.Models.Entities;
using Microsoft.Extensions.Options;
Сделайте класс открытым и добавьте конструктор, который принимает экземпляр
HttpClient
и экземпляр реализации IOptionsMonitor
. Создайте закрытую переменную типа ServiceSettings
и присвойте ей значение с использованием свойства CurrentValue
параметра IOptionsMonitor
. Код показан ниже:
public class ApiServiceWrapper : IApiServiceWrapper
{
private readonly HttpClient _client;
private readonly ApiServiceSettings _settings;
public ApiServiceWrapper(HttpClient client,
IOptionsMonitor settings)
{
_settings = settings.CurrentValue;
_client = client;
_client/BaseAddress = new Uri(_settins.Uri);
}
}
На заметку! В последующих разделах содержится много кода без какой-либо обработки ошибок. Поступать так настоятельно не рекомендуется! Обработка ошибок здесь опущена из-за экономии пространства.
Внутренние поддерживающие методы
Класс содержит четыре поддерживающих метода, которые применяются открытыми методами.
Вспомогательные методы для POST и PUT
Следующие методы являются оболочками для связанных методов
HttpClient
:
internal async Task PostAsJson(string uri, string json)
{
return await _client.PostAsync(uri, new StringContent(json, Encoding.UTF8,
"application/json"));
}
internal async Task PutAsJson(string uri, string json)
{
return await _client.PutAsync(uri, new StringContent(json, Encoding.UTF8,
"application/json"));
}
Вспомогательный метод для DELETE
Последний вспомогательный метод используется для выполнения НТТР-метода
DELETE
. Спецификация HTTP 1.1 (и более поздние версии) позволяет передавать тело в HTTP-методе DELETE
, но для этого пока еще не предусмотрено расширяющего метода HttpClient
. Экземпляр HttpRequestMessage
потребуется создавать с нуля.
Первым делом необходимо создать сообщение запроса с применением инициализации объектов для установки
Content
, Method
и RequestUri
. Затем сообщение отправляется, после чего ответ возвращается вызывающему коду. Вот код метода:
internal async Task DeleteAsJson(string uri, string json)
{
HttpRequestMessage request = new HttpRequestMessage
{
Content = new StringContent(json, Encoding.UTF8, "application/json"),
Method = HttpMethod.Delete,
RequestUri = new Uri(uri)
};
return await _client.SendAsync(request);
}
Вызовы HTTP-метода GET
Есть четыре вызова НТТР-метода
GET
: один для получения всех записей Car
, один для получения записей Car
по производителю Make
, один для получения одиночной записи Car
и один для получения всех записей Make
. Все они следуют тому же самому шаблону. Метод GetAsync()
вызывается для возвращения экземпляра HttpResponseMessage
. Успешность или неудача вызова проверяется с помощью метода EnsureSuccessStatusCode()
, который генерирует исключение, если вызов не возвратил код состояния успеха. Затем тело ответа сериализируется в тип свойства (сущность или список сущностей) и возвращается вызывающему коду. Ниже приведен код всех методов:
public async Task> GetCarsAsync()
{
var response = await _client.GetAsync($"{_settings.Uri}{_settings.CarBaseUri}");
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync>();
return result;
}
public async Task> GetCarsByMakeAsync(int id)
{
var response = await
_client.GetAsync($"{_settings.Uri}{_settings.CarBaseUri}/bymake/
{id}");
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync>();
return result;
}
public async Task GetCarAsync(int id)
{
var response = await
_client.GetAsync($"{_settings.Uri}{_settings.CarBaseUri}/{id}");
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync();
return result;
}
public async Task> GetMakesAsync()
{
var response = await
_client.GetAsync($"{_settings.Uri}{_settings.MakeBaseUri}");
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync>();
return result;
}
Вызов HTTP-метода POST
Метод для добавления записи
Car
использует HTTP-метод POST
. Он применяет вспомогательный метод для отправки сущности в формате JSON и возвращает запись Car
из тела ответа. Вот его код:
public async Task AddCarAsync(Car entity)
{
var response = await PostAsJson($"{_settings.Uri}{_settings.CarBaseUri}",
JsonSerializer.Serialize(entity));
if (response == null)
{
throw new Exception("Unable to communicate with the service");
}
return await response.Content.ReadFromJsonAsync();
}
Вызов HTTP-метода PUT
Метод для обновления записи
Car
использует HTTP-метод PUT
. Он применяет вспомогательный метод для отправки записи Car
в формате JSON и возвращает обновленную запись Car
из тела ответа:
public async Task UpdateCarAsync(int id, Car entity)
{
var response = await PutAsJson($"{_settings.Uri}{_settings.CarBaseUri}/{id}",
JsonSerializer.Serialize(entity));
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync();
}
Вызов HTTP-метода DELETE
Последний добавляемый метод предназначен для выполнения НТТР-метода
DELETE
. Шаблон соответствует остальным методам: использование вспомогательного метода и проверка ответа на предмет успешности. Он ничего не возвращает вызывающему коду, поскольку сущность была удалена. Ниже показан код метода:
public async Task DeleteCarAsync(int id, Car entity)
{
var response = await DeleteAsJson($"{_settings.Uri}{_settings.CarBaseUri}/{id}",
JsonSerializer.Serialize(entity));
response.EnsureSuccessStatusCode();
}
Конфигурирование служб
Создайте в каталоге
ApiWrapper
проекта AutoLot.Service
новый файл класса по имени ServiceConfiguration.cs
. Приведите операторы using
к следующему виду:
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
Сделайте класс открытым и статическим, после чего добавьте открытый статический расширяющий метод для
IServiceCollection
:
namespace AutoLot.Services.ApiWrapper
{
public static class ServiceConfiguration
{
public static IServiceCollection ConfigureApiServiceWrapper(
this IServiceCollection
services, IConfiguration config)
{
return services;
}
}
}
В первой строке расширяющего метода в контейнер DI добавляется
ApiServiceSettings
. Во второй строке в контейнер DI добавляется IApiServiceWrapper
и регистрируется класс с помощью фабрики HTTPClient
. Это позволяет внедрять IApiServiceWrapper
в другие классы, а фабрика HTTPClient
будет управлять внедрением и временем существования HTTPClient
:
public static IServiceCollection ConfigureApiServiceWrapper(this IServiceCollection
services, IConfiguration config)
{
services.Configure(
config.GetSection(nameof(ApiServiceSettings)));
services.AddHttpClient();
return services;
}
Откройте файл
Startup.cs
и добавьте следующий оператор using
:
using AutoLot.Services.ApiWrapper;
Перейдите к методу
ConfigureServices()
и добавьте в него показанную ниже строку:
services.ConfigureApiServiceWrapper(Configuration);
Построение класса CarsController
Текущая версия
CarsController
жестко привязана к хранилищам в библиотеке доступа к данным. Следующая итерация CarsController
для связи с базой данных будет применять оболочку службы. Переименуйте CarsController
в CarsDalController
(включая конструктор) и добавьте в каталог Controllers
новый класс по имени CarsController
. Код этого класса является практически точной копией CarsController
, но они хранятся по отдельности с целью прояснения разницы между использованием хранилищ и службы.
На заметку! При работе с одной и той же базой данных вам редко придется применять вместе уровень доступа к данным и оболочку службы. Здесь показаны оба варианта, чтобы вы смогли решить, какой из них лучше подходит в вашей ситуации.
Приведите операторы
using
к следующему виду:
using System.Threading.Tasks;
using AutoLot.Dal.Repos.Interfaces;
using AutoLot.Models.Entities;
using AutoLot.Services.ApiWrapper;
using AutoLot.Services.Logging;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
Далее сделайте класс открытым, унаследуйте его от
Controller
и добавьте атрибут Route
. Создайте конструктор, который принимает экземпляры реализаций IAutoLotServiceWrapper
и IAppLogging
, после чего присвойте оба экземпляра переменным уровня класса. Вот начальный код:
namespace AutoLot.Mvc.Controllers
{
[Route("[controller]/[action]")]
public class CarsController : Controller
{
private readonly IApiServiceWrapper _serviceWrapper;
private readonly IAppLogging _logging;
public CarsController(IApiServiceWrapper serviceWrapper,
IAppLogging
logging)
{
_serviceWrapper = serviceWrapper;
_logging = logging;
}
}
Вспомогательный метод GetMakes()
Вспомогательный метод
GetMakes()
строит экземпляр SelectList
со всеми записями Make
в базе данных. Он использует Id
в качестве значения и Name
в качестве отображаемого текста:
internal async Task GetMakesAsync()=>
new SelectList(
await _serviceWrapper.GetMakesAsync(),
nameof(Make.Id),
nameof(Make.Name));
Вспомогательный метод GetOneCar()
Вспомогательный метод
GetOneCar()
получает одиночную запись Car
:
internal async Task GetOneCarAsync(int? id)
=> !id.HasValue ? null : await _serviceWrapper.GetCarAsync(id.Value);
Открытые методы действий
Единственное отличие между открытыми методами действий в этом контроллере и аналогичными методами в
CarsDalController
связано с доступом к данным, а также с тем,что все методы определены как асинхронные. Поскольку вы уже понимаете, для чего предназначено то или иное действие, ниже приведены остальные методы, изменения в которых выделены полужирным:
[Route("/[controller]")]
[Route("/[controller]/[action]")]
public async Task Index()
=> View(await _serviceWrapper.GetCarsAsync());
[HttpGet("{makeId}/{makeName}")]
public async Task ByMake(int makeId, string makeName)
{
ViewBag.MakeName = makeName;
return View(await _serviceWrapper.GetCarsByMakeAsync(makeId));
}
[HttpGet("{id?}")]
public async Task Details(int? id)
{
if (!id.HasValue)
{
return BadRequest();
}
var car = await GetOneCarAsync(id);
if (car == null)
{
return NotFound();
}
return View(car);
}
[HttpGet]
public async Task Create()
{
ViewData["MakeId"] = await GetMakesAsync();
return View();
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task Create(Car car)
{
if (ModelState.IsValid)
{
await _serviceWrapper.AddCarAsync(car);
return RedirectToAction(nameof(Index));
}
ViewData["MakeId"] = await GetMakesAsync();
return View(car);
}
[HttpGet("{id?}")]
public async Task Edit(int? id)
{
var car = await GetOneCarAsync(id);
if (car == null)
{
return NotFound();
}
ViewData["MakeId"] = await GetMakesAsync();
return View(car);
}
[HttpPost("{id}")]
[ValidateAntiForgeryToken]
public async Task Edit(int id, Car car)
{
if (id != car.Id)
{
return BadRequest();
}
if (ModelState.IsValid)
{
await _serviceWrapper.UpdateCarAsync(id,car);
return RedirectToAction(nameof(Index));
}
ViewData["MakeId"] = await GetMakesAsync();
return View(car);
}
[HttpGet("{id?}")]
public async Task Delete(int? id)
{
var car = await GetOneCarAsync(id);
if (car == null)
{
return NotFound();
}
return View(car);
}
[HttpPost("{id}")]
[ValidateAntiForgeryToken]
public async Task Delete(int id, Car car)
{
await _serviceWrapper.DeleteCarAsync(id,car);
return RedirectToAction(nameof(Index));
}
Обновление компонента представления
В текущий момент внутри компонента представления
MenuViewComponent
применяется уровень доступа к данным и синхронная версия Invoke()
. Внесите в класс следующие изменения:
using System.Linq;
using System.Threading.Tasks;
using AutoLot.Dal.Repos.Interfaces;
using AutoLot.Services.ApiWrapper;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ViewComponents;
namespace AutoLot.Mvc.ViewComponents
{
public class MenuViewComponent : ViewComponent
{
private readonly IApiServiceWrapper _serviceWrapper;
public MenuViewComponent(IApiServiceWrapper serviceWrapper)
{
_serviceWrapper = serviceWrapper;
}
public async Task InvokeAsync()
{
var makes = await _serviceWrapper.GetMakesAsync();
if (makes == null)
{
return new ContentViewComponentResult("Unable to get the makes");
}
return View("MenuView", makes);
}
}
}
Совместный запуск приложений AutoLot.Mvc и AutoLot.Api
Приложение
AutoLot.Mvc
рассчитывает на то, что приложение AutoLot.Api
должно быть запущено. Это можно сделать с помощью Visual Studio, командной строки или через комбинацию того и другого.
На заметку! Вспомните, что приложения
AutoLot.Mvc
и AutoLot.Api
сконфигурированы на воссоздание базы данных при каждом их запуске. Обязательно отключите воссоздание хотя бы в одном из приложений, иначе возникнет конфликт. Чтобы ускорить отладку, отключите воссоздание в обоих приложений при тестировании функциональности, которая не изменяет данные.
Использование Visual Studio
Вы можете сконфигурировать среду Visual Studio на запуск нескольких проектов одновременно. Щелкните правой кнопкой мыши на имени решения в окне Solution Explorer, выберите в контекстном меню пункт Select Startup Projects (Выбрать стартовые проекты) и установите действия для проектов
AutoLot.Api
и AutoLot.Mvc
в Start (Запуск), как показано на рис. 31.11.
После нажатия клавиши <F5> (или щелчка на кнопке запуска с зеленой стрелкой) оба проекта запустятся. При этом возникает ряд сложностей. Первая сложность — среда Visual Studio запоминает последний профиль, который применялся для запуска приложения. Это значит, что если вы использовали для запуска
AutoLot.Api
веб-сервер IIS Express, то запуск обоих приложений приведет к запуску AutoLot.Api
с применением IIS Express, поэтому порт в настройках служб окажется некорректным.
Проблему легко устранить. Либо измените порты в файле
appsettings.development.json
, либо запустите приложение под управлением Kestrel, прежде чем конфигурировать совместный запуск приложений.
Вторая сложность связана с синхронизацией. Оба проекта стартуют практически одновременно. Если вы сконфигурировали приложение
AutoLot.Api
на воссоздание базы данных при каждом его запуске, тогда она не будет готова для приложения AutoLot.Mvc
, когда компонент представления запускается с целью построения меню. Проблему решит быстрое обновление браузера, отображающего AutoLot.Mvc
(как только вы увидите пользовательский интерфейс Swagger в AutoLot.Api
).
Использование командной строки
Откройте окно командной строки в каждом каталоге проекта и введите команду
dotnet watch run
. Это позволит управлять порядком и синхронизацией, а также гарантирует, что приложения выполняются с применением Kestrel, но не IIS. Информацию об отладке при запуске из командной строки ищите в главе 29.
Резюме
В настоящей главе вы завершили изучение ASP.NET Core, равно как и построение приложения
AutoLot.Mvc
. Процесс изучения начинался с исследования представлений, частичных представлений, а также шаблонов редактирования и отображения. Затем вы узнали о вспомогательных функциях дескрипторов, смешивающих разметку клиентской стороны с кодом серверной стороны.
Следующие темы касались библиотек клиентской стороны, включая управление библиотеками в проекте плюс пакетирование и минификацию. После конфигурирования компоновка была обновлена с учетом новых путей к библиотекам и разбита на набор частичных представлений, а с целью дальнейшей детализации обработки клиентских библиотек была добавлена вспомогательная функция дескриптора для среды.
Затем с использованием
HTTPClientFactory
и конфигурационной системы ASP.NET Core была создана оболочка службы, взаимодействующая с AutoLot.Api
, которая применялась для создания компонента представления, отвечающего за построение динамической системы меню. После краткого обсуждения способов одновременной загрузки обоих приложений (AutoLot.Api
и AutoLot.Mvc
) была разработана основная часть приложения.
Разработка начиналась с создания контроллера
CarsController
и всех методов действий. Далее были добавлены специальные вспомогательные функции дескрипторов и в заключение созданы все представления, касающиеся записей Car
. Конечно, был построен только один контроллер и его представления, но с помощью продемонстрированного шаблона можно создать контроллеры и представления для всех сущностей AutoLot
.