Маршрутизация в ASP.NET Core

Авторы: Райан Новак (Ryan Nowak), Кирк Ларкин (Kirk Larkin) и Рик Андерсон (Rick Anderson)

Примечание.

Это не последняя версия этой статьи. Сведения о текущем выпуске см. в ASP.NET версии Core 8.0 этой статьи.

Маршрутизация обеспечивает сопоставление входящих HTTP-запросов и их распределение по исполняемым конечным точкам приложения. Конечные точки — это блоки исполняемого кода обработки запросов приложения. Конечные точки определяются в приложении и настраиваются при его запуске. Процесс сопоставления конечных точек может извлекать значения из URL-адреса запроса и предоставлять эти значения для обработки запроса. С помощью сведений о конечных точках из приложения маршрутизация также может формировать URL-адреса, которые сопоставляются с конечными точками.

Приложения могут настраивать маршрутизацию с помощью следующего.

В этой статье представлены сведения о низкоуровневой маршрутизации 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, пользовательское ПО промежуточного слоя будет выполняться после ПО промежуточного слоя сопоставления маршрутов.

Примечание. Маршруты, добавленные непосредственно в WebApplication выполнение в конце конвейера.

Конечные точки

Для определения конечной точки используется метод MapGet. Конечная точка — это то, что можно:

  • выбрать путем сопоставления URL-адреса и метода HTTP;
  • выполнить путем запуска делегата.

Конечные точки, которые могут быть сопоставлены и выполнены приложением, настраиваются в UseEndpoints. Например, MapGet, MapPost и аналогичные методы подключают делегаты запросов к системе маршрутизации. Для подключения функций платформы ASP.NET Core к системе маршрутизации можно использовать дополнительные методы.

Ниже представлен пример маршрутизации с более сложным шаблоном маршрута.

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 добавляется ПО промежуточного слоя для проверки подлинности и авторизации. Это ПО промежуточного слоя размещается между методами 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.
  • Создайте вложенный конвейер ПО промежуточного слоя с помощью CreateApplicationBuilder.
  • Присоедините ПО промежуточного слоя к новому конвейеру. В этом случае — UseHealthChecks.
  • Build конвейер ПО промежуточного слоя в RequestDelegate.
  • Вызовите Map и укажите новый конвейер ПО промежуточного слоя.
  • Верните объект построителя, предоставленного Map, из метода расширения.

В следующем коде показано использование MapHealthChecks.

app.UseAuthentication();
app.UseAuthorization();

app.MapHealthChecks("/healthz").RequireAuthorization();

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

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

Соответствие URL-адресов

  • Это процесс, с помощью которого функция маршрутизации сопоставляет входящий запрос в конечную точку.
  • Он основан на данных в URL-пути и заголовках.
  • Его можно расширить, чтобы учесть любые данные в запросе.

При выполнении ПО промежуточного слоя маршрутизации оно задает конечную точку (Endpoint) и значения маршрута для функции запроса в HttpContext из текущего запроса.

  • Вызов HttpContext.GetEndpoint получает конечную точку.
  • HttpRequest.RouteValues получает коллекцию значений маршрута.

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

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

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

Предупреждение

Для обратной совместимости при выполнении делегата конечной точки контроллера или Razor страницы свойства RouteContext.RouteData задаются соответствующими значениями на основе обработки запросов, выполняемой до сих пор.

В следующем выпуске тип RouteContext будет помечен как устаревший.

  • Перенесите RouteData.Values в HttpRequest.RouteValues.
  • Миграция RouteData.DataTokens на получение IDataTokensMetadata из метаданных конечной точки.

Работа сопоставления URL-адресов подразделяется на этапы, которые можно настроить. На каждом этапе выходные данные представляют собой набор совпадений. По мере перехода между этапами набор совпадений можно ограничивать. Реализация маршрутизации не гарантирует порядок обработки для соответствующих конечных точек. Все возможные совпадения обрабатываются одновременно. Этапы сопоставления URL-адресов выполняются в следующем порядке. ASP.NET Core:

  1. URL-путь обрабатывается по набору конечных точек и их шаблонов маршрутов, при этом выполняется сбор всех совпадений.
  2. Принимается предыдущий список, и удаляются совпадения, которые не соответствуют примененным ограничениям маршрута.
  3. Принимает предыдущий список и удаляет совпадения, которые завершаются сбоем набора экземпляров MatcherPolicy .
  4. Используется для EndpointSelector принятия окончательного решения из предыдущего списка.

Список конечных точек определяется по приоритету в соответствии со следующим.

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

Приоритет маршрута вычисляется на основе более определенного шаблона маршрута, которому назначается более высокий приоритет. Например, рассмотрим шаблоны /hello и /{message}.

  • Оба соответствуют URL-пути /hello.
  • /hello является более конкретным, и, следовательно, ему назначается более высокий приоритет.

Как правило, приоритет маршрута помогает выбрать наилучшее соответствие для типов схем URL-адресов, используемых на практике. Используйте Order только в случае, когда необходимо избежать неоднозначности.

Ввиду различных типов расширяемости, предоставляемых службой маршрутизации, система маршрутизации не может заранее вычислить неоднозначные маршруты. Рассмотрим в качестве примера шаблоны маршрутов /{message:alpha} и /{message:int}.

  • Ограничение alpha соответствует только буквенным символам.
  • Ограничение int соответствует только числам.
  • Эти шаблоны имеют одинаковый приоритет маршрута, однако не существует одного URL-адреса, по которому они совпадают.
  • Если система маршрутизации сообщила об ошибке неоднозначности при запуске, это означает, что она заблокировала этот допустимый вариант использования.

Предупреждение

Порядок операций в UseEndpoints не влияет на поведение маршрутизации, за одним исключением. MapControllerRoute и MapAreaRoute автоматически присваивают значение порядка своим конечным точкам в соответствии с порядком их вызова. Это имитирует поведение контроллеров без системы маршрутизации в долгосрочной перспективе, предоставляя те же гарантии, что и в старых реализациях маршрутизации.

Маршрутизация конечных точек в ASP.NET Core:

  • Не имеет концепции маршрутов.
  • Не гарантирует порядок обработки. Все конечные точки обрабатываются одновременно.

Приоритет шаблонов маршрутов и порядок выбора конечных точек

Приоритет шаблонов маршрутов — это система, которая назначает каждому шаблону маршрута значение в зависимости от того, насколько он является конкретным. Приоритет шаблона маршрута

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

Например, рассмотрим шаблоны /Products/List и /Products/{id}. Разумно предположить, что для URL-пути /Products/List/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 происходит в два этапа:

  1. Адрес привязан к списку конечных точек, соответствующих адресу.
  2. RoutePattern конечной точки вычисляется, пока не будет найден шаблон маршрута, который соответствует предоставленным значениям. Полученный результат объединяется с другими частями URI, предоставленными генератору ссылок и возвращенными.

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

Метод расширения Description
GetPathByAddress Создает URI с абсолютным путем на основе предоставленных значений.
GetUriByAddress Создает абсолютный URI на основе предоставленных значений.

Предупреждение

Обратите внимание на следующие последствия вызова методов 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}

не является допустимым маршрутом, так как между и {action}не существует литерального значения{controller}. Параметрам маршрута должны быть присвоены имена, и для них могут быть определены дополнительные атрибуты.

