Примечание.
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
Авторы: Райан Новак (Ryan Nowak), Кирк Ларкин (Kirk Larkin) и Рик Андерсон (Rick Anderson)
Note
Это не последняя версия этой статьи. В текущей версии см. версию .NET 10 этой статьи.
Warning
Эта версия ASP.NET Core больше не поддерживается. Дополнительные сведения см. в политике поддержки .NET и .NET Core. В текущей версии см. версию .NET 10 этой статьи.
Маршрутизация обеспечивает сопоставление входящих HTTP-запросов и их распределение по исполняемым конечным точкам приложения. Конечные точки — это блоки исполняемого кода обработки запросов приложения. Конечные точки определяются в приложении и настраиваются при его запуске. Процесс сопоставления конечных точек может извлекать значения из URL-адреса запроса и предоставлять эти значения для обработки запроса. С помощью сведений о конечных точках из приложения маршрутизация также может формировать URL-адреса, которые сопоставляются с конечными точками.
Приложения могут настраивать маршрутизацию с помощью следующих методов:.
- Controllers
- Razor Страницы
- SignalR
- Службы gRPC
- Промежуточное ПО с поддержкой конечных точек, например проверки работоспособности.
- Делегаты и лямбда-выражения, зарегистрированные для маршрутизации
В этой статье представлены сведения о низкоуровневой маршрутизации ASP.NET Core. Дополнительные сведения о настройке маршрутизации
- Сведения о контроллерах см. в статье Маршрутизация к действиям контроллера в ASP.NET Core.
- Соглашения для Razor Pages см. в статье Соглашения для маршрутов и приложений Razor Pages в ASP.NET Core.
- Руководство Blazor по маршрутизации и навигации, которое добавляет или заменяет рекомендации, приведенные в этой статье, см. в разделе ASP.NET Core Blazor маршрутизация и ASP.NET Core Blazor навигация.
Основы маршрутизации
В следующем коде приведен базовый пример маршрутизации.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
В предыдущем примере используется одна конечная точка с помощью MapGet метода:
- При отправке HTTP-запроса
GETв корневой URL-адрес/:- Выполняется делегат запроса.
- В ответ HTTP записывается
Hello World!.
- Если метод запроса не является
GETили если корневой URL-адрес не/, сопоставление маршрута не выполняется и возвращается сообщение об ошибке HTTP 404.
Маршрутизация использует пару промежуточного ПО, зарегистрированную с помощью UseRouting и UseEndpoints.
-
UseRoutingдобавляет соответствие маршрута в конвейер ПО промежуточного слоя. Это ПО промежуточного слоя обращается к набору конечных точек, определенных в приложении, и выбирает наиболее подходящее на основе запроса. -
UseEndpointsдобавляет выполнение конечной точки в конвейер обработки промежуточного ПО. Он запускает делегат, связанный с выбранной конечной точкой.
Приложениям обычно не требуется вызывать UseRouting или UseEndpoints.
WebApplicationBuilder настраивает конвейер промежуточного программного обеспечения, который оборачивает промежуточное ПО, добавленное в Program.cs, с помощью UseRouting и UseEndpoints. Но приложения могут изменять порядок, в котором выполняются UseRouting и UseEndpoints, вызывая эти методы явным образом. Например, следующий код явным образом вызывает UseRouting:
app.Use(async (context, next) =>
{
// ...
await next(context);
});
app.UseRouting();
app.MapGet("/", () => "Hello World!");
В предыдущем коде:
- Вызов к
app.Useрегистрирует пользовательское ПО промежуточного слоя, которое выполняется в начале конвейера. - При вызове метода
UseRoutingПО промежуточного слоя сопоставления маршрутов настраивается для запуска после пользовательского ПО промежуточного слоя. - Конечная точка, зарегистрированная с использованием
MapGet, выполняется в конце конвейера.
Если предыдущий пример не включал вызов к UseRouting, пользовательское ПО промежуточного слоя будет выполняться после ПО промежуточного слоя сопоставления маршрутов.
Примечание. Маршруты, добавленные непосредственно в WebApplication, выполняются в конце последовательности.
Endpoints
Для определения конечной точки используется метод MapGet. Конечная точка — это объект, который может быть:
- Выбирается путем сопоставления URL-адреса и метода HTTP.
- Выполняется путем использования запуска делегата.
Конечные точки, которые могут быть сопоставлены и выполнены приложением, настраиваются в UseEndpoints. Например, MapGet, MapPost и аналогичные методы подключают делегаты запросов к системе маршрутизации. Для подключения функций платформы ASP.NET Core к системе маршрутизации можно использовать дополнительные методы.
- MapRazorPages для страниц Razor
- MapControllers для контроллеров
- MapHub<THub> для SignalR
- MapGrpcService<TService> для gRPC
Ниже представлен пример маршрутизации с более сложным шаблоном маршрута.
app.MapGet("/hello/{name:alpha}", (string name) => $"Hello {name}!");
Строка /hello/{name:alpha} является шаблоном маршрута. Шаблон маршрута используется для настройки способа сопоставления конечной точки. В этом случае шаблон соответствует следующим условиям.
- URL-адрес, подобный
/hello/Docs - Любой URL-путь, начинающийся с
/hello/,после которого следует набор буквенных символов.:alphaприменяет ограничение маршрута, которое соответствует только буквенным символам. Ограничения маршрута описаны далее в этой статье.
Второй сегмент URL-пути, {name:alpha}:
- привязан к параметру
name; - Записывается и хранится в HttpRequest.RouteValues.
В следующем примере показана маршрутизация с проверками работоспособности и авторизацией.
app.UseAuthentication();
app.UseAuthorization();
app.MapHealthChecks("/healthz").RequireAuthorization();
app.MapGet("/", () => "Hello World!");
В предыдущем примере, показано то, как:
- Промежуточное программное обеспечение для авторизации можно использовать вместе с маршрутизацией.
- можно использовать конечные точки для настройки режима авторизации.
Вызов MapHealthChecks добавляет конечную точку проверки состояния. Связывание RequireAuthorization с этим вызовом прикрепляет политику авторизации к конечной точке.
При вызове UseAuthentication и UseAuthorization добавляется ПО промежуточного слоя для проверки подлинности и авторизации. Эти middleware размещаются между UseRouting и UseEndpoints, чтобы они могли:
- просматривать, какая конечная точка выбрана методом
UseRouting; - применять политику авторизации до отправки UseEndpoints на конечную точку.
Метаданные конечной точки
В предыдущем примере существуют две конечные точки, но политика авторизации прикреплена только к конечной точке проверки состояния. Если запрос соответствует конечной точке проверки работоспособности, /healthz, выполняется проверка авторизации. Это служит демонстрацией того, что к конечным точкам можно прикреплять дополнительные данные. Эти дополнительные данные называются метаданными конечных точек.
- Метаданные могут обрабатываться ПО промежуточного слоя с поддержкой маршрутизации.
- Метаданные могут быть любого типа .NET.
Основные понятия маршрутизации
Система маршрутизации формируется поверх конвейера промежуточного программного обеспечения путем добавления мощного понятия конечных точек. Конечные точки представляют собой единицы функциональности приложения, отличающиеся друг от друга в плане маршрутизации, авторизации и количества систем ASP.NET Core.
Определение конечной точки ASP.NET Core
Конечная точка ASP.NET Core:
- Исполняемый: имеет RequestDelegate.
- Расширяемо: имеет коллекцию метаданных .
- Доступный вариант: при необходимости содержит сведения о маршрутизации.
- Перечисляемые: коллекцию конечных точек можно перечислить, извлекая EndpointDataSource из DI.
В следующем примере кода показано, как получить и проверить конечную точку, соответствующую текущему запросу.
app.Use(async (context, next) =>
{
var currentEndpoint = context.GetEndpoint();
if (currentEndpoint is null)
{
await next(context);
return;
}
Console.WriteLine($"Endpoint: {currentEndpoint.DisplayName}");
if (currentEndpoint is RouteEndpoint routeEndpoint)
{
Console.WriteLine($" - Route Pattern: {routeEndpoint.RoutePattern}");
}
foreach (var endpointMetadata in currentEndpoint.Metadata)
{
Console.WriteLine($" - Metadata: {endpointMetadata}");
}
await next(context);
});
app.MapGet("/", () => "Inspect Endpoint.");
Конечную точку, если она выбрана, можно получить из HttpContext. Ее свойства можно проверить. Объекты конечных точек являются неизменяемыми, и их невозможно изменить после создания. Наиболее распространенным типом конечной точки является RouteEndpoint.
RouteEndpoint содержит сведения, позволяющие системе маршрутизации выбрать эту конечную точку.
В приведенном выше коде app.Use настраивает встроенное ПО промежуточного слоя.
В следующем коде показано, что в зависимости от того, где в конвейере вызывается app.Use, может не быть конечной точки.
// Location 1: before routing runs, endpoint is always null here.
app.Use(async (context, next) =>
{
Console.WriteLine($"1. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
await next(context);
});
app.UseRouting();
// Location 2: after routing runs, endpoint will be non-null if routing found a match.
app.Use(async (context, next) =>
{
Console.WriteLine($"2. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
await next(context);
});
// Location 3: runs when this endpoint matches
app.MapGet("/", (HttpContext context) =>
{
Console.WriteLine($"3. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
return "Hello World!";
}).WithDisplayName("Hello");
app.UseEndpoints(_ => { });
// Location 4: runs after UseEndpoints - will only run if there was no match.
app.Use(async (context, next) =>
{
Console.WriteLine($"4. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
await next(context);
});
В предыдущем примере добавляются инструкции Console.WriteLine, которые показывают, выбрана ли конечная точка. Для ясности в примере указанной конечной точке / назначается отображаемое имя.
Кроме того, предыдущий пример включает вызовы к UseRouting и UseEndpoints для точного контроля того, когда именно эти ПО промежуточного слоя выполняются в конвейере.
При выполнении этого кода с URL-адресом / отображается следующее.
1. Endpoint: (null)
2. Endpoint: Hello
3. Endpoint: Hello
При выполнении этого кода с любым другим URL-адресом отображается следующее.
1. Endpoint: (null)
2. Endpoint: (null)
4. Endpoint: (null)
В этом выводе показано следующее.
- Перед вызовом
UseRoutingконечная точка всегда имеет значение NULL. - Если найдено совпадение, конечная точка отличается от NULL между
UseRoutingи UseEndpoints. - ПО промежуточного слоя
UseEndpointsявляется терминальным при обнаружении соответствия. Определение терминального ПО промежуточного слоя приведено далее в этой статье. - Промежуточное ПО после
UseEndpointsвыполняется только в случае, если совпадения не найдены.
Промежуточный слой UseRouting использует метод SetEndpoint для подключения конечной точки к текущему контексту. ПО промежуточного слоя UseRouting можно заменить на настраиваемую логику и по-прежнему использовать конечные точки. Конечные точки — это низкоуровневые примитивы, такие как ПО промежуточного слоя, которые не связаны с реализацией маршрутизации. В большинстве приложений метод UseRouting не требуется заменять настраиваемой логикой.
ПО промежуточного слоя UseEndpoints предназначено для использования совместно с ПО промежуточного слоя UseRouting. Основная логика для выполнения конечной точки достаточно проста. Чтобы получить конечную точку, используйте GetEndpoint, а затем вызовите ее свойство RequestDelegate.
В следующем примере кода показано, как ПО промежуточного слоя может влиять на маршрутизацию или реагировать на нее.
app.UseHttpMethodOverride();
app.UseRouting();
app.Use(async (context, next) =>
{
if (context.GetEndpoint()?.Metadata.GetMetadata<RequiresAuditAttribute>() is not null)
{
Console.WriteLine($"ACCESS TO SENSITIVE DATA AT: {DateTime.UtcNow}");
}
await next(context);
});
app.MapGet("/", () => "Audit isn't required.");
app.MapGet("/sensitive", () => "Audit required for sensitive data.")
.WithMetadata(new RequiresAuditAttribute());
public class RequiresAuditAttribute : Attribute { }
В предыдущем примере показаны два важных основных понятия.
- Промежуточное ПО может работать до
UseRoutingчтобы изменить данные, обрабатываемые маршрутизацией.- Обычно ПО промежуточного слоя, отображаемое перед маршрутизацией, изменяет некоторое свойство запроса, например UseRewriter, UseHttpMethodOverride или UsePathBase.
- ПО промежуточного слоя может выполняться между
UseRoutingи UseEndpoints для обработки результатов маршрутизации до выполнения конечной точки.- ПО промежуточного слоя, которое выполняется между
UseRoutingиUseEndpoints:- Обычно проверяет метаданные для получения представления о конечных точках.
- Зачастую принимает решения по обеспечению безопасности, как это делают
UseAuthorizationиUseCors.
- Сочетание ПО промежуточного слоя и метаданных позволяет настраивать политики для каждой конечной точки.
- ПО промежуточного слоя, которое выполняется между
В приведенном выше коде показан пример настраиваемого ПО промежуточного слоя, поддерживающего политики для каждой конечной точки. ПО промежуточного слоя записывает в консоль журнал аудита доступа к конфиденциальным данным. Промежуточное ПО можно настроить для выполнения аудита конечной точки с помощью метаданных RequiresAuditAttribute. В этом примере демонстрируется паттерн подписки, когда аудит выполняется только для конечных точек, помеченных как чувствительные. Эту логику можно определить в обратном порядке для аудита всего, что не отмечено, например, как надежное. Система метаданных конечной точки обеспечивает гибкость. Эту логику можно настроить под любой требуемый вариант использования.
Предыдущий пример кода предназначен для демонстрации основных понятий конечных точек. Он не предназначен для использования в рабочей среде. Более полная версия промежуточного программного обеспечения для журнала аудита будет:
- Логировать в файл или базу данных.
- Включает такие сведения, как информация о пользователе, IP-адресе, имени конфиденциальной конечной точки и многое другое.
Метаданные политики аудита RequiresAuditAttribute определены как Attribute, чтобы их было проще использовать в платформах на основе классов, таких как контроллеры и SignalR. При использовании маршрута к коду:
- Метаданные присоединяются к API-интерфейсу построителя.
- При создании конечных точек платформы на основе классов включают все атрибуты в соответствующем методе и классе.
Типы метаданных рекомендуется определить либо как интерфейсы, либо как атрибуты. Интерфейсы и атрибуты допускают повторное использование кода. Система метаданных является гибкой и не накладывает никаких ограничений.
Сравнение ПО промежуточного слоя терминала с маршрутизацией
В следующем примере демонстрируется ПО промежуточного слоя терминала и маршрутизация:
// Approach 1: Terminal Middleware.
app.Use(async (context, next) =>
{
if (context.Request.Path == "/")
{
await context.Response.WriteAsync("Terminal Middleware.");
return;
}
await next(context);
});
app.UseRouting();
// Approach 2: Routing.
app.MapGet("/Routing", () => "Routing.");
Стиль ПО промежуточного слоя, который показан в разделе Approach 1:, — терминальное ПО промежуточного слоя. ПО промежуточного слоя называется терминальным, поскольку выполняет операцию сопоставления.
- Операция сопоставления в предыдущем примере —
Path == "/"для ПО промежуточного слоя иPath == "/Routing"для маршрутизации. - Если сопоставление выполнено успешно, оно выполняет некоторые функции и возвращает результат, а не вызывает ПО промежуточного слоя
next.
Оно называется терминальным, поскольку завершает поиск, выполняет некоторые функции, а затем возвращает результат.
В следующем списке ПО промежуточного слоя терминала сравнивается с маршрутизацией:
- Оба подхода позволяют завершить конвейер обработки.
- ПО промежуточного слоя завершает конвейер, возвращая вместо вызова
next. - Конечные точки всегда являются терминальными.
- ПО промежуточного слоя завершает конвейер, возвращая вместо вызова
- Промежуточное ПО терминала позволяет разместить его в любом месте конвейера.
- Конечные точки выполняются в позиции UseEndpoints.
- Промежуточное программное обеспечение терминала позволяет произвольному коду определить, когда программное обеспечение соответствует.
- Настраиваемый код сопоставления маршрутов может быть подробным и сложным для корректной записи.
- Маршрутизация обеспечивает простые решения для обычных приложений. Большинству приложений не требуется настраиваемый код сопоставления маршрутов.
- Конечные точки взаимодействуют с ПО промежуточного слоя, таким как
UseAuthorizationиUseCors.- Использование терминального ПО промежуточного слоя с
UseAuthorizationилиUseCorsтребует взаимодействия вручную с системой авторизации.
- Использование терминального ПО промежуточного слоя с
Конечная точка определяет и то, и другое:
- делегат для обработки запросов;
- коллекцию произвольных метаданных. Метаданные используются для реализации аспектов, пронизывающих архитектуру, на основе политик и конфигурации, присоединённой к каждой конечной точке.
Программное обеспечение промежуточного уровня может быть эффективным средством, но может потребовать:
- значительный объем кода и тестирования;
- интеграция вручную с другими системами для достижения желаемого уровня гибкости.
Прежде чем создавать терминальное ПО промежуточного слоя, рассмотрите возможность интеграции с маршрутизацией.
Существующее терминальное ПО промежуточного слоя, которое интегрируется с Map или MapWhen, обычно может быть преобразовано в конечную точку, осведомленную о маршрутизации. MapHealthChecks демонстрирует шаблон для маршрутизирующего программного обеспечения.
- Напишите метод расширения в IEndpointRouteBuilder.
- Создайте вложенный конвейер middleware с помощью CreateApplicationBuilder.
- Присоедините промежуточное программное обеспечение к новому конвейеру. В этом случае — UseHealthChecks.
- Build конвейер промежуточного программного обеспечения в RequestDelegate.
- Вызовите
Mapи укажите новый посреднический программный конвейер. - Верните объект построителя, который предоставлен
Map, из метода расширения.
В следующем коде показано использование MapHealthChecks.
app.UseAuthentication();
app.UseAuthorization();
app.MapHealthChecks("/healthz").RequireAuthorization();
В предыдущем примере показано, почему возврат объекта строителя имеет большое значение. Возврат объекта построителя позволяет разработчику приложения настраивать политики, такие как авторизация для конечной точки. В этом примере промежуточное ПО для проверки состояния не имеет прямой интеграции с системой авторизации.
Для решения проблем, с которыми столкнулись авторы расширяемости при использовании программного обеспечения промежуточного слоя терминалов, была создана система метаданных. Для каждого ПО промежуточного слоя достаточно проблематично реализовать собственную интеграцию с системой авторизации.
Сопоставление URL-адресов
- Это процесс, с помощью которого функция маршрутизации сопоставляет входящий запрос в конечную точку.
- Он основан на данных из пути URL и заголовков.
- Его можно расширить, чтобы учесть любые данные в запросе.
При выполнении ПО промежуточного слоя маршрутизации оно задает конечную точку (Endpoint) и значения маршрута для функции запроса в HttpContext из текущего запроса.
- Вызов HttpContext.GetEndpoint получает конечную точку.
-
HttpRequest.RouteValuesполучает коллекцию значений маршрута.
ПО промежуточного слоя, которое выполняется после промежуточного слоя маршрутизации, может проверить конечную точку и принять меры. Например, промежуточное ПО авторизации может опрашивать коллекцию метаданных конечной точки для получения политики авторизации. После выполнения всех компонентов промежуточного программного обеспечения в конвейере обработки запросов вызывается делегат выбранной конечной точки.
Система маршрутизации в контексте маршрутизации по конечным точкам отвечает за все решения по управлению маршрутизацией. Поскольку ПО промежуточного слоя применяет политики, основанные на выбранной конечной точке, важно следующее.
- Любые решения, которые могут повлиять на распределение или применение политик безопасности, должны выполняться внутри системы маршрутизации.
Warning
Для обеспечения обратной совместимости при выполнении делегата конечной точки контроллера или Razor страниц, свойства RouteContext.RouteData устанавливаются в соответствующие значения на основе обработки запросов, выполненной к этому моменту.
В следующем выпуске тип RouteContext будет помечен как устаревший.
- Перенесите
RouteData.ValuesвHttpRequest.RouteValues. - Мигрируйте
RouteData.DataTokens, чтобы получить IDataTokensMetadata из метаданных конечной точки.
Работа сопоставления URL-адресов подразделяется на этапы, которые можно настроить. На каждом этапе выходные данные представляют собой набор совпадений. По мере перехода между этапами набор совпадений можно ограничивать. Реализация маршрутизации не гарантирует порядок обработки для соответствующих конечных точек. Все возможные совпадения обрабатываются одновременно. Этапы сопоставления URL-адресов выполняются в следующем порядке. ASP.NET Core:
- URL-путь обрабатывается по набору конечных точек и их шаблонов маршрутов, при этом выполняется сбор всех совпадений.
- Берется предыдущий список и удаляются совпадения, которые не соответствуют примененным ограничениям маршрута.
- Принимает предыдущий список и удаляет совпадения, которые не соответствуют набору экземпляров MatcherPolicy.
- EndpointSelector используется для принятия окончательного решения из предыдущего списка.
Список конечных точек определяется по приоритету в соответствии со следующим.
Все соответствующие конечные точки обрабатываются на каждом этапе до тех пор, пока не будет достигнут EndpointSelector.
EndpointSelector — это заключительный этап. Он выбирает конечную точку с наивысшим приоритетом из совпадений как наилучшее соответствие. При наличии других совпадений с таким же приоритетом, как у наилучшего соответствия, выбрасывается исключение неоднозначного соответствия.
Приоритет маршрута вычисляется на основе более определенного шаблона маршрута, которому назначается более высокий приоритет. Например, рассмотрим шаблоны /hello и /{message}.
- Оба соответствуют URL-пути
/hello. -
/helloявляется более конкретным, и, следовательно, ему назначается более высокий приоритет.
Как правило, приоритет маршрута помогает выбрать наилучшее соответствие для типов схем URL-адресов, используемых на практике. Используйте Order только в случае, когда необходимо избежать неоднозначности.
Ввиду различных типов расширяемости, предоставляемых службой маршрутизации, система маршрутизации не может заранее вычислить неоднозначные маршруты. Рассмотрим в качестве примера шаблоны маршрутов /{message:alpha} и /{message:int}.
- Ограничение
alphaсоответствует только буквенным символам. - Ограничение
intсоответствует только числам. - Эти шаблоны имеют одинаковый приоритет маршрута, однако не существует одного URL-адреса, по которому они совпадают.
- Если система маршрутизации сообщила об ошибке неоднозначности при запуске, это означает, что она заблокировала этот допустимый вариант использования.
Warning
Порядок операций в UseEndpoints не влияет на поведение маршрутизации, за одним исключением. MapControllerRoute и MapAreaRoute автоматически присваивают значение порядка своим конечным точкам в соответствии с порядком их вызова. Это имитирует поведение контроллеров без системы маршрутизации в долгосрочной перспективе, предоставляя те же гарантии, что и в старых реализациях маршрутизации.
Маршрутизация конечных точек в ASP.NET Core:
- Не имеет концепции маршрутов.
- Не гарантирует порядок обработки. Все конечные точки обрабатываются одновременно.
Приоритет шаблонов маршрутов и порядок выбора конечных точек
Приоритет шаблонов маршрутов — это система, которая назначает каждому шаблону маршрута значение в зависимости от того, насколько он является конкретным. Приоритет шаблона маршрута
- Позволяет избежать необходимости настраивать порядок конечных точек в общих случаях.
- Пытается соответствовать общепринятым ожиданиям относительно поведения маршрутизации.
Например, рассмотрим шаблоны /Products/List и /Products/{id}. Разумно предположить, что /Products/List является лучшим соответствием для URL-пути /Products/List, чем /Products/{id}. Литеральный сегмент /List считается более приоритетным, чем сегмент параметров /{id}.
Порядок определения приоритета связан с порядком определения шаблонов маршрутов.
- Шаблоны с большим количеством сегментов считаются более конкретными.
- Сегмент с литеральным текстом считается более конкретным, чем сегмент параметров.
- Сегмент параметров с ограничением считается более конкретным, чем сегмент без него.
- Сложный сегмент рассматривается как такой же специфичный, как сегмент параметра с ограничением.
- Универсальные параметры являются наименее конкретными. См. раздел Шаблоны маршрута для получения важной информации об универсальных маршрутах.
Основные понятия формирования URL-адресов
Создание URL-адресов:
- Это процесс, благодаря которому маршрутизация может формировать путь URL-адреса на основе набора значений маршрута.
- Обеспечивает логическое разделение конечных точек и URL-адресов, по которым к ним осуществляется доступ.
Маршрутизация конечных точек включает API LinkGenerator.
LinkGenerator — это одноэлементная служба, доступная в DI. API LinkGenerator можно использовать вне контекста выполнения запроса.
Mvc.IUrlHelper и сценарии, которые зависят от IUrlHelper, такие как вспомогательные функции тегов, вспомогательные методы HTML и результаты действий, используют API LinkGenerator для предоставления возможностей создания ссылок.
Генератор ссылок использует концепции адреса и схем адресов. Схема адресов — это способ определения конечных точек, которые должны рассматриваться для создания ссылки. Например, сценарии с именем маршрута и значениями маршрута, с которыми многие пользователи знакомы по контроллерам и Razor Pages, реализуются как схема адресов.
Генератор ссылок может установить связь с контроллерами и Razor Pages с помощью следующих методов расширения.
Перегрузки этих методов принимают аргументы, которые включают HttpContext. Эти методы являются функциональными эквивалентами Url.Action и Url.Page, но предлагают дополнительную гибкость и параметры.
Методы GetPath* наиболее схожи с Url.Action и Url.Page в том, что создают URI, содержащий абсолютный путь. Методы GetUri* всегда создают абсолютный URI, содержащий схему и хост. Методы, которые принимают HttpContext, создают URI в контексте выполнения запроса. Используются значения окружения маршрута, базовый URL-адрес, схема и узел из выполняющегося запроса, если не указано иное.
LinkGenerator вызывается с адресом. Создание URI происходит в два этапа:
- Адрес привязан к списку конечных точек, соответствующих адресу.
- RoutePattern конечной точки вычисляется, пока не будет найден шаблон маршрута, который соответствует предоставленным значениям. Полученный результат объединяется с другими частями URI, предоставленными генератору ссылок и возвращенными.
Методы, предоставляемые LinkGenerator, поддерживают стандартные возможности создания ссылки для любого типа адреса. Самый удобный способ использовать генератор ссылки — через методы расширения, которые выполняют операции для определенного типа адреса.
| Метод расширения | Description |
|---|---|
| GetPathByAddress | Создает URI с абсолютным путем на основе предоставленных значений. |
| GetUriByAddress | Создает абсолютный URI на основе предоставленных значений. |
Warning
Обратите внимание на следующие последствия вызова методов LinkGenerator:
Используйте методы расширения
GetUri*с осторожностью в конфигурации приложения, которая не проверяет заголовок входящих запросовHost. Если заголовок входящих запросовHostне проверен, недоверенные входные данные запроса могут быть отправлены обратно клиенту в URI в виде или на странице. Рекомендуется, чтобы все рабочие приложения настраивали свой сервер на проверку заголовкаHostотносительно известных допустимых значений.Используйте LinkGenerator с осторожностью в ПО промежуточного слоя в сочетании с
MapилиMapWhen.Map*изменяет базовый путь выполняющегося запроса, что влияет на выходные данные создания ссылки. Все API LinkGenerator разрешают указание базового пути. Укажите пустой базовый путь для отмены влиянияMap*на создание ссылок.
Пример программного обеспечения промежуточного слоя
В следующем примере ПО промежуточного слоя использует API LinkGenerator, чтобы создать ссылку на метод действия, который перечисляет хранимые продукты. Использование генератора ссылок путем его внедрения в класс и вызова GenerateLink доступно для любого класса в приложении.
public class ProductsMiddleware
{
private readonly LinkGenerator _linkGenerator;
public ProductsMiddleware(RequestDelegate next, LinkGenerator linkGenerator) =>
_linkGenerator = linkGenerator;
public async Task InvokeAsync(HttpContext httpContext)
{
httpContext.Response.ContentType = MediaTypeNames.Text.Plain;
var productsPath = _linkGenerator.GetPathByAction("Products", "Store");
await httpContext.Response.WriteAsync(
$"Go to {productsPath} to see our products.");
}
}
Шаблоны маршрутов
Токены в {} определяют параметры маршрута, которые будут привязаны, если маршрут совпадает. В сегменте маршрута можно определить несколько параметров маршрута, однако они должны разделяться литеральным значением. Рассмотрим пример.
{controller=Home}{action=Index}
не является допустимым маршрутом, потому что между {controller} и {action} нет дословного значения. Параметрам маршрута должны быть присвоены имена, и для них могут быть определены дополнительные атрибуты.
Весь текст, кроме параметров маршрута (например, {id}) и разделителя пути /, должен соответствовать тексту в URL-адресе. Сопоставление текста производится без учета регистра на основе декодированного представления пути URL-адреса. Для сопоставления с литеральным разделителем параметров маршрута ({ или }) нужно экранировать разделитель, повторив символ. Например, {{ или }}.
Звездочка * или двойная звездочка **:
- Можно использовать в качестве префикса к параметру маршрута для привязки к остальной части URI.
- Такие параметры называются универсальными. Например,
blog/{**slug}:- Соответствует любому URI, который начинается с
blog/и имеет любое значение после него. - Значение после
blog/присваивается значению динамического маршрута.
- Соответствует любому URI, который начинается с
Warning
Соответствие параметра catch-all маршрутам может быть неправильным из-за ошибки в маршрутизации. Приложения, на работу которых влияет эта ошибка, обладают следующими характеристиками:
- Маршрут общий, например
{**slug}". - Маршрут catch-all не соответствует необходимым запросам.
- После удаления других маршрутов маршрут catch-all начинает работать.
Ознакомьтесь с примерами 18677 и 16579, в которых встречается эта ошибка, на сайте GitHub.
Исправление для этой ошибки, которое можно включить по желанию, содержится в пакете SDK для .NET Core 3.1.301 или более поздней версии. Следующий код задает внутренний переключатель, исправляющий эту ошибку:
public static void Main(string[] args)
{
AppContext.SetSwitch("Microsoft.AspNetCore.Routing.UseCorrectCatchAllBehavior",
true);
CreateHostBuilder(args).Build().Run();
}
// Remaining code removed for brevity.
Универсальные параметры также могут соответствовать пустой строке.
Универсальный параметр экранирует соответствующие символы, когда маршрут используется для формирования URL-адреса, включая символы разделителей пути (/). Например, маршрут foo/{*path} со значениями маршрутов { path = "my/path" } формирует foo/my%2Fpath. Обратите внимание на экранированную косую черту. В качестве символов разделителя кругового пути используйте префикс параметра маршрута **. Маршрут foo/{**path} с { path = "my/path" } формирует foo/my/path.
Шаблоны URL-адресов, которые пытаются получить имя файла с необязательным расширением, имеют свои особенности. Например, рассмотрим шаблон files/{filename}.{ext?}. Когда значения для filename и ext существуют, заполняются оба значения. Если в URL-адресе есть только значение для filename, маршрут совпадает, так как точка в конце (.) является необязательной. Следующие URL-адреса соответствуют этому маршруту:
/files/myFile.txt/files/myFile
Параметры маршрута могут иметь значения по умолчанию. Они указываются после имени параметра и знака равенства (=). Например, {controller=Home} определяет Home в качестве значения по умолчанию для controller. Значение по умолчанию используется, если для параметра нет значения в URL-адресе. Параметры маршрута могут быть необязательными, для этого необходимо добавить вопросительный знак (?) в конец имени параметра. Например, id?. Разница между необязательными значениями и параметрами маршрута по умолчанию
- Параметр маршрута со значением по умолчанию всегда создает значение.
- Необязательный параметр имеет значение только в том случае, если в URL-адресе запроса указано значение.
Параметры маршрута могут иметь ограничения, которые должны соответствовать значению маршрута из URL-адреса. Добавив двоеточие (:) и имя ограничения после имени параметра маршрута, можно указать встроенные ограничения для параметра маршрута. Если для ограничения требуются аргументы, они указываются в скобках (...) после имени ограничения. Чтобы указать несколько встроенных ограничений, добавьте еще одно двоеточие (:) и имя ограничения.
Имя и аргументы ограничения передаются в службу IInlineConstraintResolver для создания экземпляра IRouteConstraint, который будет использоваться для обработки URL. Например, в шаблоне маршрута blog/{article:minlength(10)} определяется ограничение minlength с аргументом 10. Более подробное описание ограничений маршрутов и список ограничений, предоставляемых платформой, см. в разделе Ограничения маршрутов.
Параметры маршрута также могут иметь преобразователи параметров, которые преобразуют значение параметра при создании ссылок и сопоставлении действий и страниц с URL-адресами. Как и ограничения, преобразователи параметров можно включать в параметр маршрута, добавив двоеточие (:) и имя преобразователя после имени параметра маршрута. Например, шаблон маршрута blog/{article:slugify} задает преобразователь slugify. Дополнительные сведения о преобразователях параметров см. в разделе Преобразователи параметров.
В приведенной ниже таблице показаны некоторые примеры шаблонов маршрутов и их поведение.
| Шаблон маршрута | Пример сопоставления URI | URI запроса... |
|---|---|---|
hello |
/hello |
Соответствует только одному пути /hello. |
{Page=Home} |
/ |
Соответствует и задает для параметра Page значение Home. |
{Page=Home} |
/Contact |
Соответствует и задает для параметра Page значение Contact. |
{controller}/{action}/{id?} |
/Products/List |
Сопоставляется с контроллером Products и действием List. |
{controller}/{action}/{id?} |
/Products/Details/123 |
Соответствует контроллеру Products и действию Details, где id имеет значение 123. |
{controller=Home}/{action=Index}/{id?} |
/ |
Сопоставляется с контроллером Home и методом Index.
id не учитывается. |
{controller=Home}/{action=Index}/{id?} |
/Products |
Сопоставляется с контроллером Products и методом Index.
id не учитывается. |
Использование шаблона — это, как правило, самый простой подход к маршрутизации. Ограничения и значения по умолчанию также могут указываться вне шаблона маршрута.
Сложные сегменты
Сложные сегменты обрабатываются путем сопоставления литеральных разделителей справа налево в нежадной манере. Например, [Route("/a{b}c{d}")] является сложным сегментом.
Сложные сегменты работают определенным способом, который должен быть понятен для их успешного использования. В примере в этом разделе показано, почему сложные сегменты действительно хорошо работают только в том случае, если текст разделителя отсутствует в значениях параметров. Для более сложных случаев необходимо использовать регулярное выражение и затем вручную извлечь значения.
Warning
При использовании System.Text.RegularExpressions для обработки ненадежных входных данных передайте время ожидания. Злонамеренный пользователь может предоставить входные данные для RegularExpressions, что делает возможными атаки типа "отказ в обслуживании". API платформы ASP.NET Core, использующие RegularExpressions, передают время ожидания.
Это сводка действий, выполняемых маршрутизацией с использованием шаблона /a{b}c{d} и URL-пути /abcd.
| используется для визуализации принципа работы алгоритма.
- Первый литерал, справа налево — это
c. Таким образом,/abcdищется справа и находит/ab|c|d. - Все, что находится справа (
d), теперь сопоставляется с параметром маршрута{d}. - Следующий литерал, справа налево —
a. Поэтому поиск/ab|c|dначинается с того места, где мы остановились, после чего находитсяaв/|a|b|c|d. - Значение справа (
b) теперь сопоставляется с параметром маршрута{b}. - Текста и шаблонов маршрута больше не осталось, так что это совпадение.
Ниже приведен пример отрицательного результата с использованием того же шаблона /a{b}c{d} и URL-пути /aabcd.
| используется для визуализации принципа работы алгоритма: Это не совпадение, что объясняется тем же алгоритмом.
- Первый литерал, справа налево — это
c. Таким образом,/aabcdищется справа и находит/aab|c|d. - Все, что находится справа (
d), теперь сопоставляется с параметром маршрута{d}. - Следующий литерал, справа налево —
a. Поэтому поиск/aab|c|dначинается с того места, где мы остановились, после чего находитсяaв/a|a|b|c|d. - Значение справа (
b) теперь сопоставляется с параметром маршрута{b}. - На этом этапе имеется оставшийся текст
a, однако больше нет шаблонов маршрутов для синтаксического анализа, поэтому это не является совпадением.
Поскольку алгоритм сопоставления нежадный:
- Он сопоставляет наименьший объем текста, допустимый для каждого шага.
- Любая ситуация, когда значение разделителя появляется внутри значений параметров, приводит к несовпадению.
Регулярные выражения обеспечивают гораздо больший контроль над их поведением при сопоставлении.
Жадное сопоставление, также известное как максимальное сопоставление, пытается найти самое длинное возможное совпадение во входном тексте, которое удовлетворяет шаблону regex. Негредное сопоставление, также известное как ленивое сопоставление, ищет самое короткое возможное совпадение во входном тексте, удовлетворяющее шаблону regex.
Маршрутизация с особыми символами
Маршрутизация со специальными символами может привести к неожиданным результатам. Например, рассмотрим контроллер со следующим методом действия:
[HttpGet("{id?}/name")]
public async Task<ActionResult<string>> GetName(string id)
{
var todoItem = await _context.TodoItems.FindAsync(id);
if (todoItem == null || todoItem.Name == null)
{
return NotFound();
}
return todoItem.Name;
}
Если string id содержит следующие закодированные значения, могут возникнуть непредвиденные результаты:
| ASCII | Encoded |
|---|---|
/ |
%2F |
|
+ |
Параметры маршрута не всегда раскодированы из URL. Эта проблема может быть решена в будущем. См. эту задачу GitHub для получения дополнительной информации.
Ограничения маршрута
Ограничения маршрута применяются, когда найдено соответствие входящему URL-адресу, и путь URL-адреса разобран на элементы маршрута. Как правило, ограничения маршрута служат для проверки значения маршрута, связанного посредством шаблона маршрута, и принятия решения касательно того, является ли значение приемлемым (истина или ложь). Некоторые ограничения маршрута используют данные, не относящиеся к значению маршрута, для определения возможности маршрутизации запроса. Например, HttpMethodRouteConstraint может принимать или отклонять запрос в зависимости от HTTP-команды. Ограничения используются в маршрутизации запросов и создании ссылок.
Warning
Не используйте ограничения для проверки входных данных. Если для проверки входных данных используются ограничения, недопустимые входные данные приводят к ошибке 404 ("Не найдено"). Недопустимые входные данные должны привести к ошибке 400 ("Неверный запрос") с соответствующим сообщением об ошибке. Ограничения маршрутов следует использовать для разрешения неоднозначности похожих маршрутов, а не для проверки входных данных определенного маршрута.
В следующей таблице показаны ограничения маршрутов по умолчанию и их ожидаемое поведение:
| constraint | Example | Примеры совпадений | Notes |
|---|---|---|---|
int |
{id:int} |
123456789, -123456789 |
Соответствует любому целому числу |
bool |
{active:bool} |
true, FALSE |
Соответствует true или false. Case-insensitive |
datetime |
{dob:datetime} |
2016-12-31, 2016-12-31 7:32pm |
Соответствует допустимому значению DateTime для инвариантной культуры. См. предыдущее предупреждение |
decimal |
{price:decimal} |
49.99, -1,000.01 |
Соответствует допустимому значению decimal для инвариантной культуры. См. предыдущее предупреждение |
double |
{weight:double} |
1.234, -1,001.01e8 |
Соответствует допустимому значению double в инвариантной культуре. См. предыдущее предупреждение |
float |
{weight:float} |
1.234, -1,001.01e8 |
Соответствует допустимому значению float в инвариантной культуре. См. предыдущее предупреждение |
guid |
{id:guid} |
CD2C1638-1638-72D5-1638-DEADBEEF1638 |
Соответствует допустимому значению Guid |
long |
{ticks:long} |
123456789, -123456789 |
Соответствует допустимому значению long |
minlength(value) |
{username:minlength(4)} |
Rick |
Строка должна содержать не менее 4 символов |
maxlength(value) |
{filename:maxlength(8)} |
MyFile |
Строка должна содержать не более 8 символов |
length(length) |
{filename:length(12)} |
somefile.txt |
Длина строки должна составлять ровно 12 символов |
length(min,max) |
{filename:length(8,16)} |
somefile.txt |
Строка должна быть длиной не менее 8 и не более 16 символов |
min(value) |
{age:min(18)} |
19 |
Целочисленное значение не меньше 18 |
max(value) |
{age:max(120)} |
91 |
Целочисленное значение не больше 120 |
range(min,max) |
{age:range(18,120)} |
91 |
Целочисленное значение должно быть не менее 18 и не более 120. |
alpha |
{name:alpha} |
Rick |
Строка должна состоять из одного или нескольких алфавитных символов и a-z без учета регистра |
regex(expression) |
{ssn:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)} |
123-45-6789 |
Строка должна соответствовать регулярному выражению. Советы по определению регулярного выражения |
required |
{name:required} |
Rick |
Определяет обязательное наличие значения, не относящегося к параметру, во время формирования URL-адреса |
file |
{filename:file} |
myfile.txt |
Строка может содержать сегменты пути, но его последний сегмент должен иметь точку (.) и следовать одному или нескольким неточечным символам. |
nonfile |
{page:nonfile} |
PageName |
Строка не должна содержать точку в своем последнем сегменте пути, за которым следует один или несколько символов, не являющихся точками (.) |
Warning
При использовании System.Text.RegularExpressions для обработки ненадежных входных данных передайте время ожидания. Злонамеренный пользователь может предоставить входные данные для RegularExpressions, что делает возможными атаки типа "отказ в обслуживании". API платформы ASP.NET Core, использующие RegularExpressions, передают время ожидания.
К одному параметру может применяться несколько разделенных запятой ограничений. Например, следующее ограничение ограничивает параметр целочисленным значением 1 или больше:
[Route("users/{id:int:min(1)}")]
public User GetUserById(int id) { }
Warning
Ограничения маршрута, которые проверяют URL-адрес и могут быть преобразованы в тип CLR, всегда используют инвариантные языковые настройки. Например, преобразование в тип CLR int или DateTime. Эти ограничения предполагают, что для URL-адреса не предусмотрена локализация. Предоставляемые платформой ограничения маршрутов не изменяют значения, хранящиеся в значениях маршрута. Все значения маршрута, переданные из URL-адреса, сохраняются как строки. Например, ограничение float пытается преобразовать значение маршрута в число с плавающей запятой, но преобразованное значение используется только для проверки возможности преобразования в число с плавающей запятой.
Регулярные выражения в ограничениях
Warning
При использовании System.Text.RegularExpressions для обработки ненадежных входных данных передайте время ожидания. Злонамеренный пользователь может предоставить входные данные для RegularExpressions, что делает возможными атаки типа "отказ в обслуживании". API платформы ASP.NET Core, использующие RegularExpressions, передают время ожидания.
Регулярные выражения могут быть определены как встроенные ограничения с помощью ограничения маршрута regex(...). Методы в семействе MapControllerRoute также принимают объектный литерал ограничений. При использовании этой формы строковые значения будут интерпретироваться как регулярные выражения.
В следующем коде используется встроенное ограничение регулярного выражения.
app.MapGet("{message:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)}",
() => "Inline Regex Constraint Matched");
В следующем коде для указания ограничения регулярного выражения используется литеральный объект.
app.MapControllerRoute(
name: "people",
pattern: "people/{ssn}",
constraints: new { ssn = "^\\d{3}-\\d{2}-\\d{4}$", },
defaults: new { controller = "People", action = "List" });
В платформе ASP.NET Core в конструктор регулярных выражений добавляются члены RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant. Описание этих членов см. в разделе RegexOptions.
В регулярных выражениях применяются разделители и токены, аналогичные используемым функцией маршрутизации и в языке C#. Токены регулярного выражения должны быть экранированы. Чтобы использовать регулярное выражение ^\d{3}-\d{2}-\d{4}$ во встроенном ограничении, используйте один из следующих способов.
- Замените символы
\, представленные в строке, символами\\в исходном файле C#, чтобы экранировать escape-символ строки\. - Буквальные строковые литералы.
Чтобы экранировать символы разделения параметров маршрутизации {, }, [, ], используйте их дважды в выражении (например, {{, }}, [[, ]]). В следующей таблице показаны регулярные выражения и их экранированные варианты.
| Регулярное выражение | Экранированное регулярное выражение |
|---|---|
^\d{3}-\d{2}-\d{4}$ |
^\\d{{3}}-\\d{{2}}-\\d{{4}}$ |
^[a-z]{2}$ |
^[[a-z]]{{2}}$ |
Регулярные выражения, используемые при маршрутизации, часто начинаются с символа ^ и соответствуют начальной позиции строки. Выражения часто заканчиваются символом $ и соответствуют концу строки. Благодаря символам ^ и $ регулярное выражение сопоставляется со всем значением параметра маршрута. Если символы ^ и $ отсутствуют, регулярное выражение сопоставляется с любой подстрокой внутри строки, что обычно нежелательно. В таблице ниже представлен ряд примеров и объясняются причины соответствия или несоответствия.
| Expression | String | Match | Comment |
|---|---|---|---|
[a-z]{2} |
hello | Yes | Совпадения подстроки |
[a-z]{2} |
123abc456 | Yes | Совпадения подстроки |
[a-z]{2} |
mz | Yes | Совпадение выражения |
[a-z]{2} |
MZ | Yes | Без учета регистра |
^[a-z]{2}$ |
hello | No | См. замечания, касающиеся символов ^ и $, выше |
^[a-z]{2}$ |
123abc456 | No | См. замечания, касающиеся символов ^ и $, выше |
Дополнительные сведения о синтаксисе регулярных выражений см. в статье Регулярные выражения в .NET Framework.
Чтобы ограничить возможные значения параметра набором известных значений, используйте регулярное выражение. Например, при использовании выражения {action:regex(^(list|get|create)$)} значение маршрута action будет соответствовать только list, get или create. При передаче в словарь ограничений строка ^(list|get|create)$ будет эквивалентной. Ограничения, которые передаются в словарь ограничений и не соответствуют одному из известных ограничений, также рассматриваются как регулярные выражения. Ограничения, которые передаются в шаблоне и не соответствуют одному из известных ограничений, не рассматриваются как регулярные выражения.
Пользовательские ограничения маршрутов
Пользовательские ограничения маршрутов можно создать путем внедрения интерфейса IRouteConstraint. Интерфейс IRouteConstraint содержит метод, Match, который возвращает true, если ограничение удовлетворяется, и false — если нет.
Пользовательские ограничения маршрутов редко требуются. Перед реализацией пользовательского ограничения маршрута рассмотрите альтернативные варианты, такие как привязка модели.
В папке ASP.NET Core Constraints находятся хорошие примеры создания ограничений. Например, GuidRouteConstraint.
Чтобы применить пользовательский метод IRouteConstraint, тип ограничения маршрута необходимо зарегистрировать с помощью ConstraintMap приложения в контейнере службы. Объект ConstraintMap — это словарь, который сопоставляет ключи ограничений пути с реализациями IRouteConstraint, которые проверяют эти ограничения.
ConstraintMap приложения можно обновить в Program.cs, либо как часть вызова AddRouting, либо непосредственно настроив RouteOptions с помощью builder.Services.Configure<RouteOptions>. Рассмотрим пример.
builder.Services.AddRouting(options =>
options.ConstraintMap.Add("noZeroes", typeof(NoZeroesRouteConstraint)));
Предыдущее ограничение применяется в следующем коде.
[ApiController]
[Route("api/[controller]")]
public class NoZeroesController : ControllerBase
{
[HttpGet("{id:noZeroes}")]
public IActionResult Get(string id) =>
Content(id);
}
Реализация NoZeroesRouteConstraint препятствует применению 0 к параметру маршрута:
public class NoZeroesRouteConstraint : IRouteConstraint
{
private static readonly Regex _regex = new(
@"^[1-9]*$",
RegexOptions.CultureInvariant | RegexOptions.IgnoreCase,
TimeSpan.FromMilliseconds(100));
public bool Match(
HttpContext? httpContext, IRouter? route, string routeKey,
RouteValueDictionary values, RouteDirection routeDirection)
{
if (!values.TryGetValue(routeKey, out var routeValue))
{
return false;
}
var routeValueString = Convert.ToString(routeValue, CultureInfo.InvariantCulture);
if (routeValueString is null)
{
return false;
}
return _regex.IsMatch(routeValueString);
}
}
Warning
При использовании System.Text.RegularExpressions для обработки ненадежных входных данных передайте время ожидания. Злонамеренный пользователь может предоставить входные данные для RegularExpressions, что делает возможными атаки типа "отказ в обслуживании". API платформы ASP.NET Core, использующие RegularExpressions, передают время ожидания.
Предыдущий код:
- Предотвращает
0в сегменте{id}маршрута. - Отображается для предоставления базового примера реализации настраиваемого ограничения. Не следует использовать в рабочем приложении.
Следующий код является лучшим подходом к предотвращению обработки id, содержащего 0.
[HttpGet("{id}")]
public IActionResult Get(string id)
{
if (id.Contains('0'))
{
return StatusCode(StatusCodes.Status406NotAcceptable);
}
return Content(id);
}
Приведенный выше код имеет следующие преимущества по сравнению с подходом NoZeroesRouteConstraint.
- Пользовательское ограничение не требуется.
- Он возвращает более понятную ошибку, если параметр маршрута включает
0.
Преобразователи параметров
Преобразователи параметров:
- Выполняются при формировании ссылки с помощью LinkGenerator.
- Реализуйте Microsoft.AspNetCore.Routing.IOutboundParameterTransformer.
- Настраиваются с помощью ConstraintMap.
- Принимают значение маршрута параметра и изменяют его на новое строковое значение.
- Результатом является использование преобразованного значения в созданной ссылке.
Например, пользовательский преобразователь параметра slugify в шаблоне маршрута blog\{article:slugify} с Url.Action(new { article = "MyTestArticle" }) формирует значение blog\my-test-article.
Рассмотрим следующую реализацию IOutboundParameterTransformer.
public class SlugifyParameterTransformer : IOutboundParameterTransformer
{
public string? TransformOutbound(object? value)
{
if (value is null)
{
return null;
}
return Regex.Replace(
value.ToString()!,
"([a-z])([A-Z])",
"$1-$2",
RegexOptions.CultureInvariant,
TimeSpan.FromMilliseconds(100))
.ToLowerInvariant();
}
}
Чтобы использовать преобразователь параметров в шаблоне маршрута, настройте его с помощью ConstraintMap в Program.cs.
builder.Services.AddRouting(options =>
options.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer));
В ASP.NET Core используются преобразователи параметров для преобразования URI для определения конечной точки. Например, преобразователи параметров преобразуют значения маршрута, используемые для сопоставления area, controller, action и page:
app.MapControllerRoute(
name: "default",
pattern: "{controller:slugify=Home}/{action:slugify=Index}/{id?}");
С помощью предыдущего шаблона маршрута действие SubscriptionManagementController.GetAll сопоставляется с URI /subscription-management/get-all. Преобразователь параметра не изменяет значения маршрута, используемые для формирования ссылки. Например, Url.Action("GetAll", "SubscriptionManagement") выводит /subscription-management/get-all.
ASP.NET Core предоставляет соглашения об API для использования преобразователей параметров со сформированными маршрутами.
- Соглашение Microsoft.AspNetCore.Mvc.ApplicationModels.RouteTokenTransformerConvention MVC применяет указанный преобразователь параметров ко всем маршрутам атрибутов в приложении. Преобразователь параметров преобразует маркеры маршрутов атрибутов по мере их замены. Дополнительные сведения см. в разделе об использовании преобразователя параметров для настройки замены маркеров.
- Razor Pages использует соглашение об API PageRouteTransformerConvention. Это соглашение применяет указанный трансформатор параметров ко всем автоматически обнаруженным страницам Razor. Преобразователь параметров преобразует сегменты папок и имен файлов маршрутов Razor Pages. Дополнительные сведения см. в разделе об использовании преобразователя параметров для настройки маршрутов страниц.
Справочник по формированию URL-адресов
В этом разделе представлен справочник по алгоритму, реализованному при формировании URL-адреса. На практике в большинстве сложных примеров формирования URL-адресов используются контроллеры или Razor страницы. Дополнительные сведения см. в статье Маршрутизация в контроллерах.
Процесс создания URL-адресов начинается с вызова LinkGenerator.GetPathByAddress или аналогичного метода. Метод предоставляется с адресом, набором значений маршрута и при необходимости со сведениями о текущем запросе из HttpContext.
Первым шагом является использование адреса для разрешения набора конечных точек-кандидатов с помощью IEndpointAddressScheme<TAddress>, соответствующих типу адреса.
После того как набор кандидатов найден в схеме адресов, конечные точки упорядочиваются и обрабатываются последовательно до тех пор, пока операция формирования URL-адреса не завершится. При создании URL-адреса не проверяется наличие неоднозначностей, первый возвращенный результат является окончательным результатом.
Устранение неполадок при формировании URL-адресов с помощью ведения журнала
Первым шагом при устранении неполадок при формировании URL-адресов является установка уровня ведения журнала Microsoft.AspNetCore.Routing на TRACE.
LinkGenerator фиксирует в журнале множество сведений об обработке, которые могут быть полезны при устранении неполадок.
Дополнительные сведения о формировании URL-адресов см. в разделе Справочник по формированию URL-адресов.
Addresses
Адреса являются основным понятием в формировании URL-адресов и используются для привязки вызова генератора ссылок к набору конечных точек-кандидатов.
Адреса — это расширяемое понятие, которое по умолчанию поставляется с двумя реализациями.
- Использование имени конечной точки (
string) в качестве адреса:- Обеспечивает аналогичную функциональность, как и имя маршрута в MVC.
- Использует тип метаданных IEndpointNameMetadata.
- Обрабатывает переданную строку в соответствии с метаданными всех зарегистрированных конечных точек.
- Создает исключение при запуске, если несколько конечных точек использует одно и то же имя.
- Рекомендуется для общего использования вне контроллеров и страниц Razor.
- Использование значений маршрутов (RouteValuesAddress) в качестве адреса:
- Предоставляет аналогичную функциональность и для контроллеров, и для Pages в плане формирования устаревших URL-адресов.
- Очень сложно расширять и отлаживать.
- Предоставляет реализацию, используемую
IUrlHelper, вспомогательными функциями тегов, вспомогательными методами HTML, результатами действий и т. д.
Роль схемы адресов заключается в том, чтобы создать связь между адресом и соответствующими конечными точками по произвольным критериям.
- При использовании схемы имен конечных точек выполняется простой поиск по словарю.
- Схема значений маршрута включает сложный алгоритм для нахождения лучшего подмножества.
Значения окружения и явные значения
Из текущего запроса маршрутизация обращается к значениям маршрута текущего запроса HttpContext.Request.RouteValues. Значения, связанные с текущим запросом, называются значениями окружения. В целях ясности в документации подразумеваются значения маршрута, передаваемые в методы как явные значения.
В следующем примере показаны значения окружения и явные значения. Он предоставляет значения окружения из текущего запроса и явные значения:
public class WidgetController : ControllerBase
{
private readonly LinkGenerator _linkGenerator;
public WidgetController(LinkGenerator linkGenerator) =>
_linkGenerator = linkGenerator;
public IActionResult Index()
{
var indexPath = _linkGenerator.GetPathByAction(
HttpContext, values: new { id = 17 })!;
return Content(indexPath);
}
// ...
Предыдущий код:
- Возвращает
/Widget/Index/17. - Получает LinkGenerator через DI.
Следующий код не предоставляет значения окружения, а только явные значения:
var subscribePath = _linkGenerator.GetPathByAction(
"Subscribe", "Home", new { id = 17 })!;
Предыдущий метод возвращает /Home/Subscribe/17
Следующий код в WidgetController возвращает /Widget/Subscribe/17:
var subscribePath = _linkGenerator.GetPathByAction(
HttpContext, "Subscribe", null, new { id = 17 });
Следующий код настраивает контроллер, используя текущие значения окружения в текущем запросе и явные значения.
public class GadgetController : ControllerBase
{
public IActionResult Index() =>
Content(Url.Action("Edit", new { id = 17 })!);
}
В предыдущем коде:
- Возвращается
/Gadget/Edit/17. - Url получает IUrlHelper.
-
Action создает URL-адрес с абсолютным путем для метода действия. URL-адрес содержит указанное имя
actionи значенияroute.
Следующий код предоставляет значения окружения из текущего запроса и явные значения:
public class IndexModel : PageModel
{
public void OnGet()
{
var editUrl = Url.Page("./Edit", new { id = 17 });
// ...
}
}
Приведенный выше код задает для url значение /Edit/17, когда страница Edit Razor содержит следующую директиву:
@page "{id:int}"
Если страница Edit не содержит шаблон маршрута "{id:int}", то url будет /Edit?id=17.
Поведение IUrlHelper MVC добавляет уровень сложности, помимо правил, описанных здесь.
-
IUrlHelperвсегда предоставляет значения маршрута из текущего запроса как значения окружения. -
IUrlHelper.Action всегда копирует текущие значения маршрута
actionиcontrollerкак явные значения, если они не переопределены разработчиком. -
IUrlHelper.Page всегда копирует текущее значение маршрута
pageкак явное значение, если оно не переопределено. -
IUrlHelper.Pageвсегда переопределяет текущее значение маршрутаhandlerнаnullкак явное значение, если его не переопределили.
Пользователи часто удивляются сведениям о поведении значений окружения, поскольку MVC не следует собственным правилам. По историческим причинам и для обеспечения совместимости для некоторых значений маршрута, таких как action, controller, page и handler, предусмотрено собственное поведение в особых случаях.
Аналогичные функции, предоставляемые LinkGenerator.GetPathByAction и LinkGenerator.GetPathByPage, дублируют эти аномалии IUrlHelper для обеспечения совместимости.
Процесс формирования URL-адреса
После обнаружения набора конечных точек-кандидатов алгоритм формирования URL-адресов:
- последовательно обрабатывает конечные точки;
- Возвращает первый успешный результат.
Первый шаг этого процесса называется аннулированием значения маршрута. Аннулирование значения маршрута — это процесс, с помощью которого маршрутизация решает, какие значения маршрута должны использоваться из значений окружения, а какие следует игнорировать. Каждое значение окружения учитывается и либо объединяется с явными значениями, либо игнорируется.
Роль значений окружения заключается в том, что в некоторых распространенных случаях они позволяют сократить для разработчиков объем вводимой информации. Как правило, сценарии, в которых полезно использовать значения окружения, связаны с MVC.
- При связывании с другим действием в том же контроллере не требуется указывать имя контроллера.
- При связывании с другим контроллером в той же области не требуется указывать имя области.
- При связывании с тем же методом действия не требуется указывать значения маршрута.
- При связывании с другой частью приложения следует избегать передачи значений маршрута, не имеющих смысла в этой части приложения.
Вызовы LinkGenerator или IUrlHelper, которые возвращают null, обычно происходят из-за неправильного понимания невалидности значения маршрута. Для устранения неполадок аннулирования значения маршрута явно укажите дополнительные значения маршрута, чтобы определить, устранена ли проблема.
Аннулирование значения маршрута предполагает, что схема URL-адреса приложения является иерархической, в которой иерархия сформирована слева направо. Рассмотрим шаблон маршрута базового контроллера {controller}/{action}/{id?}, чтобы понять, как это работает на практике.
Изменение значения делает недействительными все значения маршрута, которые отображаются справа. Это отражает предположение об иерархии. Если приложение имеет значение окружения для id, а операция указывает другое значение для controller:
-
idне будет использоваться повторно, поскольку{controller}находится слева от{id?}.
Некоторые примеры, демонстрирующие этот принцип
- Если явные значения содержат значение для
id, значение окружения дляidигнорируется. Можно использовать значения окружения дляcontrollerиaction. - Если явные значения содержат значение для
action, любое значение окружения дляactionигнорируется. Можно использовать фоновые значения дляcontroller. Если явное значение дляactionотличается от значения окружения дляaction, значениеidне будет использоваться. Если явное значение дляactionсовпадает со значением окружения дляaction, можно использовать значениеid. - Если явные значения содержат значение для
controller, любое значение окружения дляcontrollerигнорируется. Если явное значение дляcontrollerотличается от значения окружения дляcontroller, значенияactionиidне будут использоваться. Если явное значение дляcontrollerсовпадает со значением окружения дляcontroller, можно использовать значенияactionиid.
Этот процесс усложняется за счет наличия атрибутных маршрутов и традиционных маршрутов. Стандартные маршруты контроллера, такие как {controller}/{action}/{id?}, указывают иерархию с помощью параметров маршрута. Для выделенных стандартных маршрутов и маршрутов атрибутов для контроллеров и Razor Pages:
- Существует иерархия значений маршрута.
- Они не отображаются в шаблоне.
В таких случаях формирование URL-адресов определяет концепцию необходимых значений. Для конечных точек, созданных контроллерами и Razor Pages, указаны обязательные значения, позволяющие использовать аннулирование значений маршрута.
Подробный алгоритм аннулирования значения маршрута
- Имена обязательных значений объединяются с параметрами маршрута, а затем обрабатываются слева направо.
- Для каждого параметра сравниваются внешние значения и явное значение:
- Если значение окружения и явное значение совпадают, процесс продолжается.
- Если значение окружения задано, а явное значение не задано, то при формировании URL-адреса используется значение окружения.
- Если значение окружения отсутствует, а явное значение задано, следует отклонить значение окружения и все последующие значения окружения.
- Если заданы и значение окружения, и явное значение, однако они отличаются, следует отклонить значение окружения и все последующие значения окружения.
На этом этапе операция формирования URL-адреса готова к оценке ограничений маршрута. Набор допустимых значений объединяется со значениями по умолчанию для параметра, предоставляемыми ограничениям. Если все ограничения пройдены, операция продолжается.
Затем допустимые значения можно использовать для расширения шаблона маршрута. Шаблон маршрута обрабатывается:
- Слева направо.
- Для каждого параметра заменяется допустимое значение.
- В следующих особых случаях:
- Если в допустимых значениях отсутствует значение и параметр имеет значение по умолчанию, используется значение по умолчанию.
- Если в допустимых значениях отсутствует значение, а параметр является необязательным, обработка продолжается.
- Если любой параметр маршрута справа от отсутствующего необязательного параметра имеет значение, операция завершается ошибкой.
- Смежные параметры со значениями по умолчанию и необязательные параметры по возможности сворачиваются.
Явно предоставленные значения, которые не соответствуют сегменту маршрута, добавляются в строку запроса. В приведенной ниже таблице показан результат использования шаблона маршрута {controller}/{action}/{id?}.
| Значения окружения | Явные значения | Result |
|---|---|---|
| контроллер = "Home" | action = "О программе" | /Home/About |
| контроллер = "Home" | controller = "Заказ", action = "О" | /Order/About |
| controller = "Home", color = "Красный" | action = "О программе" | /Home/About |
| контроллер = "Home" | action = "О программе", color = "Красный" | /Home/About?color=Red |
Необязательный порядок параметров маршрута
Необязательные параметры маршрута и литералы должны следовать за всеми обязательными параметрами маршрута. В следующем коде параметры id и name должны идти после параметра color.
using Microsoft.AspNetCore.Mvc;
namespace WebApplication1.Controllers;
[Route("api/[controller]")]
public class MyController : ControllerBase
{
// GET /api/my/red/2/joe
// GET /api/my/red/2
// GET /api/my
[HttpGet("{color}/{id:int?}/{name?}")]
public IActionResult GetByIdAndOptionalName(string color, int id = 1, string? name = null)
{
return Ok($"{color} {id} {name ?? ""}");
}
}
Проблемы с недействительностью значений маршрута
В следующем коде показан пример схемы формирования URL-адреса, которая не поддерживается маршрутизацией:
app.MapControllerRoute(
"default",
"{culture}/{controller=Home}/{action=Index}/{id?}");
app.MapControllerRoute(
"blog",
"{culture}/{**slug}",
new { controller = "Blog", action = "ReadPost" });
В приведенном выше коде параметр маршрута culture используется для локализации. Необходимо, чтобы параметр culture всегда принимался как значение окружения. Однако параметр culture не принимается как значение окружения ввиду способа работы требуемых значений.
- В шаблоне маршрута
"default"параметр маршрутаcultureнаходится слева отcontroller, поэтому измененияcontrollerне приведут к аннулированиюculture. - В шаблоне маршрута
"blog"параметр маршрутаcultureрассматривается как находящийся справа отcontroller, который имеется в требуемых значениях.
Анализ пути URL-адреса с помощью LinkParser
Класс LinkParser добавляет поддержку анализа пути URL-адреса в набор значений маршрута. Метод ParsePathByEndpointName принимает имя конечной точки и путь URL-адреса, а также возвращает набор значений маршрута, извлеченных из пути URL-адреса.
В следующем примере контроллера действие GetProduct использует шаблон маршрута api/Products/{id} и содержит параметр Name со значением GetProduct:
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
[HttpGet("{id}", Name = nameof(GetProduct))]
public IActionResult GetProduct(string id)
{
// ...
В том же классе контроллера действие AddRelatedProduct ожидает путь URL-адреса (pathToRelatedProduct), который можно предоставить в качестве параметра строки запроса:
[HttpPost("{id}/Related")]
public IActionResult AddRelatedProduct(
string id, string pathToRelatedProduct, [FromServices] LinkParser linkParser)
{
var routeValues = linkParser.ParsePathByEndpointName(
nameof(GetProduct), pathToRelatedProduct);
var relatedProductId = routeValues?["id"];
// ...
В предыдущем примере действие AddRelatedProduct извлекает значение id маршрута из пути URL-адреса. Например, если указан путь URL-адреса /api/Products/1, для relatedProductId будет задано значение 1. Такой подход позволяет клиентам API использовать пути URL-адресов при обращении к ресурсам, не обладая знаниями в структуре такого URL-адреса.
Настройка метаданных конечной точки
Сведения о настройке метаданных конечной точки см. по следующим ссылкам:
- Включение CORS с маршрутизацией конечных точек
-
Пример IAuthorizationPolicyProvider с использованием настраиваемого атрибута
[MinimumAgeAuthorize] - Тестирование проверки подлинности с использованием атрибута [Authorize]
- RequireAuthorization
- Выбор схемы с использованием атрибута [Authorize]
- Применение политик с использованием атрибута [Authorize]
- Авторизация на основе ролей в ASP.NET Core
Сопоставление хостов при маршрутизации, используя RequireHost
RequireHost применяет ограничение к маршруту, требующее указанный узел. Параметр RequireHost или [Host] может иметь следующее значение:
- Хост:
www.domain.com, соответствуетwww.domain.comс любым портом. - Хост с подстановочным знаком:
*.domain.com, соответствуетwww.domain.com,subdomain.domain.comилиwww.subdomain.domain.comдля любого порта. - Порт:
*:5000, соответствует порту 5000 с любым хостом. - Узел и порт:
www.domain.com:5000или*.domain.com:5000, соответствует узлу и порту.
С помощью RequireHost или [Host] можно указать несколько параметров. Ограничение соответствует хостам, допустимым для любого из параметров. Например, [Host("domain.com", "*.domain.com")] соответствует domain.com, www.domain.com и subdomain.domain.com.
Следующий код использует RequireHost, чтобы требовать указанный хост в маршруте:
app.MapGet("/", () => "Contoso").RequireHost("contoso.com");
app.MapGet("/", () => "AdventureWorks").RequireHost("adventure-works.com");
app.MapHealthChecks("/healthz").RequireHost("*:8080");
Следующий код использует атрибут [Host] в контроллере, чтобы требовать один из указанных хостов.
[Host("contoso.com", "adventure-works.com")]
public class HostsController : Controller
{
public IActionResult Index() =>
View();
[Host("example.com")]
public IActionResult Example() =>
View();
}
Если атрибут [Host] применяется как к контроллеру, так и к методу действия, выполняется следующее.
- Используется атрибут действия.
- Атрибут контроллера не учитывается.
Warning
API, которые полагаются на заголовок Host, такие как HttpRequest.Host и RequireHost, подвержены потенциальному спуфингу со стороны клиентов.
Чтобы предотвратить подмену узлов и портов, используйте один из следующих подходов:
- Используйте HttpContext.Connection (ConnectionInfo.LocalPort) в местах, где проверяются порты.
- Примените фильтрацию хостов.
Группы маршрутов
Метод MapGroup расширения помогает упорядочивать группы конечных точек с общим префиксом. Это уменьшает повторяющийся код и позволяет настраивать целые группы конечных точек с одним вызовом методов, таких как RequireAuthorization и WithMetadata которые добавляют метаданные конечной точки.
Например, следующий код создает две аналогичные группы конечных точек:
app.MapGroup("/public/todos")
.MapTodosApi()
.WithTags("Public");
app.MapGroup("/private/todos")
.MapTodosApi()
.WithTags("Private")
.AddEndpointFilterFactory(QueryPrivateTodos)
.RequireAuthorization();
EndpointFilterDelegate QueryPrivateTodos(EndpointFilterFactoryContext factoryContext, EndpointFilterDelegate next)
{
var dbContextIndex = -1;
foreach (var argument in factoryContext.MethodInfo.GetParameters())
{
if (argument.ParameterType == typeof(TodoDb))
{
dbContextIndex = argument.Position;
break;
}
}
// Skip filter if the method doesn't have a TodoDb parameter.
if (dbContextIndex < 0)
{
return next;
}
return async invocationContext =>
{
var dbContext = invocationContext.GetArgument<TodoDb>(dbContextIndex);
dbContext.IsPrivate = true;
try
{
return await next(invocationContext);
}
finally
{
// This should only be relevant if you're pooling or otherwise reusing the DbContext instance.
dbContext.IsPrivate = false;
}
};
}
public static RouteGroupBuilder MapTodosApi(this RouteGroupBuilder group)
{
group.MapGet("/", GetAllTodos);
group.MapGet("/{id}", GetTodo);
group.MapPost("/", CreateTodo);
group.MapPut("/{id}", UpdateTodo);
group.MapDelete("/{id}", DeleteTodo);
return group;
}
В этом сценарии можно использовать относительный адрес для Location заголовка в 201 Created результате:
public static async Task<Created<Todo>> CreateTodo(Todo todo, TodoDb database)
{
await database.AddAsync(todo);
await database.SaveChangesAsync();
return TypedResults.Created($"{todo.Id}", todo);
}
Первая группа конечных точек будет соответствовать только запросам с префиксом /public/todos и будет доступна без какой-либо аутентификации. Вторая группа конечных точек будет соответствовать только запросам с префиксом /private/todos и требовать проверки подлинности.
Фабрика QueryPrivateTodos фильтров конечных точек — это локальная функция, которая изменяет параметры обработчика маршрутов TodoDb, чтобы разрешить доступ и хранение частных данных todo.
Группы маршрутов также поддерживают вложенные группы и сложные шаблоны префикса с параметрами маршрута и ограничениями. В следующем примере обработчик маршрутов, сопоставленный с user группой, может перехватывать параметры маршрута {org} и {group}, определенные в префиксах внешней группы.
Префикс также может быть пустым. Это может быть полезно для добавления метаданных конечной точки или фильтров в группу конечных точек без изменения шаблона маршрута.
var all = app.MapGroup("").WithOpenApi();
var org = all.MapGroup("{org}");
var user = org.MapGroup("{user}");
user.MapGet("", (string org, string user) => $"{org}/{user}");
Добавление фильтров или метаданных в группу ведет себя аналогично добавлению их отдельно в каждую конечную точку, перед добавлением любых дополнительных фильтров или метаданных, которые могли быть добавлены во внутреннюю группу или конкретную конечную точку.
var outer = app.MapGroup("/outer");
var inner = outer.MapGroup("/inner");
inner.AddEndpointFilter((context, next) =>
{
app.Logger.LogInformation("/inner group filter");
return next(context);
});
outer.AddEndpointFilter((context, next) =>
{
app.Logger.LogInformation("/outer group filter");
return next(context);
});
inner.MapGet("/", () => "Hi!").AddEndpointFilter((context, next) =>
{
app.Logger.LogInformation("MapGet filter");
return next(context);
});
В приведенном выше примере внешний фильтр регистрирует входящий запрос до внутреннего фильтра, даже если он был добавлен вторым. Так как фильтры были применены к разным группам, порядок их добавления относительно друг друга не имеет значения. Порядок добавления фильтров имеет значение, если они применяются к одной и той же группе или конкретной конечной точке.
Запрос к /outer/inner/ зафиксирует следующее:
/outer group filter
/inner group filter
MapGet filter
Рекомендации по производительности для маршрутизации
Проблемы с производительностью в приложении зачастую связывали с маршрутизацией. Причиной этому является то, что такие фреймворки, как контроллеры и Razor Pages, сообщают о количестве времени, проведенном внутри фреймворка, в сообщениях журнала. Если время, указанное контроллерами, значительно отличается от общего времени запроса:
- Разработчики исключают код приложения из списка возможных источников проблемы.
- Обычно предполагается, что причиной является маршрутизация.
Для проверки производительности маршрутизации используются тысячи конечных точек. Маловероятно, что обычное приложение столкнется с проблемами с производительностью только лишь из-за того, что оно слишком большое. Наиболее распространенная причина снижения производительности маршрутизации обычно кроется в неправильной работе настраиваемого ПО промежуточного слоя.
В следующем примере кода показан базовый метод сокращения источника задержки.
var logger = app.Services.GetRequiredService<ILogger<Program>>();
app.Use(async (context, next) =>
{
var stopwatch = Stopwatch.StartNew();
await next(context);
stopwatch.Stop();
logger.LogInformation("Time 1: {ElapsedMilliseconds}ms", stopwatch.ElapsedMilliseconds);
});
app.UseRouting();
app.Use(async (context, next) =>
{
var stopwatch = Stopwatch.StartNew();
await next(context);
stopwatch.Stop();
logger.LogInformation("Time 2: {ElapsedMilliseconds}ms", stopwatch.ElapsedMilliseconds);
});
app.UseAuthorization();
app.Use(async (context, next) =>
{
var stopwatch = Stopwatch.StartNew();
await next(context);
stopwatch.Stop();
logger.LogInformation("Time 3: {ElapsedMilliseconds}ms", stopwatch.ElapsedMilliseconds);
});
app.MapGet("/", () => "Timing Test.");
Для маршрутизации времени:
- Чередуйте каждое промежуточное ПО с копией тайминг промежуточного ПО, показанной в предыдущем коде.
- Добавьте уникальный идентификатор для сопоставления данных о времени с кодом.
Это базовый способ сократить задержку, когда она является существенной, например более 10ms. Вычитание Time 2 из Time 1 позволяет получить время, затраченное в промежуточном ПО UseRouting.
Следующий код использует более компактный подход к предыдущему коду времени.
public sealed class AutoStopwatch : IDisposable
{
private readonly ILogger _logger;
private readonly string _message;
private readonly Stopwatch _stopwatch;
private bool _disposed;
public AutoStopwatch(ILogger logger, string message) =>
(_logger, _message, _stopwatch) = (logger, message, Stopwatch.StartNew());
public void Dispose()
{
if (_disposed)
{
return;
}
_logger.LogInformation("{Message}: {ElapsedMilliseconds}ms",
_message, _stopwatch.ElapsedMilliseconds);
_disposed = true;
}
}
var logger = app.Services.GetRequiredService<ILogger<Program>>();
var timerCount = 0;
app.Use(async (context, next) =>
{
using (new AutoStopwatch(logger, $"Time {++timerCount}"))
{
await next(context);
}
});
app.UseRouting();
app.Use(async (context, next) =>
{
using (new AutoStopwatch(logger, $"Time {++timerCount}"))
{
await next(context);
}
});
app.UseAuthorization();
app.Use(async (context, next) =>
{
using (new AutoStopwatch(logger, $"Time {++timerCount}"))
{
await next(context);
}
});
app.MapGet("/", () => "Timing Test.");
Потенциально ресурсоемкие функции маршрутизации
В списке ниже представлены некоторые сведения о функциях маршрутизации, которые относительно ресурсоемкие по сравнению с базовыми шаблонами маршрутов.
- Регулярные выражения: можно составить сложные регулярные выражения, которые будут выполняться долгое время с небольшим количеством входных данных.
- Сложные сегменты (
{x}-{y}-{z}):- значительно более ресурсоемкие, чем анализ обычного сегмента URL-пути.
- В результате выделяется множество дополнительных подстрок.
- Синхронный доступ к данным: многие сложные приложения имеют доступ к базам данных в рамках их маршрутизации. Используйте точки расширения, такие как MatcherPolicy и EndpointSelectorContext, которые являются асинхронными.
Руководство по большим таблицам маршрутизации
По умолчанию алгоритм маршрутизации в ASP.NET Core жертвует объемом памяти в пользу низкой нагрузки на ЦП. Это приводит к тому, что время, затрачиваемое на сопоставление маршрутов, зависит только от длины сопоставляемого пути, а не от количества маршрутов. Однако такой подход создает проблемы в тех случаях, когда приложение использует большое количество (несколько тысяч) маршрутов с большим числом переменных префиксов. Например, если в маршрутах используются параметры в ранних сегментах маршрута, как {parameter}/some/literal.
Вряд ли приложение столкнется с такой проблемой, если только:
- в приложении много маршрутов, использующих такой шаблон;
- В приложении очень много маршрутов.
Как определить, возникает ли у приложения проблема большой таблицы маршрутов
- Есть два симптома, на которые стоит обратить внимание:
- При первом запросе приложение запускается медленно.
- Обратите внимание, что это необходимо, но недостаточно. Медленный запуск приложения могут вызывать многие другие проблемы, не связанные с маршрутизацией. Чтобы точно определить, что приложение сталкивается с этой ситуацией, проверьте следующее условие.
- Приложение потребляет много памяти во время запуска, а в дампе памяти отображается большое количество экземпляров
Microsoft.AspNetCore.Routing.Matching.DfaNode.
- При первом запросе приложение запускается медленно.
Способы решения этой проблемы
Существует несколько методов и оптимизаций, которые можно применить к маршрутам, которые в значительной степени улучшают этот сценарий:
- Везде, где это возможно, примените к параметрам ограничения маршрутов, например
{parameter:int},{parameter:guid},{parameter:regex(\\d+)}и т. д.- Это позволяет алгоритму маршрутизации внутренним образом оптимизировать структуры, которые используются для сопоставления, и радикально снизить использование памяти.
- В подавляющем большинстве случаев этого будет достаточно для восстановления приемлемой производительности.
- Измените маршруты, чтобы переместить параметры в более поздние сегменты шаблона.
- Это сокращает количество возможных "путей", с которыми придется сопоставлять конечную точку по определенному пути.
- Используйте динамический маршрут и динамическое сопоставление с контроллером или страницей.
- Это можно сделать с помощью
MapDynamicControllerRouteиMapDynamicPageRoute.
- Это можно сделать с помощью
Промежуточное ПО для короткого замыкания после маршрутизации
При совпадении маршрутизации с конечной точкой обычно сначала выполняется остальная часть конвейера промежуточного слоя, прежде чем будет вызвана логика конечной точки. Службы могут снизить использование ресурсов, отфильтровав известные запросы на ранних этапах конвейера. Используйте метод расширения ShortCircuit, чтобы маршрутизация немедленно вызвала логику конечной точки и завершила запрос. Например, определенному маршруту может не потребоваться пройти проверку подлинности или ПО промежуточного слоя CORS. В следующем примере запросы, которые соответствуют маршруту /short-circuit, прерываются:
app.MapGet("/short-circuit", () => "Short circuiting!").ShortCircuit();
При ShortCircuit(IEndpointConventionBuilder, Nullable<Int32>) необходимости метод может принимать код состояния.
MapShortCircuit Используйте метод для настройки короткого замыкания одновременно для нескольких маршрутов, передав в качестве параметра массив префиксов URL-адресов. Например, браузеры и боты часто проверяют серверы на наличие хорошо известных путей, таких как robots.txt и favicon.ico. Если у приложения нет этих файлов, одна строка кода может настроить оба маршрута:
app.MapShortCircuit(404, "robots.txt", "favicon.ico");
MapShortCircuit возвращается IEndpointConventionBuilder , чтобы можно было к нему добавить дополнительные ограничения маршрута, такие как фильтрация хоста.
Методы ShortCircuit и MapShortCircuit не влияют на ПО промежуточного слоя, размещенное перед UseRouting. При попытке использовать эти методы с конечными точками, которые также имеют [Authorize] или [RequireCors] метаданные, запросы завершаются сбоем InvalidOperationException. Эти метаданные применяются атрибутами [Authorize] или [EnableCors], или методами RequireCors или RequireAuthorization.
Чтобы увидеть влияние короткозамкнутого промежуточного слоя, задайте для категории ведения журнала "Microsoft" значение Information (Сведения) в appsettings.Development.json:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Information",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}
Выполните следующий код:
var app = WebApplication.Create();
app.UseHttpLogging();
app.MapGet("/", () => "No short-circuiting!");
app.MapGet("/short-circuit", () => "Short circuiting!").ShortCircuit();
app.MapShortCircuit(404, "robots.txt", "favicon.ico");
app.Run();
Ниже приведен пример из журналов консоли, созданных при запуске конечной точки /. Он включает выходные данные из ПО промежуточного слоя ведения журнала:
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[0]
Executing endpoint 'HTTP: GET /'
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[1]
Executed endpoint 'HTTP: GET /'
info: Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware[2]
Response:
StatusCode: 200
Content-Type: text/plain; charset=utf-8
Date: Wed, 03 May 2023 21:05:59 GMT
Server: Kestrel
Alt-Svc: h3=":5182"; ma=86400
Transfer-Encoding: chunked
Следующий пример представляет собой результат выполнения конечной точки /short-circuit. В нём отсутствуют данные от промежуточного слоя ведения журнала, потому что промежуточный слой был обходится.
info: Microsoft.AspNetCore.Routing.EndpointRoutingMiddleware[4]
The endpoint 'HTTP: GET /short-circuit' is being executed without running additional middleware.
info: Microsoft.AspNetCore.Routing.EndpointRoutingMiddleware[5]
The endpoint 'HTTP: GET /short-circuit' has been executed without running additional middleware.
Руководство для авторов библиотек
В этом разделе содержатся рекомендации для авторов библиотек, создающих библиотеки на основе маршрутизации. Эти сведения предназначены для предоставления разработчикам приложений сведений об эффективном использовании библиотек и платформ, расширяющих маршрутизацию.
Определение конечных точек
Чтобы создать платформу, использующую маршрутизацию для сопоставления URL-адресов, начните с определения пользовательского интерфейса, который строится поверх UseEndpoints.
ВЫПОЛНИТЕ сборку поверх IEndpointRouteBuilder. Это позволит пользователям создать инфраструктуру с другими функциями ASP.NET Core без путаницы. Каждый шаблон ASP.NET Core включает в себя маршрутизацию. Предположим, что маршрутизация имеется и пользователи знакомы с ней.
// Your framework
app.MapMyFramework(...);
app.MapHealthChecks("/healthz");
ВЕРНИТЕ конкретный запечатанный тип из вызова объекта MapMyFramework(...), который реализует IEndpointConventionBuilder. Большинство методов Map... платформы соответствует этому шаблону. Интерфейс IEndpointConventionBuilder:
- Позволяет составить метаданные.
- Предназначен для различных методов расширения.
Объявление собственного типа позволяет добавлять в построитель собственные функции для конкретной платформы. Можно обернуть построитель, объявленный во фреймворке, и перенаправлять вызовы в него.
// Your framework
app.MapMyFramework(...)
.RequireAuthorization()
.WithMyFrameworkFeature(awesome: true);
app.MapHealthChecks("/healthz");
Рассмотрите возможность создания собственного EndpointDataSource.
EndpointDataSource — это низкоуровневый примитив для объявления и обновления коллекции конечных точек.
EndpointDataSource — это эффективный API, используемый контроллерами и Razor Pages. Дополнительные сведения см. в разделе "Динамическая маршрутизация конечных точек".
В тестах маршрутизации имеется простой пример источника данных, который не обновляется.
РАССМОТРИТЕ реализацию GetGroupedEndpoints. Это обеспечивает полный контроль над соблюдением соглашений о группах и конечных метаданных, относящихся к группированным конечным точкам. Например, это позволяет пользовательским EndpointDataSource реализациям запускать фильтры конечных точек, добавляемые в группы.
НЕ пытайтесь зарегистрировать EndpointDataSource по умолчанию. Требуйте от пользователей, чтобы они регистрировали вашу платформу в UseEndpoints. Философия маршрутизации заключается в том, что по умолчанию ничего не включено и UseEndpoints представляет собой место для регистрации конечных точек.
Создание ПО промежуточного слоя со встроенной маршрутизацией
РАССМОТРИТЕ ВОЗМОЖНОСТЬ определения типов метаданных в качестве интерфейса.
СДЕЛАЙТЕ возможным использование типов метаданных в качестве атрибута в классах и методах.
public interface ICoolMetadata
{
bool IsCool { get; }
}
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class CoolMetadataAttribute : Attribute, ICoolMetadata
{
public bool IsCool => true;
}
Платформы, такие как контроллеры и Razor Pages, поддерживают применение атрибутов метаданных к типам и методам. При объявлении типов метаданных:
- Сделайте их доступными в качестве атрибутов.
- Большинство пользователей знакомы с применением атрибутов.
Объявление типа метаданных в качестве интерфейса добавляет еще один уровень гибкости.
- Интерфейсы можно комбинировать.
- Разработчики могут объявлять собственные типы, объединяющие несколько политик.
СДЕЛАЙТЕ возможным переопределение метаданных, как показано в следующем примере.
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class SuppressCoolMetadataAttribute : Attribute, ICoolMetadata
{
public bool IsCool => false;
}
[CoolMetadata]
public class MyController : Controller
{
public void MyCool() { }
[SuppressCoolMetadata]
public void Uncool() { }
}
Следуйте этим рекомендациям, чтобы избежать определения метаданных маркера.
- Не ограничивайтесь поиском типа метаданных.
- Определите свойство метаданных и проверьте его.
Коллекция метаданных является упорядоченной и поддерживает переопределение приоритета. В случае с контроллерами метаданные в методе действия являются наиболее специфичными.
СДЕЛАЙТЕ промежуточное ПО полезным как с маршрутизацией, так и без нее:
app.UseAuthorization(new AuthorizationPolicy() { ... });
// Your framework
app.MapMyFramework(...).RequireAuthorization();
В качестве примера этой рекомендации рассмотрим ПО промежуточного слоя UseAuthorization. Промежуточное ПО авторизации позволяет задать резервную политику.
Политика отката, если она указана, применяется к обоим элементам:
- конечные точки без указанной политики;
- запросы, которые не соответствуют конечной точке.
Это сделает ПО промежуточного слоя авторизации полезным вне контекста маршрутизации. Промежуточное ПО авторизации можно использовать для традиционного программирования middleware.
Диагностика отладки
Для подробного вывода диагностики построения маршрутов задайте для Logging:LogLevel:Microsoft значение Debug. В среде Development установите уровень журнала в appsettings.Development.json:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Debug",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}
Дополнительные ресурсы
Маршрутизация обеспечивает сопоставление входящих HTTP-запросов и их распределение по исполняемым конечным точкам приложения. Конечные точки — это блоки исполняемого кода обработки запросов приложения. Конечные точки определяются в приложении и настраиваются при его запуске. Процесс сопоставления конечных точек может извлекать значения из URL-адреса запроса и предоставлять эти значения для обработки запроса. С помощью сведений о конечных точках из приложения маршрутизация также может формировать URL-адреса, которые сопоставляются с конечными точками.
Приложения могут настраивать маршрутизацию с помощью следующих методов:.
- Controllers
- Razor Страницы
- SignalR
- Службы gRPC
- Промежуточное ПО с поддержкой конечных точек, например проверки работоспособности.
- Делегаты и лямбда-выражения, зарегистрированные для маршрутизации
В этой статье представлены сведения о низкоуровневой маршрутизации ASP.NET Core. Дополнительные сведения о настройке маршрутизации
- Сведения о контроллерах см. в статье Маршрутизация к действиям контроллера в ASP.NET Core.
- Соглашения для Razor Pages см. в статье Соглашения для маршрутов и приложений Razor Pages в ASP.NET Core.
Основы маршрутизации
В следующем коде приведен базовый пример маршрутизации.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
В предыдущем примере используется одна конечная точка с помощью MapGet метода:
- При отправке HTTP-запроса
GETв корневой URL-адрес/:- Выполняется делегат запроса.
- В ответ HTTP записывается
Hello World!.
- Если метод запроса не является
GETили если корневой URL-адрес не/, сопоставление маршрута не выполняется и возвращается сообщение об ошибке HTTP 404.
Маршрутизация использует пару промежуточного ПО, зарегистрированную с помощью UseRouting и UseEndpoints.
-
UseRoutingдобавляет соответствие маршрута в конвейер ПО промежуточного слоя. Это ПО промежуточного слоя обращается к набору конечных точек, определенных в приложении, и выбирает наиболее подходящее на основе запроса. -
UseEndpointsдобавляет выполнение конечной точки в конвейер обработки промежуточного ПО. Он запускает делегат, связанный с выбранной конечной точкой.
Приложениям обычно не требуется вызывать UseRouting или UseEndpoints.
WebApplicationBuilder настраивает конвейер промежуточного программного обеспечения, который оборачивает промежуточное ПО, добавленное в Program.cs, с помощью UseRouting и UseEndpoints. Но приложения могут изменять порядок, в котором выполняются UseRouting и UseEndpoints, вызывая эти методы явным образом. Например, следующий код явным образом вызывает UseRouting:
app.Use(async (context, next) =>
{
// ...
await next(context);
});
app.UseRouting();
app.MapGet("/", () => "Hello World!");
В предыдущем коде:
- Вызов к
app.Useрегистрирует пользовательское ПО промежуточного слоя, которое выполняется в начале конвейера. - При вызове метода
UseRoutingПО промежуточного слоя сопоставления маршрутов настраивается для запуска после пользовательского ПО промежуточного слоя. - Конечная точка, зарегистрированная с использованием
MapGet, выполняется в конце конвейера.
Если предыдущий пример не включал вызов к UseRouting, пользовательское ПО промежуточного слоя будет выполняться после ПО промежуточного слоя сопоставления маршрутов.
Endpoints
Для определения конечной точки используется метод MapGet. Конечная точка — это объект, который может быть:
- Выбирается путем сопоставления URL-адреса и метода HTTP.
- Выполняется путем использования запуска делегата.
Конечные точки, которые могут быть сопоставлены и выполнены приложением, настраиваются в UseEndpoints. Например, MapGet, MapPost и аналогичные методы подключают делегаты запросов к системе маршрутизации. Для подключения функций платформы ASP.NET Core к системе маршрутизации можно использовать дополнительные методы.
- MapRazorPages для страниц Razor
- MapControllers для контроллеров
- MapHub<THub> для SignalR
- MapGrpcService<TService> для gRPC
Ниже представлен пример маршрутизации с более сложным шаблоном маршрута.
app.MapGet("/hello/{name:alpha}", (string name) => $"Hello {name}!");
Строка /hello/{name:alpha} является шаблоном маршрута. Шаблон маршрута используется для настройки способа сопоставления конечной точки. В этом случае шаблон соответствует следующим условиям.
- URL-адрес, подобный
/hello/Docs - Любой URL-путь, начинающийся с
/hello/,после которого следует набор буквенных символов.:alphaприменяет ограничение маршрута, которое соответствует только буквенным символам. Ограничения маршрута описаны далее в этой статье.
Второй сегмент URL-пути, {name:alpha}:
- привязан к параметру
name; - Записывается и хранится в HttpRequest.RouteValues.
В следующем примере показана маршрутизация с проверками работоспособности и авторизацией.
app.UseAuthentication();
app.UseAuthorization();
app.MapHealthChecks("/healthz").RequireAuthorization();
app.MapGet("/", () => "Hello World!");
В предыдущем примере, показано то, как:
- Промежуточное программное обеспечение для авторизации можно использовать вместе с маршрутизацией.
- можно использовать конечные точки для настройки режима авторизации.
Вызов MapHealthChecks добавляет конечную точку проверки состояния. Связывание RequireAuthorization с этим вызовом прикрепляет политику авторизации к конечной точке.
При вызове UseAuthentication и UseAuthorization добавляется ПО промежуточного слоя для проверки подлинности и авторизации. Эти middleware размещаются между UseRouting и UseEndpoints, чтобы они могли:
- просматривать, какая конечная точка выбрана методом
UseRouting; - применять политику авторизации до отправки UseEndpoints на конечную точку.
Метаданные конечной точки
В предыдущем примере существуют две конечные точки, но политика авторизации прикреплена только к конечной точке проверки состояния. Если запрос соответствует конечной точке проверки работоспособности, /healthz, выполняется проверка авторизации. Это служит демонстрацией того, что к конечным точкам можно прикреплять дополнительные данные. Эти дополнительные данные называются метаданными конечных точек.
- Метаданные могут обрабатываться ПО промежуточного слоя с поддержкой маршрутизации.
- Метаданные могут быть любого типа .NET.
Основные понятия маршрутизации
Система маршрутизации формируется поверх конвейера промежуточного программного обеспечения путем добавления мощного понятия конечных точек. Конечные точки представляют собой единицы функциональности приложения, отличающиеся друг от друга в плане маршрутизации, авторизации и количества систем ASP.NET Core.
Определение конечной точки ASP.NET Core
Конечная точка ASP.NET Core:
- Исполняемый: имеет RequestDelegate.
- Расширяемо: имеет коллекцию метаданных .
- Доступный вариант: при необходимости содержит сведения о маршрутизации.
- Перечисляемые: коллекцию конечных точек можно перечислить, извлекая EndpointDataSource из DI.
В следующем примере кода показано, как получить и проверить конечную точку, соответствующую текущему запросу.
app.Use(async (context, next) =>
{
var currentEndpoint = context.GetEndpoint();
if (currentEndpoint is null)
{
await next(context);
return;
}
Console.WriteLine($"Endpoint: {currentEndpoint.DisplayName}");
if (currentEndpoint is RouteEndpoint routeEndpoint)
{
Console.WriteLine($" - Route Pattern: {routeEndpoint.RoutePattern}");
}
foreach (var endpointMetadata in currentEndpoint.Metadata)
{
Console.WriteLine($" - Metadata: {endpointMetadata}");
}
await next(context);
});
app.MapGet("/", () => "Inspect Endpoint.");
Конечную точку, если она выбрана, можно получить из HttpContext. Ее свойства можно проверить. Объекты конечных точек являются неизменяемыми, и их невозможно изменить после создания. Наиболее распространенным типом конечной точки является RouteEndpoint.
RouteEndpoint содержит сведения, позволяющие системе маршрутизации выбрать эту конечную точку.
В приведенном выше коде app.Use настраивает встроенное ПО промежуточного слоя.
В следующем коде показано, что в зависимости от того, где в конвейере вызывается app.Use, может не быть конечной точки.
// Location 1: before routing runs, endpoint is always null here.
app.Use(async (context, next) =>
{
Console.WriteLine($"1. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
await next(context);
});
app.UseRouting();
// Location 2: after routing runs, endpoint will be non-null if routing found a match.
app.Use(async (context, next) =>
{
Console.WriteLine($"2. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
await next(context);
});
// Location 3: runs when this endpoint matches
app.MapGet("/", (HttpContext context) =>
{
Console.WriteLine($"3. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
return "Hello World!";
}).WithDisplayName("Hello");
app.UseEndpoints(_ => { });
// Location 4: runs after UseEndpoints - will only run if there was no match.
app.Use(async (context, next) =>
{
Console.WriteLine($"4. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
await next(context);
});
В предыдущем примере добавляются инструкции Console.WriteLine, которые показывают, выбрана ли конечная точка. Для ясности в примере указанной конечной точке / назначается отображаемое имя.
Кроме того, предыдущий пример включает вызовы к UseRouting и UseEndpoints для точного контроля того, когда именно эти ПО промежуточного слоя выполняются в конвейере.
При выполнении этого кода с URL-адресом / отображается следующее.
1. Endpoint: (null)
2. Endpoint: Hello
3. Endpoint: Hello
При выполнении этого кода с любым другим URL-адресом отображается следующее.
1. Endpoint: (null)
2. Endpoint: (null)
4. Endpoint: (null)
В этом выводе показано следующее.
- Перед вызовом
UseRoutingконечная точка всегда имеет значение NULL. - Если найдено совпадение, конечная точка отличается от NULL между
UseRoutingи UseEndpoints. - ПО промежуточного слоя
UseEndpointsявляется терминальным при обнаружении соответствия. Определение терминального ПО промежуточного слоя приведено далее в этой статье. - Промежуточное ПО после
UseEndpointsвыполняется только в случае, если совпадения не найдены.
Промежуточный слой UseRouting использует метод SetEndpoint для подключения конечной точки к текущему контексту. ПО промежуточного слоя UseRouting можно заменить на настраиваемую логику и по-прежнему использовать конечные точки. Конечные точки — это низкоуровневые примитивы, такие как ПО промежуточного слоя, которые не связаны с реализацией маршрутизации. В большинстве приложений метод UseRouting не требуется заменять настраиваемой логикой.
ПО промежуточного слоя UseEndpoints предназначено для использования совместно с ПО промежуточного слоя UseRouting. Основная логика для выполнения конечной точки достаточно проста. Чтобы получить конечную точку, используйте GetEndpoint, а затем вызовите ее свойство RequestDelegate.
В следующем примере кода показано, как ПО промежуточного слоя может влиять на маршрутизацию или реагировать на нее.
app.UseHttpMethodOverride();
app.UseRouting();
app.Use(async (context, next) =>
{
if (context.GetEndpoint()?.Metadata.GetMetadata<RequiresAuditAttribute>() is not null)
{
Console.WriteLine($"ACCESS TO SENSITIVE DATA AT: {DateTime.UtcNow}");
}
await next(context);
});
app.MapGet("/", () => "Audit isn't required.");
app.MapGet("/sensitive", () => "Audit required for sensitive data.")
.WithMetadata(new RequiresAuditAttribute());
public class RequiresAuditAttribute : Attribute { }
В предыдущем примере показаны два важных основных понятия.
- Промежуточное ПО может работать до
UseRoutingчтобы изменить данные, обрабатываемые маршрутизацией.- Обычно ПО промежуточного слоя, отображаемое перед маршрутизацией, изменяет некоторое свойство запроса, например UseRewriter, UseHttpMethodOverride или UsePathBase.
- ПО промежуточного слоя может выполняться между
UseRoutingи UseEndpoints для обработки результатов маршрутизации до выполнения конечной точки.- ПО промежуточного слоя, которое выполняется между
UseRoutingиUseEndpoints:- Обычно проверяет метаданные для получения представления о конечных точках.
- Зачастую принимает решения по обеспечению безопасности, как это делают
UseAuthorizationиUseCors.
- Сочетание ПО промежуточного слоя и метаданных позволяет настраивать политики для каждой конечной точки.
- ПО промежуточного слоя, которое выполняется между
В приведенном выше коде показан пример настраиваемого ПО промежуточного слоя, поддерживающего политики для каждой конечной точки. ПО промежуточного слоя записывает в консоль журнал аудита доступа к конфиденциальным данным. Промежуточное ПО можно настроить для выполнения аудита конечной точки с помощью метаданных RequiresAuditAttribute. В этом примере демонстрируется паттерн подписки, когда аудит выполняется только для конечных точек, помеченных как чувствительные. Эту логику можно определить в обратном порядке для аудита всего, что не отмечено, например, как надежное. Система метаданных конечной точки обеспечивает гибкость. Эту логику можно настроить под любой требуемый вариант использования.
Предыдущий пример кода предназначен для демонстрации основных понятий конечных точек. Он не предназначен для использования в рабочей среде. Более полная версия промежуточного программного обеспечения для журнала аудита будет:
- Логировать в файл или базу данных.
- Включает такие сведения, как информация о пользователе, IP-адресе, имени конфиденциальной конечной точки и многое другое.
Метаданные политики аудита RequiresAuditAttribute определены как Attribute, чтобы их было проще использовать в платформах на основе классов, таких как контроллеры и SignalR. При использовании маршрута к коду:
- Метаданные присоединяются к API-интерфейсу построителя.
- При создании конечных точек платформы на основе классов включают все атрибуты в соответствующем методе и классе.
Типы метаданных рекомендуется определить либо как интерфейсы, либо как атрибуты. Интерфейсы и атрибуты допускают повторное использование кода. Система метаданных является гибкой и не накладывает никаких ограничений.
Сравнение ПО промежуточного слоя терминала с маршрутизацией
В следующем примере демонстрируется ПО промежуточного слоя терминала и маршрутизация:
// Approach 1: Terminal Middleware.
app.Use(async (context, next) =>
{
if (context.Request.Path == "/")
{
await context.Response.WriteAsync("Terminal Middleware.");
return;
}
await next(context);
});
app.UseRouting();
// Approach 2: Routing.
app.MapGet("/Routing", () => "Routing.");
Стиль ПО промежуточного слоя, который показан в разделе Approach 1:, — терминальное ПО промежуточного слоя. ПО промежуточного слоя называется терминальным, поскольку выполняет операцию сопоставления.
- Операция сопоставления в предыдущем примере —
Path == "/"для ПО промежуточного слоя иPath == "/Routing"для маршрутизации. - Если сопоставление выполнено успешно, оно выполняет некоторые функции и возвращает результат, а не вызывает ПО промежуточного слоя
next.
Оно называется терминальным, поскольку завершает поиск, выполняет некоторые функции, а затем возвращает результат.
В следующем списке ПО промежуточного слоя терминала сравнивается с маршрутизацией:
- Оба подхода позволяют завершить конвейер обработки.
- ПО промежуточного слоя завершает конвейер, возвращая вместо вызова
next. - Конечные точки всегда являются терминальными.
- ПО промежуточного слоя завершает конвейер, возвращая вместо вызова
- Промежуточное ПО терминала позволяет разместить его в любом месте конвейера.
- Конечные точки выполняются в позиции UseEndpoints.
- Промежуточное программное обеспечение терминала позволяет произвольному коду определить, когда программное обеспечение соответствует.
- Настраиваемый код сопоставления маршрутов может быть подробным и сложным для корректной записи.
- Маршрутизация обеспечивает простые решения для обычных приложений. Большинству приложений не требуется настраиваемый код сопоставления маршрутов.
- Конечные точки взаимодействуют с ПО промежуточного слоя, таким как
UseAuthorizationиUseCors.- Использование терминального ПО промежуточного слоя с
UseAuthorizationилиUseCorsтребует взаимодействия вручную с системой авторизации.
- Использование терминального ПО промежуточного слоя с
Конечная точка определяет и то, и другое:
- делегат для обработки запросов;
- коллекцию произвольных метаданных. Метаданные используются для реализации аспектов, пронизывающих архитектуру, на основе политик и конфигурации, присоединённой к каждой конечной точке.
Программное обеспечение промежуточного уровня может быть эффективным средством, но может потребовать:
- значительный объем кода и тестирования;
- интеграция вручную с другими системами для достижения желаемого уровня гибкости.
Прежде чем создавать терминальное ПО промежуточного слоя, рассмотрите возможность интеграции с маршрутизацией.
Существующее терминальное ПО промежуточного слоя, которое интегрируется с Map или MapWhen, обычно может быть преобразовано в конечную точку, осведомленную о маршрутизации. MapHealthChecks демонстрирует шаблон для маршрутизирующего программного обеспечения.
- Напишите метод расширения в IEndpointRouteBuilder.
- Создайте вложенный конвейер middleware с помощью CreateApplicationBuilder.
- Присоедините промежуточное программное обеспечение к новому конвейеру. В этом случае — UseHealthChecks.
- Build конвейер промежуточного программного обеспечения в RequestDelegate.
- Вызовите
Mapи укажите новый посреднический программный конвейер. - Верните объект построителя, который предоставлен
Map, из метода расширения.
В следующем коде показано использование MapHealthChecks.
app.UseAuthentication();
app.UseAuthorization();
app.MapHealthChecks("/healthz").RequireAuthorization();
В предыдущем примере показано, почему возврат объекта строителя имеет большое значение. Возврат объекта построителя позволяет разработчику приложения настраивать политики, такие как авторизация для конечной точки. В этом примере промежуточное ПО для проверки состояния не имеет прямой интеграции с системой авторизации.
Для решения проблем, с которыми столкнулись авторы расширяемости при использовании программного обеспечения промежуточного слоя терминалов, была создана система метаданных. Для каждого ПО промежуточного слоя достаточно проблематично реализовать собственную интеграцию с системой авторизации.
Сопоставление URL-адресов
- Это процесс, с помощью которого функция маршрутизации сопоставляет входящий запрос в конечную точку.
- Он основан на данных из пути URL и заголовков.
- Его можно расширить, чтобы учесть любые данные в запросе.
При выполнении ПО промежуточного слоя маршрутизации оно задает конечную точку (Endpoint) и значения маршрута для функции запроса в HttpContext из текущего запроса.
- Вызов HttpContext.GetEndpoint получает конечную точку.
-
HttpRequest.RouteValuesполучает коллекцию значений маршрута.
Middleware выполняется после того, как маршрутизационное ПО может проверить конечную точку и совершить действия. Например, промежуточное ПО авторизации может опрашивать коллекцию метаданных конечной точки для получения политики авторизации. После выполнения всех компонентов промежуточного программного обеспечения в конвейере обработки запросов вызывается делегат выбранной конечной точки.
Система маршрутизации в контексте маршрутизации по конечным точкам отвечает за все решения по управлению маршрутизацией. Поскольку ПО промежуточного слоя применяет политики, основанные на выбранной конечной точке, важно следующее.
- Любые решения, которые могут повлиять на распределение или применение политик безопасности, должны выполняться внутри системы маршрутизации.
Warning
Для обеспечения обратной совместимости при выполнении делегата конечной точки контроллера или Razor страниц, свойства RouteContext.RouteData устанавливаются в соответствующие значения на основе обработки запросов, выполненной к этому моменту.
В следующем выпуске тип RouteContext будет помечен как устаревший.
- Перенесите
RouteData.ValuesвHttpRequest.RouteValues. - Мигрируйте
RouteData.DataTokens, чтобы получить IDataTokensMetadata из метаданных конечной точки.
Работа сопоставления URL-адресов подразделяется на этапы, которые можно настроить. На каждом этапе выходные данные представляют собой набор совпадений. По мере перехода между этапами набор совпадений можно ограничивать. Реализация маршрутизации не гарантирует порядок обработки для соответствующих конечных точек. Все возможные совпадения обрабатываются одновременно. Этапы сопоставления URL-адресов выполняются в следующем порядке. ASP.NET Core:
- URL-путь обрабатывается по набору конечных точек и их шаблонов маршрутов, при этом выполняется сбор всех совпадений.
- Берется предыдущий список и удаляются совпадения, которые не соответствуют примененным ограничениям маршрута.
- Принимает предыдущий список и удаляет совпадения, которые не соответствуют набору экземпляров MatcherPolicy.
- EndpointSelector используется для принятия окончательного решения из предыдущего списка.
Список конечных точек определяется по приоритету в соответствии со следующим.
Все соответствующие конечные точки обрабатываются на каждом этапе до тех пор, пока не будет достигнут EndpointSelector.
EndpointSelector — это заключительный этап. Он выбирает конечную точку с наивысшим приоритетом из совпадений как наилучшее соответствие. При наличии других совпадений с таким же приоритетом, как у наилучшего соответствия, выбрасывается исключение неоднозначного соответствия.
Приоритет маршрута вычисляется на основе более определенного шаблона маршрута, которому назначается более высокий приоритет. Например, рассмотрим шаблоны /hello и /{message}.
- Оба соответствуют URL-пути
/hello. -
/helloявляется более конкретным, и, следовательно, ему назначается более высокий приоритет.
Как правило, приоритет маршрута помогает выбрать наилучшее соответствие для типов схем URL-адресов, используемых на практике. Используйте Order только в случае, когда необходимо избежать неоднозначности.
Ввиду различных типов расширяемости, предоставляемых службой маршрутизации, система маршрутизации не может заранее вычислить неоднозначные маршруты. Рассмотрим в качестве примера шаблоны маршрутов /{message:alpha} и /{message:int}.
- Ограничение
alphaсоответствует только буквенным символам. - Ограничение
intсоответствует только числам. - Эти шаблоны имеют одинаковый приоритет маршрута, однако не существует одного URL-адреса, по которому они совпадают.
- Если система маршрутизации сообщила об ошибке неоднозначности при запуске, это означает, что она заблокировала этот допустимый вариант использования.
Warning
Порядок операций в UseEndpoints не влияет на поведение маршрутизации, за одним исключением. MapControllerRoute и MapAreaRoute автоматически присваивают значение порядка своим конечным точкам в соответствии с порядком их вызова. Это имитирует поведение контроллеров без системы маршрутизации в долгосрочной перспективе, предоставляя те же гарантии, что и в старых реализациях маршрутизации.
Маршрутизация конечных точек в ASP.NET Core:
- Не имеет концепции маршрутов.
- Не гарантирует порядок обработки. Все конечные точки обрабатываются одновременно.
Приоритет шаблонов маршрутов и порядок выбора конечных точек
Приоритет шаблонов маршрутов — это система, которая назначает каждому шаблону маршрута значение в зависимости от того, насколько он является конкретным. Приоритет шаблона маршрута
- Позволяет избежать необходимости настраивать порядок конечных точек в общих случаях.
- Пытается соответствовать общепринятым ожиданиям относительно поведения маршрутизации.
Например, рассмотрим шаблоны /Products/List и /Products/{id}. Разумно предположить, что /Products/List является лучшим соответствием для URL-пути /Products/List, чем /Products/{id}. Литеральный сегмент /List считается более приоритетным, чем сегмент параметров /{id}.
Порядок определения приоритета связан с порядком определения шаблонов маршрутов.
- Шаблоны с большим количеством сегментов считаются более конкретными.
- Сегмент с литеральным текстом считается более конкретным, чем сегмент параметров.
- Сегмент параметров с ограничением считается более конкретным, чем сегмент без него.
- Сложный сегмент рассматривается как такой же специфичный, как сегмент параметра с ограничением.
- Универсальные параметры являются наименее конкретными. См. раздел Шаблоны маршрута для получения важной информации об универсальных маршрутах.
Основные понятия формирования URL-адресов
Создание URL-адресов:
- Это процесс, благодаря которому маршрутизация может формировать путь URL-адреса на основе набора значений маршрута.
- Обеспечивает логическое разделение конечных точек и URL-адресов, по которым к ним осуществляется доступ.
Маршрутизация конечных точек включает API LinkGenerator.
LinkGenerator — это одноэлементная служба, доступная в DI. API LinkGenerator можно использовать вне контекста выполнения запроса.
Mvc.IUrlHelper и сценарии, которые зависят от IUrlHelper, такие как вспомогательные функции тегов, вспомогательные методы HTML и результаты действий, используют API LinkGenerator для предоставления возможностей создания ссылок.
Генератор ссылок использует концепции адреса и схем адресов. Схема адресов — это способ определения конечных точек, которые должны рассматриваться для создания ссылки. Например, сценарии с именем маршрута и значениями маршрута, с которыми многие пользователи знакомы по контроллерам и Razor Pages, реализуются как схема адресов.
Генератор ссылок может установить связь с контроллерами и Razor Pages с помощью следующих методов расширения.
Перегрузки этих методов принимают аргументы, которые включают HttpContext. Эти методы являются функциональными эквивалентами Url.Action и Url.Page, но предлагают дополнительную гибкость и параметры.
Методы GetPath* наиболее схожи с Url.Action и Url.Page в том, что создают URI, содержащий абсолютный путь. Методы GetUri* всегда создают абсолютный URI, содержащий схему и хост. Методы, которые принимают HttpContext, создают URI в контексте выполнения запроса. Используются значения окружения маршрута, базовый URL-адрес, схема и узел из выполняющегося запроса, если не указано иное.
LinkGenerator вызывается с адресом. Создание URI происходит в два этапа:
- Адрес привязан к списку конечных точек, соответствующих адресу.
- RoutePattern конечной точки вычисляется, пока не будет найден шаблон маршрута, который соответствует предоставленным значениям. Полученный результат объединяется с другими частями URI, предоставленными генератору ссылок и возвращенными.
Методы, предоставляемые LinkGenerator, поддерживают стандартные возможности создания ссылки для любого типа адреса. Самый удобный способ использовать генератор ссылки — через методы расширения, которые выполняют операции для определенного типа адреса.
| Метод расширения | Description |
|---|---|
| GetPathByAddress | Создает URI с абсолютным путем на основе предоставленных значений. |
| GetUriByAddress | Создает абсолютный URI на основе предоставленных значений. |
Warning
Обратите внимание на следующие последствия вызова методов LinkGenerator:
Используйте методы расширения
GetUri*с осторожностью в конфигурации приложения, которая не проверяет заголовок входящих запросовHost. Если заголовок входящих запросовHostне проверен, недоверенные входные данные запроса могут быть отправлены обратно клиенту в URI в виде или на странице. Рекомендуется, чтобы все рабочие приложения настраивали свой сервер на проверку заголовкаHostотносительно известных допустимых значений.Используйте LinkGenerator с осторожностью в ПО промежуточного слоя в сочетании с
MapилиMapWhen.Map*изменяет базовый путь выполняющегося запроса, что влияет на выходные данные создания ссылки. Все API LinkGenerator разрешают указание базового пути. Укажите пустой базовый путь для отмены влиянияMap*на создание ссылок.
Пример программного обеспечения промежуточного слоя
В следующем примере ПО промежуточного слоя использует API LinkGenerator, чтобы создать ссылку на метод действия, который перечисляет хранимые продукты. Использование генератора ссылок путем его внедрения в класс и вызова GenerateLink доступно для любого класса в приложении.
public class ProductsMiddleware
{
private readonly LinkGenerator _linkGenerator;
public ProductsMiddleware(RequestDelegate next, LinkGenerator linkGenerator) =>
_linkGenerator = linkGenerator;
public async Task InvokeAsync(HttpContext httpContext)
{
httpContext.Response.ContentType = MediaTypeNames.Text.Plain;
var productsPath = _linkGenerator.GetPathByAction("Products", "Store");
await httpContext.Response.WriteAsync(
$"Go to {productsPath} to see our products.");
}
}
Шаблоны маршрутов
Токены в {} определяют параметры маршрута, которые будут привязаны, если маршрут совпадает. В сегменте маршрута можно определить несколько параметров маршрута, однако они должны разделяться литеральным значением. Рассмотрим пример.
{controller=Home}{action=Index}
не является допустимым маршрутом, потому что между {controller} и {action} нет дословного значения. Параметрам маршрута должны быть присвоены имена, и для них могут быть определены дополнительные атрибуты.
Весь текст, кроме параметров маршрута (например, {id}) и разделителя пути /, должен соответствовать тексту в URL-адресе. Сопоставление текста производится без учета регистра на основе декодированного представления пути URL-адреса. Для сопоставления с литеральным разделителем параметров маршрута ({ или }) нужно экранировать разделитель, повторив символ. Например, {{ или }}.
Звездочка * или двойная звездочка **:
- Можно использовать в качестве префикса к параметру маршрута для привязки к остальной части URI.
- Такие параметры называются универсальными. Например,
blog/{**slug}:- Соответствует любому URI, который начинается с
blog/и имеет любое значение после него. - Значение после
blog/присваивается значению динамического маршрута.
- Соответствует любому URI, который начинается с
Warning
Соответствие параметра catch-all маршрутам может быть неправильным из-за ошибки в маршрутизации. Приложения, на работу которых влияет эта ошибка, обладают следующими характеристиками:
- Маршрут общий, например
{**slug}". - Маршрут catch-all не соответствует необходимым запросам.
- После удаления других маршрутов маршрут catch-all начинает работать.
Ознакомьтесь с примерами 18677 и 16579, в которых встречается эта ошибка, на сайте GitHub.
Исправление для этой ошибки, которое можно включить по желанию, содержится в пакете SDK для .NET Core 3.1.301 или более поздней версии. Следующий код задает внутренний переключатель, исправляющий эту ошибку:
public static void Main(string[] args)
{
AppContext.SetSwitch("Microsoft.AspNetCore.Routing.UseCorrectCatchAllBehavior",
true);
CreateHostBuilder(args).Build().Run();
}
// Remaining code removed for brevity.
Универсальные параметры также могут соответствовать пустой строке.
Универсальный параметр экранирует соответствующие символы, когда маршрут используется для формирования URL-адреса, включая символы разделителей пути (/). Например, маршрут foo/{*path} со значениями маршрутов { path = "my/path" } формирует foo/my%2Fpath. Обратите внимание на экранированную косую черту. В качестве символов разделителя кругового пути используйте префикс параметра маршрута **. Маршрут foo/{**path} с { path = "my/path" } формирует foo/my/path.
Шаблоны URL-адресов, которые пытаются получить имя файла с необязательным расширением, имеют свои особенности. Например, рассмотрим шаблон files/{filename}.{ext?}. Когда значения для filename и ext существуют, заполняются оба значения. Если в URL-адресе есть только значение для filename, маршрут совпадает, так как точка в конце (.) является необязательной. Следующие URL-адреса соответствуют этому маршруту:
/files/myFile.txt/files/myFile
Параметры маршрута могут иметь значения по умолчанию. Они указываются после имени параметра и знака равенства (=). Например, {controller=Home} определяет Home в качестве значения по умолчанию для controller. Значение по умолчанию используется, если для параметра нет значения в URL-адресе. Параметры маршрута могут быть необязательными, для этого необходимо добавить вопросительный знак (?) в конец имени параметра. Например, id?. Разница между необязательными значениями и параметрами маршрута по умолчанию
- Параметр маршрута со значением по умолчанию всегда создает значение.
- Необязательный параметр имеет значение только в том случае, если в URL-адресе запроса указано значение.
Параметры маршрута могут иметь ограничения, которые должны соответствовать значению маршрута из URL-адреса. Добавив двоеточие (:) и имя ограничения после имени параметра маршрута, можно указать встроенные ограничения для параметра маршрута. Если для ограничения требуются аргументы, они указываются в скобках (...) после имени ограничения. Чтобы указать несколько встроенных ограничений, добавьте еще одно двоеточие (:) и имя ограничения.
Имя и аргументы ограничения передаются в службу IInlineConstraintResolver для создания экземпляра IRouteConstraint, который будет использоваться для обработки URL. Например, в шаблоне маршрута blog/{article:minlength(10)} определяется ограничение minlength с аргументом 10. Более подробное описание ограничений маршрутов и список ограничений, предоставляемых платформой, см. в разделе Ограничения маршрутов.
Параметры маршрута также могут иметь преобразователи параметров, которые преобразуют значение параметра при создании ссылок и сопоставлении действий и страниц с URL-адресами. Как и ограничения, преобразователи параметров можно включать в параметр маршрута, добавив двоеточие (:) и имя преобразователя после имени параметра маршрута. Например, шаблон маршрута blog/{article:slugify} задает преобразователь slugify. Дополнительные сведения о преобразователях параметров см. в разделе Преобразователи параметров.
В приведенной ниже таблице показаны некоторые примеры шаблонов маршрутов и их поведение.
| Шаблон маршрута | Пример сопоставления URI | URI запроса... |
|---|---|---|
hello |
/hello |
Соответствует только одному пути /hello. |
{Page=Home} |
/ |
Соответствует и задает для параметра Page значение Home. |
{Page=Home} |
/Contact |
Соответствует и задает для параметра Page значение Contact. |
{controller}/{action}/{id?} |
/Products/List |
Сопоставляется с контроллером Products и действием List. |
{controller}/{action}/{id?} |
/Products/Details/123 |
Соответствует контроллеру Products и действию Details, где id имеет значение 123. |
{controller=Home}/{action=Index}/{id?} |
/ |
Сопоставляется с контроллером Home и методом Index.
id не учитывается. |
{controller=Home}/{action=Index}/{id?} |
/Products |
Сопоставляется с контроллером Products и методом Index.
id не учитывается. |
Использование шаблона — это, как правило, самый простой подход к маршрутизации. Ограничения и значения по умолчанию также могут указываться вне шаблона маршрута.
Сложные сегменты
Сложные сегменты обрабатываются путем сопоставления литеральных разделителей справа налево в нежадной манере. Например, [Route("/a{b}c{d}")] является сложным сегментом.
Сложные сегменты работают определенным способом, который должен быть понятен для их успешного использования. В примере в этом разделе показано, почему сложные сегменты действительно хорошо работают только в том случае, если текст разделителя отсутствует в значениях параметров. Для более сложных случаев необходимо использовать регулярное выражение и затем вручную извлечь значения.
Warning
При использовании System.Text.RegularExpressions для обработки ненадежных входных данных передайте время ожидания. Злонамеренный пользователь может предоставить входные данные для RegularExpressions, что делает возможными атаки типа "отказ в обслуживании". API платформы ASP.NET Core, использующие RegularExpressions, передают время ожидания.
Это сводка действий, выполняемых маршрутизацией с использованием шаблона /a{b}c{d} и URL-пути /abcd.
| используется для визуализации принципа работы алгоритма.
- Первый литерал, справа налево — это
c. Таким образом,/abcdищется справа и находит/ab|c|d. - Все, что находится справа (
d), теперь сопоставляется с параметром маршрута{d}. - Следующий литерал, справа налево —
a. Поэтому поиск/ab|c|dначинается с того места, где мы остановились, после чего находитсяaв/|a|b|c|d. - Значение справа (
b) теперь сопоставляется с параметром маршрута{b}. - Текста и шаблонов маршрута больше не осталось, так что это совпадение.
Ниже приведен пример отрицательного результата с использованием того же шаблона /a{b}c{d} и URL-пути /aabcd.
| используется для визуализации принципа работы алгоритма: Это не совпадение, что объясняется тем же алгоритмом.
- Первый литерал, справа налево — это
c. Таким образом,/aabcdищется справа и находит/aab|c|d. - Все, что находится справа (
d), теперь сопоставляется с параметром маршрута{d}. - Следующий литерал, справа налево —
a. Поэтому поиск/aab|c|dначинается с того места, где мы остановились, после чего находитсяaв/a|a|b|c|d. - Значение справа (
b) теперь сопоставляется с параметром маршрута{b}. - На этом этапе имеется оставшийся текст
a, однако больше нет шаблонов маршрутов для синтаксического анализа, поэтому это не является совпадением.
Поскольку алгоритм сопоставления нежадный:
- Он сопоставляет наименьший объем текста, допустимый для каждого шага.
- Любая ситуация, когда значение разделителя появляется внутри значений параметров, приводит к несовпадению.
Регулярные выражения обеспечивают гораздо больший контроль над их поведением при сопоставлении.
При жадном сопоставлении, также известном как ленивое сопоставление, выполняется поиск соответствия по наибольшей возможной строке. При нежадном сопоставлении выполняется поиск соответствия по наименьшей возможной строке.
Маршрутизация с особыми символами
Маршрутизация со специальными символами может привести к неожиданным результатам. Например, рассмотрим контроллер со следующим методом действия:
[HttpGet("{id?}/name")]
public async Task<ActionResult<string>> GetName(string id)
{
var todoItem = await _context.TodoItems.FindAsync(id);
if (todoItem == null || todoItem.Name == null)
{
return NotFound();
}
return todoItem.Name;
}
Если string id содержит следующие закодированные значения, могут возникнуть непредвиденные результаты:
| ASCII | Encoded |
|---|---|
/ |
%2F |
|
+ |
Параметры маршрута не всегда раскодированы из URL. Эта проблема может быть решена в будущем. См. эту задачу GitHub для получения дополнительной информации.
Ограничения маршрута
Ограничения маршрута применяются, когда найдено соответствие входящему URL-адресу, и путь URL-адреса разобран на элементы маршрута. Как правило, ограничения маршрута служат для проверки значения маршрута, связанного посредством шаблона маршрута, и принятия решения касательно того, является ли значение приемлемым (истина или ложь). Некоторые ограничения маршрута используют данные, не относящиеся к значению маршрута, для определения возможности маршрутизации запроса. Например, HttpMethodRouteConstraint может принимать или отклонять запрос в зависимости от HTTP-команды. Ограничения используются в маршрутизации запросов и создании ссылок.
Warning
Не используйте ограничения для проверки входных данных. Если для проверки входных данных используются ограничения, недопустимые входные данные приводят к ошибке 404 ("Не найдено"). Недопустимые входные данные должны привести к ошибке 400 ("Неверный запрос") с соответствующим сообщением об ошибке. Ограничения маршрутов следует использовать для разрешения неоднозначности похожих маршрутов, а не для проверки входных данных определенного маршрута.
В приведенной ниже таблице показаны примеры ограничения маршрутов и их ожидаемое поведение.
| constraint | Example | Примеры совпадений | Notes |
|---|---|---|---|
int |
{id:int} |
123456789, -123456789 |
Соответствует любому целому числу |
bool |
{active:bool} |
true, FALSE |
Соответствует true или false. Case-insensitive |
datetime |
{dob:datetime} |
2016-12-31, 2016-12-31 7:32pm |
Соответствует допустимому значению DateTime в инвариантной культуре. См. предупреждение выше. |
decimal |
{price:decimal} |
49.99, -1,000.01 |
Соответствует допустимому значению decimal в инвариантной культуре. См. предупреждение выше. |
double |
{weight:double} |
1.234, -1,001.01e8 |
Соответствует допустимому значению double в инвариантной культуре. См. предупреждение выше. |
float |
{weight:float} |
1.234, -1,001.01e8 |
Соответствует допустимому значению float в инвариантной культуре. См. предупреждение выше. |
guid |
{id:guid} |
CD2C1638-1638-72D5-1638-DEADBEEF1638 |
Соответствует допустимому значению Guid |
long |
{ticks:long} |
123456789, -123456789 |
Соответствует допустимому значению long |
minlength(value) |
{username:minlength(4)} |
Rick |
Строка должна содержать не менее 4 символов |
maxlength(value) |
{filename:maxlength(8)} |
MyFile |
Строка должна содержать не более 8 символов |
length(length) |
{filename:length(12)} |
somefile.txt |
Длина строки должна составлять ровно 12 символов |
length(min,max) |
{filename:length(8,16)} |
somefile.txt |
Строка должна быть длиной не менее 8 и не более 16 символов |
min(value) |
{age:min(18)} |
19 |
Целочисленное значение не меньше 18 |
max(value) |
{age:max(120)} |
91 |
Целочисленное значение не больше 120 |
range(min,max) |
{age:range(18,120)} |
91 |
Целочисленное значение должно быть не менее 18 и не более 120. |
alpha |
{name:alpha} |
Rick |
Строка должна состоять из одного или нескольких буквенных символов (a-z) без учета регистра. |
regex(expression) |
{ssn:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)} |
123-45-6789 |
Строка должна соответствовать регулярному выражению. См. советы по определению регулярного выражения. |
required |
{name:required} |
Rick |
Определяет обязательное наличие значения, не относящегося к параметру, во время формирования URL-адреса |
Warning
При использовании System.Text.RegularExpressions для обработки ненадежных входных данных передайте время ожидания. Злонамеренный пользователь может предоставить входные данные для RegularExpressions, что делает возможными атаки типа "отказ в обслуживании". API платформы ASP.NET Core, использующие RegularExpressions, передают время ожидания.
К одному параметру может применяться несколько разделенных запятой ограничений. Например, следующее ограничение ограничивает параметр целочисленным значением 1 или больше:
[Route("users/{id:int:min(1)}")]
public User GetUserById(int id) { }
Warning
Ограничения маршрута, которые проверяют URL-адрес и могут быть преобразованы в тип CLR, всегда используют инвариантные языковые настройки. Например, преобразование в тип CLR int или DateTime. Эти ограничения предполагают, что для URL-адреса не предусмотрена локализация. Предоставляемые платформой ограничения маршрутов не изменяют значения, хранящиеся в значениях маршрута. Все значения маршрута, переданные из URL-адреса, сохраняются как строки. Например, ограничение float пытается преобразовать значение маршрута в число с плавающей запятой, но преобразованное значение используется только для проверки возможности преобразования в число с плавающей запятой.
Регулярные выражения в ограничениях
Warning
При использовании System.Text.RegularExpressions для обработки ненадежных входных данных передайте время ожидания. Злонамеренный пользователь может предоставить входные данные для RegularExpressions, что делает возможными атаки типа "отказ в обслуживании". API платформы ASP.NET Core, использующие RegularExpressions, передают время ожидания.
Регулярные выражения могут быть определены как встроенные ограничения с помощью ограничения маршрута regex(...). Методы в семействе MapControllerRoute также принимают объектный литерал ограничений. При использовании этой формы строковые значения будут интерпретироваться как регулярные выражения.
В следующем коде используется встроенное ограничение регулярного выражения.
app.MapGet("{message:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)}",
() => "Inline Regex Constraint Matched");
В следующем коде для указания ограничения регулярного выражения используется литеральный объект.
app.MapControllerRoute(
name: "people",
pattern: "people/{ssn}",
constraints: new { ssn = "^\\d{3}-\\d{2}-\\d{4}$", },
defaults: new { controller = "People", action = "List" });
В платформе ASP.NET Core в конструктор регулярных выражений добавляются члены RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant. Описание этих членов см. в разделе RegexOptions.
В регулярных выражениях применяются разделители и токены, аналогичные используемым функцией маршрутизации и в языке C#. Токены регулярного выражения должны быть экранированы. Чтобы использовать регулярное выражение ^\d{3}-\d{2}-\d{4}$ во встроенном ограничении, используйте один из следующих способов.
- Замените символы
\, представленные в строке, символами\\в исходном файле C#, чтобы экранировать escape-символ строки\. - Буквальные строковые литералы.
Чтобы экранировать символы разделения параметров маршрутизации {, }, [, ], используйте их дважды в выражении (например, {{, }}, [[, ]]). В следующей таблице показаны регулярные выражения и их экранированные варианты.
| Регулярное выражение | Экранированное регулярное выражение |
|---|---|
^\d{3}-\d{2}-\d{4}$ |
^\\d{{3}}-\\d{{2}}-\\d{{4}}$ |
^[a-z]{2}$ |
^[[a-z]]{{2}}$ |
Регулярные выражения, используемые при маршрутизации, часто начинаются с символа ^ и соответствуют начальной позиции строки. Выражения часто заканчиваются символом $ и соответствуют концу строки. Благодаря символам ^ и $ регулярное выражение сопоставляется со всем значением параметра маршрута. Если символы ^ и $ отсутствуют, регулярное выражение сопоставляется с любой подстрокой внутри строки, что обычно нежелательно. В таблице ниже представлен ряд примеров и объясняются причины соответствия или несоответствия.
| Expression | String | Match | Comment |
|---|---|---|---|
[a-z]{2} |
hello | Yes | Совпадения подстроки |
[a-z]{2} |
123abc456 | Yes | Совпадения подстроки |
[a-z]{2} |
mz | Yes | Совпадение выражения |
[a-z]{2} |
MZ | Yes | Без учета регистра |
^[a-z]{2}$ |
hello | No | См. замечания, касающиеся символов ^ и $, выше |
^[a-z]{2}$ |
123abc456 | No | См. замечания, касающиеся символов ^ и $, выше |
Дополнительные сведения о синтаксисе регулярных выражений см. в статье Регулярные выражения в .NET Framework.
Чтобы ограничить возможные значения параметра набором известных значений, используйте регулярное выражение. Например, при использовании выражения {action:regex(^(list|get|create)$)} значение маршрута action будет соответствовать только list, get или create. При передаче в словарь ограничений строка ^(list|get|create)$ будет эквивалентной. Ограничения, которые передаются в словарь ограничений и не соответствуют одному из известных ограничений, также рассматриваются как регулярные выражения. Ограничения, которые передаются в шаблоне и не соответствуют одному из известных ограничений, не рассматриваются как регулярные выражения.
Пользовательские ограничения маршрутов
Пользовательские ограничения маршрутов можно создать путем внедрения интерфейса IRouteConstraint. Интерфейс IRouteConstraint содержит метод, Match, который возвращает true, если ограничение удовлетворяется, и false — если нет.
Пользовательские ограничения маршрутов редко требуются. Перед реализацией пользовательского ограничения маршрута рассмотрите альтернативные варианты, такие как привязка модели.
В папке ASP.NET Core Constraints находятся хорошие примеры создания ограничений. Например, GuidRouteConstraint.
Чтобы применить пользовательский метод IRouteConstraint, тип ограничения маршрута необходимо зарегистрировать с помощью ConstraintMap приложения в контейнере службы. Объект ConstraintMap — это словарь, который сопоставляет ключи ограничений пути с реализациями IRouteConstraint, которые проверяют эти ограничения.
ConstraintMap приложения можно обновить в Program.cs, либо как часть вызова AddRouting, либо непосредственно настроив RouteOptions с помощью builder.Services.Configure<RouteOptions>. Рассмотрим пример.
builder.Services.AddRouting(options =>
options.ConstraintMap.Add("noZeroes", typeof(NoZeroesRouteConstraint)));
Предыдущее ограничение применяется в следующем коде.
[ApiController]
[Route("api/[controller]")]
public class NoZeroesController : ControllerBase
{
[HttpGet("{id:noZeroes}")]
public IActionResult Get(string id) =>
Content(id);
}
Реализация NoZeroesRouteConstraint препятствует применению 0 к параметру маршрута:
public class NoZeroesRouteConstraint : IRouteConstraint
{
private static readonly Regex _regex = new(
@"^[1-9]*$",
RegexOptions.CultureInvariant | RegexOptions.IgnoreCase,
TimeSpan.FromMilliseconds(100));
public bool Match(
HttpContext? httpContext, IRouter? route, string routeKey,
RouteValueDictionary values, RouteDirection routeDirection)
{
if (!values.TryGetValue(routeKey, out var routeValue))
{
return false;
}
var routeValueString = Convert.ToString(routeValue, CultureInfo.InvariantCulture);
if (routeValueString is null)
{
return false;
}
return _regex.IsMatch(routeValueString);
}
}
Warning
При использовании System.Text.RegularExpressions для обработки ненадежных входных данных передайте время ожидания. Злонамеренный пользователь может предоставить входные данные для RegularExpressions, что делает возможными атаки типа "отказ в обслуживании". API платформы ASP.NET Core, использующие RegularExpressions, передают время ожидания.
Предыдущий код:
- Предотвращает
0в сегменте{id}маршрута. - Отображается для предоставления базового примера реализации настраиваемого ограничения. Не следует использовать в рабочем приложении.
Следующий код является лучшим подходом к предотвращению обработки id, содержащего 0.
[HttpGet("{id}")]
public IActionResult Get(string id)
{
if (id.Contains('0'))
{
return StatusCode(StatusCodes.Status406NotAcceptable);
}
return Content(id);
}
Приведенный выше код имеет следующие преимущества по сравнению с подходом NoZeroesRouteConstraint.
- Пользовательское ограничение не требуется.
- Он возвращает более понятную ошибку, если параметр маршрута включает
0.
Преобразователи параметров
Преобразователи параметров:
- Выполняются при формировании ссылки с помощью LinkGenerator.
- Реализуйте Microsoft.AspNetCore.Routing.IOutboundParameterTransformer.
- Настраиваются с помощью ConstraintMap.
- Принимают значение маршрута параметра и изменяют его на новое строковое значение.
- Результатом является использование преобразованного значения в созданной ссылке.
Например, пользовательский преобразователь параметра slugify в шаблоне маршрута blog\{article:slugify} с Url.Action(new { article = "MyTestArticle" }) формирует значение blog\my-test-article.
Рассмотрим следующую реализацию IOutboundParameterTransformer.
public class SlugifyParameterTransformer : IOutboundParameterTransformer
{
public string? TransformOutbound(object? value)
{
if (value is null)
{
return null;
}
return Regex.Replace(
value.ToString()!,
"([a-z])([A-Z])",
"$1-$2",
RegexOptions.CultureInvariant,
TimeSpan.FromMilliseconds(100))
.ToLowerInvariant();
}
}
Чтобы использовать преобразователь параметров в шаблоне маршрута, настройте его с помощью ConstraintMap в Program.cs.
builder.Services.AddRouting(options =>
options.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer));
В ASP.NET Core используются преобразователи параметров для преобразования URI для определения конечной точки. Например, преобразователи параметров преобразуют значения маршрута, используемые для сопоставления area, controller, action и page:
app.MapControllerRoute(
name: "default",
pattern: "{controller:slugify=Home}/{action:slugify=Index}/{id?}");
С помощью предыдущего шаблона маршрута действие SubscriptionManagementController.GetAll сопоставляется с URI /subscription-management/get-all. Преобразователь параметра не изменяет значения маршрута, используемые для формирования ссылки. Например, Url.Action("GetAll", "SubscriptionManagement") выводит /subscription-management/get-all.
ASP.NET Core предоставляет соглашения об API для использования преобразователей параметров со сформированными маршрутами.
- Соглашение Microsoft.AspNetCore.Mvc.ApplicationModels.RouteTokenTransformerConvention MVC применяет указанный преобразователь параметров ко всем маршрутам атрибутов в приложении. Преобразователь параметров преобразует маркеры маршрутов атрибутов по мере их замены. Дополнительные сведения см. в разделе об использовании преобразователя параметров для настройки замены маркеров.
- Razor Pages использует соглашение об API PageRouteTransformerConvention. Это соглашение применяет указанный трансформатор параметров ко всем автоматически обнаруженным страницам Razor. Преобразователь параметров преобразует сегменты папок и имен файлов маршрутов Razor Pages. Дополнительные сведения см. в разделе об использовании преобразователя параметров для настройки маршрутов страниц.
Справочник по формированию URL-адресов
В этом разделе представлен справочник по алгоритму, реализованному при формировании URL-адреса. На практике в большинстве сложных примеров формирования URL-адресов используются контроллеры или Razor страницы. Дополнительные сведения см. в статье Маршрутизация в контроллерах.
Процесс создания URL-адресов начинается с вызова LinkGenerator.GetPathByAddress или аналогичного метода. Метод предоставляется с адресом, набором значений маршрута и при необходимости со сведениями о текущем запросе из HttpContext.
Первым шагом является использование адреса для разрешения набора конечных точек-кандидатов с помощью IEndpointAddressScheme<TAddress>, соответствующих типу адреса.
После того как набор кандидатов найден в схеме адресов, конечные точки упорядочиваются и обрабатываются последовательно до тех пор, пока операция формирования URL-адреса не завершится. При создании URL-адреса не проверяется наличие неоднозначностей, первый возвращенный результат является окончательным результатом.
Устранение неполадок при формировании URL-адресов с помощью ведения журнала
Первым шагом при устранении неполадок при формировании URL-адресов является установка уровня ведения журнала Microsoft.AspNetCore.Routing на TRACE.
LinkGenerator фиксирует в журнале множество сведений об обработке, которые могут быть полезны при устранении неполадок.
Дополнительные сведения о формировании URL-адресов см. в разделе Справочник по формированию URL-адресов.
Addresses
Адреса являются основным понятием в формировании URL-адресов и используются для привязки вызова генератора ссылок к набору конечных точек-кандидатов.
Адреса — это расширяемое понятие, которое по умолчанию поставляется с двумя реализациями.
- Использование имени конечной точки (
string) в качестве адреса:- Обеспечивает аналогичную функциональность, как и имя маршрута в MVC.
- Использует тип метаданных IEndpointNameMetadata.
- Обрабатывает переданную строку в соответствии с метаданными всех зарегистрированных конечных точек.
- Создает исключение при запуске, если несколько конечных точек использует одно и то же имя.
- Рекомендуется для общего использования вне контроллеров и страниц Razor.
- Использование значений маршрутов (RouteValuesAddress) в качестве адреса:
- Предоставляет аналогичную функциональность и для контроллеров, и для Pages в плане формирования устаревших URL-адресов.
- Очень сложно расширять и отлаживать.
- Предоставляет реализацию, используемую
IUrlHelper, вспомогательными функциями тегов, вспомогательными методами HTML, результатами действий и т. д.
Роль схемы адресов заключается в том, чтобы создать связь между адресом и соответствующими конечными точками по произвольным критериям.
- При использовании схемы имен конечных точек выполняется простой поиск по словарю.
- Схема значений маршрута включает сложный алгоритм для нахождения лучшего подмножества.
Значения окружения и явные значения
Из текущего запроса маршрутизация обращается к значениям маршрута текущего запроса HttpContext.Request.RouteValues. Значения, связанные с текущим запросом, называются значениями окружения. В целях ясности в документации подразумеваются значения маршрута, передаваемые в методы как явные значения.
В следующем примере показаны значения окружения и явные значения. Он предоставляет значения окружения из текущего запроса и явные значения:
public class WidgetController : ControllerBase
{
private readonly LinkGenerator _linkGenerator;
public WidgetController(LinkGenerator linkGenerator) =>
_linkGenerator = linkGenerator;
public IActionResult Index()
{
var indexPath = _linkGenerator.GetPathByAction(
HttpContext, values: new { id = 17 })!;
return Content(indexPath);
}
// ...
Предыдущий код:
- Возвращает
/Widget/Index/17. - Получает LinkGenerator через DI.
Следующий код не предоставляет значения окружения, а только явные значения:
var subscribePath = _linkGenerator.GetPathByAction(
"Subscribe", "Home", new { id = 17 })!;
Предыдущий метод возвращает /Home/Subscribe/17
Следующий код в WidgetController возвращает /Widget/Subscribe/17:
var subscribePath = _linkGenerator.GetPathByAction(
HttpContext, "Subscribe", null, new { id = 17 });
Следующий код настраивает контроллер, используя текущие значения окружения в текущем запросе и явные значения.
public class GadgetController : ControllerBase
{
public IActionResult Index() =>
Content(Url.Action("Edit", new { id = 17 })!);
}
В предыдущем коде:
- Возвращается
/Gadget/Edit/17. - Url получает IUrlHelper.
-
Action создает URL-адрес с абсолютным путем для метода действия. URL-адрес содержит указанное имя
actionи значенияroute.
Следующий код предоставляет значения окружения из текущего запроса и явные значения:
public class IndexModel : PageModel
{
public void OnGet()
{
var editUrl = Url.Page("./Edit", new { id = 17 });
// ...
}
}
Приведенный выше код задает для url значение /Edit/17, когда страница Edit Razor содержит следующую директиву:
@page "{id:int}"
Если страница Edit не содержит шаблон маршрута "{id:int}", то url будет /Edit?id=17.
Поведение IUrlHelper MVC добавляет уровень сложности, помимо правил, описанных здесь.
-
IUrlHelperвсегда предоставляет значения маршрута из текущего запроса как значения окружения. -
IUrlHelper.Action всегда копирует текущие значения маршрута
actionиcontrollerкак явные значения, если они не переопределены разработчиком. -
IUrlHelper.Page всегда копирует текущее значение маршрута
pageкак явное значение, если оно не переопределено. -
IUrlHelper.Pageвсегда переопределяет текущее значение маршрутаhandlerнаnullкак явное значение, если его не переопределили.
Пользователи часто удивляются сведениям о поведении значений окружения, поскольку MVC не следует собственным правилам. По историческим причинам и для обеспечения совместимости для некоторых значений маршрута, таких как action, controller, page и handler, предусмотрено собственное поведение в особых случаях.
Аналогичные функции, предоставляемые LinkGenerator.GetPathByAction и LinkGenerator.GetPathByPage, дублируют эти аномалии IUrlHelper для обеспечения совместимости.
Процесс формирования URL-адреса
После обнаружения набора конечных точек-кандидатов алгоритм формирования URL-адресов:
- последовательно обрабатывает конечные точки;
- Возвращает первый успешный результат.
Первый шаг этого процесса называется аннулированием значения маршрута. Аннулирование значения маршрута — это процесс, с помощью которого маршрутизация решает, какие значения маршрута должны использоваться из значений окружения, а какие следует игнорировать. Каждое значение окружения учитывается и либо объединяется с явными значениями, либо игнорируется.
Роль значений окружения заключается в том, что в некоторых распространенных случаях они позволяют сократить для разработчиков объем вводимой информации. Как правило, сценарии, в которых полезно использовать значения окружения, связаны с MVC.
- При связывании с другим действием в том же контроллере не требуется указывать имя контроллера.
- При связывании с другим контроллером в той же области не требуется указывать имя области.
- При связывании с тем же методом действия не требуется указывать значения маршрута.
- При связывании с другой частью приложения следует избегать передачи значений маршрута, не имеющих смысла в этой части приложения.
Вызовы LinkGenerator или IUrlHelper, которые возвращают null, обычно происходят из-за неправильного понимания невалидности значения маршрута. Для устранения неполадок аннулирования значения маршрута явно укажите дополнительные значения маршрута, чтобы определить, устранена ли проблема.
Аннулирование значения маршрута предполагает, что схема URL-адреса приложения является иерархической, в которой иерархия сформирована слева направо. Рассмотрим шаблон маршрута базового контроллера {controller}/{action}/{id?}, чтобы понять, как это работает на практике.
Изменение значения делает недействительными все значения маршрута, которые отображаются справа. Это отражает предположение об иерархии. Если приложение имеет значение окружения для id, а операция указывает другое значение для controller:
-
idне будет использоваться повторно, поскольку{controller}находится слева от{id?}.
Некоторые примеры, демонстрирующие этот принцип
- Если явные значения содержат значение для
id, значение окружения дляidигнорируется. Можно использовать значения окружения дляcontrollerиaction. - Если явные значения содержат значение для
action, любое значение окружения дляactionигнорируется. Можно использовать фоновые значения дляcontroller. Если явное значение дляactionотличается от значения окружения дляaction, значениеidне будет использоваться. Если явное значение дляactionсовпадает со значением окружения дляaction, можно использовать значениеid. - Если явные значения содержат значение для
controller, любое значение окружения дляcontrollerигнорируется. Если явное значение дляcontrollerотличается от значения окружения дляcontroller, значенияactionиidне будут использоваться. Если явное значение дляcontrollerсовпадает со значением окружения дляcontroller, можно использовать значенияactionиid.
Этот процесс усложняется за счет наличия атрибутных маршрутов и традиционных маршрутов. Стандартные маршруты контроллера, такие как {controller}/{action}/{id?}, указывают иерархию с помощью параметров маршрута. Для выделенных стандартных маршрутов и маршрутов атрибутов для контроллеров и Razor Pages:
- Существует иерархия значений маршрута.
- Они не отображаются в шаблоне.
В таких случаях формирование URL-адресов определяет концепцию необходимых значений. Для конечных точек, созданных контроллерами и Razor Pages, указаны обязательные значения, позволяющие использовать аннулирование значений маршрута.
Подробный алгоритм аннулирования значения маршрута
- Имена обязательных значений объединяются с параметрами маршрута, а затем обрабатываются слева направо.
- Для каждого параметра сравниваются внешние значения и явное значение:
- Если значение окружения и явное значение совпадают, процесс продолжается.
- Если значение окружения задано, а явное значение не задано, то при формировании URL-адреса используется значение окружения.
- Если значение окружения отсутствует, а явное значение задано, следует отклонить значение окружения и все последующие значения окружения.
- Если заданы и значение окружения, и явное значение, однако они отличаются, следует отклонить значение окружения и все последующие значения окружения.
На этом этапе операция формирования URL-адреса готова к оценке ограничений маршрута. Набор допустимых значений объединяется со значениями по умолчанию для параметра, предоставляемыми ограничениям. Если все ограничения пройдены, операция продолжается.
Затем допустимые значения можно использовать для расширения шаблона маршрута. Шаблон маршрута обрабатывается:
- Слева направо.
- Для каждого параметра заменяется допустимое значение.
- В следующих особых случаях:
- Если в допустимых значениях отсутствует значение и параметр имеет значение по умолчанию, используется значение по умолчанию.
- Если в допустимых значениях отсутствует значение, а параметр является необязательным, обработка продолжается.
- Если любой параметр маршрута справа от отсутствующего необязательного параметра имеет значение, операция завершается ошибкой.
- Смежные параметры со значениями по умолчанию и необязательные параметры по возможности сворачиваются.
Явно предоставленные значения, которые не соответствуют сегменту маршрута, добавляются в строку запроса. В приведенной ниже таблице показан результат использования шаблона маршрута {controller}/{action}/{id?}.
| Значения окружения | Явные значения | Result |
|---|---|---|
| контроллер = "Home" | action = "О программе" | /Home/About |
| контроллер = "Home" | controller = "Заказ", action = "О" | /Order/About |
| controller = "Home", color = "Красный" | action = "О программе" | /Home/About |
| контроллер = "Home" | action = "О программе", color = "Красный" | /Home/About?color=Red |
Необязательный порядок параметров маршрута
Необязательные параметры маршрута должны идти после всех обязательных параметров. В следующем коде параметры id и name должны идти после параметра color.
using Microsoft.AspNetCore.Mvc;
namespace WebApplication1.Controllers;
[Route("api/[controller]")]
public class MyController : ControllerBase
{
// GET /api/my/red/2/joe
// GET /api/my/red/2
// GET /api/my
[HttpGet("{color}/{id:int?}/{name?}")]
public IActionResult GetByIdAndOptionalName(string color, int id = 1, string? name = null)
{
return Ok($"{color} {id} {name ?? ""}");
}
}
Проблемы с недействительностью значений маршрута
В следующем коде показан пример схемы формирования URL-адреса, которая не поддерживается маршрутизацией:
app.MapControllerRoute(
"default",
"{culture}/{controller=Home}/{action=Index}/{id?}");
app.MapControllerRoute(
"blog",
"{culture}/{**slug}",
new { controller = "Blog", action = "ReadPost" });
В приведенном выше коде параметр маршрута culture используется для локализации. Необходимо, чтобы параметр culture всегда принимался как значение окружения. Однако параметр culture не принимается как значение окружения ввиду способа работы требуемых значений.
- В шаблоне маршрута
"default"параметр маршрутаcultureнаходится слева отcontroller, поэтому измененияcontrollerне приведут к аннулированиюculture. - В шаблоне маршрута
"blog"параметр маршрутаcultureрассматривается как находящийся справа отcontroller, который имеется в требуемых значениях.
Анализ пути URL-адреса с помощью LinkParser
Класс LinkParser добавляет поддержку анализа пути URL-адреса в набор значений маршрута. Метод ParsePathByEndpointName принимает имя конечной точки и путь URL-адреса, а также возвращает набор значений маршрута, извлеченных из пути URL-адреса.
В следующем примере контроллера действие GetProduct использует шаблон маршрута api/Products/{id} и содержит параметр Name со значением GetProduct:
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
[HttpGet("{id}", Name = nameof(GetProduct))]
public IActionResult GetProduct(string id)
{
// ...
В том же классе контроллера действие AddRelatedProduct ожидает путь URL-адреса (pathToRelatedProduct), который можно предоставить в качестве параметра строки запроса:
[HttpPost("{id}/Related")]
public IActionResult AddRelatedProduct(
string id, string pathToRelatedProduct, [FromServices] LinkParser linkParser)
{
var routeValues = linkParser.ParsePathByEndpointName(
nameof(GetProduct), pathToRelatedProduct);
var relatedProductId = routeValues?["id"];
// ...
В предыдущем примере действие AddRelatedProduct извлекает значение id маршрута из пути URL-адреса. Например, если указан путь URL-адреса /api/Products/1, для relatedProductId будет задано значение 1. Такой подход позволяет клиентам API использовать пути URL-адресов при обращении к ресурсам, не обладая знаниями в структуре такого URL-адреса.
Настройка метаданных конечной точки
Сведения о настройке метаданных конечной точки см. по следующим ссылкам:
- Включение CORS с маршрутизацией конечных точек
-
Пример IAuthorizationPolicyProvider с использованием настраиваемого атрибута
[MinimumAgeAuthorize] - Тестирование проверки подлинности с использованием атрибута [Authorize]
- RequireAuthorization
- Выбор схемы с использованием атрибута [Authorize]
- Применение политик с использованием атрибута [Authorize]
- Авторизация на основе ролей в ASP.NET Core
Сопоставление хостов при маршрутизации, используя RequireHost
RequireHost применяет ограничение к маршруту, требующее указанный узел. Параметр RequireHost или [Host] может иметь следующее значение:
- Хост:
www.domain.com, соответствуетwww.domain.comс любым портом. - Хост с подстановочным знаком:
*.domain.com, соответствуетwww.domain.com,subdomain.domain.comилиwww.subdomain.domain.comдля любого порта. - Порт:
*:5000, соответствует порту 5000 с любым хостом. - Узел и порт:
www.domain.com:5000или*.domain.com:5000, соответствует узлу и порту.
С помощью RequireHost или [Host] можно указать несколько параметров. Ограничение соответствует хостам, допустимым для любого из параметров. Например, [Host("domain.com", "*.domain.com")] соответствует domain.com, www.domain.com и subdomain.domain.com.
Следующий код использует RequireHost, чтобы требовать указанный хост в маршруте:
app.MapGet("/", () => "Contoso").RequireHost("contoso.com");
app.MapGet("/", () => "AdventureWorks").RequireHost("adventure-works.com");
app.MapHealthChecks("/healthz").RequireHost("*:8080");
Следующий код использует атрибут [Host] в контроллере, чтобы требовать один из указанных хостов.
[Host("contoso.com", "adventure-works.com")]
public class HostsController : Controller
{
public IActionResult Index() =>
View();
[Host("example.com")]
public IActionResult Example() =>
View();
}
Если атрибут [Host] применяется как к контроллеру, так и к методу действия, выполняется следующее.
- Используется атрибут действия.
- Атрибут контроллера не учитывается.
Группы маршрутов
Метод MapGroup расширения помогает упорядочивать группы конечных точек с общим префиксом. Это уменьшает повторяющийся код и позволяет настраивать целые группы конечных точек с одним вызовом методов, таких как RequireAuthorization и WithMetadata которые добавляют метаданные конечной точки.
Например, следующий код создает две аналогичные группы конечных точек:
app.MapGroup("/public/todos")
.MapTodosApi()
.WithTags("Public");
app.MapGroup("/private/todos")
.MapTodosApi()
.WithTags("Private")
.AddEndpointFilterFactory(QueryPrivateTodos)
.RequireAuthorization();
EndpointFilterDelegate QueryPrivateTodos(EndpointFilterFactoryContext factoryContext, EndpointFilterDelegate next)
{
var dbContextIndex = -1;
foreach (var argument in factoryContext.MethodInfo.GetParameters())
{
if (argument.ParameterType == typeof(TodoDb))
{
dbContextIndex = argument.Position;
break;
}
}
// Skip filter if the method doesn't have a TodoDb parameter.
if (dbContextIndex < 0)
{
return next;
}
return async invocationContext =>
{
var dbContext = invocationContext.GetArgument<TodoDb>(dbContextIndex);
dbContext.IsPrivate = true;
try
{
return await next(invocationContext);
}
finally
{
// This should only be relevant if you're pooling or otherwise reusing the DbContext instance.
dbContext.IsPrivate = false;
}
};
}
public static RouteGroupBuilder MapTodosApi(this RouteGroupBuilder group)
{
group.MapGet("/", GetAllTodos);
group.MapGet("/{id}", GetTodo);
group.MapPost("/", CreateTodo);
group.MapPut("/{id}", UpdateTodo);
group.MapDelete("/{id}", DeleteTodo);
return group;
}
В этом сценарии можно использовать относительный адрес для Location заголовка в 201 Created результате:
public static async Task<Created<Todo>> CreateTodo(Todo todo, TodoDb database)
{
await database.AddAsync(todo);
await database.SaveChangesAsync();
return TypedResults.Created($"{todo.Id}", todo);
}
Первая группа конечных точек будет соответствовать только запросам с префиксом /public/todos и будет доступна без какой-либо аутентификации. Вторая группа конечных точек будет соответствовать только запросам с префиксом /private/todos и требовать проверки подлинности.
Фабрика QueryPrivateTodos фильтров конечных точек — это локальная функция, которая изменяет параметры обработчика маршрутов TodoDb, чтобы разрешить доступ и хранение частных данных todo.
Группы маршрутов также поддерживают вложенные группы и сложные шаблоны префикса с параметрами маршрута и ограничениями. В следующем примере обработчик маршрутов, сопоставленный с user группой, может перехватывать параметры маршрута {org} и {group}, определенные в префиксах внешней группы.
Префикс также может быть пустым. Это может быть полезно для добавления метаданных конечной точки или фильтров в группу конечных точек без изменения шаблона маршрута.
var all = app.MapGroup("").WithOpenApi();
var org = all.MapGroup("{org}");
var user = org.MapGroup("{user}");
user.MapGet("", (string org, string user) => $"{org}/{user}");
Добавление фильтров или метаданных в группу ведет себя аналогично добавлению их отдельно в каждую конечную точку, перед добавлением любых дополнительных фильтров или метаданных, которые могли быть добавлены во внутреннюю группу или конкретную конечную точку.
var outer = app.MapGroup("/outer");
var inner = outer.MapGroup("/inner");
inner.AddEndpointFilter((context, next) =>
{
app.Logger.LogInformation("/inner group filter");
return next(context);
});
outer.AddEndpointFilter((context, next) =>
{
app.Logger.LogInformation("/outer group filter");
return next(context);
});
inner.MapGet("/", () => "Hi!").AddEndpointFilter((context, next) =>
{
app.Logger.LogInformation("MapGet filter");
return next(context);
});
В приведенном выше примере внешний фильтр регистрирует входящий запрос до внутреннего фильтра, даже если он был добавлен вторым. Так как фильтры были применены к разным группам, порядок их добавления относительно друг друга не имеет значения. Порядок добавления фильтров имеет значение, если они применяются к одной и той же группе или конкретной конечной точке.
Запрос к /outer/inner/ зафиксирует следующее:
/outer group filter
/inner group filter
MapGet filter
Рекомендации по производительности для маршрутизации
Проблемы с производительностью в приложении зачастую связывали с маршрутизацией. Причиной этому является то, что такие фреймворки, как контроллеры и Razor Pages, сообщают о количестве времени, проведенном внутри фреймворка, в сообщениях журнала. Если время, указанное контроллерами, значительно отличается от общего времени запроса:
- Разработчики исключают код приложения из списка возможных источников проблемы.
- Обычно предполагается, что причиной является маршрутизация.
Для проверки производительности маршрутизации используются тысячи конечных точек. Маловероятно, что обычное приложение столкнется с проблемами с производительностью только лишь из-за того, что оно слишком большое. Наиболее распространенная причина снижения производительности маршрутизации обычно кроется в неправильной работе настраиваемого ПО промежуточного слоя.
В следующем примере кода показан базовый метод сокращения источника задержки.
var logger = app.Services.GetRequiredService<ILogger<Program>>();
app.Use(async (context, next) =>
{
var stopwatch = Stopwatch.StartNew();
await next(context);
stopwatch.Stop();
logger.LogInformation("Time 1: {ElapsedMilliseconds}ms", stopwatch.ElapsedMilliseconds);
});
app.UseRouting();
app.Use(async (context, next) =>
{
var stopwatch = Stopwatch.StartNew();
await next(context);
stopwatch.Stop();
logger.LogInformation("Time 2: {ElapsedMilliseconds}ms", stopwatch.ElapsedMilliseconds);
});
app.UseAuthorization();
app.Use(async (context, next) =>
{
var stopwatch = Stopwatch.StartNew();
await next(context);
stopwatch.Stop();
logger.LogInformation("Time 3: {ElapsedMilliseconds}ms", stopwatch.ElapsedMilliseconds);
});
app.MapGet("/", () => "Timing Test.");
Для маршрутизации времени:
- Чередуйте каждое промежуточное ПО с копией тайминг промежуточного ПО, показанной в предыдущем коде.
- Добавьте уникальный идентификатор для сопоставления данных о времени с кодом.
Это базовый способ сократить задержку, когда она является существенной, например более 10ms. Вычитание Time 2 из Time 1 позволяет получить время, затраченное в промежуточном ПО UseRouting.
Следующий код использует более компактный подход к предыдущему коду времени.
public sealed class AutoStopwatch : IDisposable
{
private readonly ILogger _logger;
private readonly string _message;
private readonly Stopwatch _stopwatch;
private bool _disposed;
public AutoStopwatch(ILogger logger, string message) =>
(_logger, _message, _stopwatch) = (logger, message, Stopwatch.StartNew());
public void Dispose()
{
if (_disposed)
{
return;
}
_logger.LogInformation("{Message}: {ElapsedMilliseconds}ms",
_message, _stopwatch.ElapsedMilliseconds);
_disposed = true;
}
}
var logger = app.Services.GetRequiredService<ILogger<Program>>();
var timerCount = 0;
app.Use(async (context, next) =>
{
using (new AutoStopwatch(logger, $"Time {++timerCount}"))
{
await next(context);
}
});
app.UseRouting();
app.Use(async (context, next) =>
{
using (new AutoStopwatch(logger, $"Time {++timerCount}"))
{
await next(context);
}
});
app.UseAuthorization();
app.Use(async (context, next) =>
{
using (new AutoStopwatch(logger, $"Time {++timerCount}"))
{
await next(context);
}
});
app.MapGet("/", () => "Timing Test.");
Потенциально ресурсоемкие функции маршрутизации
В списке ниже представлены некоторые сведения о функциях маршрутизации, которые относительно ресурсоемкие по сравнению с базовыми шаблонами маршрутов.
- Регулярные выражения: можно составить сложные регулярные выражения, которые будут выполняться долгое время с небольшим количеством входных данных.
- Сложные сегменты (
{x}-{y}-{z}):- значительно более ресурсоемкие, чем анализ обычного сегмента URL-пути.
- В результате выделяется множество дополнительных подстрок.
- Синхронный доступ к данным: многие сложные приложения имеют доступ к базам данных в рамках их маршрутизации. Используйте точки расширения, такие как MatcherPolicy и EndpointSelectorContext, которые являются асинхронными.
Руководство по большим таблицам маршрутизации
По умолчанию алгоритм маршрутизации в ASP.NET Core жертвует объемом памяти в пользу низкой нагрузки на ЦП. Это приводит к тому, что время, затрачиваемое на сопоставление маршрутов, зависит только от длины сопоставляемого пути, а не от количества маршрутов. Однако такой подход создает проблемы в тех случаях, когда приложение использует большое количество (несколько тысяч) маршрутов с большим числом переменных префиксов. Например, если в маршрутах используются параметры в ранних сегментах маршрута, как {parameter}/some/literal.
Вряд ли приложение столкнется с такой проблемой, если только:
- в приложении много маршрутов, использующих такой шаблон;
- В приложении очень много маршрутов.
Как определить, возникает ли у приложения проблема большой таблицы маршрутов
- Есть два симптома, на которые стоит обратить внимание:
- При первом запросе приложение запускается медленно.
- Обратите внимание, что это необходимо, но недостаточно. Медленный запуск приложения могут вызывать многие другие проблемы, не связанные с маршрутизацией. Чтобы точно определить, что приложение сталкивается с этой ситуацией, проверьте следующее условие.
- Приложение потребляет много памяти во время запуска, а в дампе памяти отображается большое количество экземпляров
Microsoft.AspNetCore.Routing.Matching.DfaNode.
- При первом запросе приложение запускается медленно.
Способы решения этой проблемы
Есть несколько разных методов и оптимизаций, которые можно применить к маршрутам для значительного улучшения этой ситуации:
- Везде, где это возможно, примените к параметрам ограничения маршрутов, например
{parameter:int},{parameter:guid},{parameter:regex(\\d+)}и т. д.- Это позволяет алгоритму маршрутизации внутренним образом оптимизировать структуры, которые используются для сопоставления, и радикально снизить использование памяти.
- В подавляющем большинстве случаев этого будет достаточно для восстановления приемлемой производительности.
- Измените маршруты, чтобы переместить параметры в более поздние сегменты шаблона.
- Это сокращает количество возможных "путей", с которыми придется сопоставлять конечную точку по определенному пути.
- Используйте динамический маршрут и динамическое сопоставление с контроллером или страницей.
- Это можно сделать с помощью
MapDynamicControllerRouteиMapDynamicPageRoute.
- Это можно сделать с помощью
Руководство для авторов библиотек
В этом разделе содержатся рекомендации для авторов библиотек, создающих библиотеки на основе маршрутизации. Эти сведения предназначены для предоставления разработчикам приложений сведений об эффективном использовании библиотек и платформ, расширяющих маршрутизацию.
Определение конечных точек
Чтобы создать платформу, использующую маршрутизацию для сопоставления URL-адресов, начните с определения пользовательского интерфейса, который строится поверх UseEndpoints.
ВЫПОЛНИТЕ сборку поверх IEndpointRouteBuilder. Это позволит пользователям создать инфраструктуру с другими функциями ASP.NET Core без путаницы. Каждый шаблон ASP.NET Core включает в себя маршрутизацию. Предположим, что маршрутизация имеется и пользователи знакомы с ней.
// Your framework
app.MapMyFramework(...);
app.MapHealthChecks("/healthz");
ВЕРНИТЕ конкретный запечатанный тип из вызова объекта MapMyFramework(...), который реализует IEndpointConventionBuilder. Большинство методов Map... платформы соответствует этому шаблону. Интерфейс IEndpointConventionBuilder:
- Позволяет составить метаданные.
- Предназначен для различных методов расширения.
Объявление собственного типа позволяет добавлять в построитель собственные функции для конкретной платформы. Можно обернуть построитель, объявленный во фреймворке, и перенаправлять вызовы в него.
// Your framework
app.MapMyFramework(...)
.RequireAuthorization()
.WithMyFrameworkFeature(awesome: true);
app.MapHealthChecks("/healthz");
Рассмотрите возможность создания собственного EndpointDataSource.
EndpointDataSource — это низкоуровневый примитив для объявления и обновления коллекции конечных точек.
EndpointDataSource — это эффективный API, используемый контроллерами и Razor Pages.
В тестах маршрутизации имеется простой пример источника данных, который не обновляется.
РАССМОТРИТЕ реализацию GetGroupedEndpoints. Это обеспечивает полный контроль над соблюдением соглашений о группах и конечных метаданных, относящихся к группированным конечным точкам. Например, это позволяет пользовательским EndpointDataSource реализациям запускать фильтры конечных точек, добавляемые в группы.
НЕ пытайтесь зарегистрировать EndpointDataSource по умолчанию. Требуйте от пользователей, чтобы они регистрировали вашу платформу в UseEndpoints. Философия маршрутизации заключается в том, что по умолчанию ничего не включено и UseEndpoints представляет собой место для регистрации конечных точек.
Создание ПО промежуточного слоя со встроенной маршрутизацией
РАССМОТРИТЕ ВОЗМОЖНОСТЬ определения типов метаданных в качестве интерфейса.
СДЕЛАЙТЕ возможным использование типов метаданных в качестве атрибута в классах и методах.
public interface ICoolMetadata
{
bool IsCool { get; }
}
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class CoolMetadataAttribute : Attribute, ICoolMetadata
{
public bool IsCool => true;
}
Платформы, такие как контроллеры и Razor Pages, поддерживают применение атрибутов метаданных к типам и методам. При объявлении типов метаданных:
- Сделайте их доступными в качестве атрибутов.
- Большинство пользователей знакомы с применением атрибутов.
Объявление типа метаданных в качестве интерфейса добавляет еще один уровень гибкости.
- Интерфейсы можно комбинировать.
- Разработчики могут объявлять собственные типы, объединяющие несколько политик.
СДЕЛАЙТЕ возможным переопределение метаданных, как показано в следующем примере.
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class SuppressCoolMetadataAttribute : Attribute, ICoolMetadata
{
public bool IsCool => false;
}
[CoolMetadata]
public class MyController : Controller
{
public void MyCool() { }
[SuppressCoolMetadata]
public void Uncool() { }
}
Следуйте этим рекомендациям, чтобы избежать определения метаданных маркера.
- Не ограничивайтесь поиском типа метаданных.
- Определите свойство метаданных и проверьте его.
Коллекция метаданных является упорядоченной и поддерживает переопределение приоритета. В случае с контроллерами метаданные в методе действия являются наиболее специфичными.
СДЕЛАЙТЕ промежуточное ПО полезным как с маршрутизацией, так и без нее:
app.UseAuthorization(new AuthorizationPolicy() { ... });
// Your framework
app.MapMyFramework(...).RequireAuthorization();
В качестве примера этой рекомендации рассмотрим ПО промежуточного слоя UseAuthorization. Промежуточное ПО авторизации позволяет задать резервную политику.
Политика отката, если она указана, применяется к обоим элементам:
- конечные точки без указанной политики;
- запросы, которые не соответствуют конечной точке.
Это сделает ПО промежуточного слоя авторизации полезным вне контекста маршрутизации. Промежуточное ПО авторизации можно использовать для традиционного программирования middleware.
Диагностика отладки
Для подробного вывода диагностики построения маршрутов задайте для Logging:LogLevel:Microsoft значение Debug. В среде Development установите уровень журнала в appsettings.Development.json:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Debug",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}
Дополнительные ресурсы
Маршрутизация обеспечивает сопоставление входящих HTTP-запросов и их распределение по исполняемым конечным точкам приложения. Конечные точки — это блоки исполняемого кода обработки запросов приложения. Конечные точки определяются в приложении и настраиваются при его запуске. Процесс сопоставления конечных точек может извлекать значения из URL-адреса запроса и предоставлять эти значения для обработки запроса. С помощью сведений о конечных точках из приложения маршрутизация также может формировать URL-адреса, которые сопоставляются с конечными точками.
Приложения могут настраивать маршрутизацию с помощью следующих методов:.
- Controllers
- Razor Страницы
- SignalR
- Службы gRPC
- Промежуточное ПО с поддержкой конечных точек, например проверки работоспособности.
- Делегаты и лямбда-выражения, зарегистрированные для маршрутизации
В этой статье представлены сведения о низкоуровневой маршрутизации ASP.NET Core. Дополнительные сведения о настройке маршрутизации
- Сведения о контроллерах см. в статье Маршрутизация к действиям контроллера в ASP.NET Core.
- Соглашения для Razor Pages см. в статье Соглашения для маршрутов и приложений Razor Pages в ASP.NET Core.
Основы маршрутизации
В следующем коде приведен базовый пример маршрутизации.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
В предыдущем примере используется одна конечная точка с помощью MapGet метода:
- При отправке HTTP-запроса
GETв корневой URL-адрес/:- Выполняется делегат запроса.
- В ответ HTTP записывается
Hello World!.
- Если метод запроса не является
GETили если корневой URL-адрес не/, сопоставление маршрута не выполняется и возвращается сообщение об ошибке HTTP 404.
Маршрутизация использует пару промежуточного ПО, зарегистрированную с помощью UseRouting и UseEndpoints.
-
UseRoutingдобавляет соответствие маршрута в конвейер ПО промежуточного слоя. Это ПО промежуточного слоя обращается к набору конечных точек, определенных в приложении, и выбирает наиболее подходящее на основе запроса. -
UseEndpointsдобавляет выполнение конечной точки в конвейер обработки промежуточного ПО. Он запускает делегат, связанный с выбранной конечной точкой.
Приложениям обычно не требуется вызывать UseRouting или UseEndpoints.
WebApplicationBuilder настраивает конвейер промежуточного программного обеспечения, который оборачивает промежуточное ПО, добавленное в Program.cs, с помощью UseRouting и UseEndpoints. Но приложения могут изменять порядок, в котором выполняются UseRouting и UseEndpoints, вызывая эти методы явным образом. Например, следующий код явным образом вызывает UseRouting:
app.Use(async (context, next) =>
{
// ...
await next(context);
});
app.UseRouting();
app.MapGet("/", () => "Hello World!");
В предыдущем коде:
- Вызов к
app.Useрегистрирует пользовательское ПО промежуточного слоя, которое выполняется в начале конвейера. - При вызове метода
UseRoutingПО промежуточного слоя сопоставления маршрутов настраивается для запуска после пользовательского ПО промежуточного слоя. - Конечная точка, зарегистрированная с использованием
MapGet, выполняется в конце конвейера.
Если предыдущий пример не включал вызов к UseRouting, пользовательское ПО промежуточного слоя будет выполняться после ПО промежуточного слоя сопоставления маршрутов.
Endpoints
Для определения конечной точки используется метод MapGet. Конечная точка — это объект, который может быть:
- Выбирается путем сопоставления URL-адреса и метода HTTP.
- Выполняется путем использования запуска делегата.
Конечные точки, которые могут быть сопоставлены и выполнены приложением, настраиваются в UseEndpoints. Например, MapGet, MapPost и аналогичные методы подключают делегаты запросов к системе маршрутизации. Для подключения функций платформы ASP.NET Core к системе маршрутизации можно использовать дополнительные методы.
- MapRazorPages для страниц Razor
- MapControllers для контроллеров
- MapHub<THub> для SignalR
- MapGrpcService<TService> для gRPC
Ниже представлен пример маршрутизации с более сложным шаблоном маршрута.
app.MapGet("/hello/{name:alpha}", (string name) => $"Hello {name}!");
Строка /hello/{name:alpha} является шаблоном маршрута. Шаблон маршрута используется для настройки способа сопоставления конечной точки. В этом случае шаблон соответствует следующим условиям.
- URL-адрес, подобный
/hello/Docs - Любой URL-путь, начинающийся с
/hello/,после которого следует набор буквенных символов.:alphaприменяет ограничение маршрута, которое соответствует только буквенным символам. Ограничения маршрута описаны далее в этой статье.
Второй сегмент URL-пути, {name:alpha}:
- привязан к параметру
name; - Записывается и хранится в HttpRequest.RouteValues.
В следующем примере показана маршрутизация с проверками работоспособности и авторизацией.
app.UseAuthentication();
app.UseAuthorization();
app.MapHealthChecks("/healthz").RequireAuthorization();
app.MapGet("/", () => "Hello World!");
В предыдущем примере, показано то, как:
- Промежуточное программное обеспечение для авторизации можно использовать вместе с маршрутизацией.
- можно использовать конечные точки для настройки режима авторизации.
Вызов MapHealthChecks добавляет конечную точку проверки состояния. Связывание RequireAuthorization с этим вызовом прикрепляет политику авторизации к конечной точке.
При вызове UseAuthentication и UseAuthorization добавляется ПО промежуточного слоя для проверки подлинности и авторизации. Эти middleware размещаются между UseRouting и UseEndpoints, чтобы они могли:
- просматривать, какая конечная точка выбрана методом
UseRouting; - применять политику авторизации до отправки UseEndpoints на конечную точку.
Метаданные конечной точки
В предыдущем примере существуют две конечные точки, но политика авторизации прикреплена только к конечной точке проверки состояния. Если запрос соответствует конечной точке проверки работоспособности, /healthz, выполняется проверка авторизации. Это служит демонстрацией того, что к конечным точкам можно прикреплять дополнительные данные. Эти дополнительные данные называются метаданными конечных точек.
- Метаданные могут обрабатываться ПО промежуточного слоя с поддержкой маршрутизации.
- Метаданные могут быть любого типа .NET.
Основные понятия маршрутизации
Система маршрутизации формируется поверх конвейера промежуточного программного обеспечения путем добавления мощного понятия конечных точек. Конечные точки представляют собой единицы функциональности приложения, отличающиеся друг от друга в плане маршрутизации, авторизации и количества систем ASP.NET Core.
Определение конечной точки ASP.NET Core
Конечная точка ASP.NET Core:
- Исполняемый: имеет RequestDelegate.
- Расширяемо: имеет коллекцию метаданных .
- Доступный вариант: при необходимости содержит сведения о маршрутизации.
- Перечисляемые: коллекцию конечных точек можно перечислить, извлекая EndpointDataSource из DI.
В следующем примере кода показано, как получить и проверить конечную точку, соответствующую текущему запросу.
app.Use(async (context, next) =>
{
var currentEndpoint = context.GetEndpoint();
if (currentEndpoint is null)
{
await next(context);
return;
}
Console.WriteLine($"Endpoint: {currentEndpoint.DisplayName}");
if (currentEndpoint is RouteEndpoint routeEndpoint)
{
Console.WriteLine($" - Route Pattern: {routeEndpoint.RoutePattern}");
}
foreach (var endpointMetadata in currentEndpoint.Metadata)
{
Console.WriteLine($" - Metadata: {endpointMetadata}");
}
await next(context);
});
app.MapGet("/", () => "Inspect Endpoint.");
Конечную точку, если она выбрана, можно получить из HttpContext. Ее свойства можно проверить. Объекты конечных точек являются неизменяемыми, и их невозможно изменить после создания. Наиболее распространенным типом конечной точки является RouteEndpoint.
RouteEndpoint содержит сведения, позволяющие системе маршрутизации выбрать эту конечную точку.
В приведенном выше коде app.Use настраивает встроенное ПО промежуточного слоя.
В следующем коде показано, что в зависимости от того, где в конвейере вызывается app.Use, может не быть конечной точки.
// Location 1: before routing runs, endpoint is always null here.
app.Use(async (context, next) =>
{
Console.WriteLine($"1. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
await next(context);
});
app.UseRouting();
// Location 2: after routing runs, endpoint will be non-null if routing found a match.
app.Use(async (context, next) =>
{
Console.WriteLine($"2. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
await next(context);
});
// Location 3: runs when this endpoint matches
app.MapGet("/", (HttpContext context) =>
{
Console.WriteLine($"3. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
return "Hello World!";
}).WithDisplayName("Hello");
app.UseEndpoints(_ => { });
// Location 4: runs after UseEndpoints - will only run if there was no match.
app.Use(async (context, next) =>
{
Console.WriteLine($"4. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
await next(context);
});
В предыдущем примере добавляются инструкции Console.WriteLine, которые показывают, выбрана ли конечная точка. Для ясности в примере указанной конечной точке / назначается отображаемое имя.
Кроме того, предыдущий пример включает вызовы к UseRouting и UseEndpoints для точного контроля того, когда именно эти ПО промежуточного слоя выполняются в конвейере.
При выполнении этого кода с URL-адресом / отображается следующее.
1. Endpoint: (null)
2. Endpoint: Hello
3. Endpoint: Hello
При выполнении этого кода с любым другим URL-адресом отображается следующее.
1. Endpoint: (null)
2. Endpoint: (null)
4. Endpoint: (null)
В этом выводе показано следующее.
- Перед вызовом
UseRoutingконечная точка всегда имеет значение NULL. - Если найдено совпадение, конечная точка отличается от NULL между
UseRoutingи UseEndpoints. - ПО промежуточного слоя
UseEndpointsявляется терминальным при обнаружении соответствия. Определение терминального ПО промежуточного слоя приведено далее в этой статье. - Промежуточное ПО после
UseEndpointsвыполняется только в случае, если совпадения не найдены.
Промежуточный слой UseRouting использует метод SetEndpoint для подключения конечной точки к текущему контексту. ПО промежуточного слоя UseRouting можно заменить на настраиваемую логику и по-прежнему использовать конечные точки. Конечные точки — это низкоуровневые примитивы, такие как ПО промежуточного слоя, которые не связаны с реализацией маршрутизации. В большинстве приложений метод UseRouting не требуется заменять настраиваемой логикой.
ПО промежуточного слоя UseEndpoints предназначено для использования совместно с ПО промежуточного слоя UseRouting. Основная логика для выполнения конечной точки достаточно проста. Чтобы получить конечную точку, используйте GetEndpoint, а затем вызовите ее свойство RequestDelegate.
В следующем примере кода показано, как ПО промежуточного слоя может влиять на маршрутизацию или реагировать на нее.
app.UseHttpMethodOverride();
app.UseRouting();
app.Use(async (context, next) =>
{
if (context.GetEndpoint()?.Metadata.GetMetadata<RequiresAuditAttribute>() is not null)
{
Console.WriteLine($"ACCESS TO SENSITIVE DATA AT: {DateTime.UtcNow}");
}
await next(context);
});
app.MapGet("/", () => "Audit isn't required.");
app.MapGet("/sensitive", () => "Audit required for sensitive data.")
.WithMetadata(new RequiresAuditAttribute());
public class RequiresAuditAttribute : Attribute { }
В предыдущем примере показаны два важных основных понятия.
- Промежуточное ПО может работать до
UseRoutingчтобы изменить данные, обрабатываемые маршрутизацией.- Обычно ПО промежуточного слоя, отображаемое перед маршрутизацией, изменяет некоторое свойство запроса, например UseRewriter, UseHttpMethodOverride или UsePathBase.
- ПО промежуточного слоя может выполняться между
UseRoutingи UseEndpoints для обработки результатов маршрутизации до выполнения конечной точки.- ПО промежуточного слоя, которое выполняется между
UseRoutingиUseEndpoints:- Обычно проверяет метаданные для получения представления о конечных точках.
- Зачастую принимает решения по обеспечению безопасности, как это делают
UseAuthorizationиUseCors.
- Сочетание ПО промежуточного слоя и метаданных позволяет настраивать политики для каждой конечной точки.
- ПО промежуточного слоя, которое выполняется между
В приведенном выше коде показан пример настраиваемого ПО промежуточного слоя, поддерживающего политики для каждой конечной точки. ПО промежуточного слоя записывает в консоль журнал аудита доступа к конфиденциальным данным. Промежуточное ПО можно настроить для выполнения аудита конечной точки с помощью метаданных RequiresAuditAttribute. В этом примере демонстрируется паттерн подписки, когда аудит выполняется только для конечных точек, помеченных как чувствительные. Эту логику можно определить в обратном порядке для аудита всего, что не отмечено, например, как надежное. Система метаданных конечной точки обеспечивает гибкость. Эту логику можно настроить под любой требуемый вариант использования.
Предыдущий пример кода предназначен для демонстрации основных понятий конечных точек. Он не предназначен для использования в рабочей среде. Более полная версия промежуточного программного обеспечения для журнала аудита будет:
- Логировать в файл или базу данных.
- Включает такие сведения, как информация о пользователе, IP-адресе, имени конфиденциальной конечной точки и многое другое.
Метаданные политики аудита RequiresAuditAttribute определены как Attribute, чтобы их было проще использовать в платформах на основе классов, таких как контроллеры и SignalR. При использовании маршрута к коду:
- Метаданные присоединяются к API-интерфейсу построителя.
- При создании конечных точек платформы на основе классов включают все атрибуты в соответствующем методе и классе.
Типы метаданных рекомендуется определить либо как интерфейсы, либо как атрибуты. Интерфейсы и атрибуты допускают повторное использование кода. Система метаданных является гибкой и не накладывает никаких ограничений.
Сравнение ПО промежуточного слоя терминала с маршрутизацией
В следующем примере демонстрируется ПО промежуточного слоя терминала и маршрутизация:
// Approach 1: Terminal Middleware.
app.Use(async (context, next) =>
{
if (context.Request.Path == "/")
{
await context.Response.WriteAsync("Terminal Middleware.");
return;
}
await next(context);
});
app.UseRouting();
// Approach 2: Routing.
app.MapGet("/Routing", () => "Routing.");
Стиль ПО промежуточного слоя, который показан в разделе Approach 1:, — терминальное ПО промежуточного слоя. ПО промежуточного слоя называется терминальным, поскольку выполняет операцию сопоставления.
- Операция сопоставления в предыдущем примере —
Path == "/"для ПО промежуточного слоя иPath == "/Routing"для маршрутизации. - Если сопоставление выполнено успешно, оно выполняет некоторые функции и возвращает результат, а не вызывает ПО промежуточного слоя
next.
Оно называется терминальным, поскольку завершает поиск, выполняет некоторые функции, а затем возвращает результат.
В следующем списке ПО промежуточного слоя терминала сравнивается с маршрутизацией:
- Оба подхода позволяют завершить конвейер обработки.
- ПО промежуточного слоя завершает конвейер, возвращая вместо вызова
next. - Конечные точки всегда являются терминальными.
- ПО промежуточного слоя завершает конвейер, возвращая вместо вызова
- Промежуточное ПО терминала позволяет разместить его в любом месте конвейера.
- Конечные точки выполняются в позиции UseEndpoints.
- Промежуточное программное обеспечение терминала позволяет произвольному коду определить, когда программное обеспечение соответствует.
- Настраиваемый код сопоставления маршрутов может быть подробным и сложным для корректной записи.
- Маршрутизация обеспечивает простые решения для обычных приложений. Большинству приложений не требуется настраиваемый код сопоставления маршрутов.
- Конечные точки взаимодействуют с ПО промежуточного слоя, таким как
UseAuthorizationиUseCors.- Использование терминального ПО промежуточного слоя с
UseAuthorizationилиUseCorsтребует взаимодействия вручную с системой авторизации.
- Использование терминального ПО промежуточного слоя с
Конечная точка определяет и то, и другое:
- делегат для обработки запросов;
- коллекцию произвольных метаданных. Метаданные используются для реализации аспектов, пронизывающих архитектуру, на основе политик и конфигурации, присоединённой к каждой конечной точке.
Программное обеспечение промежуточного уровня может быть эффективным средством, но может потребовать:
- значительный объем кода и тестирования;
- интеграция вручную с другими системами для достижения желаемого уровня гибкости.
Прежде чем создавать терминальное ПО промежуточного слоя, рассмотрите возможность интеграции с маршрутизацией.
Существующее терминальное ПО промежуточного слоя, которое интегрируется с Map или MapWhen, обычно может быть преобразовано в конечную точку, осведомленную о маршрутизации. MapHealthChecks демонстрирует шаблон для маршрутизирующего программного обеспечения.
- Напишите метод расширения в IEndpointRouteBuilder.
- Создайте вложенный конвейер middleware с помощью CreateApplicationBuilder.
- Присоедините промежуточное программное обеспечение к новому конвейеру. В этом случае — UseHealthChecks.
- Build конвейер промежуточного программного обеспечения в RequestDelegate.
- Вызовите
Mapи укажите новый посреднический программный конвейер. - Верните объект построителя, который предоставлен
Map, из метода расширения.
В следующем коде показано использование MapHealthChecks.
app.UseAuthentication();
app.UseAuthorization();
app.MapHealthChecks("/healthz").RequireAuthorization();
В предыдущем примере показано, почему возврат объекта строителя имеет большое значение. Возврат объекта построителя позволяет разработчику приложения настраивать политики, такие как авторизация для конечной точки. В этом примере промежуточное ПО для проверки состояния не имеет прямой интеграции с системой авторизации.
Для решения проблем, с которыми столкнулись авторы расширяемости при использовании программного обеспечения промежуточного слоя терминалов, была создана система метаданных. Для каждого ПО промежуточного слоя достаточно проблематично реализовать собственную интеграцию с системой авторизации.
Сопоставление URL-адресов
- Это процесс, с помощью которого функция маршрутизации сопоставляет входящий запрос в конечную точку.
- Он основан на данных из пути URL и заголовков.
- Его можно расширить, чтобы учесть любые данные в запросе.
При выполнении ПО промежуточного слоя маршрутизации оно задает конечную точку (Endpoint) и значения маршрута для функции запроса в HttpContext из текущего запроса.
- Вызов HttpContext.GetEndpoint получает конечную точку.
-
HttpRequest.RouteValuesполучает коллекцию значений маршрута.
Middleware выполняется после того, как маршрутизационное ПО может проверить конечную точку и совершить действия. Например, промежуточное ПО авторизации может опрашивать коллекцию метаданных конечной точки для получения политики авторизации. После выполнения всех компонентов промежуточного программного обеспечения в конвейере обработки запросов вызывается делегат выбранной конечной точки.
Система маршрутизации в контексте маршрутизации по конечным точкам отвечает за все решения по управлению маршрутизацией. Поскольку ПО промежуточного слоя применяет политики, основанные на выбранной конечной точке, важно следующее.
- Любые решения, которые могут повлиять на распределение или применение политик безопасности, должны выполняться внутри системы маршрутизации.
Warning
Для обеспечения обратной совместимости при выполнении делегата конечной точки контроллера или Razor страниц, свойства RouteContext.RouteData устанавливаются в соответствующие значения на основе обработки запросов, выполненной к этому моменту.
В следующем выпуске тип RouteContext будет помечен как устаревший.
- Перенесите
RouteData.ValuesвHttpRequest.RouteValues. - Мигрируйте
RouteData.DataTokens, чтобы получить IDataTokensMetadata из метаданных конечной точки.
Работа сопоставления URL-адресов подразделяется на этапы, которые можно настроить. На каждом этапе выходные данные представляют собой набор совпадений. По мере перехода между этапами набор совпадений можно ограничивать. Реализация маршрутизации не гарантирует порядок обработки для соответствующих конечных точек. Все возможные совпадения обрабатываются одновременно. Этапы сопоставления URL-адресов выполняются в следующем порядке. ASP.NET Core:
- URL-путь обрабатывается по набору конечных точек и их шаблонов маршрутов, при этом выполняется сбор всех совпадений.
- Берется предыдущий список и удаляются совпадения, которые не соответствуют примененным ограничениям маршрута.
- Принимает предыдущий список и удаляет совпадения, которые не соответствуют набору экземпляров MatcherPolicy.
- EndpointSelector используется для принятия окончательного решения из предыдущего списка.
Список конечных точек определяется по приоритету в соответствии со следующим.
Все соответствующие конечные точки обрабатываются на каждом этапе до тех пор, пока не будет достигнут EndpointSelector.
EndpointSelector — это заключительный этап. Он выбирает конечную точку с наивысшим приоритетом из совпадений как наилучшее соответствие. При наличии других совпадений с таким же приоритетом, как у наилучшего соответствия, выбрасывается исключение неоднозначного соответствия.
Приоритет маршрута вычисляется на основе более определенного шаблона маршрута, которому назначается более высокий приоритет. Например, рассмотрим шаблоны /hello и /{message}.
- Оба соответствуют URL-пути
/hello. -
/helloявляется более конкретным, и, следовательно, ему назначается более высокий приоритет.
Как правило, приоритет маршрута помогает выбрать наилучшее соответствие для типов схем URL-адресов, используемых на практике. Используйте Order только в случае, когда необходимо избежать неоднозначности.
Ввиду различных типов расширяемости, предоставляемых службой маршрутизации, система маршрутизации не может заранее вычислить неоднозначные маршруты. Рассмотрим в качестве примера шаблоны маршрутов /{message:alpha} и /{message:int}.
- Ограничение
alphaсоответствует только буквенным символам. - Ограничение
intсоответствует только числам. - Эти шаблоны имеют одинаковый приоритет маршрута, однако не существует одного URL-адреса, по которому они совпадают.
- Если система маршрутизации сообщила об ошибке неоднозначности при запуске, это означает, что она заблокировала этот допустимый вариант использования.
Warning
Порядок операций в UseEndpoints не влияет на поведение маршрутизации, за одним исключением. MapControllerRoute и MapAreaRoute автоматически присваивают значение порядка своим конечным точкам в соответствии с порядком их вызова. Это имитирует поведение контроллеров без системы маршрутизации в долгосрочной перспективе, предоставляя те же гарантии, что и в старых реализациях маршрутизации.
Маршрутизация конечных точек в ASP.NET Core:
- Не имеет концепции маршрутов.
- Не гарантирует порядок обработки. Все конечные точки обрабатываются одновременно.
Приоритет шаблонов маршрутов и порядок выбора конечных точек
Приоритет шаблонов маршрутов — это система, которая назначает каждому шаблону маршрута значение в зависимости от того, насколько он является конкретным. Приоритет шаблона маршрута
- Позволяет избежать необходимости настраивать порядок конечных точек в общих случаях.
- Пытается соответствовать общепринятым ожиданиям относительно поведения маршрутизации.
Например, рассмотрим шаблоны /Products/List и /Products/{id}. Разумно предположить, что /Products/List является лучшим соответствием для URL-пути /Products/List, чем /Products/{id}. Литеральный сегмент /List считается более приоритетным, чем сегмент параметров /{id}.
Порядок определения приоритета связан с порядком определения шаблонов маршрутов.
- Шаблоны с большим количеством сегментов считаются более конкретными.
- Сегмент с литеральным текстом считается более конкретным, чем сегмент параметров.
- Сегмент параметров с ограничением считается более конкретным, чем сегмент без него.
- Сложный сегмент рассматривается как такой же специфичный, как сегмент параметра с ограничением.
- Универсальные параметры являются наименее конкретными. См. раздел Шаблоны маршрута для получения важной информации об универсальных маршрутах.
Основные понятия формирования URL-адресов
Создание URL-адресов:
- Это процесс, благодаря которому маршрутизация может формировать путь URL-адреса на основе набора значений маршрута.
- Обеспечивает логическое разделение конечных точек и URL-адресов, по которым к ним осуществляется доступ.
Маршрутизация конечных точек включает API LinkGenerator.
LinkGenerator — это одноэлементная служба, доступная в DI. API LinkGenerator можно использовать вне контекста выполнения запроса.
Mvc.IUrlHelper и сценарии, которые зависят от IUrlHelper, такие как вспомогательные функции тегов, вспомогательные методы HTML и результаты действий, используют API LinkGenerator для предоставления возможностей создания ссылок.
Генератор ссылок использует концепции адреса и схем адресов. Схема адресов — это способ определения конечных точек, которые должны рассматриваться для создания ссылки. Например, сценарии с именем маршрута и значениями маршрута, с которыми многие пользователи знакомы по контроллерам и Razor Pages, реализуются как схема адресов.
Генератор ссылок может установить связь с контроллерами и Razor Pages с помощью следующих методов расширения.
Перегрузки этих методов принимают аргументы, которые включают HttpContext. Эти методы являются функциональными эквивалентами Url.Action и Url.Page, но предлагают дополнительную гибкость и параметры.
Методы GetPath* наиболее схожи с Url.Action и Url.Page в том, что создают URI, содержащий абсолютный путь. Методы GetUri* всегда создают абсолютный URI, содержащий схему и хост. Методы, которые принимают HttpContext, создают URI в контексте выполнения запроса. Используются значения окружения маршрута, базовый URL-адрес, схема и узел из выполняющегося запроса, если не указано иное.
LinkGenerator вызывается с адресом. Создание URI происходит в два этапа:
- Адрес привязан к списку конечных точек, соответствующих адресу.
- RoutePattern конечной точки вычисляется, пока не будет найден шаблон маршрута, который соответствует предоставленным значениям. Полученный результат объединяется с другими частями URI, предоставленными генератору ссылок и возвращенными.
Методы, предоставляемые LinkGenerator, поддерживают стандартные возможности создания ссылки для любого типа адреса. Самый удобный способ использовать генератор ссылки — через методы расширения, которые выполняют операции для определенного типа адреса.
| Метод расширения | Description |
|---|---|
| GetPathByAddress | Создает URI с абсолютным путем на основе предоставленных значений. |
| GetUriByAddress | Создает абсолютный URI на основе предоставленных значений. |
Warning
Обратите внимание на следующие последствия вызова методов LinkGenerator:
Используйте методы расширения
GetUri*с осторожностью в конфигурации приложения, которая не проверяет заголовок входящих запросовHost. Если заголовок входящих запросовHostне проверен, недоверенные входные данные запроса могут быть отправлены обратно клиенту в URI в виде или на странице. Рекомендуется, чтобы все рабочие приложения настраивали свой сервер на проверку заголовкаHostотносительно известных допустимых значений.Используйте LinkGenerator с осторожностью в ПО промежуточного слоя в сочетании с
MapилиMapWhen.Map*изменяет базовый путь выполняющегося запроса, что влияет на выходные данные создания ссылки. Все API LinkGenerator разрешают указание базового пути. Укажите пустой базовый путь для отмены влиянияMap*на создание ссылок.
Пример программного обеспечения промежуточного слоя
В следующем примере ПО промежуточного слоя использует API LinkGenerator, чтобы создать ссылку на метод действия, который перечисляет хранимые продукты. Использование генератора ссылок путем его внедрения в класс и вызова GenerateLink доступно для любого класса в приложении.
public class ProductsMiddleware
{
private readonly LinkGenerator _linkGenerator;
public ProductsMiddleware(RequestDelegate next, LinkGenerator linkGenerator) =>
_linkGenerator = linkGenerator;
public async Task InvokeAsync(HttpContext httpContext)
{
httpContext.Response.ContentType = MediaTypeNames.Text.Plain;
var productsPath = _linkGenerator.GetPathByAction("Products", "Store");
await httpContext.Response.WriteAsync(
$"Go to {productsPath} to see our products.");
}
}
Шаблоны маршрутов
Токены в {} определяют параметры маршрута, которые будут привязаны, если маршрут совпадает. В сегменте маршрута можно определить несколько параметров маршрута, однако они должны разделяться литеральным значением. Рассмотрим пример.
{controller=Home}{action=Index}
не является допустимым маршрутом, потому что между {controller} и {action} нет дословного значения. Параметрам маршрута должны быть присвоены имена, и для них могут быть определены дополнительные атрибуты.
Весь текст, кроме параметров маршрута (например, {id}) и разделителя пути /, должен соответствовать тексту в URL-адресе. Сопоставление текста производится без учета регистра на основе декодированного представления пути URL-адреса. Для сопоставления с литеральным разделителем параметров маршрута ({ или }) нужно экранировать разделитель, повторив символ. Например, {{ или }}.
Звездочка * или двойная звездочка **:
- Можно использовать в качестве префикса к параметру маршрута для привязки к остальной части URI.
- Такие параметры называются универсальными. Например,
blog/{**slug}:- Соответствует любому URI, который начинается с
blog/и имеет любое значение после него. - Значение после
blog/присваивается значению динамического маршрута.
- Соответствует любому URI, который начинается с
Warning
Соответствие параметра catch-all маршрутам может быть неправильным из-за ошибки в маршрутизации. Приложения, на работу которых влияет эта ошибка, обладают следующими характеристиками:
- Маршрут общий, например
{**slug}". - Маршрут catch-all не соответствует необходимым запросам.
- После удаления других маршрутов маршрут catch-all начинает работать.
Ознакомьтесь с примерами 18677 и 16579, в которых встречается эта ошибка, на сайте GitHub.
Исправление для этой ошибки, которое можно включить по желанию, содержится в пакете SDK для .NET Core 3.1.301 или более поздней версии. Следующий код задает внутренний переключатель, исправляющий эту ошибку:
public static void Main(string[] args)
{
AppContext.SetSwitch("Microsoft.AspNetCore.Routing.UseCorrectCatchAllBehavior",
true);
CreateHostBuilder(args).Build().Run();
}
// Remaining code removed for brevity.
Универсальные параметры также могут соответствовать пустой строке.
Универсальный параметр экранирует соответствующие символы, когда маршрут используется для формирования URL-адреса, включая символы разделителей пути (/). Например, маршрут foo/{*path} со значениями маршрутов { path = "my/path" } формирует foo/my%2Fpath. Обратите внимание на экранированную косую черту. В качестве символов разделителя кругового пути используйте префикс параметра маршрута **. Маршрут foo/{**path} с { path = "my/path" } формирует foo/my/path.
Шаблоны URL-адресов, которые пытаются получить имя файла с необязательным расширением, имеют свои особенности. Например, рассмотрим шаблон files/{filename}.{ext?}. Когда значения для filename и ext существуют, заполняются оба значения. Если в URL-адресе есть только значение для filename, маршрут совпадает, так как точка в конце (.) является необязательной. Следующие URL-адреса соответствуют этому маршруту:
/files/myFile.txt/files/myFile
Параметры маршрута могут иметь значения по умолчанию. Они указываются после имени параметра и знака равенства (=). Например, {controller=Home} определяет Home в качестве значения по умолчанию для controller. Значение по умолчанию используется, если для параметра нет значения в URL-адресе. Параметры маршрута могут быть необязательными, для этого необходимо добавить вопросительный знак (?) в конец имени параметра. Например, id?. Разница между необязательными значениями и параметрами маршрута по умолчанию
- Параметр маршрута со значением по умолчанию всегда создает значение.
- Необязательный параметр имеет значение только в том случае, если в URL-адресе запроса указано значение.
Параметры маршрута могут иметь ограничения, которые должны соответствовать значению маршрута из URL-адреса. Добавив двоеточие (:) и имя ограничения после имени параметра маршрута, можно указать встроенные ограничения для параметра маршрута. Если для ограничения требуются аргументы, они указываются в скобках (...) после имени ограничения. Чтобы указать несколько встроенных ограничений, добавьте еще одно двоеточие (:) и имя ограничения.
Имя и аргументы ограничения передаются в службу IInlineConstraintResolver для создания экземпляра IRouteConstraint, который будет использоваться для обработки URL. Например, в шаблоне маршрута blog/{article:minlength(10)} определяется ограничение minlength с аргументом 10. Более подробное описание ограничений маршрутов и список ограничений, предоставляемых платформой, см. в разделе Ограничения маршрутов.
Параметры маршрута также могут иметь преобразователи параметров, которые преобразуют значение параметра при создании ссылок и сопоставлении действий и страниц с URL-адресами. Как и ограничения, преобразователи параметров можно включать в параметр маршрута, добавив двоеточие (:) и имя преобразователя после имени параметра маршрута. Например, шаблон маршрута blog/{article:slugify} задает преобразователь slugify. Дополнительные сведения о преобразователях параметров см. в разделе Преобразователи параметров.
В приведенной ниже таблице показаны некоторые примеры шаблонов маршрутов и их поведение.
| Шаблон маршрута | Пример сопоставления URI | URI запроса... |
|---|---|---|
hello |
/hello |
Соответствует только одному пути /hello. |
{Page=Home} |
/ |
Соответствует и задает для параметра Page значение Home. |
{Page=Home} |
/Contact |
Соответствует и задает для параметра Page значение Contact. |
{controller}/{action}/{id?} |
/Products/List |
Сопоставляется с контроллером Products и действием List. |
{controller}/{action}/{id?} |
/Products/Details/123 |
Соответствует контроллеру Products и действию Details, где id имеет значение 123. |
{controller=Home}/{action=Index}/{id?} |
/ |
Сопоставляется с контроллером Home и методом Index.
id не учитывается. |
{controller=Home}/{action=Index}/{id?} |
/Products |
Сопоставляется с контроллером Products и методом Index.
id не учитывается. |
Использование шаблона — это, как правило, самый простой подход к маршрутизации. Ограничения и значения по умолчанию также могут указываться вне шаблона маршрута.
Сложные сегменты
Сложные сегменты обрабатываются путем сопоставления литеральных разделителей справа налево в нежадной манере. Например, [Route("/a{b}c{d}")] является сложным сегментом.
Сложные сегменты работают определенным способом, который должен быть понятен для их успешного использования. В примере в этом разделе показано, почему сложные сегменты действительно хорошо работают только в том случае, если текст разделителя отсутствует в значениях параметров. Для более сложных случаев необходимо использовать регулярное выражение и затем вручную извлечь значения.
Warning
При использовании System.Text.RegularExpressions для обработки ненадежных входных данных передайте время ожидания. Злонамеренный пользователь может предоставить входные данные для RegularExpressions, что делает возможными атаки типа "отказ в обслуживании". API платформы ASP.NET Core, использующие RegularExpressions, передают время ожидания.
Это сводка действий, выполняемых маршрутизацией с использованием шаблона /a{b}c{d} и URL-пути /abcd.
| используется для визуализации принципа работы алгоритма.
- Первый литерал, справа налево — это
c. Таким образом,/abcdищется справа и находит/ab|c|d. - Все, что находится справа (
d), теперь сопоставляется с параметром маршрута{d}. - Следующий литерал, справа налево —
a. Поэтому поиск/ab|c|dначинается с того места, где мы остановились, после чего находитсяaв/|a|b|c|d. - Значение справа (
b) теперь сопоставляется с параметром маршрута{b}. - Текста и шаблонов маршрута больше не осталось, так что это совпадение.
Ниже приведен пример отрицательного результата с использованием того же шаблона /a{b}c{d} и URL-пути /aabcd.
| используется для визуализации принципа работы алгоритма: Это не совпадение, что объясняется тем же алгоритмом.
- Первый литерал, справа налево — это
c. Таким образом,/aabcdищется справа и находит/aab|c|d. - Все, что находится справа (
d), теперь сопоставляется с параметром маршрута{d}. - Следующий литерал, справа налево —
a. Поэтому поиск/aab|c|dначинается с того места, где мы остановились, после чего находитсяaв/a|a|b|c|d. - Значение справа (
b) теперь сопоставляется с параметром маршрута{b}. - На этом этапе имеется оставшийся текст
a, однако больше нет шаблонов маршрутов для синтаксического анализа, поэтому это не является совпадением.
Поскольку алгоритм сопоставления нежадный:
- Он сопоставляет наименьший объем текста, допустимый для каждого шага.
- Любая ситуация, когда значение разделителя появляется внутри значений параметров, приводит к несовпадению.
Регулярные выражения обеспечивают гораздо больший контроль над их поведением при сопоставлении.
При жадном сопоставлении, также известном как ленивое сопоставление, выполняется поиск соответствия по наибольшей возможной строке. При нежадном сопоставлении выполняется поиск соответствия по наименьшей возможной строке.
Маршрутизация с особыми символами
Маршрутизация со специальными символами может привести к неожиданным результатам. Например, рассмотрим контроллер со следующим методом действия:
[HttpGet("{id?}/name")]
public async Task<ActionResult<string>> GetName(string id)
{
var todoItem = await _context.TodoItems.FindAsync(id);
if (todoItem == null || todoItem.Name == null)
{
return NotFound();
}
return todoItem.Name;
}
Если string id содержит следующие закодированные значения, могут возникнуть непредвиденные результаты:
| ASCII | Encoded |
|---|---|
/ |
%2F |
|
+ |
Параметры маршрута не всегда раскодированы из URL. Эта проблема может быть решена в будущем. См. эту задачу GitHub для получения дополнительной информации.
Ограничения маршрута
Ограничения маршрута применяются, когда найдено соответствие входящему URL-адресу, и путь URL-адреса разобран на элементы маршрута. Как правило, ограничения маршрута служат для проверки значения маршрута, связанного посредством шаблона маршрута, и принятия решения касательно того, является ли значение приемлемым (истина или ложь). Некоторые ограничения маршрута используют данные, не относящиеся к значению маршрута, для определения возможности маршрутизации запроса. Например, HttpMethodRouteConstraint может принимать или отклонять запрос в зависимости от HTTP-команды. Ограничения используются в маршрутизации запросов и создании ссылок.
Warning
Не используйте ограничения для проверки входных данных. Если для проверки входных данных используются ограничения, недопустимые входные данные приводят к ошибке 404 ("Не найдено"). Недопустимые входные данные должны привести к ошибке 400 ("Неверный запрос") с соответствующим сообщением об ошибке. Ограничения маршрутов следует использовать для разрешения неоднозначности похожих маршрутов, а не для проверки входных данных определенного маршрута.
В приведенной ниже таблице показаны примеры ограничения маршрутов и их ожидаемое поведение.
| constraint | Example | Примеры совпадений | Notes |
|---|---|---|---|
int |
{id:int} |
123456789, -123456789 |
Соответствует любому целому числу |
bool |
{active:bool} |
true, FALSE |
Соответствует true или false. Case-insensitive |
datetime |
{dob:datetime} |
2016-12-31, 2016-12-31 7:32pm |
Соответствует допустимому значению DateTime в инвариантной культуре. См. предупреждение выше. |
decimal |
{price:decimal} |
49.99, -1,000.01 |
Соответствует допустимому значению decimal в инвариантной культуре. См. предупреждение выше. |
double |
{weight:double} |
1.234, -1,001.01e8 |
Соответствует допустимому значению double в инвариантной культуре. См. предупреждение выше. |
float |
{weight:float} |
1.234, -1,001.01e8 |
Соответствует допустимому значению float в инвариантной культуре. См. предупреждение выше. |
guid |
{id:guid} |
CD2C1638-1638-72D5-1638-DEADBEEF1638 |
Соответствует допустимому значению Guid |
long |
{ticks:long} |
123456789, -123456789 |
Соответствует допустимому значению long |
minlength(value) |
{username:minlength(4)} |
Rick |
Строка должна содержать не менее 4 символов |
maxlength(value) |
{filename:maxlength(8)} |
MyFile |
Строка должна содержать не более 8 символов |
length(length) |
{filename:length(12)} |
somefile.txt |
Длина строки должна составлять ровно 12 символов |
length(min,max) |
{filename:length(8,16)} |
somefile.txt |
Строка должна быть длиной не менее 8 и не более 16 символов |
min(value) |
{age:min(18)} |
19 |
Целочисленное значение не меньше 18 |
max(value) |
{age:max(120)} |
91 |
Целочисленное значение не больше 120 |
range(min,max) |
{age:range(18,120)} |
91 |
Целочисленное значение должно быть не менее 18 и не более 120. |
alpha |
{name:alpha} |
Rick |
Строка должна состоять из одного или нескольких буквенных символов (a-z) без учета регистра. |
regex(expression) |
{ssn:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)} |
123-45-6789 |
Строка должна соответствовать регулярному выражению. См. советы по определению регулярного выражения. |
required |
{name:required} |
Rick |
Определяет обязательное наличие значения, не относящегося к параметру, во время формирования URL-адреса |
Warning
При использовании System.Text.RegularExpressions для обработки ненадежных входных данных передайте время ожидания. Злонамеренный пользователь может предоставить входные данные для RegularExpressions, что делает возможными атаки типа "отказ в обслуживании". API платформы ASP.NET Core, использующие RegularExpressions, передают время ожидания.
К одному параметру может применяться несколько разделенных запятой ограничений. Например, следующее ограничение ограничивает параметр целочисленным значением 1 или больше:
[Route("users/{id:int:min(1)}")]
public User GetUserById(int id) { }
Warning
Ограничения маршрута, которые проверяют URL-адрес и могут быть преобразованы в тип CLR, всегда используют инвариантные языковые настройки. Например, преобразование в тип CLR int или DateTime. Эти ограничения предполагают, что для URL-адреса не предусмотрена локализация. Предоставляемые платформой ограничения маршрутов не изменяют значения, хранящиеся в значениях маршрута. Все значения маршрута, переданные из URL-адреса, сохраняются как строки. Например, ограничение float пытается преобразовать значение маршрута в число с плавающей запятой, но преобразованное значение используется только для проверки возможности преобразования в число с плавающей запятой.
Регулярные выражения в ограничениях
Warning
При использовании System.Text.RegularExpressions для обработки ненадежных входных данных передайте время ожидания. Злонамеренный пользователь может предоставить входные данные для RegularExpressions, что делает возможными атаки типа "отказ в обслуживании". API платформы ASP.NET Core, использующие RegularExpressions, передают время ожидания.
Регулярные выражения могут быть определены как встроенные ограничения с помощью ограничения маршрута regex(...). Методы в семействе MapControllerRoute также принимают объектный литерал ограничений. При использовании этой формы строковые значения будут интерпретироваться как регулярные выражения.
В следующем коде используется встроенное ограничение регулярного выражения.
app.MapGet("{message:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)}",
() => "Inline Regex Constraint Matched");
В следующем коде для указания ограничения регулярного выражения используется литеральный объект.
app.MapControllerRoute(
name: "people",
pattern: "people/{ssn}",
constraints: new { ssn = "^\\d{3}-\\d{2}-\\d{4}$", },
defaults: new { controller = "People", action = "List" });
В платформе ASP.NET Core в конструктор регулярных выражений добавляются члены RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant. Описание этих членов см. в разделе RegexOptions.
В регулярных выражениях применяются разделители и токены, аналогичные используемым функцией маршрутизации и в языке C#. Токены регулярного выражения должны быть экранированы. Чтобы использовать регулярное выражение ^\d{3}-\d{2}-\d{4}$ во встроенном ограничении, используйте один из следующих способов.
- Замените символы
\, представленные в строке, символами\\в исходном файле C#, чтобы экранировать escape-символ строки\. - Буквальные строковые литералы.
Чтобы экранировать символы разделения параметров маршрутизации {, }, [, ], используйте их дважды в выражении (например, {{, }}, [[, ]]). В следующей таблице показаны регулярные выражения и их экранированные варианты.
| Регулярное выражение | Экранированное регулярное выражение |
|---|---|
^\d{3}-\d{2}-\d{4}$ |
^\\d{{3}}-\\d{{2}}-\\d{{4}}$ |
^[a-z]{2}$ |
^[[a-z]]{{2}}$ |
Регулярные выражения, используемые при маршрутизации, часто начинаются с символа ^ и соответствуют начальной позиции строки. Выражения часто заканчиваются символом $ и соответствуют концу строки. Благодаря символам ^ и $ регулярное выражение сопоставляется со всем значением параметра маршрута. Если символы ^ и $ отсутствуют, регулярное выражение сопоставляется с любой подстрокой внутри строки, что обычно нежелательно. В таблице ниже представлен ряд примеров и объясняются причины соответствия или несоответствия.
| Expression | String | Match | Comment |
|---|---|---|---|
[a-z]{2} |
hello | Yes | Совпадения подстроки |
[a-z]{2} |
123abc456 | Yes | Совпадения подстроки |
[a-z]{2} |
mz | Yes | Совпадение выражения |
[a-z]{2} |
MZ | Yes | Без учета регистра |
^[a-z]{2}$ |
hello | No | См. замечания, касающиеся символов ^ и $, выше |
^[a-z]{2}$ |
123abc456 | No | См. замечания, касающиеся символов ^ и $, выше |
Дополнительные сведения о синтаксисе регулярных выражений см. в статье Регулярные выражения в .NET Framework.
Чтобы ограничить возможные значения параметра набором известных значений, используйте регулярное выражение. Например, при использовании выражения {action:regex(^(list|get|create)$)} значение маршрута action будет соответствовать только list, get или create. При передаче в словарь ограничений строка ^(list|get|create)$ будет эквивалентной. Ограничения, которые передаются в словарь ограничений и не соответствуют одному из известных ограничений, также рассматриваются как регулярные выражения. Ограничения, которые передаются в шаблоне и не соответствуют одному из известных ограничений, не рассматриваются как регулярные выражения.
Пользовательские ограничения маршрутов
Пользовательские ограничения маршрутов можно создать путем внедрения интерфейса IRouteConstraint. Интерфейс IRouteConstraint содержит метод, Match, который возвращает true, если ограничение удовлетворяется, и false — если нет.
Пользовательские ограничения маршрутов редко требуются. Перед реализацией пользовательского ограничения маршрута рассмотрите альтернативные варианты, такие как привязка модели.
В папке ASP.NET Core Constraints находятся хорошие примеры создания ограничений. Например, GuidRouteConstraint.
Чтобы применить пользовательский метод IRouteConstraint, тип ограничения маршрута необходимо зарегистрировать с помощью ConstraintMap приложения в контейнере службы. Объект ConstraintMap — это словарь, который сопоставляет ключи ограничений пути с реализациями IRouteConstraint, которые проверяют эти ограничения.
ConstraintMap приложения можно обновить в Program.cs, либо как часть вызова AddRouting, либо непосредственно настроив RouteOptions с помощью builder.Services.Configure<RouteOptions>. Рассмотрим пример.
builder.Services.AddRouting(options =>
options.ConstraintMap.Add("noZeroes", typeof(NoZeroesRouteConstraint)));
Предыдущее ограничение применяется в следующем коде.
[ApiController]
[Route("api/[controller]")]
public class NoZeroesController : ControllerBase
{
[HttpGet("{id:noZeroes}")]
public IActionResult Get(string id) =>
Content(id);
}
Реализация NoZeroesRouteConstraint препятствует применению 0 к параметру маршрута:
public class NoZeroesRouteConstraint : IRouteConstraint
{
private static readonly Regex _regex = new(
@"^[1-9]*$",
RegexOptions.CultureInvariant | RegexOptions.IgnoreCase,
TimeSpan.FromMilliseconds(100));
public bool Match(
HttpContext? httpContext, IRouter? route, string routeKey,
RouteValueDictionary values, RouteDirection routeDirection)
{
if (!values.TryGetValue(routeKey, out var routeValue))
{
return false;
}
var routeValueString = Convert.ToString(routeValue, CultureInfo.InvariantCulture);
if (routeValueString is null)
{
return false;
}
return _regex.IsMatch(routeValueString);
}
}
Warning
При использовании System.Text.RegularExpressions для обработки ненадежных входных данных передайте время ожидания. Злонамеренный пользователь может предоставить входные данные для RegularExpressions, что делает возможными атаки типа "отказ в обслуживании". API платформы ASP.NET Core, использующие RegularExpressions, передают время ожидания.
Предыдущий код:
- Предотвращает
0в сегменте{id}маршрута. - Отображается для предоставления базового примера реализации настраиваемого ограничения. Не следует использовать в рабочем приложении.
Следующий код является лучшим подходом к предотвращению обработки id, содержащего 0.
[HttpGet("{id}")]
public IActionResult Get(string id)
{
if (id.Contains('0'))
{
return StatusCode(StatusCodes.Status406NotAcceptable);
}
return Content(id);
}
Приведенный выше код имеет следующие преимущества по сравнению с подходом NoZeroesRouteConstraint.
- Пользовательское ограничение не требуется.
- Он возвращает более понятную ошибку, если параметр маршрута включает
0.
Преобразователи параметров
Преобразователи параметров:
- Выполняются при формировании ссылки с помощью LinkGenerator.
- Реализуйте Microsoft.AspNetCore.Routing.IOutboundParameterTransformer.
- Настраиваются с помощью ConstraintMap.
- Принимают значение маршрута параметра и изменяют его на новое строковое значение.
- Результатом является использование преобразованного значения в созданной ссылке.
Например, пользовательский преобразователь параметра slugify в шаблоне маршрута blog\{article:slugify} с Url.Action(new { article = "MyTestArticle" }) формирует значение blog\my-test-article.
Рассмотрим следующую реализацию IOutboundParameterTransformer.
public class SlugifyParameterTransformer : IOutboundParameterTransformer
{
public string? TransformOutbound(object? value)
{
if (value is null)
{
return null;
}
return Regex.Replace(
value.ToString()!,
"([a-z])([A-Z])",
"$1-$2",
RegexOptions.CultureInvariant,
TimeSpan.FromMilliseconds(100))
.ToLowerInvariant();
}
}
Чтобы использовать преобразователь параметров в шаблоне маршрута, настройте его с помощью ConstraintMap в Program.cs.
builder.Services.AddRouting(options =>
options.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer));
В ASP.NET Core используются преобразователи параметров для преобразования URI для определения конечной точки. Например, преобразователи параметров преобразуют значения маршрута, используемые для сопоставления area, controller, action и page:
app.MapControllerRoute(
name: "default",
pattern: "{controller:slugify=Home}/{action:slugify=Index}/{id?}");
С помощью предыдущего шаблона маршрута действие SubscriptionManagementController.GetAll сопоставляется с URI /subscription-management/get-all. Преобразователь параметра не изменяет значения маршрута, используемые для формирования ссылки. Например, Url.Action("GetAll", "SubscriptionManagement") выводит /subscription-management/get-all.
ASP.NET Core предоставляет соглашения об API для использования преобразователей параметров со сформированными маршрутами.
- Соглашение Microsoft.AspNetCore.Mvc.ApplicationModels.RouteTokenTransformerConvention MVC применяет указанный преобразователь параметров ко всем маршрутам атрибутов в приложении. Преобразователь параметров преобразует маркеры маршрутов атрибутов по мере их замены. Дополнительные сведения см. в разделе об использовании преобразователя параметров для настройки замены маркеров.
- Razor Pages использует соглашение об API PageRouteTransformerConvention. Это соглашение применяет указанный трансформатор параметров ко всем автоматически обнаруженным страницам Razor. Преобразователь параметров преобразует сегменты папок и имен файлов маршрутов Razor Pages. Дополнительные сведения см. в разделе об использовании преобразователя параметров для настройки маршрутов страниц.
Справочник по формированию URL-адресов
В этом разделе представлен справочник по алгоритму, реализованному при формировании URL-адреса. На практике в большинстве сложных примеров формирования URL-адресов используются контроллеры или Razor страницы. Дополнительные сведения см. в статье Маршрутизация в контроллерах.
Процесс создания URL-адресов начинается с вызова LinkGenerator.GetPathByAddress или аналогичного метода. Метод предоставляется с адресом, набором значений маршрута и при необходимости со сведениями о текущем запросе из HttpContext.
Первым шагом является использование адреса для разрешения набора конечных точек-кандидатов с помощью IEndpointAddressScheme<TAddress>, соответствующих типу адреса.
После того как набор кандидатов найден в схеме адресов, конечные точки упорядочиваются и обрабатываются последовательно до тех пор, пока операция формирования URL-адреса не завершится. При создании URL-адреса не проверяется наличие неоднозначностей, первый возвращенный результат является окончательным результатом.
Устранение неполадок при формировании URL-адресов с помощью ведения журнала
Первым шагом при устранении неполадок при формировании URL-адресов является установка уровня ведения журнала Microsoft.AspNetCore.Routing на TRACE.
LinkGenerator фиксирует в журнале множество сведений об обработке, которые могут быть полезны при устранении неполадок.
Дополнительные сведения о формировании URL-адресов см. в разделе Справочник по формированию URL-адресов.
Addresses
Адреса являются основным понятием в формировании URL-адресов и используются для привязки вызова генератора ссылок к набору конечных точек-кандидатов.
Адреса — это расширяемое понятие, которое по умолчанию поставляется с двумя реализациями.
- Использование имени конечной точки (
string) в качестве адреса:- Обеспечивает аналогичную функциональность, как и имя маршрута в MVC.
- Использует тип метаданных IEndpointNameMetadata.
- Обрабатывает переданную строку в соответствии с метаданными всех зарегистрированных конечных точек.
- Создает исключение при запуске, если несколько конечных точек использует одно и то же имя.
- Рекомендуется для общего использования вне контроллеров и страниц Razor.
- Использование значений маршрутов (RouteValuesAddress) в качестве адреса:
- Предоставляет аналогичную функциональность и для контроллеров, и для Pages в плане формирования устаревших URL-адресов.
- Очень сложно расширять и отлаживать.
- Предоставляет реализацию, используемую
IUrlHelper, вспомогательными функциями тегов, вспомогательными методами HTML, результатами действий и т. д.
Роль схемы адресов заключается в том, чтобы создать связь между адресом и соответствующими конечными точками по произвольным критериям.
- При использовании схемы имен конечных точек выполняется простой поиск по словарю.
- Схема значений маршрута включает сложный алгоритм для нахождения лучшего подмножества.
Значения окружения и явные значения
Из текущего запроса маршрутизация обращается к значениям маршрута текущего запроса HttpContext.Request.RouteValues. Значения, связанные с текущим запросом, называются значениями окружения. В целях ясности в документации подразумеваются значения маршрута, передаваемые в методы как явные значения.
В следующем примере показаны значения окружения и явные значения. Он предоставляет значения окружения из текущего запроса и явные значения:
public class WidgetController : ControllerBase
{
private readonly LinkGenerator _linkGenerator;
public WidgetController(LinkGenerator linkGenerator) =>
_linkGenerator = linkGenerator;
public IActionResult Index()
{
var indexPath = _linkGenerator.GetPathByAction(
HttpContext, values: new { id = 17 })!;
return Content(indexPath);
}
// ...
Предыдущий код:
- Возвращает
/Widget/Index/17. - Получает LinkGenerator через DI.
Следующий код не предоставляет значения окружения, а только явные значения:
var subscribePath = _linkGenerator.GetPathByAction(
"Subscribe", "Home", new { id = 17 })!;
Предыдущий метод возвращает /Home/Subscribe/17
Следующий код в WidgetController возвращает /Widget/Subscribe/17:
var subscribePath = _linkGenerator.GetPathByAction(
HttpContext, "Subscribe", null, new { id = 17 });
Следующий код настраивает контроллер, используя текущие значения окружения в текущем запросе и явные значения.
public class GadgetController : ControllerBase
{
public IActionResult Index() =>
Content(Url.Action("Edit", new { id = 17 })!);
}
В предыдущем коде:
- Возвращается
/Gadget/Edit/17. - Url получает IUrlHelper.
-
Action создает URL-адрес с абсолютным путем для метода действия. URL-адрес содержит указанное имя
actionи значенияroute.
Следующий код предоставляет значения окружения из текущего запроса и явные значения:
public class IndexModel : PageModel
{
public void OnGet()
{
var editUrl = Url.Page("./Edit", new { id = 17 });
// ...
}
}
Приведенный выше код задает для url значение /Edit/17, когда страница Edit Razor содержит следующую директиву:
@page "{id:int}"
Если страница Edit не содержит шаблон маршрута "{id:int}", то url будет /Edit?id=17.
Поведение IUrlHelper MVC добавляет уровень сложности, помимо правил, описанных здесь.
-
IUrlHelperвсегда предоставляет значения маршрута из текущего запроса как значения окружения. -
IUrlHelper.Action всегда копирует текущие значения маршрута
actionиcontrollerкак явные значения, если они не переопределены разработчиком. -
IUrlHelper.Page всегда копирует текущее значение маршрута
pageкак явное значение, если оно не переопределено. -
IUrlHelper.Pageвсегда переопределяет текущее значение маршрутаhandlerнаnullкак явное значение, если его не переопределили.
Пользователи часто удивляются сведениям о поведении значений окружения, поскольку MVC не следует собственным правилам. По историческим причинам и для обеспечения совместимости для некоторых значений маршрута, таких как action, controller, page и handler, предусмотрено собственное поведение в особых случаях.
Аналогичные функции, предоставляемые LinkGenerator.GetPathByAction и LinkGenerator.GetPathByPage, дублируют эти аномалии IUrlHelper для обеспечения совместимости.
Процесс формирования URL-адреса
После обнаружения набора конечных точек-кандидатов алгоритм формирования URL-адресов:
- последовательно обрабатывает конечные точки;
- Возвращает первый успешный результат.
Первый шаг этого процесса называется аннулированием значения маршрута. Аннулирование значения маршрута — это процесс, с помощью которого маршрутизация решает, какие значения маршрута должны использоваться из значений окружения, а какие следует игнорировать. Каждое значение окружения учитывается и либо объединяется с явными значениями, либо игнорируется.
Роль значений окружения заключается в том, что в некоторых распространенных случаях они позволяют сократить для разработчиков объем вводимой информации. Как правило, сценарии, в которых полезно использовать значения окружения, связаны с MVC.
- При связывании с другим действием в том же контроллере не требуется указывать имя контроллера.
- При связывании с другим контроллером в той же области не требуется указывать имя области.
- При связывании с тем же методом действия не требуется указывать значения маршрута.
- При связывании с другой частью приложения следует избегать передачи значений маршрута, не имеющих смысла в этой части приложения.
Вызовы LinkGenerator или IUrlHelper, которые возвращают null, обычно происходят из-за неправильного понимания невалидности значения маршрута. Для устранения неполадок аннулирования значения маршрута явно укажите дополнительные значения маршрута, чтобы определить, устранена ли проблема.
Аннулирование значения маршрута предполагает, что схема URL-адреса приложения является иерархической, в которой иерархия сформирована слева направо. Рассмотрим шаблон маршрута базового контроллера {controller}/{action}/{id?}, чтобы понять, как это работает на практике.
Изменение значения делает недействительными все значения маршрута, которые отображаются справа. Это отражает предположение об иерархии. Если приложение имеет значение окружения для id, а операция указывает другое значение для controller:
-
idне будет использоваться повторно, поскольку{controller}находится слева от{id?}.
Некоторые примеры, демонстрирующие этот принцип
- Если явные значения содержат значение для
id, значение окружения дляidигнорируется. Можно использовать значения окружения дляcontrollerиaction. - Если явные значения содержат значение для
action, любое значение окружения дляactionигнорируется. Можно использовать фоновые значения дляcontroller. Если явное значение дляactionотличается от значения окружения дляaction, значениеidне будет использоваться. Если явное значение дляactionсовпадает со значением окружения дляaction, можно использовать значениеid. - Если явные значения содержат значение для
controller, любое значение окружения дляcontrollerигнорируется. Если явное значение дляcontrollerотличается от значения окружения дляcontroller, значенияactionиidне будут использоваться. Если явное значение дляcontrollerсовпадает со значением окружения дляcontroller, можно использовать значенияactionиid.
Этот процесс усложняется за счет наличия атрибутных маршрутов и традиционных маршрутов. Стандартные маршруты контроллера, такие как {controller}/{action}/{id?}, указывают иерархию с помощью параметров маршрута. Для выделенных стандартных маршрутов и маршрутов атрибутов для контроллеров и Razor Pages:
- Существует иерархия значений маршрута.
- Они не отображаются в шаблоне.
В таких случаях формирование URL-адресов определяет концепцию необходимых значений. Для конечных точек, созданных контроллерами и Razor Pages, указаны обязательные значения, позволяющие использовать аннулирование значений маршрута.
Подробный алгоритм аннулирования значения маршрута
- Имена обязательных значений объединяются с параметрами маршрута, а затем обрабатываются слева направо.
- Для каждого параметра сравниваются внешние значения и явное значение:
- Если значение окружения и явное значение совпадают, процесс продолжается.
- Если значение окружения задано, а явное значение не задано, то при формировании URL-адреса используется значение окружения.
- Если значение окружения отсутствует, а явное значение задано, следует отклонить значение окружения и все последующие значения окружения.
- Если заданы и значение окружения, и явное значение, однако они отличаются, следует отклонить значение окружения и все последующие значения окружения.
На этом этапе операция формирования URL-адреса готова к оценке ограничений маршрута. Набор допустимых значений объединяется со значениями по умолчанию для параметра, предоставляемыми ограничениям. Если все ограничения пройдены, операция продолжается.
Затем допустимые значения можно использовать для расширения шаблона маршрута. Шаблон маршрута обрабатывается:
- Слева направо.
- Для каждого параметра заменяется допустимое значение.
- В следующих особых случаях:
- Если в допустимых значениях отсутствует значение и параметр имеет значение по умолчанию, используется значение по умолчанию.
- Если в допустимых значениях отсутствует значение, а параметр является необязательным, обработка продолжается.
- Если любой параметр маршрута справа от отсутствующего необязательного параметра имеет значение, операция завершается ошибкой.
- Смежные параметры со значениями по умолчанию и необязательные параметры по возможности сворачиваются.
Явно предоставленные значения, которые не соответствуют сегменту маршрута, добавляются в строку запроса. В приведенной ниже таблице показан результат использования шаблона маршрута {controller}/{action}/{id?}.
| Значения окружения | Явные значения | Result |
|---|---|---|
| контроллер = "Home" | action = "О программе" | /Home/About |
| контроллер = "Home" | controller = "Заказ", action = "О" | /Order/About |
| controller = "Home", color = "Красный" | action = "О программе" | /Home/About |
| контроллер = "Home" | action = "О программе", color = "Красный" | /Home/About?color=Red |
Проблемы с недействительностью значений маршрута
В следующем коде показан пример схемы формирования URL-адреса, которая не поддерживается маршрутизацией:
app.MapControllerRoute(
"default",
"{culture}/{controller=Home}/{action=Index}/{id?}");
app.MapControllerRoute(
"blog",
"{culture}/{**slug}",
new { controller = "Blog", action = "ReadPost" });
В приведенном выше коде параметр маршрута culture используется для локализации. Необходимо, чтобы параметр culture всегда принимался как значение окружения. Однако параметр culture не принимается как значение окружения ввиду способа работы требуемых значений.
- В шаблоне маршрута
"default"параметр маршрутаcultureнаходится слева отcontroller, поэтому измененияcontrollerне приведут к аннулированиюculture. - В шаблоне маршрута
"blog"параметр маршрутаcultureрассматривается как находящийся справа отcontroller, который имеется в требуемых значениях.
Анализ пути URL-адреса с помощью LinkParser
Класс LinkParser добавляет поддержку анализа пути URL-адреса в набор значений маршрута. Метод ParsePathByEndpointName принимает имя конечной точки и путь URL-адреса, а также возвращает набор значений маршрута, извлеченных из пути URL-адреса.
В следующем примере контроллера действие GetProduct использует шаблон маршрута api/Products/{id} и содержит параметр Name со значением GetProduct:
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
[HttpGet("{id}", Name = nameof(GetProduct))]
public IActionResult GetProduct(string id)
{
// ...
В том же классе контроллера действие AddRelatedProduct ожидает путь URL-адреса (pathToRelatedProduct), который можно предоставить в качестве параметра строки запроса:
[HttpPost("{id}/Related")]
public IActionResult AddRelatedProduct(
string id, string pathToRelatedProduct, [FromServices] LinkParser linkParser)
{
var routeValues = linkParser.ParsePathByEndpointName(
nameof(GetProduct), pathToRelatedProduct);
var relatedProductId = routeValues?["id"];
// ...
В предыдущем примере действие AddRelatedProduct извлекает значение id маршрута из пути URL-адреса. Например, если указан путь URL-адреса /api/Products/1, для relatedProductId будет задано значение 1. Такой подход позволяет клиентам API использовать пути URL-адресов при обращении к ресурсам, не обладая знаниями в структуре такого URL-адреса.
Настройка метаданных конечной точки
Сведения о настройке метаданных конечной точки см. по следующим ссылкам:
- Включение CORS с маршрутизацией конечных точек
-
Пример IAuthorizationPolicyProvider с использованием настраиваемого атрибута
[MinimumAgeAuthorize] - Тестирование проверки подлинности с использованием атрибута [Authorize]
- RequireAuthorization
- Выбор схемы с использованием атрибута [Authorize]
- Применение политик с использованием атрибута [Authorize]
- Авторизация на основе ролей в ASP.NET Core
Сопоставление хостов при маршрутизации, используя RequireHost
RequireHost применяет ограничение к маршруту, требующее указанный узел. Параметр RequireHost или [Host] может иметь следующее значение:
- Хост:
www.domain.com, соответствуетwww.domain.comс любым портом. - Хост с подстановочным знаком:
*.domain.com, соответствуетwww.domain.com,subdomain.domain.comилиwww.subdomain.domain.comдля любого порта. - Порт:
*:5000, соответствует порту 5000 с любым хостом. - Узел и порт:
www.domain.com:5000или*.domain.com:5000, соответствует узлу и порту.
С помощью RequireHost или [Host] можно указать несколько параметров. Ограничение соответствует хостам, допустимым для любого из параметров. Например, [Host("domain.com", "*.domain.com")] соответствует domain.com, www.domain.com и subdomain.domain.com.
Следующий код использует RequireHost, чтобы требовать указанный хост в маршруте:
app.MapGet("/", () => "Contoso").RequireHost("contoso.com");
app.MapGet("/", () => "AdventureWorks").RequireHost("adventure-works.com");
app.MapHealthChecks("/healthz").RequireHost("*:8080");
Следующий код использует атрибут [Host] в контроллере, чтобы требовать один из указанных хостов.
[Host("contoso.com", "adventure-works.com")]
public class HostsController : Controller
{
public IActionResult Index() =>
View();
[Host("example.com")]
public IActionResult Example() =>
View();
}
Если атрибут [Host] применяется как к контроллеру, так и к методу действия, выполняется следующее.
- Используется атрибут действия.
- Атрибут контроллера не учитывается.
Рекомендации по производительности для маршрутизации
Проблемы с производительностью в приложении зачастую связывали с маршрутизацией. Причиной этому является то, что такие фреймворки, как контроллеры и Razor Pages, сообщают о количестве времени, проведенном внутри фреймворка, в сообщениях журнала. Если время, указанное контроллерами, значительно отличается от общего времени запроса:
- Разработчики исключают код приложения из списка возможных источников проблемы.
- Обычно предполагается, что причиной является маршрутизация.
Для проверки производительности маршрутизации используются тысячи конечных точек. Маловероятно, что обычное приложение столкнется с проблемами с производительностью только лишь из-за того, что оно слишком большое. Наиболее распространенная причина снижения производительности маршрутизации обычно кроется в неправильной работе настраиваемого ПО промежуточного слоя.
В следующем примере кода показан базовый метод сокращения источника задержки.
var logger = app.Services.GetRequiredService<ILogger<Program>>();
app.Use(async (context, next) =>
{
var stopwatch = Stopwatch.StartNew();
await next(context);
stopwatch.Stop();
logger.LogInformation("Time 1: {ElapsedMilliseconds}ms", stopwatch.ElapsedMilliseconds);
});
app.UseRouting();
app.Use(async (context, next) =>
{
var stopwatch = Stopwatch.StartNew();
await next(context);
stopwatch.Stop();
logger.LogInformation("Time 2: {ElapsedMilliseconds}ms", stopwatch.ElapsedMilliseconds);
});
app.UseAuthorization();
app.Use(async (context, next) =>
{
var stopwatch = Stopwatch.StartNew();
await next(context);
stopwatch.Stop();
logger.LogInformation("Time 3: {ElapsedMilliseconds}ms", stopwatch.ElapsedMilliseconds);
});
app.MapGet("/", () => "Timing Test.");
Для маршрутизации времени:
- Чередуйте каждое промежуточное ПО с копией тайминг промежуточного ПО, показанной в предыдущем коде.
- Добавьте уникальный идентификатор для сопоставления данных о времени с кодом.
Это базовый способ сократить задержку, когда она является существенной, например более 10ms. Вычитание Time 2 из Time 1 позволяет получить время, затраченное в промежуточном ПО UseRouting.
Следующий код использует более компактный подход к предыдущему коду времени.
public sealed class AutoStopwatch : IDisposable
{
private readonly ILogger _logger;
private readonly string _message;
private readonly Stopwatch _stopwatch;
private bool _disposed;
public AutoStopwatch(ILogger logger, string message) =>
(_logger, _message, _stopwatch) = (logger, message, Stopwatch.StartNew());
public void Dispose()
{
if (_disposed)
{
return;
}
_logger.LogInformation("{Message}: {ElapsedMilliseconds}ms",
_message, _stopwatch.ElapsedMilliseconds);
_disposed = true;
}
}
var logger = app.Services.GetRequiredService<ILogger<Program>>();
var timerCount = 0;
app.Use(async (context, next) =>
{
using (new AutoStopwatch(logger, $"Time {++timerCount}"))
{
await next(context);
}
});
app.UseRouting();
app.Use(async (context, next) =>
{
using (new AutoStopwatch(logger, $"Time {++timerCount}"))
{
await next(context);
}
});
app.UseAuthorization();
app.Use(async (context, next) =>
{
using (new AutoStopwatch(logger, $"Time {++timerCount}"))
{
await next(context);
}
});
app.MapGet("/", () => "Timing Test.");
Потенциально ресурсоемкие функции маршрутизации
В списке ниже представлены некоторые сведения о функциях маршрутизации, которые относительно ресурсоемкие по сравнению с базовыми шаблонами маршрутов.
- Регулярные выражения: можно составить сложные регулярные выражения, которые будут выполняться долгое время с небольшим количеством входных данных.
- Сложные сегменты (
{x}-{y}-{z}):- значительно более ресурсоемкие, чем анализ обычного сегмента URL-пути.
- В результате выделяется множество дополнительных подстрок.
- Синхронный доступ к данным: многие сложные приложения имеют доступ к базам данных в рамках их маршрутизации. Используйте точки расширения, такие как MatcherPolicy и EndpointSelectorContext, которые являются асинхронными.
Руководство по большим таблицам маршрутизации
По умолчанию алгоритм маршрутизации в ASP.NET Core жертвует объемом памяти в пользу низкой нагрузки на ЦП. Это приводит к тому, что время, затрачиваемое на сопоставление маршрутов, зависит только от длины сопоставляемого пути, а не от количества маршрутов. Однако такой подход создает проблемы в тех случаях, когда приложение использует большое количество (несколько тысяч) маршрутов с большим числом переменных префиксов. Например, если в маршрутах используются параметры в ранних сегментах маршрута, как {parameter}/some/literal.
Вряд ли приложение столкнется с такой проблемой, если только:
- в приложении много маршрутов, использующих такой шаблон;
- В приложении очень много маршрутов.
Как определить, возникает ли у приложения проблема большой таблицы маршрутов
- Есть два симптома, на которые стоит обратить внимание:
- При первом запросе приложение запускается медленно.
- Обратите внимание, что это необходимо, но недостаточно. Медленный запуск приложения могут вызывать многие другие проблемы, не связанные с маршрутизацией. Чтобы точно определить, что приложение сталкивается с этой ситуацией, проверьте следующее условие.
- Приложение потребляет много памяти во время запуска, а в дампе памяти отображается большое количество экземпляров
Microsoft.AspNetCore.Routing.Matching.DfaNode.
- При первом запросе приложение запускается медленно.
Способы решения этой проблемы
Есть несколько разных методов и оптимизаций, которые можно применить к маршрутам для значительного улучшения этой ситуации:
- Везде, где это возможно, примените к параметрам ограничения маршрутов, например
{parameter:int},{parameter:guid},{parameter:regex(\\d+)}и т. д.- Это позволяет алгоритму маршрутизации внутренним образом оптимизировать структуры, которые используются для сопоставления, и радикально снизить использование памяти.
- В подавляющем большинстве случаев этого будет достаточно для восстановления приемлемой производительности.
- Измените маршруты, чтобы переместить параметры в более поздние сегменты шаблона.
- Это сокращает количество возможных "путей", с которыми придется сопоставлять конечную точку по определенному пути.
- Используйте динамический маршрут и динамическое сопоставление с контроллером или страницей.
- Это можно сделать с помощью
MapDynamicControllerRouteиMapDynamicPageRoute.
- Это можно сделать с помощью
Руководство для авторов библиотек
В этом разделе содержатся рекомендации для авторов библиотек, создающих библиотеки на основе маршрутизации. Эти сведения предназначены для предоставления разработчикам приложений сведений об эффективном использовании библиотек и платформ, расширяющих маршрутизацию.
Определение конечных точек
Чтобы создать платформу, использующую маршрутизацию для сопоставления URL-адресов, начните с определения пользовательского интерфейса, который строится поверх UseEndpoints.
ВЫПОЛНИТЕ сборку поверх IEndpointRouteBuilder. Это позволит пользователям создать инфраструктуру с другими функциями ASP.NET Core без путаницы. Каждый шаблон ASP.NET Core включает в себя маршрутизацию. Предположим, что маршрутизация имеется и пользователи знакомы с ней.
// Your framework
app.MapMyFramework(...);
app.MapHealthChecks("/healthz");
ВЕРНИТЕ конкретный запечатанный тип из вызова объекта MapMyFramework(...), который реализует IEndpointConventionBuilder. Большинство методов Map... платформы соответствует этому шаблону. Интерфейс IEndpointConventionBuilder:
- Позволяет составить метаданные.
- Предназначен для различных методов расширения.
Объявление собственного типа позволяет добавлять в построитель собственные функции для конкретной платформы. Можно обернуть построитель, объявленный во фреймворке, и перенаправлять вызовы в него.
// Your framework
app.MapMyFramework(...)
.RequireAuthorization()
.WithMyFrameworkFeature(awesome: true);
app.MapHealthChecks("/healthz");
Рассмотрите возможность создания собственного EndpointDataSource.
EndpointDataSource — это низкоуровневый примитив для объявления и обновления коллекции конечных точек.
EndpointDataSource — это эффективный API, используемый контроллерами и Razor Pages.
В тестах маршрутизации имеется простой пример источника данных, который не обновляется.
НЕ пытайтесь зарегистрировать EndpointDataSource по умолчанию. Требуйте от пользователей, чтобы они регистрировали вашу платформу в UseEndpoints. Философия маршрутизации заключается в том, что по умолчанию ничего не включено и UseEndpoints представляет собой место для регистрации конечных точек.
Создание ПО промежуточного слоя со встроенной маршрутизацией
РАССМОТРИТЕ ВОЗМОЖНОСТЬ определения типов метаданных в качестве интерфейса.
СДЕЛАЙТЕ возможным использование типов метаданных в качестве атрибута в классах и методах.
public interface ICoolMetadata
{
bool IsCool { get; }
}
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class CoolMetadataAttribute : Attribute, ICoolMetadata
{
public bool IsCool => true;
}
Платформы, такие как контроллеры и Razor Pages, поддерживают применение атрибутов метаданных к типам и методам. При объявлении типов метаданных:
- Сделайте их доступными в качестве атрибутов.
- Большинство пользователей знакомы с применением атрибутов.
Объявление типа метаданных в качестве интерфейса добавляет еще один уровень гибкости.
- Интерфейсы можно комбинировать.
- Разработчики могут объявлять собственные типы, объединяющие несколько политик.
СДЕЛАЙТЕ возможным переопределение метаданных, как показано в следующем примере.
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class SuppressCoolMetadataAttribute : Attribute, ICoolMetadata
{
public bool IsCool => false;
}
[CoolMetadata]
public class MyController : Controller
{
public void MyCool() { }
[SuppressCoolMetadata]
public void Uncool() { }
}
Следуйте этим рекомендациям, чтобы избежать определения метаданных маркера.
- Не ограничивайтесь поиском типа метаданных.
- Определите свойство метаданных и проверьте его.
Коллекция метаданных является упорядоченной и поддерживает переопределение приоритета. В случае с контроллерами метаданные в методе действия являются наиболее специфичными.
СДЕЛАЙТЕ промежуточное ПО полезным как с маршрутизацией, так и без нее:
app.UseAuthorization(new AuthorizationPolicy() { ... });
// Your framework
app.MapMyFramework(...).RequireAuthorization();
В качестве примера этой рекомендации рассмотрим ПО промежуточного слоя UseAuthorization. Промежуточное ПО авторизации позволяет задать резервную политику.
Политика отката, если она указана, применяется к обоим элементам:
- конечные точки без указанной политики;
- запросы, которые не соответствуют конечной точке.
Это сделает ПО промежуточного слоя авторизации полезным вне контекста маршрутизации. Промежуточное ПО авторизации можно использовать для традиционного программирования middleware.
Диагностика отладки
Для подробного вывода диагностики построения маршрутов задайте для Logging:LogLevel:Microsoft значение Debug. В среде Development установите уровень журнала в appsettings.Development.json:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Debug",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}
Дополнительные ресурсы
Маршрутизация обеспечивает сопоставление входящих HTTP-запросов и их распределение по исполняемым конечным точкам приложения. Конечные точки — это блоки исполняемого кода обработки запросов приложения. Конечные точки определяются в приложении и настраиваются при его запуске. Процесс сопоставления конечных точек может извлекать значения из URL-адреса запроса и предоставлять эти значения для обработки запроса. С помощью сведений о конечных точках из приложения маршрутизация также может формировать URL-адреса, которые сопоставляются с конечными точками.
Приложения могут настраивать маршрутизацию с помощью следующих методов:.
- Controllers
- Razor Страницы
- SignalR
- Службы gRPC
- Промежуточное ПО с поддержкой конечных точек, например проверки работоспособности.
- Делегаты и лямбда-выражения, зарегистрированные для маршрутизации
В этом документе представлены сведения о низкоуровневой маршрутизации ASP.NET Core. Дополнительные сведения о настройке маршрутизации
- Сведения о контроллерах см. в статье Маршрутизация к действиям контроллера в ASP.NET Core.
- Соглашения для Razor Pages см. в статье Соглашения для маршрутов и приложений Razor Pages в ASP.NET Core.
Система маршрутизации конечных точек, описанная в этом документе, применяется к ASP.NET Core 3.0 или более поздней версии. Чтобы получить сведения о предыдущей системе маршрутизации на основе IRouter, выберите версию ASP.NET Core 2.1, используя один из следующих методов.
- Селектор версий для выбора предыдущей версии.
- Выберите маршрутизацию ASP.NET Core 2.1.
Просмотреть или скачать образец кода (описание загрузки)
Образцы для загрузки этого документа включены определённым классом Startup. Чтобы выполнить конкретный пример, измените Program.cs для вызова нужного класса Startup.
Основы маршрутизации
Все шаблоны ASP.NET Core включают маршрутизацию в созданном коде. Маршрутизация регистрируется в конвейере промежуточного программного обеспечения в Startup.Configure.
В следующем коде приведен базовый пример маршрутизации.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapGet("/", async context =>
{
await context.Response.WriteAsync("Hello World!");
});
});
}
Маршрутизация использует пару промежуточного ПО, зарегистрированную с помощью UseRouting и UseEndpoints.
-
UseRoutingдобавляет соответствие маршрута в конвейер ПО промежуточного слоя. Это ПО промежуточного слоя обращается к набору конечных точек, определенных в приложении, и выбирает наиболее подходящее на основе запроса. -
UseEndpointsдобавляет выполнение конечной точки в конвейер обработки промежуточного ПО. Он запускает делегат, связанный с выбранной конечной точкой.
В предыдущем примере имеется один маршрут к конечной точке кода с помощью метода MapGet.
- При отправке HTTP-запроса
GETв корневой URL-адрес/:- Данный делегат запроса выполняется.
- В ответ HTTP записывается
Hello World!. Корневой URL-адрес/по умолчанию —https://localhost:5001/.
- Если метод запроса не является
GETили если корневой URL-адрес не/, сопоставление маршрута не выполняется и возвращается сообщение об ошибке HTTP 404.
Endpoint
Для определения конечной точки используется метод MapGet. Конечная точка — это объект, который может быть:
- Выбирается путем сопоставления URL-адреса и метода HTTP.
- Выполняется путем использования запуска делегата.
Конечные точки, которые могут быть сопоставлены и выполнены приложением, настраиваются в UseEndpoints. Например, MapGet, MapPost и аналогичные методы подключают делегаты запросов к системе маршрутизации. Для подключения функций платформы ASP.NET Core к системе маршрутизации можно использовать дополнительные методы.
- MapRazorPages для страниц Razor
- MapControllers для контроллеров
- MapHub<THub> для SignalR
- MapGrpcService<TService> для gRPC
Ниже представлен пример маршрутизации с более сложным шаблоном маршрута.
app.UseEndpoints(endpoints =>
{
endpoints.MapGet("/hello/{name:alpha}", async context =>
{
var name = context.Request.RouteValues["name"];
await context.Response.WriteAsync($"Hello {name}!");
});
});
Строка /hello/{name:alpha} является шаблоном маршрута. Она используется для настройки способа сопоставления конечной точки. В этом случае шаблон соответствует следующим условиям.
- URL-адрес, подобный
/hello/Ryan - Любой URL-путь, начинающийся с
/hello/,после которого следует набор буквенных символов.:alphaприменяет ограничение маршрута, которое соответствует только буквенным символам. Ограничения маршрута описаны далее в этом документе.
Второй сегмент URL-пути, {name:alpha}:
- привязан к параметру
name; - записывается и сохраняется в HttpRequest.RouteValues.
Система маршрутизации конечных точек, описанная в этом документе, является новой и применяется, начиная с ASP.NET Core версии 3.0 и более поздней. Однако все версии ASP.NET Core поддерживают один и тот же набор функций шаблона маршрута и ограничений маршрута.
В следующем примере показана маршрутизация с проверками работоспособности и авторизацией.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
// Matches request to an endpoint.
app.UseRouting();
// Endpoint aware middleware.
// Middleware can use metadata from the matched endpoint.
app.UseAuthentication();
app.UseAuthorization();
// Execute the matched endpoint.
app.UseEndpoints(endpoints =>
{
// Configure the Health Check endpoint and require an authorized user.
endpoints.MapHealthChecks("/healthz").RequireAuthorization();
// Configure another endpoint, no authorization requirements.
endpoints.MapGet("/", async context =>
{
await context.Response.WriteAsync("Hello World!");
});
});
}
В предыдущем примере, показано то, как:
- Промежуточное программное обеспечение для авторизации можно использовать вместе с маршрутизацией.
- можно использовать конечные точки для настройки режима авторизации.
Вызов MapHealthChecks добавляет конечную точку проверки состояния. Связывание RequireAuthorization с этим вызовом прикрепляет политику авторизации к конечной точке.
При вызове UseAuthentication и UseAuthorization добавляется ПО промежуточного слоя для проверки подлинности и авторизации. Эти middleware размещаются между UseRouting и UseEndpoints, чтобы они могли:
- просматривать, какая конечная точка выбрана методом
UseRouting; - применять политику авторизации до отправки UseEndpoints на конечную точку.
Метаданные конечной точки
В предыдущем примере существуют две конечные точки, но политика авторизации прикреплена только к конечной точке проверки состояния. Если запрос соответствует конечной точке проверки работоспособности, /healthz, выполняется проверка авторизации. Это служит демонстрацией того, что к конечным точкам можно прикреплять дополнительные данные. Эти дополнительные данные называются метаданными конечных точек.
- Метаданные могут обрабатываться ПО промежуточного слоя с поддержкой маршрутизации.
- Метаданные могут быть любого типа .NET.
Основные понятия маршрутизации
Система маршрутизации формируется поверх конвейера промежуточного программного обеспечения путем добавления мощного понятия конечных точек. Конечные точки представляют собой единицы функциональности приложения, отличающиеся друг от друга в плане маршрутизации, авторизации и количества систем ASP.NET Core.
Определение конечной точки ASP.NET Core
Конечная точка ASP.NET Core:
- Исполняемый: имеет RequestDelegate.
- Расширяемо: имеет коллекцию метаданных .
- Доступный вариант: при необходимости содержит сведения о маршрутизации.
- Перечисляемые: коллекцию конечных точек можно перечислить, извлекая EndpointDataSource из DI.
В следующем примере кода показано, как получить и проверить конечную точку, соответствующую текущему запросу.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.Use(next => context =>
{
var endpoint = context.GetEndpoint();
if (endpoint is null)
{
return Task.CompletedTask;
}
Console.WriteLine($"Endpoint: {endpoint.DisplayName}");
if (endpoint is RouteEndpoint routeEndpoint)
{
Console.WriteLine("Endpoint has route pattern: " +
routeEndpoint.RoutePattern.RawText);
}
foreach (var metadata in endpoint.Metadata)
{
Console.WriteLine($"Endpoint has metadata: {metadata}");
}
return Task.CompletedTask;
});
app.UseEndpoints(endpoints =>
{
endpoints.MapGet("/", async context =>
{
await context.Response.WriteAsync("Hello World!");
});
});
}
Конечную точку, если она выбрана, можно получить из HttpContext. Ее свойства можно проверить. Объекты конечных точек являются неизменяемыми, и их невозможно изменить после создания. Наиболее распространенным типом конечной точки является RouteEndpoint.
RouteEndpoint содержит сведения, позволяющие системе маршрутизации выбрать эту конечную точку.
В приведенном выше коде app.Use настраивает встроенное ПО промежуточного слоя.
В следующем коде показано, что в зависимости от того, где в конвейере вызывается app.Use, может не быть конечной точки.
// Location 1: before routing runs, endpoint is always null here
app.Use(next => context =>
{
Console.WriteLine($"1. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
return next(context);
});
app.UseRouting();
// Location 2: after routing runs, endpoint will be non-null if routing found a match
app.Use(next => context =>
{
Console.WriteLine($"2. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
return next(context);
});
app.UseEndpoints(endpoints =>
{
// Location 3: runs when this endpoint matches
endpoints.MapGet("/", context =>
{
Console.WriteLine(
$"3. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
return Task.CompletedTask;
}).WithDisplayName("Hello");
});
// Location 4: runs after UseEndpoints - will only run if there was no match
app.Use(next => context =>
{
Console.WriteLine($"4. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
return next(context);
});
В предыдущем примере добавляются инструкции Console.WriteLine, которые показывают, выбрана ли конечная точка. Для ясности в примере указанной конечной точке / назначается отображаемое имя.
При выполнении этого кода с URL-адресом / отображается следующее.
1. Endpoint: (null)
2. Endpoint: Hello
3. Endpoint: Hello
При выполнении этого кода с любым другим URL-адресом отображается следующее.
1. Endpoint: (null)
2. Endpoint: (null)
4. Endpoint: (null)
В этом выводе показано следующее.
- Перед вызовом
UseRoutingконечная точка всегда имеет значение NULL. - Если найдено совпадение, конечная точка отличается от NULL между
UseRoutingи UseEndpoints. - ПО промежуточного слоя
UseEndpointsявляется терминальным при обнаружении соответствия. Определение терминального ПО промежуточного слоя приведено далее в этом документе. - Промежуточное ПО после
UseEndpointsвыполняется только в случае, если совпадения не найдены.
ПО промежуточного слоя UseRouting использует метод SetEndpoint, чтобы присоединить конечную точку к текущему контексту. ПО промежуточного слоя UseRouting можно заменить на настраиваемую логику и по-прежнему использовать конечные точки. Конечные точки — это низкоуровневые примитивы, такие как ПО промежуточного слоя, которые не связаны с реализацией маршрутизации. В большинстве приложений метод UseRouting не требуется заменять настраиваемой логикой.
ПО промежуточного слоя UseEndpoints предназначено для использования совместно с ПО промежуточного слоя UseRouting. Основная логика для выполнения конечной точки достаточно проста. Чтобы получить конечную точку, используйте GetEndpoint, а затем вызовите ее свойство RequestDelegate.
В следующем примере кода показано, как ПО промежуточного слоя может влиять на маршрутизацию или реагировать на нее.
public class IntegratedMiddlewareStartup
{
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
// Location 1: Before routing runs. Can influence request before routing runs.
app.UseHttpMethodOverride();
app.UseRouting();
// Location 2: After routing runs. Middleware can match based on metadata.
app.Use(next => context =>
{
var endpoint = context.GetEndpoint();
if (endpoint?.Metadata.GetMetadata<AuditPolicyAttribute>()?.NeedsAudit
== true)
{
Console.WriteLine($"ACCESS TO SENSITIVE DATA AT: {DateTime.UtcNow}");
}
return next(context);
});
app.UseEndpoints(endpoints =>
{
endpoints.MapGet("/", async context =>
{
await context.Response.WriteAsync("Hello world!");
});
// Using metadata to configure the audit policy.
endpoints.MapGet("/sensitive", async context =>
{
await context.Response.WriteAsync("sensitive data");
})
.WithMetadata(new AuditPolicyAttribute(needsAudit: true));
});
}
}
public class AuditPolicyAttribute : Attribute
{
public AuditPolicyAttribute(bool needsAudit)
{
NeedsAudit = needsAudit;
}
public bool NeedsAudit { get; }
}
В предыдущем примере показаны два важных основных понятия.
- Промежуточное ПО может работать до
UseRoutingчтобы изменить данные, обрабатываемые маршрутизацией.- Обычно ПО промежуточного слоя, отображаемое перед маршрутизацией, изменяет некоторое свойство запроса, например UseRewriter, UseHttpMethodOverride или UsePathBase.
- ПО промежуточного слоя может выполняться между
UseRoutingи UseEndpoints для обработки результатов маршрутизации до выполнения конечной точки.- ПО промежуточного слоя, которое выполняется между
UseRoutingиUseEndpoints:- Обычно проверяет метаданные для получения представления о конечных точках.
- Зачастую принимает решения по обеспечению безопасности, как это делают
UseAuthorizationиUseCors.
- Сочетание ПО промежуточного слоя и метаданных позволяет настраивать политики для каждой конечной точки.
- ПО промежуточного слоя, которое выполняется между
В приведенном выше коде показан пример настраиваемого ПО промежуточного слоя, поддерживающего политики для каждой конечной точки. ПО промежуточного слоя записывает в консоль журнал аудита доступа к конфиденциальным данным. Промежуточное ПО можно настроить для выполнения аудита конечной точки с помощью метаданных AuditPolicyAttribute. В этом примере демонстрируется паттерн подписки, когда аудит выполняется только для конечных точек, помеченных как чувствительные. Эту логику можно определить в обратном порядке для аудита всего, что не отмечено, например, как надежное. Система метаданных конечной точки обеспечивает гибкость. Эту логику можно настроить под любой требуемый вариант использования.
Предыдущий пример кода предназначен для демонстрации основных понятий конечных точек. Он не предназначен для использования в рабочей среде. Более полная версия промежуточного программного обеспечения для журнала аудита будет:
- Логировать в файл или базу данных.
- Включает такие сведения, как информация о пользователе, IP-адресе, имени конфиденциальной конечной точки и многое другое.
Метаданные политики аудита AuditPolicyAttribute определены как Attribute, чтобы их было проще использовать в платформах на основе классов, таких как контроллеры и SignalR. При использовании маршрута к коду:
- Метаданные присоединяются к API-интерфейсу построителя.
- При создании конечных точек платформы на основе классов включают все атрибуты в соответствующем методе и классе.
Типы метаданных рекомендуется определить либо как интерфейсы, либо как атрибуты. Интерфейсы и атрибуты допускают повторное использование кода. Система метаданных является гибкой и не накладывает никаких ограничений.
Сравнение терминального промежуточного ПО и маршрутизации
В следующем примере кода использование ПО промежуточного слоя сравнивается с маршрутизацией.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
// Approach 1: Writing a terminal middleware.
app.Use(next => async context =>
{
if (context.Request.Path == "/")
{
await context.Response.WriteAsync("Hello terminal middleware!");
return;
}
await next(context);
});
app.UseRouting();
app.UseEndpoints(endpoints =>
{
// Approach 2: Using routing.
endpoints.MapGet("/Movie", async context =>
{
await context.Response.WriteAsync("Hello routing!");
});
});
}
Стиль ПО промежуточного слоя, который показан в разделе Approach 1:, — терминальное ПО промежуточного слоя. ПО промежуточного слоя называется терминальным, поскольку выполняет операцию сопоставления.
- Операция сопоставления в предыдущем примере —
Path == "/"для ПО промежуточного слоя иPath == "/Movie"для маршрутизации. - Если сопоставление выполнено успешно, оно выполняет некоторые функции и возвращает результат, а не вызывает ПО промежуточного слоя
next.
Оно называется терминальным, поскольку завершает поиск, выполняет некоторые функции, а затем возвращает результат.
Сравнение терминального промежуточного ПО и маршрутизации
- Оба подхода позволяют завершить конвейер обработки.
- ПО промежуточного слоя завершает конвейер, возвращая вместо вызова
next. - Конечные точки всегда являются терминальными.
- ПО промежуточного слоя завершает конвейер, возвращая вместо вызова
- Промежуточное ПО терминала позволяет разместить его в любом месте конвейера.
- Конечные точки выполняются в позиции UseEndpoints.
- Промежуточное программное обеспечение терминала позволяет произвольному коду определить, когда программное обеспечение соответствует.
- Настраиваемый код сопоставления маршрутов может быть подробным и сложным для корректной записи.
- Маршрутизация обеспечивает простые решения для обычных приложений. Большинству приложений не требуется настраиваемый код сопоставления маршрутов.
- Конечные точки взаимодействуют с ПО промежуточного слоя, таким как
UseAuthorizationиUseCors.- Использование терминального ПО промежуточного слоя с
UseAuthorizationилиUseCorsтребует взаимодействия вручную с системой авторизации.
- Использование терминального ПО промежуточного слоя с
Конечная точка определяет и то, и другое:
- делегат для обработки запросов;
- коллекцию произвольных метаданных. Метаданные используются для реализации аспектов, пронизывающих архитектуру, на основе политик и конфигурации, присоединённой к каждой конечной точке.
Программное обеспечение промежуточного уровня может быть эффективным средством, но может потребовать:
- значительный объем кода и тестирования;
- интеграция вручную с другими системами для достижения желаемого уровня гибкости.
Прежде чем создавать терминальное ПО промежуточного слоя, рассмотрите возможность интеграции с маршрутизацией.
Существующее терминальное ПО промежуточного слоя, которое интегрируется с Map или MapWhen, обычно может быть преобразовано в конечную точку, осведомленную о маршрутизации. MapHealthChecks демонстрирует шаблон для маршрутизирующего программного обеспечения.
- Напишите метод расширения в IEndpointRouteBuilder.
- Создайте вложенный конвейер middleware с помощью CreateApplicationBuilder.
- Присоедините промежуточное программное обеспечение к новому конвейеру. В этом случае — UseHealthChecks.
- Build конвейер промежуточного программного обеспечения в RequestDelegate.
- Вызовите
Mapи укажите новый посреднический программный конвейер. - Верните объект построителя, который предоставлен
Map, из метода расширения.
В следующем коде показано использование MapHealthChecks.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
// Matches request to an endpoint.
app.UseRouting();
// Endpoint aware middleware.
// Middleware can use metadata from the matched endpoint.
app.UseAuthentication();
app.UseAuthorization();
// Execute the matched endpoint.
app.UseEndpoints(endpoints =>
{
// Configure the Health Check endpoint and require an authorized user.
endpoints.MapHealthChecks("/healthz").RequireAuthorization();
// Configure another endpoint, no authorization requirements.
endpoints.MapGet("/", async context =>
{
await context.Response.WriteAsync("Hello World!");
});
});
}
В предыдущем примере показано, почему возврат объекта строителя имеет большое значение. Возврат объекта построителя позволяет разработчику приложения настраивать политики, такие как авторизация для конечной точки. В этом примере промежуточное ПО для проверки состояния не имеет прямой интеграции с системой авторизации.
Для решения проблем, с которыми столкнулись авторы расширяемости при использовании программного обеспечения промежуточного слоя терминалов, была создана система метаданных. Для каждого ПО промежуточного слоя достаточно проблематично реализовать собственную интеграцию с системой авторизации.
Сопоставление URL-адресов
- Это процесс, с помощью которого функция маршрутизации сопоставляет входящий запрос в конечную точку.
- Он основан на данных из пути URL и заголовков.
- Его можно расширить, чтобы учесть любые данные в запросе.
При выполнении ПО промежуточного слоя маршрутизации оно задает конечную точку (Endpoint) и значения маршрута для функции запроса в HttpContext из текущего запроса.
- Вызов HttpContext.GetEndpoint получает конечную точку.
-
HttpRequest.RouteValuesполучает коллекцию значений маршрута.
Middleware выполняется после того, как маршрутизационное ПО может проверить конечную точку и совершить действия. Например, промежуточное ПО авторизации может опрашивать коллекцию метаданных конечной точки для получения политики авторизации. После выполнения всех компонентов промежуточного программного обеспечения в конвейере обработки запросов вызывается делегат выбранной конечной точки.
Система маршрутизации в контексте маршрутизации по конечным точкам отвечает за все решения по управлению маршрутизацией. Поскольку ПО промежуточного слоя применяет политики, основанные на выбранной конечной точке, важно следующее.
- Любые решения, которые могут повлиять на распределение или применение политик безопасности, должны выполняться внутри системы маршрутизации.
Warning
Для обеспечения обратной совместимости при выполнении делегата конечной точки контроллера или Razor страницы свойства RouteContext.RouteData задаются соответствующими значениями на основе обработки запросов, выполняемой до сих пор.
В следующем выпуске тип RouteContext будет помечен как устаревший.
- Перенесите
RouteData.ValuesвHttpRequest.RouteValues. - Мигрируйте
RouteData.DataTokens, чтобы получить IDataTokensMetadata из метаданных конечной точки.
Работа сопоставления URL-адресов подразделяется на этапы, которые можно настроить. На каждом этапе выходные данные представляют собой набор совпадений. По мере перехода между этапами набор совпадений можно ограничивать. Реализация маршрутизации не гарантирует порядок обработки для соответствующих конечных точек. Все возможные совпадения обрабатываются одновременно. Этапы сопоставления URL-адресов выполняются в следующем порядке. ASP.NET Core:
- URL-путь обрабатывается по набору конечных точек и их шаблонов маршрутов, при этом выполняется сбор всех совпадений.
- Берется предыдущий список и удаляются совпадения, которые не соответствуют примененным ограничениям маршрута.
- Берется предыдущий список, из него удаляются совпадения, которые не соответствуют группе экземпляров MatcherPolicy.
- Использование EndpointSelector для принятия окончательного решения из предыдущего списка.
Список конечных точек определяется по приоритету в соответствии со следующим.
Все соответствующие конечные точки обрабатываются на каждом этапе до тех пор, пока не будет достигнут EndpointSelector.
EndpointSelector — это заключительный этап. Он выбирает конечную точку с наивысшим приоритетом из совпадений как наилучшее соответствие. При наличии других совпадений с таким же приоритетом, как у наилучшего соответствия, выбрасывается исключение неоднозначного соответствия.
Приоритет маршрута вычисляется на основе более определенного шаблона маршрута, которому назначается более высокий приоритет. Например, рассмотрим шаблоны /hello и /{message}.
- Оба соответствуют URL-пути
/hello. -
/helloявляется более конкретным, и, следовательно, ему назначается более высокий приоритет.
Как правило, приоритет маршрута помогает выбрать наилучшее соответствие для типов схем URL-адресов, используемых на практике. Используйте Order только в случае, когда необходимо избежать неоднозначности.
Ввиду различных типов расширяемости, предоставляемых службой маршрутизации, система маршрутизации не может заранее вычислить неоднозначные маршруты. Рассмотрим в качестве примера шаблоны маршрутов /{message:alpha} и /{message:int}.
- Ограничение
alphaсоответствует только буквенным символам. - Ограничение
intсоответствует только числам. - Эти шаблоны имеют одинаковый приоритет маршрута, однако не существует одного URL-адреса, по которому они совпадают.
- Если система маршрутизации сообщила об ошибке неоднозначности при запуске, это означает, что она заблокировала этот допустимый вариант использования.
Warning
Порядок операций в UseEndpoints не влияет на поведение маршрутизации, за одним исключением. MapControllerRoute и MapAreaRoute автоматически присваивают значение порядка своим конечным точкам в соответствии с порядком их вызова. Это имитирует поведение контроллеров без системы маршрутизации в долгосрочной перспективе, предоставляя те же гарантии, что и в старых реализациях маршрутизации.
В старой реализации маршрутизации можно реализовать расширяемость маршрутизации, которая зависит от порядка обработки маршрутов. Маршрутизация конечных точек в ASP.NET Core 3.0 или более поздней версии:
- Не имеет концепции маршрутов.
- Не гарантирует порядок обработки. Все конечные точки обрабатываются одновременно.
Приоритет шаблонов маршрутов и порядок выбора конечных точек
Приоритет шаблонов маршрутов — это система, которая назначает каждому шаблону маршрута значение в зависимости от того, насколько он является конкретным. Приоритет шаблона маршрута
- Позволяет избежать необходимости настраивать порядок конечных точек в общих случаях.
- Пытается соответствовать общепринятым ожиданиям относительно поведения маршрутизации.
Например, рассмотрим шаблоны /Products/List и /Products/{id}. Разумно предположить, что /Products/List является лучшим соответствием для URL-пути /Products/List, чем /Products/{id}. Литеральный сегмент /List считается более приоритетным, чем сегмент параметров /{id}.
Порядок определения приоритета связан с порядком определения шаблонов маршрутов.
- Шаблоны с большим количеством сегментов считаются более конкретными.
- Сегмент с литеральным текстом считается более конкретным, чем сегмент параметров.
- Сегмент параметров с ограничением считается более конкретным, чем сегмент без него.
- Сложный сегмент рассматривается как такой же специфичный, как сегмент параметра с ограничением.
- Универсальные параметры являются наименее конкретными. Важные сведения об универсальных маршрутах см. в разделе об универсальных параметрах в справочнике по шаблонам маршрута.
Справка по точным значениям приведена в исходном коде в GitHub.
Основные понятия формирования URL-адресов
Создание URL-адресов:
- Это процесс, благодаря которому маршрутизация может формировать путь URL-адреса на основе набора значений маршрута.
- Обеспечивает логическое разделение конечных точек и URL-адресов, по которым к ним осуществляется доступ.
Маршрутизация конечных точек включает API LinkGenerator.
LinkGenerator — это одноэлементная служба, доступная в DI. API LinkGenerator можно использовать вне контекста выполнения запроса.
Mvc.IUrlHelper и сценарии, которые зависят от IUrlHelper, такие как вспомогательные функции тегов, вспомогательные методы HTML и результаты действий, используют API LinkGenerator для предоставления возможностей создания ссылок.
Генератор ссылок использует концепции адреса и схем адресов. Схема адресов — это способ определения конечных точек, которые должны рассматриваться для создания ссылки. Например, сценарии с именем маршрута и значениями маршрута, с которыми многие пользователи знакомы по контроллерам и Razor Pages, реализуются как схема адресов.
Генератор ссылок может установить связь с контроллерами и Razor Pages с помощью следующих методов расширения.
Перегрузки этих методов принимают аргументы, которые включают HttpContext. Эти методы являются функциональными эквивалентами Url.Action и Url.Page, но предлагают дополнительную гибкость и параметры.
Методы GetPath* наиболее схожи с Url.Action и Url.Page в том, что создают URI, содержащий абсолютный путь. Методы GetUri* всегда создают абсолютный URI, содержащий схему и хост. Методы, которые принимают HttpContext, создают URI в контексте выполнения запроса. Используются значения окружения маршрута, базовый URL-адрес, схема и узел из выполняющегося запроса, если не указано иное.
LinkGenerator вызывается с адресом. Создание URI происходит в два этапа:
- Адрес привязан к списку конечных точек, соответствующих адресу.
- RoutePattern конечной точки вычисляется, пока не будет найден шаблон маршрута, который соответствует предоставленным значениям. Полученный результат объединяется с другими частями URI, предоставленными генератору ссылок и возвращенными.
Методы, предоставляемые LinkGenerator, поддерживают стандартные возможности создания ссылки для любого типа адреса. Самый удобный способ использовать генератор ссылки — через методы расширения, которые выполняют операции для определенного типа адреса.
| Метод расширения | Description |
|---|---|
| GetPathByAddress | Создает URI с абсолютным путем на основе предоставленных значений. |
| GetUriByAddress | Создает абсолютный URI на основе предоставленных значений. |
Warning
Обратите внимание на следующие последствия вызова методов LinkGenerator:
Используйте методы расширения
GetUri*с осторожностью в конфигурации приложения, которая не проверяет заголовок входящих запросовHost. Если заголовок входящих запросовHostне проверен, недоверенные входные данные запроса могут быть отправлены обратно клиенту в URI в виде или на странице. Рекомендуется, чтобы все рабочие приложения настраивали свой сервер на проверку заголовкаHostотносительно известных допустимых значений.Используйте LinkGenerator с осторожностью в ПО промежуточного слоя в сочетании с
MapилиMapWhen.Map*изменяет базовый путь выполняющегося запроса, что влияет на выходные данные создания ссылки. Все API LinkGenerator разрешают указание базового пути. Укажите пустой базовый путь для отмены влиянияMap*на создание ссылок.
Пример программного обеспечения промежуточного слоя
В следующем примере ПО промежуточного слоя использует API LinkGenerator, чтобы создать ссылку на метод действия, который перечисляет хранимые продукты. Использование генератора ссылок путем его внедрения в класс и вызова GenerateLink доступно для любого класса в приложении.
public class ProductsLinkMiddleware
{
private readonly LinkGenerator _linkGenerator;
public ProductsLinkMiddleware(RequestDelegate next, LinkGenerator linkGenerator)
{
_linkGenerator = linkGenerator;
}
public async Task InvokeAsync(HttpContext httpContext)
{
var url = _linkGenerator.GetPathByAction("ListProducts", "Store");
httpContext.Response.ContentType = "text/plain";
await httpContext.Response.WriteAsync($"Go to {url} to see our products.");
}
}
Справочник по шаблону маршрута
Токены в {} определяют параметры маршрута, которые будут привязаны, если маршрут совпадает. В сегменте маршрута можно определить несколько параметров маршрута, однако они должны разделяться литеральным значением. Например, {controller=Home}{action=Index} будет недопустимым маршрутом, так как между {controller} и {action} нет литерального значения. Параметрам маршрута должны быть присвоены имена, и для них могут быть определены дополнительные атрибуты.
Весь текст, кроме параметров маршрута (например, {id}) и разделителя пути /, должен соответствовать тексту в URL-адресе. Сопоставление текста производится без учета регистра на основе декодированного представления пути URL-адреса. Для сопоставления с литеральным разделителем параметров маршрута ({ или }) нужно экранировать разделитель, повторив символ. Например, {{ или }}.
Звездочка * или двойная звездочка **:
- Можно использовать в качестве префикса к параметру маршрута для привязки к остальной части URI.
- Такие параметры называются универсальными. Например,
blog/{**slug}:- Соответствует любому URI, который начинается с
/blogи имеет любое значение после него. - Значение после
/blogприсваивается значению динамического маршрута.
- Соответствует любому URI, который начинается с
Warning
Соответствие параметра catch-all маршрутам может быть неправильным из-за ошибки в маршрутизации. Приложения, на работу которых влияет эта ошибка, обладают следующими характеристиками:
- Маршрут общий, например
{**slug}". - Маршрут catch-all не соответствует необходимым запросам.
- После удаления других маршрутов маршрут catch-all начинает работать.
Ознакомьтесь с примерами 18677 и 16579, в которых встречается эта ошибка, на сайте GitHub.
Исправление для этой ошибки, которое можно включить по желанию, содержится в пакете SDK для .NET Core 3.1.301 или более поздней версии. Следующий код задает внутренний переключатель, исправляющий эту ошибку:
public static void Main(string[] args)
{
AppContext.SetSwitch("Microsoft.AspNetCore.Routing.UseCorrectCatchAllBehavior",
true);
CreateHostBuilder(args).Build().Run();
}
// Remaining code removed for brevity.
Универсальные параметры также могут соответствовать пустой строке.
Универсальный параметр экранирует соответствующие символы, когда маршрут используется для формирования URL-адреса, включая символы разделителей пути (/). Например, маршрут foo/{*path} со значениями маршрутов { path = "my/path" } формирует foo/my%2Fpath. Обратите внимание на экранированную косую черту. В качестве символов разделителя кругового пути используйте префикс параметра маршрута **. Маршрут foo/{**path} с { path = "my/path" } формирует foo/my/path.
Шаблоны URL-адресов, которые пытаются получить имя файла с необязательным расширением, имеют свои особенности. Например, рассмотрим шаблон files/{filename}.{ext?}. Когда значения для filename и ext существуют, заполняются оба значения. Если в URL-адресе есть только значение для filename, маршрут совпадает, так как точка в конце (.) является необязательной. Следующие URL-адреса соответствуют этому маршруту:
/files/myFile.txt/files/myFile
Параметры маршрута могут иметь значения по умолчанию. Они указываются после имени параметра и знака равенства (=). Например, {controller=Home} определяет Home в качестве значения по умолчанию для controller. Значение по умолчанию используется, если для параметра нет значения в URL-адресе. Параметры маршрута могут быть необязательными, для этого необходимо добавить вопросительный знак (?) в конец имени параметра. Например, id?. Разница между необязательными значениями и параметрами маршрута по умолчанию
- Параметр маршрута со значением по умолчанию всегда создает значение.
- Необязательный параметр имеет значение только в том случае, если в URL-адресе запроса указано значение.
Параметры маршрута могут иметь ограничения, которые должны соответствовать значению маршрута из URL-адреса. Добавив двоеточие (:) и имя ограничения после имени параметра маршрута, можно указать встроенные ограничения для параметра маршрута. Если для ограничения требуются аргументы, они указываются в скобках (...) после имени ограничения. Чтобы указать несколько встроенных ограничений, добавьте еще одно двоеточие (:) и имя ограничения.
Имя и аргументы ограничения передаются в службу IInlineConstraintResolver для создания экземпляра IRouteConstraint, который будет использоваться для обработки URL. Например, в шаблоне маршрута blog/{article:minlength(10)} определяется ограничение minlength с аргументом 10. Более подробное описание ограничений маршрутов и список ограничений, предоставляемых платформой, см. в разделе Справочник по ограничениям маршрутов.
Параметры маршрута также могут иметь преобразователи параметров, которые преобразуют значение параметра при создании ссылок и сопоставлении действий и страниц с URL-адресами. Как и ограничения, преобразователи параметров можно включать в параметр маршрута, добавив двоеточие (:) и имя преобразователя после имени параметра маршрута. Например, шаблон маршрута blog/{article:slugify} задает преобразователь slugify. Дополнительные сведения о преобразователях параметров см. в разделе Справочник по преобразователям параметров.
В приведенной ниже таблице показаны некоторые примеры шаблонов маршрутов и их поведение.
| Шаблон маршрута | Пример сопоставления URI | URI запроса... |
|---|---|---|
hello |
/hello |
Соответствует только одному пути /hello. |
{Page=Home} |
/ |
Соответствует и задает для параметра Page значение Home. |
{Page=Home} |
/Contact |
Соответствует и задает для параметра Page значение Contact. |
{controller}/{action}/{id?} |
/Products/List |
Сопоставляется с контроллером Products и действием List. |
{controller}/{action}/{id?} |
/Products/Details/123 |
Соответствует контроллеру Products и действию Details, где id имеет значение 123. |
{controller=Home}/{action=Index}/{id?} |
/ |
Сопоставляется с контроллером Home и методом Index.
id не учитывается. |
{controller=Home}/{action=Index}/{id?} |
/Products |
Сопоставляется с контроллером Products и методом Index.
id не учитывается. |
Использование шаблона — это, как правило, самый простой подход к маршрутизации. Ограничения и значения по умолчанию также могут указываться вне шаблона маршрута.
Сложные сегменты
Сложные сегменты обрабатываются путем сопоставления литеральных разделителей справа налево в нежадной манере. Например, [Route("/a{b}c{d}")] является сложным сегментом.
Сложные сегменты работают определенным способом, который должен быть понятен для их успешного использования. В примере в этом разделе показано, почему сложные сегменты действительно хорошо работают только в том случае, если текст разделителя отсутствует в значениях параметров. Для более сложных случаев необходимо использовать регулярное выражение и затем вручную извлечь значения.
Warning
При использовании System.Text.RegularExpressions для обработки ненадежных входных данных передайте время ожидания. Злонамеренный пользователь может предоставить входные данные для RegularExpressions, что делает возможными атаки типа "отказ в обслуживании". API платформы ASP.NET Core, использующие RegularExpressions, передают время ожидания.
Это сводка действий, выполняемых маршрутизацией с использованием шаблона /a{b}c{d} и URL-пути /abcd.
| используется для визуализации принципа работы алгоритма.
- Первый литерал, справа налево — это
c. Таким образом,/abcdищется справа и находит/ab|c|d. - Все, что находится справа (
d), теперь сопоставляется с параметром маршрута{d}. - Следующий литерал, справа налево —
a. Поэтому поиск/ab|c|dначинается с того места, где мы остановились, после чего находитсяaв/|a|b|c|d. - Значение справа (
b) теперь сопоставляется с параметром маршрута{b}. - Текста и шаблонов маршрута больше не осталось, так что это совпадение.
Ниже приведен пример отрицательного результата с использованием того же шаблона /a{b}c{d} и URL-пути /aabcd.
| используется для визуализации принципа работы алгоритма: Это не совпадение, что объясняется тем же алгоритмом.
- Первый литерал, справа налево — это
c. Таким образом,/aabcdищется справа и находит/aab|c|d. - Все, что находится справа (
d), теперь сопоставляется с параметром маршрута{d}. - Следующий литерал, справа налево —
a. Поэтому поиск/aab|c|dначинается с того места, где мы остановились, после чего находитсяaв/a|a|b|c|d. - Значение справа (
b) теперь сопоставляется с параметром маршрута{b}. - На этом этапе имеется оставшийся текст
a, однако больше нет шаблонов маршрутов для синтаксического анализа, поэтому это не является совпадением.
Поскольку алгоритм сопоставления нежадный:
- Он сопоставляет наименьший объем текста, допустимый для каждого шага.
- Любая ситуация, когда значение разделителя появляется внутри значений параметров, приводит к несовпадению.
Регулярные выражения обеспечивают гораздо больший контроль над их поведением при сопоставлении.
При жадном сопоставлении, также известном как ленивое сопоставление, выполняется поиск соответствия по наибольшей возможной строке. При нежадном сопоставлении выполняется поиск соответствия по наименьшей возможной строке.
Справочник по ограничениям маршрутов
Ограничения маршрута применяются, когда найдено соответствие входящему URL-адресу, и путь URL-адреса разобран на элементы маршрута. Как правило, ограничения маршрута служат для проверки значения маршрута, связанного посредством шаблона маршрута, и принятия решения касательно того, является ли значение приемлемым (истина или ложь). Некоторые ограничения маршрута используют данные, не относящиеся к значению маршрута, для определения возможности маршрутизации запроса. Например, HttpMethodRouteConstraint может принимать или отклонять запрос в зависимости от HTTP-команды. Ограничения используются в маршрутизации запросов и создании ссылок.
Warning
Не используйте ограничения для проверки входных данных. Если для проверки входных данных используются ограничения, недопустимые входные данные приводят к ошибке 404 ("Не найдено"). Недопустимые входные данные должны привести к ошибке 400 ("Неверный запрос") с соответствующим сообщением об ошибке. Ограничения маршрутов следует использовать для разрешения неоднозначности похожих маршрутов, а не для проверки входных данных определенного маршрута.
В приведенной ниже таблице показаны примеры ограничения маршрутов и их ожидаемое поведение.
| constraint | Example | Примеры совпадений | Notes |
|---|---|---|---|
int |
{id:int} |
123456789, -123456789 |
Соответствует любому целому числу |
bool |
{active:bool} |
true, FALSE |
Соответствует true или false. Case-insensitive |
datetime |
{dob:datetime} |
2016-12-31, 2016-12-31 7:32pm |
Соответствует допустимому значению DateTime в инвариантной культуре. См. предупреждение выше. |
decimal |
{price:decimal} |
49.99, -1,000.01 |
Соответствует допустимому значению decimal в инвариантной культуре. См. предупреждение выше. |
double |
{weight:double} |
1.234, -1,001.01e8 |
Соответствует допустимому значению double в инвариантной культуре. См. предупреждение выше. |
float |
{weight:float} |
1.234, -1,001.01e8 |
Соответствует допустимому значению float в инвариантной культуре. См. предупреждение выше. |
guid |
{id:guid} |
CD2C1638-1638-72D5-1638-DEADBEEF1638 |
Соответствует допустимому значению Guid |
long |
{ticks:long} |
123456789, -123456789 |
Соответствует допустимому значению long |
minlength(value) |
{username:minlength(4)} |
Rick |
Строка должна содержать не менее 4 символов |
maxlength(value) |
{filename:maxlength(8)} |
MyFile |
Строка должна содержать не более 8 символов |
length(length) |
{filename:length(12)} |
somefile.txt |
Длина строки должна составлять ровно 12 символов |
length(min,max) |
{filename:length(8,16)} |
somefile.txt |
Строка должна быть длиной не менее 8 и не более 16 символов |
min(value) |
{age:min(18)} |
19 |
Целочисленное значение не меньше 18 |
max(value) |
{age:max(120)} |
91 |
Целочисленное значение не больше 120 |
range(min,max) |
{age:range(18,120)} |
91 |
Целочисленное значение должно быть не менее 18 и не более 120. |
alpha |
{name:alpha} |
Rick |
Строка должна состоять из одного или нескольких буквенных символов (a-z) без учета регистра. |
regex(expression) |
{ssn:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)} |
123-45-6789 |
Строка должна соответствовать регулярному выражению. См. советы по определению регулярного выражения. |
required |
{name:required} |
Rick |
Определяет обязательное наличие значения, не относящегося к параметру, во время формирования URL-адреса |
Warning
При использовании System.Text.RegularExpressions для обработки ненадежных входных данных передайте время ожидания. Злонамеренный пользователь может предоставить входные данные для RegularExpressions, что делает возможными атаки типа "отказ в обслуживании". API платформы ASP.NET Core, использующие RegularExpressions, передают время ожидания.
К одному параметру может применяться несколько разделенных запятой ограничений. Например, следующее ограничение ограничивает параметр целочисленным значением 1 или больше:
[Route("users/{id:int:min(1)}")]
public User GetUserById(int id) { }
Warning
Ограничения маршрута, которые проверяют URL-адрес и могут быть преобразованы в тип CLR, всегда используют инвариантные языковые настройки. Например, преобразование в тип CLR int или DateTime. Эти ограничения предполагают, что для URL-адреса не предусмотрена локализация. Предоставляемые платформой ограничения маршрутов не изменяют значения, хранящиеся в значениях маршрута. Все значения маршрута, переданные из URL-адреса, сохраняются как строки. Например, ограничение float пытается преобразовать значение маршрута в число с плавающей запятой, но преобразованное значение используется только для проверки возможности преобразования в число с плавающей запятой.
Регулярные выражения в ограничениях
Warning
При использовании System.Text.RegularExpressions для обработки ненадежных входных данных передайте время ожидания. Злонамеренный пользователь может предоставить входные данные для RegularExpressions, что делает возможными атаки типа "отказ в обслуживании". API платформы ASP.NET Core, использующие RegularExpressions, передают время ожидания.
Регулярные выражения могут быть определены как встроенные ограничения с помощью ограничения маршрута regex(...). Методы в семействе MapControllerRoute также принимают объектный литерал ограничений. При использовании этой формы строковые значения будут интерпретироваться как регулярные выражения.
В следующем коде используется встроенное ограничение регулярного выражения.
app.UseEndpoints(endpoints =>
{
endpoints.MapGet("{message:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)}",
context =>
{
return context.Response.WriteAsync("inline-constraint match");
});
});
В следующем коде для указания ограничения регулярного выражения используется литеральный объект.
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "people",
pattern: "People/{ssn}",
constraints: new { ssn = "^\\d{3}-\\d{2}-\\d{4}$", },
defaults: new { controller = "People", action = "List", });
});
В платформе ASP.NET Core в конструктор регулярных выражений добавляются члены RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant. Описание этих членов см. в разделе RegexOptions.
В регулярных выражениях применяются разделители и токены, аналогичные используемым функцией маршрутизации и в языке C#. Токены регулярного выражения должны быть экранированы. Чтобы использовать регулярное выражение ^\d{3}-\d{2}-\d{4}$ во встроенном ограничении, используйте один из следующих способов.
- Замените символы
\, представленные в строке, символами\\в исходном файле C#, чтобы экранировать escape-символ строки\. - Буквальные строковые литералы.
Чтобы экранировать символы разделения параметров маршрутизации {, }, [, ], используйте их дважды в выражении (например, {{, }}, [[, ]]). В следующей таблице показаны регулярные выражения и их экранированные варианты.
| Регулярное выражение | Экранированное регулярное выражение |
|---|---|
^\d{3}-\d{2}-\d{4}$ |
^\\d{{3}}-\\d{{2}}-\\d{{4}}$ |
^[a-z]{2}$ |
^[[a-z]]{{2}}$ |
Регулярные выражения, используемые при маршрутизации, часто начинаются с символа ^ и соответствуют начальной позиции строки. Выражения часто заканчиваются символом $ и соответствуют концу строки. Благодаря символам ^ и $ регулярное выражение сопоставляется со всем значением параметра маршрута. Если символы ^ и $ отсутствуют, регулярное выражение сопоставляется с любой подстрокой внутри строки, что обычно нежелательно. В таблице ниже представлен ряд примеров и объясняются причины соответствия или несоответствия.
| Expression | String | Match | Comment |
|---|---|---|---|
[a-z]{2} |
hello | Yes | Совпадения подстроки |
[a-z]{2} |
123abc456 | Yes | Совпадения подстроки |
[a-z]{2} |
mz | Yes | Совпадение выражения |
[a-z]{2} |
MZ | Yes | Без учета регистра |
^[a-z]{2}$ |
hello | No | См. замечания, касающиеся символов ^ и $, выше |
^[a-z]{2}$ |
123abc456 | No | См. замечания, касающиеся символов ^ и $, выше |
Дополнительные сведения о синтаксисе регулярных выражений см. в статье Регулярные выражения в .NET Framework.
Чтобы ограничить возможные значения параметра набором известных значений, используйте регулярное выражение. Например, при использовании выражения {action:regex(^(list|get|create)$)} значение маршрута action будет соответствовать только list, get или create. При передаче в словарь ограничений строка ^(list|get|create)$ будет эквивалентной. Ограничения, которые передаются в словарь ограничений и не соответствуют одному из известных ограничений, также рассматриваются как регулярные выражения. Ограничения, которые передаются в шаблоне и не соответствуют одному из известных ограничений, не рассматриваются как регулярные выражения.
Пользовательские ограничения маршрутов
Пользовательские ограничения маршрутов можно создать путем внедрения интерфейса IRouteConstraint. Интерфейс IRouteConstraint содержит метод, Match, который возвращает true, если ограничение удовлетворяется, и false — если нет.
Пользовательские ограничения маршрутов редко требуются. Перед реализацией пользовательского ограничения маршрута рассмотрите альтернативные варианты, такие как привязка модели.
В папке ASP.NET Core Constraints находятся хорошие примеры создания ограничений. Например, GuidRouteConstraint.
Чтобы применить пользовательский метод IRouteConstraint, тип ограничения маршрута необходимо зарегистрировать с помощью ConstraintMap приложения в контейнере службы. Объект ConstraintMap — это словарь, который сопоставляет ключи ограничений пути с реализациями IRouteConstraint, которые проверяют эти ограничения.
ConstraintMap приложения можно преобразовать в Startup.ConfigureServices как часть вызова services.AddRouting или путем настройки RouteOptions непосредственно с помощью services.Configure<RouteOptions>. Рассмотрим пример.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddRouting(options =>
{
options.ConstraintMap.Add("customName", typeof(MyCustomConstraint));
});
}
Предыдущее ограничение применяется в следующем коде.
[Route("api/[controller]")]
[ApiController]
public class TestController : ControllerBase
{
// GET /api/test/3
[HttpGet("{id:customName}")]
public IActionResult Get(string id)
{
return ControllerContext.MyDisplayRouteInfo(id);
}
// GET /api/test/my/3
[HttpGet("my/{id:customName}")]
public IActionResult Get(int id)
{
return ControllerContext.MyDisplayRouteInfo(id);
}
}
MyDisplayRouteInfo предоставляется пакетом NuGet Rick.Docs.Samples.RouteInfo и отображает информацию о маршруте.
Реализация MyCustomConstraint препятствует применению 0 к параметру маршрута:
class MyCustomConstraint : IRouteConstraint
{
private Regex _regex;
public MyCustomConstraint()
{
_regex = new Regex(@"^[1-9]*$",
RegexOptions.CultureInvariant | RegexOptions.IgnoreCase,
TimeSpan.FromMilliseconds(100));
}
public bool Match(HttpContext httpContext, IRouter route, string routeKey,
RouteValueDictionary values, RouteDirection routeDirection)
{
if (values.TryGetValue(routeKey, out object value))
{
var parameterValueString = Convert.ToString(value,
CultureInfo.InvariantCulture);
if (parameterValueString == null)
{
return false;
}
return _regex.IsMatch(parameterValueString);
}
return false;
}
}
Warning
При использовании System.Text.RegularExpressions для обработки ненадежных входных данных передайте время ожидания. Злонамеренный пользователь может предоставить входные данные для RegularExpressions, что делает возможными атаки типа "отказ в обслуживании". API платформы ASP.NET Core, использующие RegularExpressions, передают время ожидания.
Предыдущий код:
- Предотвращает
0в сегменте{id}маршрута. - Отображается для предоставления базового примера реализации настраиваемого ограничения. Не следует использовать в рабочем приложении.
Следующий код является лучшим подходом к предотвращению обработки id, содержащего 0.
[HttpGet("{id}")]
public IActionResult Get(string id)
{
if (id.Contains('0'))
{
return StatusCode(StatusCodes.Status406NotAcceptable);
}
return ControllerContext.MyDisplayRouteInfo(id);
}
Приведенный выше код имеет следующие преимущества по сравнению с подходом MyCustomConstraint.
- Пользовательское ограничение не требуется.
- Он возвращает более понятную ошибку, если параметр маршрута включает
0.
Справочник по преобразователям параметров
Преобразователи параметров:
- Выполняются при формировании ссылки с помощью LinkGenerator.
- Реализуйте Microsoft.AspNetCore.Routing.IOutboundParameterTransformer.
- Настраиваются с помощью ConstraintMap.
- Принимают значение маршрута параметра и изменяют его на новое строковое значение.
- Результатом является использование преобразованного значения в созданной ссылке.
Например, пользовательский преобразователь параметра slugify в шаблоне маршрута blog\{article:slugify} с Url.Action(new { article = "MyTestArticle" }) формирует значение blog\my-test-article.
Рассмотрим следующую реализацию IOutboundParameterTransformer.
public class SlugifyParameterTransformer : IOutboundParameterTransformer
{
public string TransformOutbound(object value)
{
if (value == null) { return null; }
return Regex.Replace(value.ToString(),
"([a-z])([A-Z])",
"$1-$2",
RegexOptions.CultureInvariant,
TimeSpan.FromMilliseconds(100)).ToLowerInvariant();
}
}
Чтобы использовать преобразователь параметров в шаблоне маршрута, настройте его с помощью ConstraintMap в Startup.ConfigureServices.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddRouting(options =>
{
options.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer);
});
}
В ASP.NET Core используются преобразователи параметров для преобразования URI для определения конечной точки. Например, преобразователи параметров преобразуют значения маршрута, используемые для сопоставления area, controller, action и page.
routes.MapControllerRoute(
name: "default",
template: "{controller:slugify=Home}/{action:slugify=Index}/{id?}");
С помощью предыдущего шаблона маршрута действие SubscriptionManagementController.GetAll сопоставляется с URI /subscription-management/get-all. Преобразователь параметра не изменяет значения маршрута, используемые для формирования ссылки. Например, Url.Action("GetAll", "SubscriptionManagement") выводит /subscription-management/get-all.
ASP.NET Core предоставляет соглашения об API для использования преобразователей параметров со сформированными маршрутами.
- Соглашение Microsoft.AspNetCore.Mvc.ApplicationModels.RouteTokenTransformerConvention MVC применяет указанный преобразователь параметров ко всем маршрутам атрибутов в приложении. Преобразователь параметров преобразует маркеры маршрутов атрибутов по мере их замены. Дополнительные сведения см. в разделе об использовании преобразователя параметров для настройки замены маркеров.
- Razor Pages использует соглашение об API PageRouteTransformerConvention. Это соглашение применяет указанный трансформатор параметров ко всем автоматически обнаруженным страницам Razor. Преобразователь параметров преобразует сегменты папок и имен файлов маршрутов Razor Pages. Дополнительные сведения см. в разделе об использовании преобразователя параметров для настройки маршрутов страниц.
Справочник по формированию URL-адресов
В этом разделе представлен справочник по алгоритму, реализованному при формировании URL-адреса. На практике в большинстве сложных примеров формирования URL-адресов используются контроллеры или Razor страницы. Дополнительные сведения см. в статье Маршрутизация в контроллерах.
Процесс формирования URL-адреса начинается с вызова LinkGenerator.GetPathByAddress или аналогичного метода. Метод предоставляется с адресом, набором значений маршрута и при необходимости со сведениями о текущем запросе из HttpContext.
Первым шагом является использование адреса для разрешения набора конечных точек-кандидатов с помощью IEndpointAddressScheme<TAddress>, соответствующих типу адреса.
После того как набор кандидатов найден в схеме адресов, конечные точки упорядочиваются и обрабатываются последовательно до тех пор, пока операция формирования URL-адреса не завершится. При создании URL-адреса не проверяется наличие неоднозначностей, первый возвращенный результат является окончательным результатом.
Устранение неполадок при формировании URL-адресов с помощью ведения журнала
Первым шагом при устранении неполадок при формировании URL-адресов является установка уровня ведения журнала Microsoft.AspNetCore.Routing на TRACE.
LinkGenerator фиксирует в журнале множество сведений об обработке, которые могут быть полезны при устранении неполадок.
Дополнительные сведения о формировании URL-адресов см. в разделе Справочник по формированию URL-адресов.
Addresses
Адреса являются основным понятием в формировании URL-адресов и используются для привязки вызова генератора ссылок к набору конечных точек-кандидатов.
Адреса — это расширяемое понятие, которое по умолчанию поставляется с двумя реализациями.
- Использование имени конечной точки (
string) в качестве адреса:- Обеспечивает аналогичную функциональность, как и имя маршрута в MVC.
- Использует тип метаданных IEndpointNameMetadata.
- Обрабатывает переданную строку в соответствии с метаданными всех зарегистрированных конечных точек.
- Создает исключение при запуске, если несколько конечных точек использует одно и то же имя.
- Рекомендуется для общего использования вне контроллеров и страниц Razor.
- Использование значений маршрутов (RouteValuesAddress) в качестве адреса:
- Предоставляет аналогичную функциональность и для контроллеров, и для Pages в плане формирования устаревших URL-адресов.
- Очень сложно расширять и отлаживать.
- Предоставляет реализацию, используемую
IUrlHelper, вспомогательными функциями тегов, вспомогательными методами HTML, результатами действий и т. д.
Роль схемы адресов заключается в том, чтобы создать связь между адресом и соответствующими конечными точками по произвольным критериям.
- При использовании схемы имен конечных точек выполняется простой поиск по словарю.
- Схема значений маршрута включает сложный алгоритм для нахождения лучшего подмножества.
Значения окружения и явные значения
Из текущего запроса маршрутизация обращается к значениям маршрута текущего запроса HttpContext.Request.RouteValues. Значения, связанные с текущим запросом, называются значениями окружения. В целях ясности в документации подразумеваются значения маршрута, передаваемые в методы как явные значения.
В следующем примере показаны значения окружения и явные значения. Он предоставляет значения окружения из текущего запроса и явные значения: { id = 17, }:
public class WidgetController : Controller
{
private readonly LinkGenerator _linkGenerator;
public WidgetController(LinkGenerator linkGenerator)
{
_linkGenerator = linkGenerator;
}
public IActionResult Index()
{
var url = _linkGenerator.GetPathByAction(HttpContext,
null, null,
new { id = 17, });
return Content(url);
}
Предыдущий код:
- Возвращает
/Widget/Index/17. - Получает LinkGenerator через DI.
Следующий код не предоставляет фоновые значения и явные значения: { controller = "Home", action = "Subscribe", id = 17, }:
public IActionResult Index2()
{
var url = _linkGenerator.GetPathByAction("Subscribe", "Home",
new { id = 17, });
return Content(url);
}
Предыдущий метод возвращает /Home/Subscribe/17
Следующий код в WidgetController возвращает /Widget/Subscribe/17:
var url = _linkGenerator.GetPathByAction("Subscribe", null,
new { id = 17, });
Следующий код предоставляет контроллер из значений окружения в текущем запросе и явные значения: { action = "Edit", id = 17, }:
public class GadgetController : Controller
{
public IActionResult Index()
{
var url = Url.Action("Edit", new { id = 17, });
return Content(url);
}
В предыдущем коде:
- Возвращается
/Gadget/Edit/17. - Url получает IUrlHelper.
-
Action создает URL-адрес с абсолютным путем для метода действия. URL-адрес содержит указанное имя
actionи значенияroute.
Следующий код предоставляет значения окружения из текущего запроса и явные значения: { page = "./Edit, id = 17, }:
public class IndexModel : PageModel
{
public void OnGet()
{
var url = Url.Page("./Edit", new { id = 17, });
ViewData["URL"] = url;
}
}
Приведенный выше код задает для url значение /Edit/17, когда страница Edit Razor содержит следующую директиву:
@page "{id:int}"
Если страница Edit не содержит шаблон маршрута "{id:int}", то url будет /Edit?id=17.
Поведение IUrlHelper MVC добавляет уровень сложности, помимо правил, описанных здесь.
-
IUrlHelperвсегда предоставляет значения маршрута из текущего запроса как значения окружения. -
IUrlHelper.Action всегда копирует текущие значения маршрута
actionиcontrollerкак явные значения, если они не переопределены разработчиком. -
IUrlHelper.Page всегда копирует текущее значение маршрута
pageкак явное значение, если оно не переопределено. -
IUrlHelper.Pageвсегда переопределяет текущее значение маршрутаhandlerнаnullкак явное значение, если его не переопределили.
Пользователи часто удивляются сведениям о поведении значений окружения, поскольку MVC не следует собственным правилам. По историческим причинам и для обеспечения совместимости для некоторых значений маршрута, таких как action, controller, page и handler, предусмотрено собственное поведение в особых случаях.
Аналогичные функции, предоставляемые LinkGenerator.GetPathByAction и LinkGenerator.GetPathByPage, дублируют эти аномалии IUrlHelper для обеспечения совместимости.
Процесс формирования URL-адреса
После обнаружения набора конечных точек-кандидатов алгоритм формирования URL-адресов:
- последовательно обрабатывает конечные точки;
- Возвращает первый успешный результат.
Первый шаг этого процесса называется аннулированием значения маршрута. Аннулирование значения маршрута — это процесс, с помощью которого маршрутизация решает, какие значения маршрута должны использоваться из значений окружения, а какие следует игнорировать. Каждое значение окружения учитывается и либо объединяется с явными значениями, либо игнорируется.
Роль значений окружения заключается в том, что в некоторых распространенных случаях они позволяют сократить для разработчиков объем вводимой информации. Как правило, сценарии, в которых полезно использовать значения окружения, связаны с MVC.
- При связывании с другим действием в том же контроллере не требуется указывать имя контроллера.
- При связывании с другим контроллером в той же области не требуется указывать имя области.
- При связывании с тем же методом действия не требуется указывать значения маршрута.
- При связывании с другой частью приложения следует избегать передачи значений маршрута, не имеющих смысла в этой части приложения.
Вызовы LinkGenerator или IUrlHelper, которые возвращают null, обычно происходят из-за неправильного понимания невалидности значения маршрута. Для устранения неполадок аннулирования значения маршрута явно укажите дополнительные значения маршрута, чтобы определить, устранена ли проблема.
Аннулирование значения маршрута предполагает, что схема URL-адреса приложения является иерархической, в которой иерархия сформирована слева направо. Рассмотрим шаблон маршрута базового контроллера {controller}/{action}/{id?}, чтобы понять, как это работает на практике.
Изменение значения делает недействительными все значения маршрута, которые отображаются справа. Это отражает предположение об иерархии. Если приложение имеет значение окружения для id, а операция указывает другое значение для controller:
-
idне будет использоваться повторно, поскольку{controller}находится слева от{id?}.
Некоторые примеры, демонстрирующие этот принцип
- Если явные значения содержат значение для
id, значение окружения дляidигнорируется. Можно использовать значения окружения дляcontrollerиaction. - Если явные значения содержат значение для
action, любое значение окружения дляactionигнорируется. Можно использовать фоновые значения дляcontroller. Если явное значение дляactionотличается от значения окружения дляaction, значениеidне будет использоваться. Если явное значение дляactionсовпадает со значением окружения дляaction, можно использовать значениеid. - Если явные значения содержат значение для
controller, любое значение окружения дляcontrollerигнорируется. Если явное значение дляcontrollerотличается от значения окружения дляcontroller, значенияactionиidне будут использоваться. Если явное значение дляcontrollerсовпадает со значением окружения дляcontroller, можно использовать значенияactionиid.
Этот процесс усложняется за счет наличия атрибутных маршрутов и традиционных маршрутов. Стандартные маршруты контроллера, такие как {controller}/{action}/{id?}, указывают иерархию с помощью параметров маршрута. Для выделенных стандартных маршрутов и маршрутов атрибутов для контроллеров и Razor Pages:
- Существует иерархия значений маршрута.
- Они не отображаются в шаблоне.
В таких случаях формирование URL-адресов определяет концепцию необходимых значений. Для конечных точек, созданных контроллерами и Razor Pages, указаны обязательные значения, позволяющие использовать аннулирование значений маршрута.
Подробный алгоритм аннулирования значения маршрута
- Имена обязательных значений объединяются с параметрами маршрута, а затем обрабатываются слева направо.
- Для каждого параметра сравниваются внешние значения и явное значение:
- Если значение окружения и явное значение совпадают, процесс продолжается.
- Если значение окружения задано, а явное значение не задано, то при формировании URL-адреса используется значение окружения.
- Если значение окружения отсутствует, а явное значение задано, следует отклонить значение окружения и все последующие значения окружения.
- Если заданы и значение окружения, и явное значение, однако они отличаются, следует отклонить значение окружения и все последующие значения окружения.
На этом этапе операция формирования URL-адреса готова к оценке ограничений маршрута. Набор допустимых значений объединяется со значениями по умолчанию для параметра, предоставляемыми ограничениям. Если все ограничения пройдены, операция продолжается.
Затем допустимые значения можно использовать для расширения шаблона маршрута. Шаблон маршрута обрабатывается:
- Слева направо.
- Для каждого параметра заменяется допустимое значение.
- В следующих особых случаях:
- Если в допустимых значениях отсутствует значение и параметр имеет значение по умолчанию, используется значение по умолчанию.
- Если в допустимых значениях отсутствует значение, а параметр является необязательным, обработка продолжается.
- Если любой параметр маршрута справа от отсутствующего необязательного параметра имеет значение, операция завершается ошибкой.
- Смежные параметры со значениями по умолчанию и необязательные параметры по возможности сворачиваются.
Явно предоставленные значения, которые не соответствуют сегменту маршрута, добавляются в строку запроса. В приведенной ниже таблице показан результат использования шаблона маршрута {controller}/{action}/{id?}.
| Значения окружения | Явные значения | Result |
|---|---|---|
| контроллер = "Home" | action = "О программе" | /Home/About |
| контроллер = "Home" | controller = "Заказ", action = "О" | /Order/About |
| controller = "Home", color = "Красный" | action = "О программе" | /Home/About |
| контроллер = "Home" | action = "О программе", color = "Красный" | /Home/About?color=Red |
Проблемы с недействительностью значений маршрута
Начиная с ASP.NET Core 3.0, некоторые схемы формирования URL-адресов, которые используются в более ранних версиях ASP.NET Core, не подходят для формирования URL-адресов. Команда ASP.NET Core планирует добавить функции для решения этих задач в будущем выпуске. В настоящее время самое лучшее решение — использовать устаревшую маршрутизацию.
В следующем коде показан пример схемы формирования URL-адреса, которая не поддерживается маршрутизацией.
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute("default",
"{culture}/{controller=Home}/{action=Index}/{id?}");
endpoints.MapControllerRoute("blog", "{culture}/{**slug}",
new { controller = "Blog", action = "ReadPost", });
});
В приведенном выше коде параметр маршрута culture используется для локализации. Необходимо, чтобы параметр culture всегда принимался как значение окружения. Однако параметр culture не принимается как значение окружения ввиду способа работы требуемых значений.
- В шаблоне маршрута
"default"параметр маршрутаcultureнаходится слева отcontroller, поэтому измененияcontrollerне приведут к аннулированиюculture. - В шаблоне маршрута
"blog"параметр маршрутаcultureрассматривается как находящийся справа отcontroller, который имеется в требуемых значениях.
Настройка метаданных конечной точки
Сведения о настройке метаданных конечной точки см. на следующих веб-страницах:
- Включение CORS с маршрутизацией конечных точек
-
Пример IAuthorizationPolicyProvider с использованием настраиваемого атрибута
[MinimumAgeAuthorize] - Тестирование проверки подлинности с использованием атрибута [Authorize]
- RequireAuthorization
- Выбор схемы с использованием атрибута [Authorize]
- Применение политик с использованием атрибута [Authorize]
- Авторизация на основе ролей в ASP.NET Core
Сопоставление хостов при маршрутизации, используя RequireHost
RequireHost применяет ограничение к маршруту, требующее указанный узел. Параметр RequireHost или [Host] может иметь следующее значение.
- Хост:
www.domain.com, соответствуетwww.domain.comс любым портом. - Хост с подстановочным знаком:
*.domain.com, соответствуетwww.domain.com,subdomain.domain.comилиwww.subdomain.domain.comдля любого порта. - Порт:
*:5000, соответствует порту 5000 с любым хостом. - Узел и порт:
www.domain.com:5000или*.domain.com:5000, соответствует узлу и порту.
С помощью RequireHost или [Host] можно указать несколько параметров. Ограничение соответствует хостам, допустимым для любого из параметров. Например, [Host("domain.com", "*.domain.com")] соответствует domain.com, www.domain.com и subdomain.domain.com.
Следующий код использует RequireHost, чтобы требовать указанный хост в маршруте:
public void Configure(IApplicationBuilder app)
{
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapGet("/", context => context.Response.WriteAsync("Hi Contoso!"))
.RequireHost("contoso.com");
endpoints.MapGet("/", context => context.Response.WriteAsync("AdventureWorks!"))
.RequireHost("adventure-works.com");
endpoints.MapHealthChecks("/healthz").RequireHost("*:8080");
});
}
Следующий код использует атрибут [Host] в контроллере, чтобы требовать один из указанных хостов.
[Host("contoso.com", "adventure-works.com")]
public class ProductController : Controller
{
public IActionResult Index()
{
return ControllerContext.MyDisplayRouteInfo();
}
[Host("example.com:8080")]
public IActionResult Privacy()
{
return ControllerContext.MyDisplayRouteInfo();
}
}
Если атрибут [Host] применяется как к контроллеру, так и к методу действия, выполняется следующее.
- Используется атрибут действия.
- Атрибут контроллера не учитывается.
Рекомендации по производительности для маршрутизации
В ASP.NET Core 3.0 была обновлена большая часть маршрутизации, чтобы повысить производительность.
Проблемы с производительностью в приложении зачастую связывали с маршрутизацией. Причиной этому является то, что такие фреймворки, как контроллеры и Razor Pages, сообщают о количестве времени, проведенном внутри фреймворка, в сообщениях журнала. Если время, указанное контроллерами, значительно отличается от общего времени запроса:
- Разработчики исключают код приложения из списка возможных источников проблемы.
- Обычно предполагается, что причиной является маршрутизация.
Для проверки производительности маршрутизации используются тысячи конечных точек. Маловероятно, что обычное приложение столкнется с проблемами с производительностью только лишь из-за того, что оно слишком большое. Наиболее распространенная причина снижения производительности маршрутизации обычно кроется в неправильной работе настраиваемого ПО промежуточного слоя.
В следующем примере кода показан базовый метод сокращения источника задержки.
public void Configure(IApplicationBuilder app, ILogger<Startup> logger)
{
app.Use(next => async context =>
{
var sw = Stopwatch.StartNew();
await next(context);
sw.Stop();
logger.LogInformation("Time 1: {ElapsedMilliseconds}ms", sw.ElapsedMilliseconds);
});
app.UseRouting();
app.Use(next => async context =>
{
var sw = Stopwatch.StartNew();
await next(context);
sw.Stop();
logger.LogInformation("Time 2: {ElapsedMilliseconds}ms", sw.ElapsedMilliseconds);
});
app.UseAuthorization();
app.Use(next => async context =>
{
var sw = Stopwatch.StartNew();
await next(context);
sw.Stop();
logger.LogInformation("Time 3: {ElapsedMilliseconds}ms", sw.ElapsedMilliseconds);
});
app.UseEndpoints(endpoints =>
{
endpoints.MapGet("/", async context =>
{
await context.Response.WriteAsync("Timing test.");
});
});
}
Для маршрутизации времени:
- Чередуйте каждое промежуточное ПО с копией тайминг промежуточного ПО, показанной в предыдущем коде.
- Добавьте уникальный идентификатор для сопоставления данных о времени с кодом.
Это базовый способ сократить задержку, когда она является существенной, например более 10ms. Вычитание Time 2 из Time 1 позволяет получить время, затраченное в промежуточном ПО UseRouting.
Следующий код использует более компактный подход к предыдущему коду времени.
public sealed class MyStopwatch : IDisposable
{
ILogger<Startup> _logger;
string _message;
Stopwatch _sw;
public MyStopwatch(ILogger<Startup> logger, string message)
{
_logger = logger;
_message = message;
_sw = Stopwatch.StartNew();
}
private bool disposed = false;
public void Dispose()
{
if (!disposed)
{
_logger.LogInformation("{Message }: {ElapsedMilliseconds}ms",
_message, _sw.ElapsedMilliseconds);
disposed = true;
}
}
}
public void Configure(IApplicationBuilder app, ILogger<Startup> logger)
{
int count = 0;
app.Use(next => async context =>
{
using (new MyStopwatch(logger, $"Time {++count}"))
{
await next(context);
}
});
app.UseRouting();
app.Use(next => async context =>
{
using (new MyStopwatch(logger, $"Time {++count}"))
{
await next(context);
}
});
app.UseAuthorization();
app.Use(next => async context =>
{
using (new MyStopwatch(logger, $"Time {++count}"))
{
await next(context);
}
});
app.UseEndpoints(endpoints =>
{
endpoints.MapGet("/", async context =>
{
await context.Response.WriteAsync("Timing test.");
});
});
}
Потенциально ресурсоемкие функции маршрутизации
В списке ниже представлены некоторые сведения о функциях маршрутизации, которые относительно ресурсоемкие по сравнению с базовыми шаблонами маршрутов.
- Регулярные выражения: можно составить сложные регулярные выражения, которые будут выполняться долгое время с небольшим количеством входных данных.
- Сложные сегменты (
{x}-{y}-{z}):- значительно более ресурсоемкие, чем анализ обычного сегмента URL-пути.
- В результате выделяется множество дополнительных подстрок.
- В обновлении производительности маршрутизации ASP.NET Core 3.0 не была обновлена логика комплексного сегмента.
- Синхронный доступ к данным: многие сложные приложения имеют доступ к базам данных в рамках их маршрутизации. ASP.NET Core 2.2 или более ранние версии могут не предоставлять подходящие точки расширения для маршрутизации доступа к базе данных. Например, IRouteConstraint и IActionConstraint являются синхронными. Точки расширения, такие как MatcherPolicy и EndpointSelectorContext, являются асинхронными.
Руководство для авторов библиотек
В этом разделе содержатся рекомендации для авторов библиотек, создающих библиотеки на основе маршрутизации. Эти сведения предназначены для предоставления разработчикам приложений сведений об эффективном использовании библиотек и платформ, расширяющих маршрутизацию.
Определение конечных точек
Чтобы создать платформу, использующую маршрутизацию для сопоставления URL-адресов, начните с определения пользовательского интерфейса, который строится поверх UseEndpoints.
ВЫПОЛНИТЕ сборку поверх IEndpointRouteBuilder. Это позволит пользователям создать инфраструктуру с другими функциями ASP.NET Core без путаницы. Каждый шаблон ASP.NET Core включает в себя маршрутизацию. Предположим, что маршрутизация имеется и пользователи знакомы с ней.
app.UseEndpoints(endpoints =>
{
// Your framework
endpoints.MapMyFramework(...);
endpoints.MapHealthChecks("/healthz");
});
ВЕРНИТЕ конкретный запечатанный тип из вызова объекта MapMyFramework(...), который реализует IEndpointConventionBuilder. Большинство методов Map... платформы соответствует этому шаблону. Интерфейс IEndpointConventionBuilder:
- Обеспечивает сочетаемость метаданных.
- Предназначен для различных методов расширения.
Объявление собственного типа позволяет добавлять в построитель собственные функции для конкретной платформы. Можно обернуть построитель, объявленный во фреймворке, и перенаправлять вызовы в него.
app.UseEndpoints(endpoints =>
{
// Your framework
endpoints.MapMyFramework(...).RequireAuthorization()
.WithMyFrameworkFeature(awesome: true);
endpoints.MapHealthChecks("/healthz");
});
Рассмотрите возможность создания собственного EndpointDataSource.
EndpointDataSource — это низкоуровневый примитив для объявления и обновления коллекции конечных точек.
EndpointDataSource — это эффективный API, используемый контроллерами и Razor Pages.
В тестах маршрутизации имеется простой пример источника данных, который не обновляется.
НЕ пытайтесь зарегистрировать EndpointDataSource по умолчанию. Требуйте от пользователей, чтобы они регистрировали вашу платформу в UseEndpoints. Философия маршрутизации заключается в том, что по умолчанию ничего не включено и UseEndpoints представляет собой место для регистрации конечных точек.
Создание ПО промежуточного слоя со встроенной маршрутизацией
РАССМОТРИТЕ ВОЗМОЖНОСТЬ определения типов метаданных в качестве интерфейса.
СДЕЛАЙТЕ возможным использование типов метаданных в качестве атрибута в классах и методах.
public interface ICoolMetadata
{
bool IsCool { get; }
}
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class CoolMetadataAttribute : Attribute, ICoolMetadata
{
public bool IsCool => true;
}
Платформы, такие как контроллеры и Razor Pages, поддерживают применение атрибутов метаданных к типам и методам. При объявлении типов метаданных:
- Сделайте их доступными в качестве атрибутов.
- Большинство пользователей знакомы с применением атрибутов.
Объявление типа метаданных в качестве интерфейса добавляет еще один уровень гибкости.
- Интерфейсы можно комбинировать.
- Разработчики могут объявлять собственные типы, объединяющие несколько политик.
СДЕЛАЙТЕ возможным переопределение метаданных, как показано в следующем примере.
public interface ICoolMetadata
{
bool IsCool { get; }
}
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class CoolMetadataAttribute : Attribute, ICoolMetadata
{
public bool IsCool => true;
}
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class SuppressCoolMetadataAttribute : Attribute, ICoolMetadata
{
public bool IsCool => false;
}
[CoolMetadata]
public class MyController : Controller
{
public void MyCool() { }
[SuppressCoolMetadata]
public void Uncool() { }
}
Следуйте этим рекомендациям, чтобы избежать определения метаданных маркера.
- Не ограничивайтесь поиском типа метаданных.
- Определите свойство метаданных и проверьте его.
Коллекция метаданных является упорядоченной и поддерживает переопределение приоритета. В случае с контроллерами метаданные в методе действия являются наиболее специфичными.
Сделайте промежуточное ПО полезным как при использовании маршрутизации, так и без нее.
app.UseRouting();
app.UseAuthorization(new AuthorizationPolicy() { ... });
app.UseEndpoints(endpoints =>
{
// Your framework
endpoints.MapMyFramework(...).RequireAuthorization();
});
В качестве примера этой рекомендации рассмотрим ПО промежуточного слоя UseAuthorization. Промежуточное ПО авторизации позволяет задать резервную политику.
Политика отката, если она указана, применяется к обоим элементам:
- конечные точки без указанной политики;
- запросы, которые не соответствуют конечной точке.
Это сделает ПО промежуточного слоя авторизации полезным вне контекста маршрутизации. Промежуточное ПО авторизации можно использовать для традиционного программирования middleware.
Диагностика отладки
Для подробного вывода диагностики построения маршрутов задайте для Logging:LogLevel:Microsoft значение Debug. В среде Development установите уровень журнала в appsettings.Development.json:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Debug",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}
ASP.NET Core