Весь текст, кроме параметров маршрута (например, {id}) и разделителя пути /, должен соответствовать тексту в URL-адресе. Сопоставление текста производится без учета регистра на основе декодированного представления пути URL-адреса. Для сопоставления с литеральным разделителем параметров маршрута ({ или }) разделитель следует экранировать путем повтора символа. Например, {{ или }}.

Звездочка * или двойная звездочка **:

  • Можно использовать в качестве префикса параметра маршрута для привязки к остальной части URI.
  • Такие параметры называются универсальными. Например, blog/{**slug}:
    • Соответствует любому URI, который начинается с blog/ и имеет любое значение после него.
    • Значение после blog/ присваивается значению динамического маршрута.

Предупреждение

Соответствие параметра catch-all маршрутам может быть неправильным из-за ошибки в маршрутизации. Приложения, на работу которых влияет эта ошибка, обладают следующими характеристиками:

  • Маршрут 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}")] является сложным сегментом. Сложные сегменты работают определенным способом, который должен быть понятен для их успешного использования. В примере в этом разделе показано, почему сложные сегменты действительно хорошо работают только в том случае, если текст разделителя отсутствует в значениях параметров. Для более сложных случаев требуется использовать регулярное выражение, а затем вручную извлечь значения.

Предупреждение

При использовании 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-команды. Ограничения используются в маршрутизации запросов и создании ссылок.

Предупреждение

Не используйте ограничения для проверки входных данных. Если для проверки входных данных используются ограничения, недопустимые входные данные приводят к ошибке 404 ("Не найдено"). Недопустимые входные данные должны привести к ошибке 400 ("Неверный запрос") с соответствующим сообщением об ошибке. Ограничения маршрутов следует использовать для разрешения неоднозначности похожих маршрутов, а не для проверки входных данных определенного маршрута.

В приведенной ниже таблице показаны примеры ограничения маршрутов и их ожидаемое поведение.

ограничение Пример Примеры совпадений Примечания.
int {id:int} 123456789, -123456789 Соответствует любому целому числу
bool {active:bool} true, FALSE Соответствует true или false. Без учета регистра
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-адреса

Предупреждение

При использовании System.Text.RegularExpressions для обработки ненадежных входных данных передайте время ожидания. Злонамеренный пользователь может предоставить входные данные для RegularExpressions, что делает возможными атаки типа "отказ в обслуживании". API платформы ASP.NET Core, использующие RegularExpressions, передают время ожидания.

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

[Route("users/{id:int:min(1)}")]
public User GetUserById(int id) { }

Предупреждение

Ограничения маршрута, которые проверяют URL-адрес и могут быть преобразованы в тип CLR, всегда используют инвариантный язык и региональные параметры. Например, преобразование в тип CLR int или DateTime. Эти ограничения предполагают, что для URL-адреса не предусмотрена локализация. Предоставляемые платформой ограничения маршрутов не изменяют значения, хранящиеся в значениях маршрута. Все значения маршрута, переданные из URL-адреса, сохраняются как строки. Например, ограничение float пытается преобразовать значение маршрута в число с плавающей запятой, но преобразованное значение служит только для проверки возможности такого преобразования.

Регулярные выражения в ограничениях

Предупреждение

При использовании 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}$ во встроенном ограничении, используйте один из следующих способов.

Чтобы экранировать символы разделения параметров маршрутизации {, }, [, ], используйте их дважды в выражении (например, {{, }}, [[, ]]). В следующей таблице показаны регулярные выражения и их экранированные варианты.

Регулярное выражение Экранированное регулярное выражение
^\d{3}-\d{2}-\d{4}$ ^\\d{{3}}-\\d{{2}}-\\d{{4}}$
^[a-z]{2}$ ^[[a-z]]{{2}}$

Регулярные выражения, используемые при маршрутизации, часто начинаются с символа ^ и соответствуют начальной позиции строки. Выражения часто заканчиваются символом $ и соответствуют концу строки. Благодаря символам ^ и $ регулярное выражение сопоставляется со всем значением параметра маршрута. Если символы ^ и $ отсутствуют, регулярное выражение сопоставляется с любой подстрокой внутри строки, что обычно нежелательно. В таблице ниже представлен ряд примеров и объясняются причины соответствия или несоответствия.

Выражение Строка Поиск совпадений (Match) Комментарий
[a-z]{2} hello Да Соответствие подстроки
[a-z]{2} 123abc456 Да Соответствие подстроки
[a-z]{2} mz Да Соответствует выражению
[a-z]{2} MZ Да Без учета регистра
^[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);
    }
}

Предупреждение

При использовании 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 для использования преобразователей параметров со сформированными маршрутами.

Справочник по формированию URL-адресов

В этом разделе представлен справочник по алгоритму, реализованному при формировании URL-адреса. На практике в большинстве сложных примеров формирования URL-адресов используются контроллеры или Razor Pages. Дополнительные сведения см. в статье Маршрутизация в контроллерах.

Процесс создания URL-адресов начинается с вызова LinkGenerator.GetPathByAddress или аналогичного метода. Метод предоставляется с адресом, набором значений маршрута и при необходимости со сведениями о текущем запросе из HttpContext.

Первым шагом является использование адреса для разрешения набора конечных точек-кандидатов с помощью IEndpointAddressScheme<TAddress>, соответствующих типу адреса.

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

Устранение неполадок при формировании URL-адресов с помощью ведения журнала

Первым шагом при устранении неполадок при формировании URL-адресов является установка уровня ведения журнала Microsoft.AspNetCore.Routing для TRACE. LinkGenerator фиксирует в журнале множество сведений об обработке, которые могут быть полезны при устранении неполадок.

Дополнительные сведения о формировании URL-адресов см. в разделе Справочник по формированию URL-адресов.

Адреса

Адреса являются основным понятием в формировании URL-адресов и используются для привязки вызова генератора ссылок к набору конечных точек-кандидатов.

Адреса — это расширяемое понятие, которое по умолчанию поставляется с двумя реализациями.

  • Использование имени конечной точки (string) в качестве адреса:
    • Предоставляет аналогичные функции для имени маршрута MVC.
    • Использует тип метаданных IEndpointNameMetadata.
    • Разрешает указанную строку в соответствии с метаданными всех зарегистрированных конечных точек.
    • Создает исключение при запуске, если несколько конечных точек использует одно и то же имя.
    • Рекомендуется для общего использования за пределами контроллеров и Razor Pages.
  • Использование значений маршрутов (RouteValuesAddress) в качестве адреса:
    • Предоставляет аналогичные устаревшие функции по формированию URL-адресов для контроллеров и Razor Pages.
    • Очень сложные расширение и отладка.
    • Предоставляет реализацию, используемую 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 });

        // ...
    }
}

Приведенный выше код задает для /Edit/17 значение url, когда страница 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?}.

Значения окружения Явные значения Результат
controller = "Home" action = "About" /Home/About
controller = "Home" controller = "Order", action = "About" /Order/About
controller = "Home", color = "Red" action = "About" /Home/About
controller = "Home" action = "About", color = "Red" /Home/About?color=Red

Необязательный порядок параметров маршрута

Необязательные параметры маршрута должны поступать после всех необходимых параметров маршрута и литералы. В следующем коде idname параметры должны поступать после 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-адреса.

Настройка метаданных конечной точки

Сведения о настройке метаданных конечной точки см. по следующим ссылкам:

Сопоставление узлов в маршрутах с помощью 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] применяется как к контроллеру, так и к методу действия, выполняется следующее.

  • Используется атрибут действия.
  • Атрибут контроллера не учитывается.

Предупреждение

API, основанный на заголовке узла, например HttpRequest.Host и RequireHost, подвержены потенциальному спуфинду клиентов.

Чтобы предотвратить спуфинирование узлов и портов, используйте один из следующих подходов:

Группы маршрутов

Метод 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;
}

В этом сценарии можно использовать относительный адрес заголовка Location201 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 маршрутов, чтобы разрешить доступ к частным данным и хранить данные о частных объектах.

Группы маршрутов также поддерживают вложенные группы и сложные шаблоны префикса с параметрами маршрута и ограничениями. В следующем примере обработчик маршрутов, сопоставленный с 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 и методы не влияют на ПО промежуточного слоя, размещенного раньшеUseRouting.MapShortCircuit При попытке использовать эти методы с конечными точками, которые также имеют [Authorize] или [RequireCors] метаданные, запросы завершаются сбоем InvalidOperationException. Эти метаданные применяются либо атрибутами[Authorize], либо [EnableCors] методамиRequireCors.RequireAuthorization

Чтобы увидеть влияние по промежуточного слоя короткого канала, задайте для категории ведения журнала "Майкрософт" значение 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. ПО промежуточного слоя авторизации позволяет передавать политику отката. Политика отката, если она указана, применяется к обоим элементам:

  • конечные точки без указанной политики;
  • запросы, которые не соответствуют конечной точке.

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

Отладка диагностики

Для подробного вывода диагностики построения маршрутов задайте для Logging:LogLevel:Microsoft значение Debug. В среде разработки задайте уровень журнала в appsettings.Development.json:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Debug",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  }
}

Дополнительные ресурсы

Маршрутизация обеспечивает сопоставление входящих HTTP-запросов и их распределение по исполняемым конечным точкам приложения. Конечные точки — это блоки исполняемого кода обработки запросов приложения. Конечные точки определяются в приложении и настраиваются при его запуске. Процесс сопоставления конечных точек может извлекать значения из URL-адреса запроса и предоставлять эти значения для обработки запроса. С помощью сведений о конечных точках из приложения маршрутизация также может формировать URL-адреса, которые сопоставляются с конечными точками.

Приложения могут настраивать маршрутизацию с помощью следующего.

В этой статье представлены сведения о низкоуровневой маршрутизации 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, пользовательское ПО промежуточного слоя будет выполняться после ПО промежуточного слоя сопоставления маршрутов.

Конечные точки

Для определения конечной точки используется метод MapGet. Конечная точка — это то, что можно:

  • выбрать путем сопоставления URL-адреса и метода HTTP;
  • выполнить путем запуска делегата.

Конечные точки, которые могут быть сопоставлены и выполнены приложением, настраиваются в UseEndpoints. Например, MapGet, MapPost и аналогичные методы подключают делегаты запросов к системе маршрутизации. Для подключения функций платформы ASP.NET Core к системе маршрутизации можно использовать дополнительные методы.

Ниже представлен пример маршрутизации с более сложным шаблоном маршрута.

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 добавляется ПО промежуточного слоя для проверки подлинности и авторизации. Это ПО промежуточного слоя размещается между методами 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.
  • Создайте вложенный конвейер ПО промежуточного слоя с помощью CreateApplicationBuilder.
  • Присоедините ПО промежуточного слоя к новому конвейеру. В этом случае — UseHealthChecks.
  • Build конвейер ПО промежуточного слоя в RequestDelegate.
  • Вызовите Map и укажите новый конвейер ПО промежуточного слоя.
  • Верните объект построителя, предоставленного Map, из метода расширения.

В следующем коде показано использование MapHealthChecks.

app.UseAuthentication();
app.UseAuthorization();

app.MapHealthChecks("/healthz").RequireAuthorization();

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

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

Соответствие URL-адресов

  • Это процесс, с помощью которого функция маршрутизации сопоставляет входящий запрос в конечную точку.
  • Он основан на данных в URL-пути и заголовках.
  • Его можно расширить, чтобы учесть любые данные в запросе.

При выполнении ПО промежуточного слоя маршрутизации оно задает конечную точку (Endpoint) и значения маршрута для функции запроса в HttpContext из текущего запроса.

  • Вызов HttpContext.GetEndpoint получает конечную точку.
  • HttpRequest.RouteValues получает коллекцию значений маршрута.

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

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

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

Предупреждение

Для обратной совместимости при выполнении делегата конечной точки контроллера или Razor страницы свойства RouteContext.RouteData задаются соответствующими значениями на основе обработки запросов, выполняемой до сих пор.

В следующем выпуске тип RouteContext будет помечен как устаревший.

  • Перенесите RouteData.Values в HttpRequest.RouteValues.
  • Миграция RouteData.DataTokens на получение IDataTokensMetadata из метаданных конечной точки.

Работа сопоставления URL-адресов подразделяется на этапы, которые можно настроить. На каждом этапе выходные данные представляют собой набор совпадений. По мере перехода между этапами набор совпадений можно ограничивать. Реализация маршрутизации не гарантирует порядок обработки для соответствующих конечных точек. Все возможные совпадения обрабатываются одновременно. Этапы сопоставления URL-адресов выполняются в следующем порядке. ASP.NET Core:

  1. URL-путь обрабатывается по набору конечных точек и их шаблонов маршрутов, при этом выполняется сбор всех совпадений.
  2. Принимается предыдущий список, и удаляются совпадения, которые не соответствуют примененным ограничениям маршрута.
  3. Принимает предыдущий список и удаляет совпадения, которые завершаются сбоем набора экземпляров MatcherPolicy .
  4. Используется для EndpointSelector принятия окончательного решения из предыдущего списка.

Список конечных точек определяется по приоритету в соответствии со следующим.

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

Приоритет маршрута вычисляется на основе более определенного шаблона маршрута, которому назначается более высокий приоритет. Например, рассмотрим шаблоны /hello и /{message}.

  • Оба соответствуют URL-пути /hello.
  • /hello является более конкретным, и, следовательно, ему назначается более высокий приоритет.

Как правило, приоритет маршрута помогает выбрать наилучшее соответствие для типов схем URL-адресов, используемых на практике. Используйте Order только в случае, когда необходимо избежать неоднозначности.

Ввиду различных типов расширяемости, предоставляемых службой маршрутизации, система маршрутизации не может заранее вычислить неоднозначные маршруты. Рассмотрим в качестве примера шаблоны маршрутов /{message:alpha} и /{message:int}.

  • Ограничение alpha соответствует только буквенным символам.
  • Ограничение int соответствует только числам.
  • Эти шаблоны имеют одинаковый приоритет маршрута, однако не существует одного URL-адреса, по которому они совпадают.
  • Если система маршрутизации сообщила об ошибке неоднозначности при запуске, это означает, что она заблокировала этот допустимый вариант использования.

Предупреждение

Порядок операций в UseEndpoints не влияет на поведение маршрутизации, за одним исключением. MapControllerRoute и MapAreaRoute автоматически присваивают значение порядка своим конечным точкам в соответствии с порядком их вызова. Это имитирует поведение контроллеров без системы маршрутизации в долгосрочной перспективе, предоставляя те же гарантии, что и в старых реализациях маршрутизации.

Маршрутизация конечных точек в ASP.NET Core:

  • Не имеет концепции маршрутов.
  • Не гарантирует порядок обработки. Все конечные точки обрабатываются одновременно.

Приоритет шаблонов маршрутов и порядок выбора конечных точек

Приоритет шаблонов маршрутов — это система, которая назначает каждому шаблону маршрута значение в зависимости от того, насколько он является конкретным. Приоритет шаблона маршрута

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

Например, рассмотрим шаблоны /Products/List и /Products/{id}. Разумно предположить, что для URL-пути /Products/List/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 происходит в два этапа:

  1. Адрес привязан к списку конечных точек, соответствующих адресу.
  2. RoutePattern конечной точки вычисляется, пока не будет найден шаблон маршрута, который соответствует предоставленным значениям. Полученный результат объединяется с другими частями URI, предоставленными генератору ссылок и возвращенными.

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

Метод расширения Description
GetPathByAddress Создает URI с абсолютным путем на основе предоставленных значений.
GetUriByAddress Создает абсолютный URI на основе предоставленных значений.

Предупреждение

Обратите внимание на следующие последствия вызова методов 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}

не является допустимым маршрутом, так как между и {action}не существует литерального значения{controller}. Параметрам маршрута должны быть присвоены имена, и для них могут быть определены дополнительные атрибуты.

Весь текст, кроме параметров маршрута (например, {id}) и разделителя пути /, должен соответствовать тексту в URL-адресе. Сопоставление текста производится без учета регистра на основе декодированного представления пути URL-адреса. Для сопоставления с литеральным разделителем параметров маршрута ({ или }) разделитель следует экранировать путем повтора символа. Например, {{ или }}.

Звездочка * или двойная звездочка **:

  • Можно использовать в качестве префикса параметра маршрута для привязки к остальной части URI.
  • Такие параметры называются универсальными. Например, blog/{**slug}:
    • Соответствует любому URI, который начинается с blog/ и имеет любое значение после него.
    • Значение после blog/ присваивается значению динамического маршрута.

Предупреждение

Соответствие параметра catch-all маршрутам может быть неправильным из-за ошибки в маршрутизации. Приложения, на работу которых влияет эта ошибка, обладают следующими характеристиками:

  • Маршрут 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}")] является сложным сегментом. Сложные сегменты работают определенным способом, который должен быть понятен для их успешного использования. В примере в этом разделе показано, почему сложные сегменты действительно хорошо работают только в том случае, если текст разделителя отсутствует в значениях параметров. Для более сложных случаев требуется использовать регулярное выражение, а затем вручную извлечь значения.

Предупреждение

При использовании 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-команды. Ограничения используются в маршрутизации запросов и создании ссылок.

Предупреждение

Не используйте ограничения для проверки входных данных. Если для проверки входных данных используются ограничения, недопустимые входные данные приводят к ошибке 404 ("Не найдено"). Недопустимые входные данные должны привести к ошибке 400 ("Неверный запрос") с соответствующим сообщением об ошибке. Ограничения маршрутов следует использовать для разрешения неоднозначности похожих маршрутов, а не для проверки входных данных определенного маршрута.

В приведенной ниже таблице показаны примеры ограничения маршрутов и их ожидаемое поведение.

ограничение Пример Примеры совпадений Примечания.
int {id:int} 123456789, -123456789 Соответствует любому целому числу
bool {active:bool} true, FALSE Соответствует true или false. Без учета регистра
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-адреса

Предупреждение

При использовании System.Text.RegularExpressions для обработки ненадежных входных данных передайте время ожидания. Злонамеренный пользователь может предоставить входные данные для RegularExpressions, что делает возможными атаки типа "отказ в обслуживании". API платформы ASP.NET Core, использующие RegularExpressions, передают время ожидания.

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

[Route("users/{id:int:min(1)}")]
public User GetUserById(int id) { }

Предупреждение

Ограничения маршрута, которые проверяют URL-адрес и могут быть преобразованы в тип CLR, всегда используют инвариантный язык и региональные параметры. Например, преобразование в тип CLR int или DateTime. Эти ограничения предполагают, что для URL-адреса не предусмотрена локализация. Предоставляемые платформой ограничения маршрутов не изменяют значения, хранящиеся в значениях маршрута. Все значения маршрута, переданные из URL-адреса, сохраняются как строки. Например, ограничение float пытается преобразовать значение маршрута в число с плавающей запятой, но преобразованное значение служит только для проверки возможности такого преобразования.

Регулярные выражения в ограничениях

Предупреждение

При использовании 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}$ во встроенном ограничении, используйте один из следующих способов.

Чтобы экранировать символы разделения параметров маршрутизации {, }, [, ], используйте их дважды в выражении (например, {{, }}, [[, ]]). В следующей таблице показаны регулярные выражения и их экранированные варианты.

Регулярное выражение Экранированное регулярное выражение
^\d{3}-\d{2}-\d{4}$ ^\\d{{3}}-\\d{{2}}-\\d{{4}}$
^[a-z]{2}$ ^[[a-z]]{{2}}$

Регулярные выражения, используемые при маршрутизации, часто начинаются с символа ^ и соответствуют начальной позиции строки. Выражения часто заканчиваются символом $ и соответствуют концу строки. Благодаря символам ^ и $ регулярное выражение сопоставляется со всем значением параметра маршрута. Если символы ^ и $ отсутствуют, регулярное выражение сопоставляется с любой подстрокой внутри строки, что обычно нежелательно. В таблице ниже представлен ряд примеров и объясняются причины соответствия или несоответствия.

Выражение Строка Поиск совпадений (Match) Комментарий
[a-z]{2} hello Да Соответствие подстроки
[a-z]{2} 123abc456 Да Соответствие подстроки
[a-z]{2} mz Да Соответствует выражению
[a-z]{2} MZ Да Без учета регистра
^[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);
    }
}

Предупреждение

При использовании 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 для использования преобразователей параметров со сформированными маршрутами.

Справочник по формированию URL-адресов

В этом разделе представлен справочник по алгоритму, реализованному при формировании URL-адреса. На практике в большинстве сложных примеров формирования URL-адресов используются контроллеры или Razor Pages. Дополнительные сведения см. в статье Маршрутизация в контроллерах.

Процесс создания URL-адресов начинается с вызова LinkGenerator.GetPathByAddress или аналогичного метода. Метод предоставляется с адресом, набором значений маршрута и при необходимости со сведениями о текущем запросе из HttpContext.

Первым шагом является использование адреса для разрешения набора конечных точек-кандидатов с помощью IEndpointAddressScheme<TAddress>, соответствующих типу адреса.

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

Устранение неполадок при формировании URL-адресов с помощью ведения журнала

Первым шагом при устранении неполадок при формировании URL-адресов является установка уровня ведения журнала Microsoft.AspNetCore.Routing для TRACE. LinkGenerator фиксирует в журнале множество сведений об обработке, которые могут быть полезны при устранении неполадок.

Дополнительные сведения о формировании URL-адресов см. в разделе Справочник по формированию URL-адресов.

Адреса

Адреса являются основным понятием в формировании URL-адресов и используются для привязки вызова генератора ссылок к набору конечных точек-кандидатов.

Адреса — это расширяемое понятие, которое по умолчанию поставляется с двумя реализациями.

  • Использование имени конечной точки (string) в качестве адреса:
    • Предоставляет аналогичные функции для имени маршрута MVC.
    • Использует тип метаданных IEndpointNameMetadata.
    • Разрешает указанную строку в соответствии с метаданными всех зарегистрированных конечных точек.
    • Создает исключение при запуске, если несколько конечных точек использует одно и то же имя.
    • Рекомендуется для общего использования за пределами контроллеров и Razor Pages.
  • Использование значений маршрутов (RouteValuesAddress) в качестве адреса:
    • Предоставляет аналогичные устаревшие функции по формированию URL-адресов для контроллеров и Razor Pages.
    • Очень сложные расширение и отладка.
    • Предоставляет реализацию, используемую 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 });

        // ...
    }
}

Приведенный выше код задает для /Edit/17 значение url, когда страница 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?}.

Значения окружения Явные значения Результат
controller = "Home" action = "About" /Home/About
controller = "Home" controller = "Order", action = "About" /Order/About
controller = "Home", color = "Red" action = "About" /Home/About
controller = "Home" action = "About", color = "Red" /Home/About?color=Red

Необязательный порядок параметров маршрута

Необязательные параметры маршрута должны поступать после всех необходимых параметров маршрута. В следующем коде idname параметры должны поступать после 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-адреса.

Настройка метаданных конечной точки

Сведения о настройке метаданных конечной точки см. по следующим ссылкам:

Сопоставление узлов в маршрутах с помощью 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;
}

В этом сценарии можно использовать относительный адрес заголовка Location201 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 маршрутов, чтобы разрешить доступ к частным данным и хранить данные о частных объектах.

Группы маршрутов также поддерживают вложенные группы и сложные шаблоны префикса с параметрами маршрута и ограничениями. В следующем примере обработчик маршрутов, сопоставленный с 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. ПО промежуточного слоя авторизации позволяет передавать политику отката. Политика отката, если она указана, применяется к обоим элементам:

  • конечные точки без указанной политики;
  • запросы, которые не соответствуют конечной точке.

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

Отладка диагностики

Для подробного вывода диагностики построения маршрутов задайте для Logging:LogLevel:Microsoft значение Debug. В среде разработки задайте уровень журнала в appsettings.Development.json:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Debug",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  }
}

Дополнительные ресурсы

Маршрутизация обеспечивает сопоставление входящих HTTP-запросов и их распределение по исполняемым конечным точкам приложения. Конечные точки — это блоки исполняемого кода обработки запросов приложения. Конечные точки определяются в приложении и настраиваются при его запуске. Процесс сопоставления конечных точек может извлекать значения из URL-адреса запроса и предоставлять эти значения для обработки запроса. С помощью сведений о конечных точках из приложения маршрутизация также может формировать URL-адреса, которые сопоставляются с конечными точками.

Приложения могут настраивать маршрутизацию с помощью следующего.

В этой статье представлены сведения о низкоуровневой маршрутизации 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, пользовательское ПО промежуточного слоя будет выполняться после ПО промежуточного слоя сопоставления маршрутов.

Конечные точки

Для определения конечной точки используется метод MapGet. Конечная точка — это то, что можно:

  • выбрать путем сопоставления URL-адреса и метода HTTP;
  • выполнить путем запуска делегата.

Конечные точки, которые могут быть сопоставлены и выполнены приложением, настраиваются в UseEndpoints. Например, MapGet, MapPost и аналогичные методы подключают делегаты запросов к системе маршрутизации. Для подключения функций платформы ASP.NET Core к системе маршрутизации можно использовать дополнительные методы.

Ниже представлен пример маршрутизации с более сложным шаблоном маршрута.

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 добавляется ПО промежуточного слоя для проверки подлинности и авторизации. Это ПО промежуточного слоя размещается между методами 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.
  • Создайте вложенный конвейер ПО промежуточного слоя с помощью CreateApplicationBuilder.
  • Присоедините ПО промежуточного слоя к новому конвейеру. В этом случае — UseHealthChecks.
  • Build конвейер ПО промежуточного слоя в RequestDelegate.
  • Вызовите Map и укажите новый конвейер ПО промежуточного слоя.
  • Верните объект построителя, предоставленного Map, из метода расширения.

В следующем коде показано использование MapHealthChecks.

app.UseAuthentication();
app.UseAuthorization();

app.MapHealthChecks("/healthz").RequireAuthorization();

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

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

Соответствие URL-адресов

  • Это процесс, с помощью которого функция маршрутизации сопоставляет входящий запрос в конечную точку.
  • Он основан на данных в URL-пути и заголовках.
  • Его можно расширить, чтобы учесть любые данные в запросе.

При выполнении ПО промежуточного слоя маршрутизации оно задает конечную точку (Endpoint) и значения маршрута для функции запроса в HttpContext из текущего запроса.

  • Вызов HttpContext.GetEndpoint получает конечную точку.
  • HttpRequest.RouteValues получает коллекцию значений маршрута.

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

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

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

Предупреждение

Для обратной совместимости при выполнении делегата конечной точки контроллера или Razor страницы свойства RouteContext.RouteData задаются соответствующими значениями на основе обработки запросов, выполняемой до сих пор.

В следующем выпуске тип RouteContext будет помечен как устаревший.

  • Перенесите RouteData.Values в HttpRequest.RouteValues.
  • Миграция RouteData.DataTokens на получение IDataTokensMetadata из метаданных конечной точки.

Работа сопоставления URL-адресов подразделяется на этапы, которые можно настроить. На каждом этапе выходные данные представляют собой набор совпадений. По мере перехода между этапами набор совпадений можно ограничивать. Реализация маршрутизации не гарантирует порядок обработки для соответствующих конечных точек. Все возможные совпадения обрабатываются одновременно. Этапы сопоставления URL-адресов выполняются в следующем порядке. ASP.NET Core:

  1. URL-путь обрабатывается по набору конечных точек и их шаблонов маршрутов, при этом выполняется сбор всех совпадений.
  2. Принимается предыдущий список, и удаляются совпадения, которые не соответствуют примененным ограничениям маршрута.
  3. Принимает предыдущий список и удаляет совпадения, которые завершаются сбоем набора экземпляров MatcherPolicy .
  4. Используется для EndpointSelector принятия окончательного решения из предыдущего списка.

Список конечных точек определяется по приоритету в соответствии со следующим.

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

Приоритет маршрута вычисляется на основе более определенного шаблона маршрута, которому назначается более высокий приоритет. Например, рассмотрим шаблоны /hello и /{message}.

  • Оба соответствуют URL-пути /hello.
  • /hello является более конкретным, и, следовательно, ему назначается более высокий приоритет.

Как правило, приоритет маршрута помогает выбрать наилучшее соответствие для типов схем URL-адресов, используемых на практике. Используйте Order только в случае, когда необходимо избежать неоднозначности.

Ввиду различных типов расширяемости, предоставляемых службой маршрутизации, система маршрутизации не может заранее вычислить неоднозначные маршруты. Рассмотрим в качестве примера шаблоны маршрутов /{message:alpha} и /{message:int}.

  • Ограничение alpha соответствует только буквенным символам.
  • Ограничение int соответствует только числам.
  • Эти шаблоны имеют одинаковый приоритет маршрута, однако не существует одного URL-адреса, по которому они совпадают.
  • Если система маршрутизации сообщила об ошибке неоднозначности при запуске, это означает, что она заблокировала этот допустимый вариант использования.

Предупреждение

Порядок операций в UseEndpoints не влияет на поведение маршрутизации, за одним исключением. MapControllerRoute и MapAreaRoute автоматически присваивают значение порядка своим конечным точкам в соответствии с порядком их вызова. Это имитирует поведение контроллеров без системы маршрутизации в долгосрочной перспективе, предоставляя те же гарантии, что и в старых реализациях маршрутизации.

Маршрутизация конечных точек в ASP.NET Core:

  • Не имеет концепции маршрутов.
  • Не гарантирует порядок обработки. Все конечные точки обрабатываются одновременно.

Приоритет шаблонов маршрутов и порядок выбора конечных точек

Приоритет шаблонов маршрутов — это система, которая назначает каждому шаблону маршрута значение в зависимости от того, насколько он является конкретным. Приоритет шаблона маршрута

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

Например, рассмотрим шаблоны /Products/List и /Products/{id}. Разумно предположить, что для URL-пути /Products/List/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 происходит в два этапа:

  1. Адрес привязан к списку конечных точек, соответствующих адресу.
  2. RoutePattern конечной точки вычисляется, пока не будет найден шаблон маршрута, который соответствует предоставленным значениям. Полученный результат объединяется с другими частями URI, предоставленными генератору ссылок и возвращенными.

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

Метод расширения Description
GetPathByAddress Создает URI с абсолютным путем на основе предоставленных значений.
GetUriByAddress Создает абсолютный URI на основе предоставленных значений.

Предупреждение

Обратите внимание на следующие последствия вызова методов 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}

не является допустимым маршрутом, так как между и {action}не существует литерального значения{controller}. Параметрам маршрута должны быть присвоены имена, и для них могут быть определены дополнительные атрибуты.

Весь текст, кроме параметров маршрута (например, {id}) и разделителя пути /, должен соответствовать тексту в URL-адресе. Сопоставление текста производится без учета регистра на основе декодированного представления пути URL-адреса. Для сопоставления с литеральным разделителем параметров маршрута ({ или }) разделитель следует экранировать путем повтора символа. Например, {{ или }}.

Звездочка * или двойная звездочка **:

  • Можно использовать в качестве префикса параметра маршрута для привязки к остальной части URI.
  • Такие параметры называются универсальными. Например, blog/{**slug}:
    • Соответствует любому URI, который начинается с blog/ и имеет любое значение после него.
    • Значение после blog/ присваивается значению динамического маршрута.

Предупреждение

Соответствие параметра catch-all маршрутам может быть неправильным из-за ошибки в маршрутизации. Приложения, на работу которых влияет эта ошибка, обладают следующими характеристиками:

  • Маршрут 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}")] является сложным сегментом. Сложные сегменты работают определенным способом, который должен быть понятен для их успешного использования. В примере в этом разделе показано, почему сложные сегменты действительно хорошо работают только в том случае, если текст разделителя отсутствует в значениях параметров. Для более сложных случаев требуется использовать регулярное выражение, а затем вручную извлечь значения.

Предупреждение

При использовании 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-команды. Ограничения используются в маршрутизации запросов и создании ссылок.

Предупреждение

Не используйте ограничения для проверки входных данных. Если для проверки входных данных используются ограничения, недопустимые входные данные приводят к ошибке 404 ("Не найдено"). Недопустимые входные данные должны привести к ошибке 400 ("Неверный запрос") с соответствующим сообщением об ошибке. Ограничения маршрутов следует использовать для разрешения неоднозначности похожих маршрутов, а не для проверки входных данных определенного маршрута.

В приведенной ниже таблице показаны примеры ограничения маршрутов и их ожидаемое поведение.

ограничение Пример Примеры совпадений Примечания.
int {id:int} 123456789, -123456789 Соответствует любому целому числу
bool {active:bool} true, FALSE Соответствует true или false. Без учета регистра
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-адреса

Предупреждение

При использовании System.Text.RegularExpressions для обработки ненадежных входных данных передайте время ожидания. Злонамеренный пользователь может предоставить входные данные для RegularExpressions, что делает возможными атаки типа "отказ в обслуживании". API платформы ASP.NET Core, использующие RegularExpressions, передают время ожидания.

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

[Route("users/{id:int:min(1)}")]
public User GetUserById(int id) { }

Предупреждение

Ограничения маршрута, которые проверяют URL-адрес и могут быть преобразованы в тип CLR, всегда используют инвариантный язык и региональные параметры. Например, преобразование в тип CLR int или DateTime. Эти ограничения предполагают, что для URL-адреса не предусмотрена локализация. Предоставляемые платформой ограничения маршрутов не изменяют значения, хранящиеся в значениях маршрута. Все значения маршрута, переданные из URL-адреса, сохраняются как строки. Например, ограничение float пытается преобразовать значение маршрута в число с плавающей запятой, но преобразованное значение служит только для проверки возможности такого преобразования.

Регулярные выражения в ограничениях

Предупреждение

При использовании 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}$ во встроенном ограничении, используйте один из следующих способов.

Чтобы экранировать символы разделения параметров маршрутизации {, }, [, ], используйте их дважды в выражении (например, {{, }}, [[, ]]). В следующей таблице показаны регулярные выражения и их экранированные варианты.

Регулярное выражение Экранированное регулярное выражение
^\d{3}-\d{2}-\d{4}$ ^\\d{{3}}-\\d{{2}}-\\d{{4}}$
^[a-z]{2}$ ^[[a-z]]{{2}}$

Регулярные выражения, используемые при маршрутизации, часто начинаются с символа ^ и соответствуют начальной позиции строки. Выражения часто заканчиваются символом $ и соответствуют концу строки. Благодаря символам ^ и $ регулярное выражение сопоставляется со всем значением параметра маршрута. Если символы ^ и $ отсутствуют, регулярное выражение сопоставляется с любой подстрокой внутри строки, что обычно нежелательно. В таблице ниже представлен ряд примеров и объясняются причины соответствия или несоответствия.

Выражение Строка Поиск совпадений (Match) Комментарий
[a-z]{2} hello Да Соответствие подстроки
[a-z]{2} 123abc456 Да Соответствие подстроки
[a-z]{2} mz Да Соответствует выражению
[a-z]{2} MZ Да Без учета регистра
^[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);
    }
}

Предупреждение

При использовании 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 для использования преобразователей параметров со сформированными маршрутами.

Справочник по формированию URL-адресов

В этом разделе представлен справочник по алгоритму, реализованному при формировании URL-адреса. На практике в большинстве сложных примеров формирования URL-адресов используются контроллеры или Razor Pages. Дополнительные сведения см. в статье Маршрутизация в контроллерах.

Процесс создания URL-адресов начинается с вызова LinkGenerator.GetPathByAddress или аналогичного метода. Метод предоставляется с адресом, набором значений маршрута и при необходимости со сведениями о текущем запросе из HttpContext.

Первым шагом является использование адреса для разрешения набора конечных точек-кандидатов с помощью IEndpointAddressScheme<TAddress>, соответствующих типу адреса.

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

Устранение неполадок при формировании URL-адресов с помощью ведения журнала

Первым шагом при устранении неполадок при формировании URL-адресов является установка уровня ведения журнала Microsoft.AspNetCore.Routing для TRACE. LinkGenerator фиксирует в журнале множество сведений об обработке, которые могут быть полезны при устранении неполадок.

Дополнительные сведения о формировании URL-адресов см. в разделе Справочник по формированию URL-адресов.

Адреса

Адреса являются основным понятием в формировании URL-адресов и используются для привязки вызова генератора ссылок к набору конечных точек-кандидатов.

Адреса — это расширяемое понятие, которое по умолчанию поставляется с двумя реализациями.

  • Использование имени конечной точки (string) в качестве адреса:
    • Предоставляет аналогичные функции для имени маршрута MVC.
    • Использует тип метаданных IEndpointNameMetadata.
    • Разрешает указанную строку в соответствии с метаданными всех зарегистрированных конечных точек.
    • Создает исключение при запуске, если несколько конечных точек использует одно и то же имя.
    • Рекомендуется для общего использования за пределами контроллеров и Razor Pages.
  • Использование значений маршрутов (RouteValuesAddress) в качестве адреса:
    • Предоставляет аналогичные устаревшие функции по формированию URL-адресов для контроллеров и Razor Pages.
    • Очень сложные расширение и отладка.
    • Предоставляет реализацию, используемую 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 });

        // ...
    }
}

Приведенный выше код задает для /Edit/17 значение url, когда страница 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?}.

Значения окружения Явные значения Результат
controller = "Home" action = "About" /Home/About
controller = "Home" controller = "Order", action = "About" /Order/About
controller = "Home", color = "Red" action = "About" /Home/About
controller = "Home" action = "About", color = "Red" /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-адреса.

Настройка метаданных конечной точки

Сведения о настройке метаданных конечной точки см. по следующим ссылкам:

Сопоставление узлов в маршрутах с помощью 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. ПО промежуточного слоя авторизации позволяет передавать политику отката. Политика отката, если она указана, применяется к обоим элементам:

  • конечные точки без указанной политики;
  • запросы, которые не соответствуют конечной точке.

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

Отладка диагностики

Для подробного вывода диагностики построения маршрутов задайте для Logging:LogLevel:Microsoft значение Debug. В среде разработки задайте уровень журнала в appsettings.Development.json:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Debug",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  }
}

Дополнительные ресурсы

Маршрутизация обеспечивает сопоставление входящих HTTP-запросов и их распределение по исполняемым конечным точкам приложения. Конечные точки — это блоки исполняемого кода обработки запросов приложения. Конечные точки определяются в приложении и настраиваются при его запуске. Процесс сопоставления конечных точек может извлекать значения из URL-адреса запроса и предоставлять эти значения для обработки запроса. С помощью сведений о конечных точках из приложения маршрутизация также может формировать URL-адреса, которые сопоставляются с конечными точками.

Приложения могут настраивать маршрутизацию с помощью следующего.

В этом документе представлены сведения о низкоуровневой маршрутизации ASP.NET Core. Дополнительные сведения о настройке маршрутизации

Система маршрутизации конечных точек, описанная в этом документе, применима к ASP.NET Core 3.0 и более поздних версий. Чтобы получить сведения о предыдущей системе маршрутизации на основе IRouter, выберите версию 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.

Конечная точка

Для определения конечной точки используется метод MapGet. Конечная точка — это то, что можно:

  • выбрать путем сопоставления URL-адреса и метода HTTP;
  • выполнить путем запуска делегата.

Конечные точки, которые могут быть сопоставлены и выполнены приложением, настраиваются в UseEndpoints. Например, MapGet, MapPost и аналогичные методы подключают делегаты запросов к системе маршрутизации. Для подключения функций платформы ASP.NET Core к системе маршрутизации можно использовать дополнительные методы.

Ниже представлен пример маршрутизации с более сложным шаблоном маршрута.

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!");
        });
    });
}

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

В предыдущем примере, показано то, как:

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

При вызове MapHealthChecks добавляется конечная точка проверки работоспособности. Связывание RequireAuthorization с этим вызовом прикрепляет политику авторизации к конечной точке.

При вызове UseAuthentication и UseAuthorization добавляется ПО промежуточного слоя для проверки подлинности и авторизации. Это ПО промежуточного слоя размещается между методами 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.
  • Создайте вложенный конвейер ПО промежуточного слоя с помощью 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 получает коллекцию значений маршрута.

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

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

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

Предупреждение

Для обеспечения обратной совместимости при выполнении делегата конечной точки контроллера или Razor страницы свойства RouteContext.RouteData задаются соответствующими значениями на основе обработки запросов, выполняемой до сих пор.

В следующем выпуске тип RouteContext будет помечен как устаревший.

  • Перенесите RouteData.Values в HttpRequest.RouteValues.
  • Перенесите RouteData.DataTokens, чтобы получить IDataTokensMetadata из метаданных конечной точки.

Работа сопоставления URL-адресов подразделяется на этапы, которые можно настроить. На каждом этапе выходные данные представляют собой набор совпадений. По мере перехода между этапами набор совпадений можно ограничивать. Реализация маршрутизации не гарантирует порядок обработки для соответствующих конечных точек. Все возможные совпадения обрабатываются одновременно. Этапы сопоставления URL-адресов выполняются в следующем порядке. ASP.NET Core:

  1. URL-путь обрабатывается по набору конечных точек и их шаблонов маршрутов, при этом выполняется сбор всех совпадений.
  2. Принимается предыдущий список, и удаляются совпадения, которые не соответствуют примененным ограничениям маршрута.
  3. Принимается предыдущий список, и удаляются совпадения, которые не соответствуют набору экземпляров MatcherPolicy.
  4. Используется EndpointSelector для принятия окончательного решения из предыдущего списка.

Список конечных точек определяется по приоритету в соответствии со следующим.

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

Приоритет маршрута вычисляется на основе более определенного шаблона маршрута, которому назначается более высокий приоритет. Например, рассмотрим шаблоны /hello и /{message}.

  • Оба соответствуют URL-пути /hello.
  • /hello является более конкретным, и, следовательно, ему назначается более высокий приоритет.

Как правило, приоритет маршрута помогает выбрать наилучшее соответствие для типов схем URL-адресов, используемых на практике. Используйте Order только в случае, когда необходимо избежать неоднозначности.

Ввиду различных типов расширяемости, предоставляемых службой маршрутизации, система маршрутизации не может заранее вычислить неоднозначные маршруты. Рассмотрим в качестве примера шаблоны маршрутов /{message:alpha} и /{message:int}.

  • Ограничение alpha соответствует только буквенным символам.
  • Ограничение int соответствует только числам.
  • Эти шаблоны имеют одинаковый приоритет маршрута, однако не существует одного URL-адреса, по которому они совпадают.
  • Если система маршрутизации сообщила об ошибке неоднозначности при запуске, это означает, что она заблокировала этот допустимый вариант использования.

Предупреждение

Порядок операций в UseEndpoints не влияет на поведение маршрутизации, за одним исключением. MapControllerRoute и MapAreaRoute автоматически присваивают значение порядка своим конечным точкам в соответствии с порядком их вызова. Это имитирует поведение контроллеров без системы маршрутизации в долгосрочной перспективе, предоставляя те же гарантии, что и в старых реализациях маршрутизации.

В старой реализации маршрутизации можно реализовать расширяемость маршрутизации, которая зависит от порядка обработки маршрутов. Маршрутизация конечных точек в ASP.NET Core 3.0 и более поздних версий

  • Не имеет концепции маршрутов.
  • Не гарантирует порядок обработки. Все конечные точки обрабатываются одновременно.

Приоритет шаблонов маршрутов и порядок выбора конечных точек

Приоритет шаблонов маршрутов — это система, которая назначает каждому шаблону маршрута значение в зависимости от того, насколько он является конкретным. Приоритет шаблона маршрута

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

Например, рассмотрим шаблоны /Products/List и /Products/{id}. Разумно предположить, что для URL-пути /Products/List/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 происходит в два этапа:

  1. Адрес привязан к списку конечных точек, соответствующих адресу.
  2. RoutePattern конечной точки вычисляется, пока не будет найден шаблон маршрута, который соответствует предоставленным значениям. Полученный результат объединяется с другими частями URI, предоставленными генератору ссылок и возвращенными.

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

Метод расширения Description
GetPathByAddress Создает URI с абсолютным путем на основе предоставленных значений.
GetUriByAddress Создает абсолютный URI на основе предоставленных значений.

Предупреждение

Обратите внимание на следующие последствия вызова методов 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 присваивается значению динамического маршрута.

Предупреждение

Соответствие параметра catch-all маршрутам может быть неправильным из-за ошибки в маршрутизации. Приложения, на работу которых влияет эта ошибка, обладают следующими характеристиками:

  • Маршрут 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}")] является сложным сегментом. Сложные сегменты работают определенным способом, который должен быть понятен для их успешного использования. В примере в этом разделе показано, почему сложные сегменты действительно хорошо работают только в том случае, если текст разделителя отсутствует в значениях параметров. Для более сложных случаев требуется использовать регулярное выражение, а затем вручную извлечь значения.

Предупреждение

При использовании 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-команды. Ограничения используются в маршрутизации запросов и создании ссылок.

Предупреждение

Не используйте ограничения для проверки входных данных. Если для проверки входных данных используются ограничения, недопустимые входные данные приводят к ошибке 404 ("Не найдено"). Недопустимые входные данные должны привести к ошибке 400 ("Неверный запрос") с соответствующим сообщением об ошибке. Ограничения маршрутов следует использовать для разрешения неоднозначности похожих маршрутов, а не для проверки входных данных определенного маршрута.

В приведенной ниже таблице показаны примеры ограничения маршрутов и их ожидаемое поведение.

ограничение Пример Примеры совпадений Примечания.
int {id:int} 123456789, -123456789 Соответствует любому целому числу
bool {active:bool} true, FALSE Соответствует true или false. Без учета регистра
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-адреса

Предупреждение

При использовании System.Text.RegularExpressions для обработки ненадежных входных данных передайте время ожидания. Злонамеренный пользователь может предоставить входные данные для RegularExpressions, что делает возможными атаки типа "отказ в обслуживании". API платформы ASP.NET Core, использующие RegularExpressions, передают время ожидания.

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

[Route("users/{id:int:min(1)}")]
public User GetUserById(int id) { }

Предупреждение

Ограничения маршрута, которые проверяют URL-адрес и могут быть преобразованы в тип CLR, всегда используют инвариантный язык и региональные параметры. Например, преобразование в тип CLR int или DateTime. Эти ограничения предполагают, что для URL-адреса не предусмотрена локализация. Предоставляемые платформой ограничения маршрутов не изменяют значения, хранящиеся в значениях маршрута. Все значения маршрута, переданные из URL-адреса, сохраняются как строки. Например, ограничение float пытается преобразовать значение маршрута в число с плавающей запятой, но преобразованное значение служит только для проверки возможности такого преобразования.

Регулярные выражения в ограничениях

Предупреждение

При использовании 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}$ во встроенном ограничении, используйте один из следующих способов.

Чтобы экранировать символы разделения параметров маршрутизации {, }, [, ], используйте их дважды в выражении (например, {{, }}, [[, ]]). В следующей таблице показаны регулярные выражения и их экранированные варианты.

Регулярное выражение Экранированное регулярное выражение
^\d{3}-\d{2}-\d{4}$ ^\\d{{3}}-\\d{{2}}-\\d{{4}}$
^[a-z]{2}$ ^[[a-z]]{{2}}$

Регулярные выражения, используемые при маршрутизации, часто начинаются с символа ^ и соответствуют начальной позиции строки. Выражения часто заканчиваются символом $ и соответствуют концу строки. Благодаря символам ^ и $ регулярное выражение сопоставляется со всем значением параметра маршрута. Если символы ^ и $ отсутствуют, регулярное выражение сопоставляется с любой подстрокой внутри строки, что обычно нежелательно. В таблице ниже представлен ряд примеров и объясняются причины соответствия или несоответствия.

Выражение Строка Поиск совпадений (Match) Комментарий
[a-z]{2} hello Да Соответствие подстроки
[a-z]{2} 123abc456 Да Соответствие подстроки
[a-z]{2} mz Да Соответствует выражению
[a-z]{2} MZ Да Без учета регистра
^[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;
    }
}

Предупреждение

При использовании 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 для использования преобразователей параметров со сформированными маршрутами.

Справочник по формированию URL-адресов

В этом разделе представлен справочник по алгоритму, реализованному при формировании URL-адреса. На практике в большинстве сложных примеров формирования URL-адресов используются контроллеры или Razor Pages. Дополнительные сведения см. в статье Маршрутизация в контроллерах.

Процесс формирования URL-адреса начинается с вызова LinkGenerator.GetPathByAddress или аналогичного метода. Метод предоставляется с адресом, набором значений маршрута и при необходимости со сведениями о текущем запросе из HttpContext.

Первым шагом является использование адреса для разрешения набора конечных точек-кандидатов с помощью IEndpointAddressScheme<TAddress>, соответствующих типу адреса.

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

Устранение неполадок при формировании URL-адресов с помощью ведения журнала

Первым шагом при устранении неполадок при формировании URL-адресов является установка уровня ведения журнала Microsoft.AspNetCore.Routing для TRACE. LinkGenerator фиксирует в журнале множество сведений об обработке, которые могут быть полезны при устранении неполадок.

Дополнительные сведения о формировании URL-адресов см. в разделе Справочник по формированию URL-адресов.

Адреса

Адреса являются основным понятием в формировании URL-адресов и используются для привязки вызова генератора ссылок к набору конечных точек-кандидатов.

Адреса — это расширяемое понятие, которое по умолчанию поставляется с двумя реализациями.

  • Использование имени конечной точки (string) в качестве адреса:
    • Предоставляет аналогичные функции для имени маршрута MVC.
    • Использует тип метаданных IEndpointNameMetadata.
    • Разрешает указанную строку в соответствии с метаданными всех зарегистрированных конечных точек.
    • Создает исключение при запуске, если несколько конечных точек использует одно и то же имя.
    • Рекомендуется для общего использования за пределами контроллеров и Razor Pages.
  • Использование значений маршрутов (RouteValuesAddress) в качестве адреса:
    • Предоставляет аналогичные устаревшие функции по формированию URL-адресов для контроллеров и Razor Pages.
    • Очень сложные расширение и отладка.
    • Предоставляет реализацию, используемую 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;
    }
}

Приведенный выше код задает для /Edit/17 значение url, когда страница 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?}.

Значения окружения Явные значения Результат
controller = "Home" action = "About" /Home/About
controller = "Home" controller = "Order", action = "About" /Order/About
controller = "Home", color = "Red" action = "About" /Home/About
controller = "Home" action = "About", color = "Red" /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, который имеется в требуемых значениях.

Настройка метаданных конечной точки

Сведения о настройке метаданных конечной точки см. на следующих веб-страницах:

Сопоставление узлов в маршрутах с помощью 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. ПО промежуточного слоя авторизации позволяет передавать политику отката. Политика отката, если она указана, применяется к обоим элементам:

  • конечные точки без указанной политики;
  • запросы, которые не соответствуют конечной точке.

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

Отладка диагностики

Для подробного вывода диагностики построения маршрутов задайте для Logging:LogLevel:Microsoft значение Debug. В среде разработки задайте уровень журнала в appsettings.Development.json:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Debug",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  }
}