Enrutamiento en ASP.NET Core

Por Ryan Nowak, Kirk Larkin y Rick Anderson

Nota

Esta no es la versión más reciente de este artículo. Para la versión actual, consulte la versión .NET 8 de este artículo.

Importante

Esta información hace referencia a un producto en versión preliminar, el cual puede sufrir importantes modificaciones antes de que se publique la versión comercial. Microsoft no proporciona ninguna garantía, expresa o implícita, con respecto a la información proporcionada aquí.

Para la versión actual, consulte la versión .NET 8 de este artículo.

El enrutamiento es responsable de hacer coincidir las solicitudes HTTP entrantes y de enviarlas a los puntos de conexión ejecutables de la aplicación. Los puntos de conexión son las unidades de código de control de solicitudes ejecutable de la aplicación. Se definen en la aplicación y se configuran al iniciarla. El proceso de búsqueda de coincidencias de puntos de conexión puede extraer valores de la dirección URL de la solicitud y proporcionarlos para el procesamiento de la solicitud. Con la información de los puntos de conexión de la aplicación, el enrutamiento también puede generar direcciones URL que se asignan a los puntos de conexión.

Las aplicaciones pueden configurar el enrutamiento mediante:

  • Controladores
  • Razor Pages
  • SignalR
  • Servicios gRPC
  • Middleware habilitado para puntos de conexión, como las comprobaciones de estado.
  • Delegados y expresiones lambda registrados con el enrutamiento.

En este artículo se describen los detalles de bajo nivel del enrutamiento de ASP.NET Core. Para obtener información sobre la configuración del enrutamiento:

Fundamentos del enrutamiento

En el código siguiente se muestra un ejemplo básico de enrutamiento:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();

En el ejemplo anterior se incluye un único punto de conexión que usa el método MapGet:

  • Al enviar una solicitud HTTP GET a la dirección URL raíz /:
    • Se ejecuta el delegado de la solicitud.
    • Se escribe Hello World! en la respuesta HTTP.
  • Si el método de solicitud no es GET o la dirección URL raíz no es /, no se detecta ninguna ruta y se devuelve HTTP 404.

El enrutamiento usa un par de middleware, registrado por UseRouting y UseEndpoints:

  • UseRouting agrega coincidencia de rutas a la canalización de middleware. Este middleware examina el conjunto de puntos de conexión definidos en la aplicación y selecciona la mejor coincidencia en función de la solicitud.
  • UseEndpoints agrega la ejecución del punto de conexión a la canalización de middleware. Ejecuta el delegado asociado al punto de conexión seleccionado.

Normalmente, las aplicaciones no necesitan llamar a UseRouting ni a UseEndpoints. WebApplicationBuilder configura una canalización de middleware que encapsula el middleware agregado en Program.cs con UseRouting y UseEndpoints. Sin embargo, las aplicaciones pueden cambiar el orden en que se ejecutan UseRouting y UseEndpoints llamando a estos métodos explícitamente. Por ejemplo, el código siguiente realiza una llamada explícita a UseRouting:

app.Use(async (context, next) =>
{
    // ...
    await next(context);
});

app.UseRouting();

app.MapGet("/", () => "Hello World!");

En el código anterior:

  • La llamada a app.Use registra un middleware personalizado que se ejecuta al principio de la canalización.
  • La llamada a UseRouting configura el middleware de coincidencia de rutas para que se ejecute después del middleware personalizado.
  • El punto de conexión registrado con MapGet se ejecuta al final de la canalización.

Si el ejemplo anterior no incluyese una llamada a UseRouting, el middleware personalizado se ejecutaría después del middleware de coincidencia de rutas.

Nota: Las rutas agregadas directamente a WebApplication se ejecutan al final de la canalización.

Puntos de conexión

El método MapGet se usa para definir un punto de conexión. Un punto de conexión es algo que se puede:

  • Seleccionar, si se hacen coincidir la dirección URL y el método HTTP.
  • Ejecutar, mediante la ejecución del delegado.

Los puntos de conexión que la aplicación puede ejecutar y hacer coincidir se configuran en UseEndpoints. Por ejemplo, MapGet, MapPost y métodos similares conectan delegados de solicitud al sistema de enrutamiento. Se pueden usar métodos adicionales para conectar características del marco ASP.NET Core al sistema de enrutamiento:

En el ejemplo siguiente se muestra el enrutamiento con una plantilla de ruta más sofisticada:

app.MapGet("/hello/{name:alpha}", (string name) => $"Hello {name}!");

La cadena /hello/{name:alpha} es una plantilla de ruta. Se usa una plantilla de ruta para configurar la coincidencia del punto de conexión. En este caso, la plantilla coincide con:

  • Una dirección URL como /hello/Docs.
  • Cualquier ruta de dirección URL que comience por /hello/, seguido de una secuencia de caracteres alfabéticos. :alpha aplica una restricción de ruta que solo coincide con caracteres alfabéticos. Las restricciones de ruta se explican más adelante en este artículo.

El segundo segmento de la ruta de dirección URL, {name:alpha}:

En el ejemplo siguiente se muestra el enrutamiento con comprobaciones de estado y autorización:

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

app.MapHealthChecks("/healthz").RequireAuthorization();
app.MapGet("/", () => "Hello World!");

En el ejemplo anterior se muestra cómo:

  • El middleware de autorización se puede usar con el enrutamiento.
  • Los puntos de conexión se pueden usar para configurar el comportamiento de la autorización.

La llamada a MapHealthChecks agrega un punto de conexión de comprobación de estado. Al encadenar RequireAuthorization a esta llamada, se adjunta una directiva de autorización al punto de conexión.

La llamada a UseAuthentication y UseAuthorization agrega el middleware de autenticación y autorización. Estos middleware se colocan entre UseRouting y UseEndpoints para que puedan:

  • Vea qué punto de conexión ha seleccionado UseRouting.
  • Aplique una directiva de autorización antes de que UseEndpoints envíe al punto de conexión.

Metadatos de punto de conexión

En el ejemplo anterior, hay dos puntos de conexión, pero solo el de comprobación de estado tiene una directiva de autorización adjunta. Si la solicitud coincide con el punto de conexión de comprobación de estado, /healthz, se realiza una comprobación de autorización. Esto demuestra que los puntos de conexión pueden tener datos adicionales adjuntos. Estos datos adicionales se denominan metadatos de punto de conexión:

  • Los metadatos pueden ser procesados mediante middleware compatible con el enrutamiento.
  • Los metadatos pueden ser de cualquier tipo de .NET.

Conceptos de enrutamiento

El sistema de enrutamiento se basa en la canalización de middleware mediante la adición del eficaz concepto de punto de conexión. Los puntos de conexión representan unidades de la funcionalidad de la aplicación que son diferentes entre sí en cuanto al enrutamiento, la autorización y cualquier número de sistemas de ASP.NET Core.

Definición de punto de conexión de ASP.NET Core

Un punto de conexión de ASP.NET Core es:

En el código siguiente se muestra cómo recuperar e inspeccionar el punto de conexión que coincide con la solicitud actual:

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.");

El punto de conexión, si se selecciona, se puede recuperar de HttpContext. Se pueden inspeccionar sus propiedades. Los objetos de punto de conexión son inmutables y no se pueden modificar después de crearlos. El tipo más común de punto de conexión es RouteEndpoint. RouteEndpoint incluye información que permite que el sistema de enrutamiento lo seleccione.

En el código anterior, app.Use configura un middleware insertado.

En el código siguiente se muestra que, en función de dónde se llame a app.Use en la canalización, es posible que no haya un punto de conexión:

// 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);
});

En el ejemplo anterior se agregan instrucciones Console.WriteLine que muestran si se ha seleccionado un punto de conexión o no. Para mayor claridad, en el ejemplo se asigna un nombre para mostrar al punto de conexión / proporcionado.

El ejemplo anterior también incluye llamadas a UseRouting y UseEndpoints para controlar exactamente cuándo se ejecuta este middleware dentro de la canalización.

Al ejecutar este código con una dirección URL de / se muestra lo siguiente:

1. Endpoint: (null)
2. Endpoint: Hello
3. Endpoint: Hello

Al ejecutar este código con otra dirección URL se muestra lo siguiente:

1. Endpoint: (null)
2. Endpoint: (null)
4. Endpoint: (null)

Este resultado muestra que:

  • El punto de conexión siempre es NULL antes de que se llame a UseRouting.
  • Si se encuentra una coincidencia, el extremo no es NULL entre UseRouting y UseEndpoints.
  • El middleware UseEndpoints es terminal cuando se encuentra una coincidencia. El middleware de terminal se define más adelante en este artículo.
  • El middleware después de UseEndpoints solo se ejecuta cuando no se encuentra ninguna coincidencia.

El middleware UseRouting usa el método SetEndpoint para asociar el punto de conexión al contexto actual. Se puede reemplazar el middleware UseRouting con lógica personalizada y seguir aprovechando las ventajas del uso de puntos de conexión. Los puntos de conexión son una primitiva de bajo nivel como middleware y no están unidos a la implementación de enrutamiento. La mayoría de las aplicaciones no necesitan reemplazar UseRouting por lógica personalizada.

El middleware UseEndpoints está diseñado para usarse junto con el middleware UseRouting. La lógica básica para ejecutar un punto de conexión no es complicada. Use GetEndpoint para recuperar el punto de conexión y, después, invoque su propiedad RequestDelegate.

En el código siguiente se muestra cómo el middleware puede influir en el enrutamiento o reaccionar ante este:

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 { }

En el ejemplo anterior se muestran dos conceptos importantes:

  • El middleware se puede ejecutar antes de UseRouting para modificar los datos sobre los que funciona el enrutamiento.
  • El middleware se puede ejecutar entre UseRouting y UseEndpoints para procesar los resultados del enrutamiento antes de que se ejecute el punto de conexión.
    • El middleware que se ejecuta entre UseRouting y UseEndpoints:
      • Normalmente inspecciona los metadatos para entender los puntos de conexión.
      • A menudo toma decisiones de seguridad, como UseAuthorization y UseCors.
    • La combinación de middleware y metadatos permite configurar directivas por punto de conexión.

En el código anterior se muestra un ejemplo de middleware personalizado que admite directivas por punto de conexión. El middleware escribe un registro de auditoría de acceso a datos confidenciales en la consola. El middleware se puede configurar para auditar un punto de conexión con los metadatos de RequiresAuditAttribute. En este ejemplo se muestra un patrón opcional en el que solo se auditan los puntos de conexión marcados como confidenciales. Esta lógica se puede definir en orden inverso, para auditar todo lo que no esté marcado como seguro, por ejemplo. El sistema de metadatos de punto de conexión es flexible. Esta lógica se puede diseñar de la manera que mejor se adapte al caso de uso.

El código del ejemplo anterior está diseñado para mostrar los conceptos básicos de los puntos de conexión. No está pensado para su uso en producción. Una versión más completa de un middleware de registro de auditoría:

  • Realizaría el registro en un archivo o una base de datos.
  • Incluiría detalles como el usuario, la dirección IP, el nombre del punto de conexión confidencial, etc.

El valor RequiresAuditAttribute de metadatos de directiva de auditoría se define como Attribute para facilitar su uso con marcos basados en clases como los controladores y SignalR. Cuando se usa de ruta a código:

  • Los metadatos se asocian con una API de generador.
  • Los marcos basados en clases incluyen todos los atributos en el método y la clase correspondientes al crear los puntos de conexión.

Los procedimientos recomendados para los tipos de metadatos son definirlos como interfaces o atributos. Las interfaces y los atributos permiten la reutilización del código. El sistema de metadatos es flexible y no impone ninguna limitación.

Comparación del middleware de terminal con el enrutamiento

En el ejemplo siguiente se muestra el middleware de terminal y el enrutamiento:

// 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.");

El estilo de middleware que se muestra con Approach 1: es middleware de terminal. Se denomina middleware de terminal porque realiza una operación de búsqueda de coincidencias:

  • La operación de búsqueda de coincidencias en el ejemplo anterior es Path == "/" para el middleware y Path == "/Routing" para el enrutamiento.
  • Cuando una coincidencia es correcta, ejecuta alguna funcionalidad y devuelve un valor, en lugar de invocar el middleware next.

Se denomina middleware de terminal porque finaliza la búsqueda, ejecuta alguna funcionalidad y, después, devuelve un valor.

En la lista siguiente se compara el middleware de terminal con el enrutamiento:

  • Los dos enfoques permiten terminar la canalización de procesamiento:
    • El middleware finaliza la canalización mediante la devolución de un valor en lugar de invocar next.
    • Los puntos de conexión siempre son de terminal.
  • El middleware de terminal permite colocar el middleware en un lugar arbitrario de la canalización:
    • Los puntos de conexión se ejecutan en la posición de UseEndpoints.
  • El middleware de terminal permite que el código arbitrario determine cuándo coincide el middleware:
    • El código personalizado de búsqueda de coincidencia de rutas puede ser detallado y difícil de escribir correctamente.
    • El enrutamiento proporciona soluciones sencillas para las aplicaciones típicas. La mayoría de las aplicaciones no requieren código personalizado de búsqueda de coincidencia de rutas.
  • Los puntos de conexión interactúan con middleware como UseAuthorization y UseCors.
    • Para usar un middleware de terminal con UseAuthorization o UseCors se necesita interactuar de forma manual con el sistema de autorización.

Un punto de conexión define:

  • Un delegado para procesar solicitudes.
  • Una colección de metadatos arbitrarios. Los metadatos se usan para implementar cuestiones transversales según las directivas y la configuración asociada a cada punto de conexión.

El middleware de terminal puede ser una herramienta eficaz, pero puede requerir:

  • Una cantidad significativa de código y pruebas.
  • La integración manual con otros sistemas para lograr el nivel deseado de flexibilidad.

Considere la posibilidad de realizar la integración con el enrutamiento antes de escribir middleware de terminal.

El middleware de terminal existente que se integra con Map o MapWhen normalmente se puede convertir en un punto de conexión compatible con el enrutamiento. MapHealthChecks muestra el patrón para enrutadores:

  • Escriba un método de extensión en IEndpointRouteBuilder.
  • Cree una canalización de middleware anidada mediante CreateApplicationBuilder.
  • Adjunte el middleware a la nueva canalización. En este caso, UseHealthChecks.
  • Aplique Build a la canalización de middleware en un objeto RequestDelegate.
  • Llame a Map y proporcione la nueva canalización de middleware.
  • Devuelva el objeto de generador proporcionado por Map desde el método de extensión.

En el código siguiente se muestra el uso de MapHealthChecks:

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

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

En el ejemplo anterior se muestra la importancia de devolver el objeto de generador. Al devolver el objeto de generador, el desarrollador de aplicaciones puede configurar directivas como la autorización para el punto de conexión. En este ejemplo, el middleware de comprobaciones de estado no tiene una integración directa con el sistema de autorización.

El sistema de metadatos se ha creado como respuesta a los problemas detectados por los autores de extensibilidad mediante el middleware de terminal. El problema de cada middleware es implementar su propia integración con el sistema de autorización.

Coincidencia de dirección URL

  • Es el proceso por el cual el enrutamiento hace coincidir una solicitud entrante a un punto de conexión.
  • Se basa en los datos de la ruta de acceso y los encabezados de la dirección URL.
  • Se puede extender para tener en cuenta los datos de la solicitud.

Cuando se ejecuta un middleware de enrutamiento, se establece un objeto Endpoint y se enrutan los valores a una característica de solicitud en el objeto HttpContext desde la solicitud actual:

  • La llamada a HttpContext.GetEndpoint obtiene el punto de conexión.
  • HttpRequest.RouteValues obtiene la colección de valores de ruta.

El middleware que se ejecuta después del middleware de enrutamiento puede inspeccionar el punto de conexión y tomar medidas. Por ejemplo, un middleware de autorización puede consultar la colección de metadatos del punto de conexión de una directiva de autorización. Después de que se ejecuta todo el middleware en la canalización de procesamiento de solicitudes, se invoca al delegado del punto de conexión seleccionado.

El sistema de enrutamiento en el enrutamiento de punto de conexión es responsable de todas las decisiones relativas al envío. Como el middleware aplica directivas en función del punto de conexión seleccionado, es importante que:

  • Cualquier decisión que pueda afectar al envío o a la aplicación de directivas de seguridad se realice dentro del sistema de enrutamiento.

Advertencia

En cuanto a la compatibilidad con versiones anteriores, cuando se ejecuta un delegado del punto de conexión de controlador o Razor Pages, las propiedades de RouteContext.RouteData se establecen en los valores adecuados en función del procesamiento de solicitudes realizado hasta el momento.

El tipo RouteContext se marcará como obsoleto en una versión futura:

  • Migre RouteData.Values a HttpRequest.RouteValues.
  • Migre RouteData.DataTokens para recuperar IDataTokensMetadata de los metadatos del punto de conexión.

La coincidencia de direcciones URL funciona en un conjunto configurable de fases. En cada fase, la salida es un conjunto de coincidencias. El conjunto de coincidencias se puede reducir más en la fase siguiente. La implementación de enrutamiento no garantiza un orden de procesamiento para los puntos de conexión coincidentes. Todas las coincidencias posibles se procesan a la vez. Las fases de coincidencia de direcciones URL se producen en el orden siguiente. ASP.NET Core:

  1. Procesa la ruta de dirección URL con el conjunto de puntos de conexión y sus plantillas de ruta, y se recopilan todas las coincidencias.
  2. Toma la lista anterior y quita las coincidencias en las que se produce un error con restricciones de ruta aplicadas.
  3. Toma la lista anterior y quita las coincidencias en las que se produce un error en el conjunto de instancias de MatcherPolicy.
  4. Usa EndpointSelector para tomar una decisión final de la lista anterior.

La lista de puntos de conexión se prioriza según:

Todos los puntos de conexión coincidentes se procesan en cada fase hasta que se alcanza EndpointSelector. EndpointSelector es la fase final. Elige el punto de conexión de prioridad más alta entre las coincidencias como la mejor coincidencia. Si hay otras coincidencias con la misma prioridad que la mejor, se inicia una excepción de coincidencia ambigua.

La prioridad de ruta se calcula en función de una plantilla de ruta más específica a la que se le asigna una prioridad más alta. Por ejemplo, considere las plantillas /hello y /{message}:

  • Las dos coinciden con la ruta de dirección URL /hello.
  • /hello es más específica y, por tanto, tiene mayor prioridad.

Por lo general, la precedencia de rutas realiza un buen trabajo de elegir la mejor coincidencia para los tipos de esquemas de dirección URL que se usan en la práctica. Use Order solo cuando sea necesario para evitar una ambigüedad.

Debido a los tipos de extensibilidad que proporciona el enrutamiento, el sistema de enrutamiento no puede calcular las rutas ambiguas por adelantado. Considere un ejemplo como las plantillas de ruta /{message:alpha} y /{message:int}:

  • La restricción alpha solo coincide con caracteres alfabéticos.
  • La restricción int solo coincide con números.
  • Estas plantillas tienen la misma prioridad de ruta, pero no hay ninguna dirección URL única con la que coincidan.
  • Si el sistema de enrutamiento ha notificado un error de ambigüedad al iniciarse, bloquearía este caso de uso válido.

Advertencia

El orden de las operaciones dentro de UseEndpoints no influye en el comportamiento del enrutamiento, con una excepción. MapControllerRoute y MapAreaRoute asignan de forma automática un valor de orden a sus puntos de conexión en función del orden en el que se hayan invocado. Esto simula el comportamiento a largo plazo de los controladores sin que el sistema de enrutamiento proporcione las mismas garantías que las implementaciones de enrutamiento anteriores.

Enrutamiento de puntos de conexión en ASP.NET Core:

  • No tiene el concepto de rutas.
  • No proporciona garantías de ordenación. Todos los puntos de conexión se procesan a la vez.

Prioridad de la plantilla de ruta y orden de selección de los puntos de conexión

La prioridad de la plantilla de ruta es un sistema que asigna a cada plantilla de ruta un valor en función de su especificidad. Precedencia de la plantilla de ruta:

  • Evita la necesidad de ajustar el orden de los puntos de conexión en casos comunes.
  • Intenta hacer coincidir las expectativas comunes del comportamiento del enrutamiento.

Por ejemplo, considere las plantillas /Products/List y /Products/{id}. Sería razonable suponer que /Products/List es una mejor coincidencia que /Products/{id} para la ruta de dirección URL /Products/List. Funciona porque el segmento literal /List se considera que tiene una mayor prioridad que el segmento de parámetro /{id}.

Los detalles de cómo funciona la precedencia están vinculados a cómo se definen las plantillas de ruta:

  • Las plantillas con más segmentos se consideran más específicas.
  • Un segmento con texto literal se considera más específico que un segmento de parámetro.
  • Un segmento de parámetro con una restricción se considera más específico que uno que no la tenga.
  • Un segmento complejo se considera igual de específico que un segmento de parámetro con una restricción.
  • Los parámetros comodín son los menos específicos. Vea comodín en la sección Plantillas de ruta para obtener información importante sobre las rutas comodín.

Conceptos de generación de direcciones URL

Generación de direcciones URL:

  • Es el proceso por el cual el enrutamiento puede crear una ruta de dirección URL en función de un conjunto de valores de ruta.
  • Permite una separación lógica entre los puntos de conexión y las direcciones URL que acceden a ellos.

El enrutamiento de punto de conexión incluye la API LinkGenerator. LinkGenerator es un servicio singleton disponible desde la DI. La API LinkGenerator se puede usar fuera del contexto de una solicitud en ejecución. Mvc.IUrlHelper y los escenarios que dependen de IUrlHelper, como los asistentes de etiquetas, los de HTML y los resultados de acción, usan de forma interna la API LinkGenerator para proporcionar funciones de generación de vínculos.

El generador de vínculos está respaldado por el concepto de una dirección y esquemas de direcciones. Un esquema de direcciones es una manera de determinar los puntos de conexión que se deben tener en cuenta para la generación de vínculos. Por ejemplo, los escenarios de nombre y valores de ruta de controladores y Razor Pages con los que muchos usuarios están familiarizados se implementan como un esquema de direcciones.

El generador de vínculos puede vincular a controladores y Razor Pages a través de los métodos de extensión siguientes:

Las sobrecargas de estos métodos aceptan argumentos que incluyan HttpContext. Estos métodos son funcionalmente equivalentes a Url.Action y Url.Page, pero ofrecen flexibilidad y opciones adicionales.

Los métodos GetPath* son más similares a Url.Action y Url.Page, dado que generan un URI que contiene una ruta de acceso absoluta. Los métodos GetUri* siempre generan un URI absoluto que contiene un esquema y un host. Los métodos que aceptan HttpContext generan un URI en el contexto de la solicitud que se ejecuta. A menos que se reemplacen, se usan los valores de ruta de ambiente, la ruta de acceso base de la dirección URL, el esquema y el host de la solicitud en ejecución.

Se llama a LinkGenerator con una dirección. La generación de un URI se produce en dos pasos:

  1. Se enlaza una dirección a una lista de puntos de conexión que coincidan con la dirección.
  2. Se evalúa el elemento RoutePattern de cada punto de conexión hasta que se encuentra un patrón de ruta que coincida con los valores proporcionados. La salida resultante se combina con otras partes del URI proporcionadas al generador de vínculos y devueltas.

Los métodos proporcionados por LinkGenerator admiten funciones estándar de generación de vínculos para cualquier tipo de dirección. La forma más práctica de usar el generador de vínculos es a través de métodos de extensión que realicen operaciones para un tipo de dirección específica:

Método de extensión Descripción
GetPathByAddress Genera un URI con una ruta de acceso absoluta en función de los valores proporcionados.
GetUriByAddress Genera un URI absoluto en función de los valores proporcionados.

Advertencia

Preste atención a las consecuencias siguientes de llamar a los métodos LinkGenerator:

  • Use los métodos de extensión GetUri* con precaución en una configuración de aplicación en la que no se valide el encabezado Host de las solicitudes entrantes. Si no se valida el encabezado Host de las solicitudes entrantes, la entrada de la solicitud que no sea de confianza se puede devolver al cliente en los URI de una página o vista. Se recomienda que todas las aplicaciones de producción configuren su servidor para validar el encabezado Host en función de valores válidos conocidos.

  • Use LinkGenerator con precaución en el middleware junto con Map o MapWhen. Map* cambia la ruta de acceso base de la solicitud que se ejecuta, lo que afecta a la salida de la generación de vínculos. Todas las API de LinkGenerator permiten especificar una ruta de acceso base. Especifique una ruta de acceso base vacía para deshacer el efecto de Map* en la generación de vínculos.

Ejemplo de middleware

En el ejemplo siguiente, un middleware usa la API LinkGenerator para crear un vínculo a un método de acción que enumera los productos de la tienda. El uso del generador de vínculos mediante su inserción en una clase y la llamada a GenerateLink está disponible para cualquier clase de una aplicación:

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

Plantillas de ruta

Los tokens de {} definen parámetros de ruta que se enlazan si se encuentran coincidencias con la ruta. Se puede definir más de un parámetro de ruta en un segmento de ruta, pero deben estar separados por un valor literal. Por ejemplo:

{controller=Home}{action=Index}

No es una ruta válida, ya que no hay ningún valor literal entre {controller} y {action}. Los parámetros de ruta deben tener un nombre y, opcionalmente, atributos adicionales especificados.

El texto literal diferente de los parámetros de ruta (por ejemplo, {id}) y el separador de ruta / deben coincidir con el texto de la dirección URL. La coincidencia de texto no distingue mayúsculas de minúsculas y se basa en la representación descodificada de la ruta de las direcciones URL. Para que el delimitador de parámetro de ruta literal { o } coincida, repita el carácter para aplicar escape al carácter. Por ejemplo, {{ o }}.

Asterisco * o asterisco doble **:

  • Se puede usar como prefijo de un parámetro de ruta para enlazar con el resto del URI.
  • Se denominan parámetros comodín. Por ejemplo, blog/{**slug}:
    • Coincide con cualquier URI que empiece por blog/ y después tenga cualquier valor.
    • El valor que aparece detrás de blog/ se asigna al valor de ruta slug.

Advertencia

Un parámetro catch-all puede relacionar rutas de forma incorrecta debido a un error en el enrutamiento. Las aplicaciones afectadas por este error tienen las características siguientes:

  • Una ruta catch-all (por ejemplo, {**slug}")
  • La ruta catch-all causa un error al relacionar solicitudes que sí que debería relacionar.
  • Al quitar otras rutas, la ruta catch-all empieza a funcionar.

Para ver casos de ejemplo relacionados con este error, consulte los errores 18677 y 16579 en GitHub.

Se incluye una corrección de participación para este error en el SDK de .NET Core 3.1.301 y versiones posteriores. En el código que hay a continuación se establece un cambio interno que corrige este error:

public static void Main(string[] args)
{
   AppContext.SetSwitch("Microsoft.AspNetCore.Routing.UseCorrectCatchAllBehavior", 
                         true);
   CreateHostBuilder(args).Build().Run();
}
// Remaining code removed for brevity.

Los parámetros comodín también pueden coincidir con una cadena vacía.

El parámetro comodín inserta los caracteres de escape correspondientes cuando se usa la ruta para generar una dirección URL, incluidos los caracteres / de separación de ruta de acceso. Por ejemplo, la ruta foo/{*path} con valores de ruta { path = "my/path" } genera foo/my%2Fpath. Tenga en cuenta la barra diagonal de escape. Para los caracteres separadores de ruta de acceso de ida y vuelta, use el prefijo de parámetro de ruta **. La ruta foo/{**path} con { path = "my/path" } genera foo/my/path.

Los patrones de dirección URL que intentan capturar un nombre de archivo con una extensión de archivo opcional tienen consideraciones adicionales. Por ejemplo, considere la plantilla files/{filename}.{ext?}. Cuando existen valores para filename y ext, los dos valores se rellenan. Si solo existe un valor para filename en la dirección URL, la ruta coincide porque el carácter . final es opcional. Las direcciones URL siguientes coinciden con esta ruta:

  • /files/myFile.txt
  • /files/myFile

Los parámetros de ruta pueden tener valores predeterminados designados mediante la especificación del valor predeterminado después del nombre de parámetro, separado por un signo igual (=). Por ejemplo, {controller=Home} define Home como el valor predeterminado de controller. El valor predeterminado se usa si no hay ningún valor en la dirección URL para el parámetro. Los parámetros de ruta se pueden convertir en opcionales si se anexa un signo de interrogación (?) al final del nombre del parámetro. Por ejemplo: id?. La diferencia entre los valores opcionales y los parámetros de ruta predeterminados es:

  • Un parámetro de ruta con un valor predeterminado siempre produce un valor.
  • Un parámetro opcional solo tiene un valor cuando la dirección URL de la solicitud proporciona un valor.

Los parámetros de ruta pueden tener restricciones que deben coincidir con el valor de ruta enlazado desde la dirección URL. Al agregar : y un nombre de restricción después del nombre del parámetro de ruta, se especifica una restricción insertada en un parámetro de ruta. Si la restricción requiere argumentos, se incluyen entre paréntesis (...) después del nombre de restricción. Se pueden especificar varias restricciones insertadas si se anexa otro carácter : y un nombre de restricción.

El nombre de restricción y los argumentos se pasan al servicio IInlineConstraintResolver para crear una instancia de IRouteConstraint para su uso en el procesamiento de direcciones URL. Por ejemplo, la plantilla de ruta blog/{article:minlength(10)} especifica una restricción minlength con el argumento 10. Para obtener más información sobre las restricciones de ruta y una lista de las restricciones proporcionadas por el marco, vea la sección Restricciones de ruta.

Los parámetros de ruta también pueden tener transformadores de parámetros. Los transformadores de parámetros transforman el valor de un parámetro al generar vínculos y hacer coincidir acciones y páginas con direcciones URL. Como sucede con las restricciones, los transformadores de parámetros se pueden agregar en línea a un parámetro de ruta mediante la incorporación un carácter : y un nombre de transformador después del nombre del parámetro de ruta. Por ejemplo, la plantilla de ruta blog/{article:slugify} especifica un transformador slugify. Para obtener más información sobre los transformadores de parámetros, vea la sección Transformadores de parámetros.

En la tabla siguiente se muestran plantillas de ruta de ejemplo y su comportamiento:

Plantilla de ruta URI coincidente de ejemplo El URI de la solicitud...
hello /hello Solo coincide con la ruta de acceso única /hello.
{Page=Home} / Coincide y establece Page en Home.
{Page=Home} /Contact Coincide y establece Page en Contact.
{controller}/{action}/{id?} /Products/List Se asigna al controlador Products y la acción List.
{controller}/{action}/{id?} /Products/Details/123 Se asigna al controlador Products y la acción Details con id establecido en 123.
{controller=Home}/{action=Index}/{id?} / Se asigna al controlador Home y al método Index. id se pasa por alto.
{controller=Home}/{action=Index}/{id?} /Products Se asigna al controlador Products y al método Index. id se pasa por alto.

El uso de una plantilla suele ser el método de enrutamiento más sencillo. Las restricciones y los valores predeterminados también se pueden especificar fuera de la plantilla de ruta.

Segmentos complejos

Los segmentos complejos se procesan mediante la búsqueda de coincidencias de delimitadores literales de derecha a izquierda de un modo no expansivo. Por ejemplo, [Route("/a{b}c{d}")] es un segmento complejo. Los segmentos complejos funcionan de una manera determinada que se debe entender para usarlos correctamente. En el ejemplo de esta sección se muestra por qué los segmentos complejos solo funcionan bien cuando el texto del delimitador no aparece dentro de los valores de los parámetros. En casos más complejos es necesario usar una expresión regular y extraer los valores de forma manual.

Advertencia

Cuando se usa System.Text.RegularExpressions para procesar entradas que no son de confianza, pase un tiempo de expiración. Un usuario malintencionado puede proporcionar entradas a RegularExpressions y provocar un ataque por denegación de servicio. Las API del marco ASP.NET Core en las que se usa RegularExpressions pasan un tiempo de expiración.

Este es un resumen de los pasos que realiza el enrutamiento con la plantilla /a{b}c{d} y la ruta de dirección URL /abcd. | se usa para ayudar a visualizar cómo funciona el algoritmo:

  • El primer literal, de derecha a izquierda, es c. Por tanto, se busca en /abcd desde la derecha y se encuentra /ab|c|d.
  • Todo lo que se encuentra a la derecha (d) coincide ahora con el parámetro de ruta {d}.
  • El siguiente literal, de derecha a izquierda, es a. Por tanto, se busca en /ab|c|d a partir de donde se ha parado antes, después a y se encuentra /|a|b|c|d.
  • El valor situado a la derecha (b) coincide ahora con el parámetro de ruta {b}.
  • No queda ningún texto ni ninguna plantilla de ruta, por lo que se trata de una coincidencia.

Este es un ejemplo de un caso negativo en el que se usa la misma plantilla /a{b}c{d} y la ruta de dirección URL /aabcd. | se usa para ayudar a visualizar cómo funciona el algoritmo. Este caso no es una coincidencia, que se explica mediante el mismo algoritmo:

  • El primer literal, de derecha a izquierda, es c. Por tanto, se busca en /aabcd desde la derecha y se encuentra /aab|c|d.
  • Todo lo que se encuentra a la derecha (d) coincide ahora con el parámetro de ruta {d}.
  • El siguiente literal, de derecha a izquierda, es a. Por tanto, se busca en /aab|c|d a partir de donde se ha parado antes, después a y se encuentra /a|a|b|c|d.
  • El valor situado a la derecha (b) coincide ahora con el parámetro de ruta {b}.
  • En este momento, todavía hay texto a, pero el algoritmo se ha quedado sin plantilla de ruta para analizar, por lo que no es una coincidencia.

Como el algoritmo de búsqueda de coincidencias es no expansivo:

  • Coincide con la menor cantidad de texto posible en cada paso.
  • Cualquier caso en el que el valor de delimitador aparezca dentro de los valores de parámetro provoca que no coincida.

Las expresiones regulares proporcionan un mayor control sobre el comportamiento de búsqueda de coincidencias.

La coincidencia expansiva, también conocida como coincidencia máxima intenta encontrar la coincidencia más larga posible en el texto de entrada que satisface el patrón regex. La coincidencia no expansiva, también conocida como coincidencia diferida intenta encontrar la coincidencia más larga posible en el texto de entrada que satisface el patrón regex.

Enrutamiento con caracteres especiales

El enrutamiento con caracteres especiales puede dar lugar a resultados inesperados. Por ejemplo, considere un controlador con el siguiente método de acción:

[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;
}

Cuando string id contiene los siguientes valores codificados, pueden producirse resultados inesperados:

ASCII Encoded
/ %2F
+

Los parámetros de ruta no siempre están descodificados por URL. Este problema puede abordarse en el futuro. Para obtener más información, vea esta incidencia de GitHub.

Restricciones de ruta

Las restricciones de ruta se ejecutan cuando se ha producido una coincidencia con la dirección URL entrante y la ruta de dirección URL se convierte en tokens en valores de ruta. En general, las restricciones de ruta inspeccionan el valor de ruta asociado a través de la plantilla de ruta y deciden si el valor es aceptable o no. Algunas restricciones de ruta usan datos ajenos al valor de ruta para decidir si la solicitud se puede enrutar. Por ejemplo, HttpMethodRouteConstraint puede aceptar o rechazar una solicitud basada en su verbo HTTP. Las restricciones se usan en las solicitudes de enrutamiento y la generación de vínculos.

Advertencia

No use las restricciones para la validación de entradas. Si se usan restricciones para la validación de entradas, las que no sean válidas generan una respuesta 404 No encontrado. Una entrada no válida debería generar 400 Solicitud incorrecta con un mensaje de error adecuado. Las restricciones de ruta se usan para eliminar la ambigüedad entre rutas similares, no para validar las entradas de una ruta determinada.

En la tabla siguiente se muestran restricciones de ruta de ejemplo y su comportamiento esperado:

restricción Ejemplo Coincidencias de ejemplo Notas
int {id:int} 123456789, -123456789 Coincide con cualquier entero
bool {active:bool} true, FALSE Coincide con true o false. No distingue mayúsculas de minúsculas
datetime {dob:datetime} 2016-12-31, 2016-12-31 7:32pm Coincide con un valor DateTime válido en la referencia cultural invariable. Vea la advertencia anterior.
decimal {price:decimal} 49.99, -1,000.01 Coincide con un valor decimal válido en la referencia cultural invariable. Vea la advertencia anterior.
double {weight:double} 1.234, -1,001.01e8 Coincide con un valor double válido en la referencia cultural invariable. Vea la advertencia anterior.
float {weight:float} 1.234, -1,001.01e8 Coincide con un valor float válido en la referencia cultural invariable. Vea la advertencia anterior.
guid {id:guid} CD2C1638-1638-72D5-1638-DEADBEEF1638 Coincide con un valor Guid válido
long {ticks:long} 123456789, -123456789 Coincide con un valor long válido
minlength(value) {username:minlength(4)} Rick La cadena debe tener al menos cuatro caracteres
maxlength(value) {filename:maxlength(8)} MyFile La cadena no debe tener más de ocho caracteres
length(length) {filename:length(12)} somefile.txt La cadena debe tener una longitud de exactamente 12 caracteres
length(min,max) {filename:length(8,16)} somefile.txt La cadena debe tener una longitud como mínimo de ocho caracteres y como máximo de 16
min(value) {age:min(18)} 19 El valor entero debe ser como mínimo 18
max(value) {age:max(120)} 91 El valor entero debe ser como máximo 120
range(min,max) {age:range(18,120)} 91 El valor entero debe ser como mínimo 18 y máximo 120
alpha {name:alpha} Rick La cadena debe constar de uno o más caracteres alfabéticos, a-z y no distinguir mayúsculas de minúsculas.
regex(expression) {ssn:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)} 123-45-6789 La cadena debe coincidir con la expresión regular. Vea las sugerencias sobre cómo definir una expresión regular.
required {name:required} Rick Se usa para exigir que un valor que no es de parámetro esté presente durante la generación de dirección URL

Advertencia

Cuando se usa System.Text.RegularExpressions para procesar entradas que no son de confianza, pase un tiempo de expiración. Un usuario malintencionado puede proporcionar entradas a RegularExpressions y provocar un ataque por denegación de servicio. Las API del marco ASP.NET Core en las que se usa RegularExpressions pasan un tiempo de expiración.

Es posible aplicar varias restricciones delimitadas por dos puntos a un único parámetro. Por ejemplo, la siguiente restricción permite limitar un parámetro a un valor entero de 1 o superior:

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

Advertencia

Las restricciones de ruta que comprueban la dirección URL y que se convierten en un tipo CLR siempre usan la referencia cultural invariable. Por ejemplo, la conversión al tipo int o DateTime de CLR. Estas restricciones dan por supuesto que la dirección URL no es localizable. Las restricciones de ruta proporcionadas por el marco de trabajo no modifican los valores almacenados en los valores de ruta. Todos los valores de ruta analizados desde la dirección URL se almacenan como cadenas. Por ejemplo, la restricción float intenta convertir el valor de ruta en un valor Float, pero el valor convertido se usa exclusivamente para comprobar que se puede convertir en Float.

Expresiones regulares en restricciones

Advertencia

Cuando se usa System.Text.RegularExpressions para procesar entradas que no son de confianza, pase un tiempo de expiración. Un usuario malintencionado puede proporcionar entradas a RegularExpressions y provocar un ataque por denegación de servicio. Las API del marco ASP.NET Core en las que se usa RegularExpressions pasan un tiempo de expiración.

Las expresiones regulares se pueden especificar como restricciones insertadas mediante la restricción de ruta regex(...). Los métodos de la familia MapControllerRoute también aceptan un literal de objeto de restricciones. Si se usa ese formato, los valores de cadena se interpretan como expresiones regulares.

En el código siguiente se usa una restricción de expresión regular insertada:

app.MapGet("{message:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)}",
    () => "Inline Regex Constraint Matched");

En el código siguiente se usa un literal de objeto para especificar una restricción de expresión regular:

app.MapControllerRoute(
    name: "people",
    pattern: "people/{ssn}",
    constraints: new { ssn = "^\\d{3}-\\d{2}-\\d{4}$", },
    defaults: new { controller = "People", action = "List" });

El marco de trabajo de ASP.NET Core agrega RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant al constructor de expresiones regulares. Vea RegexOptions para obtener una descripción de estos miembros.

Las expresiones regulares usan delimitadores y tokens similares a los que usan el enrutamiento y el lenguaje C#. Es necesario usar secuencias de escape con los tokens de expresiones regulares. Para usar la expresión regular ^\d{3}-\d{2}-\d{4}$ en una restricción insertada, utilice una de las opciones siguientes:

  • Reemplace los caracteres \ proporcionados en la cadena como caracteres \\ en el archivo de código fuente de C# para aplicar secuencias de escape al carácter de escape de cadena \.
  • Literales de cadena textual.

Para aplicar secuencias de escape a los caracteres delimitadores de parámetro de enrutamiento ({, }, [ y ]), duplique los caracteres en la expresión, por ejemplo {{, }}, [[ y ]]. En la tabla siguiente se muestra una expresión regular y su versión con la secuencia de escape:

Expresión regular Expresión regular con secuencia de escape
^\d{3}-\d{2}-\d{4}$ ^\\d{{3}}-\\d{{2}}-\\d{{4}}$
^[a-z]{2}$ ^[[a-z]]{{2}}$

Las expresiones regulares que se usan en el enrutamiento suelen empezar con el carácter ^ y coincidir con la posición inicial de la cadena. Las expresiones suelen terminar con el carácter $ y coincidir con el final de la cadena. Los caracteres ^ y $ garantizan que la expresión regular coincide con el valor completo del parámetro de ruta. Sin los caracteres ^ y $, la expresión regular coincide con cualquier subcadena de la cadena, lo que normalmente no es deseable. En la tabla siguiente se proporcionan ejemplos y se explica por qué coinciden o no:

Expresión String Coincidir con Comentario
[a-z]{2} hello Coincidencias de subcadenas
[a-z]{2} 123abc456 Coincidencias de subcadenas
[a-z]{2} mz Coincide con la expresión
[a-z]{2} MZ No distingue mayúsculas de minúsculas
^[a-z]{2}$ hello No Vea ^ y $ más arriba
^[a-z]{2}$ 123abc456 No Vea ^ y $ más arriba

Para obtener más información sobre la sintaxis de expresiones regulares, vea Expresiones regulares de .NET Framework.

Para restringir un parámetro a un conjunto conocido de valores posibles, use una expresión regular. Por ejemplo, {action:regex(^(list|get|create)$)} solo hace coincidir el valor de ruta action con list, get o create. Si se pasa al diccionario de restricciones, la cadena ^(list|get|create)$ es equivalente. Las restricciones que se pasan al diccionario de restricciones que no coinciden con una de las conocidas también se tratan como expresiones regulares. Las restricciones que se pasan en una plantilla y que no coinciden con una de las conocidas no se tratan como expresiones regulares.

Restricciones de ruta personalizadas

Se pueden crear restricciones de ruta personalizadas mediante la implementación de la interfaz IRouteConstraint. La interfaz IRouteConstraint contiene Match, que devuelve true si se cumple la restricción, y false en caso contrario.

Las restricciones de ruta personalizadas rara vez son necesarias. Antes de implementar una restricción de ruta personalizada, considere alternativas, como el enlace de modelos.

En la carpeta Constraints de ASP.NET Core se proporcionan buenos ejemplos de creación de restricciones. Por ejemplo, GuidRouteConstraint.

Para usar una restricción IRouteConstraint personalizada, el tipo de restricción de ruta se debe registrar con el parámetro ConstraintMap de la aplicación en el contenedor de servicios. ConstraintMap es un diccionario que asigna claves de restricciones de ruta a implementaciones de IRouteConstraint que validen esas restricciones. El parámetro ConstraintMap de una aplicación puede actualizarse en Program.cs como parte de una llamada a AddRouting o configurando RouteOptions directamente con builder.Services.Configure<RouteOptions>. Por ejemplo:

builder.Services.AddRouting(options =>
    options.ConstraintMap.Add("noZeroes", typeof(NoZeroesRouteConstraint)));

La restricción anterior se aplica en el código siguiente:

[ApiController]
[Route("api/[controller]")]
public class NoZeroesController : ControllerBase
{
    [HttpGet("{id:noZeroes}")]
    public IActionResult Get(string id) =>
        Content(id);
}

La implementación de NoZeroesRouteConstraint impide que 0 se use en un parámetro de ruta:

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

Advertencia

Cuando se usa System.Text.RegularExpressions para procesar entradas que no son de confianza, pase un tiempo de expiración. Un usuario malintencionado puede proporcionar entradas a RegularExpressions y provocar un ataque por denegación de servicio. Las API del marco ASP.NET Core en las que se usa RegularExpressions pasan un tiempo de expiración.

El código anterior:

  • Impide 0 en el segmento {id} de la ruta.
  • Se muestra para proporcionar un ejemplo básico de implementación de una restricción personalizada. No se debe usar en una aplicación de producción.

El código siguiente es un enfoque mejor para impedir que se procese un valor id que contenga 0:

[HttpGet("{id}")]
public IActionResult Get(string id)
{
    if (id.Contains('0'))
    {
        return StatusCode(StatusCodes.Status406NotAcceptable);
    }

    return Content(id);
}

El código anterior tiene las ventajas siguientes con respecto al enfoque de NoZeroesRouteConstraint:

  • No requiere una restricción personalizada.
  • Devuelve un error más descriptivo cuando el parámetro de ruta incluye 0.

Transformadores de parámetros

Transformadores de parámetros:

Por ejemplo, un transformador de parámetros personalizado slugify en el patrón de ruta blog\{article:slugify} con Url.Action(new { article = "MyTestArticle" }) genera blog\my-test-article.

Considere la siguiente implementación de 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();
    }
}

Para usar un transformador de parámetros en un patrón de ruta, configúrelo con ConstraintMap en Program.cs:

builder.Services.AddRouting(options =>
    options.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer));

El marco ASP.NET Core usa los transformadores de parámetros para transformar el URI en el que se resuelve un punto de conexión. Por ejemplo, los transformadores de parámetros transforman los valores de ruta que se usan para hacer coincidir objetos area, controller, action y page:

app.MapControllerRoute(
    name: "default",
    pattern: "{controller:slugify=Home}/{action:slugify=Index}/{id?}");

Con la plantilla de ruta anterior, la acción SubscriptionManagementController.GetAll coincide con el URI /subscription-management/get-all. Un transformador de parámetros no cambia los valores de ruta usados para generar un vínculo. Por ejemplo, Url.Action("GetAll", "SubscriptionManagement") genera /subscription-management/get-all.

ASP.NET Core proporciona convenciones de API para usar transformadores de parámetros con rutas generadas:

Referencia de generación de direcciones URL

Esta sección contiene una referencia para el algoritmo implementado por la generación de direcciones URL. En la práctica, los ejemplos más complejos de generación de direcciones URL usan controladores o Razor Pages. Vea Enrutamiento en controladores para obtener información adicional.

El proceso de generación de direcciones URL comienza con una llamada a LinkGenerator.GetPathByAddress o un método similar. Al método se le proporciona una dirección, un conjunto de valores de ruta y, opcionalmente, información sobre la solicitud actual de HttpContext.

El primer paso consiste en usar la dirección para resolver un conjunto de puntos de conexión candidatos con una instancia de IEndpointAddressScheme<TAddress> que coincide con el tipo de la dirección.

Una vez que el esquema de direcciones encuentra el conjunto de candidatos, los puntos de conexión se ordenan y procesan de forma iterativa hasta que se realiza correctamente una operación de generación de direcciones URL. La generación de direcciones URL no comprueba si hay ambigüedades; el primer resultado devuelto es el resultado final.

Solución de problemas de generación de direcciones URL con registro

El primer paso para solucionar problemas de generación de direcciones URL consiste en establecer el nivel de registro de Microsoft.AspNetCore.Routing en TRACE. LinkGenerator registra muchos detalles sobre su procesamiento que pueden ser útiles para solucionar problemas.

Vea Referencia de generación de direcciones URL para obtener más información sobre la generación de direcciones URL.

Direcciones

Las direcciones son el concepto de la generación de direcciones URL que se usa para enlazar una llamada al generador de vínculos a un conjunto de puntos de conexión candidatos.

Las direcciones son un concepto extensible que incluyen dos implementaciones de forma predeterminada:

  • Con el nombre del punto de conexión (string) como dirección:
    • Proporciona una funcionalidad similar al nombre de ruta de MVC.
    • Usa el tipo de metadatos de IEndpointNameMetadata.
    • Resuelve la cadena proporcionada con los metadatos de todos los puntos de conexión registrados.
    • Inicia una excepción durante el inicio si varios puntos de conexión usan el mismo nombre.
    • Se recomienda para uso general fuera de los controladores y Razor Pages.
  • Con los valores de ruta (RouteValuesAddress) como dirección:
    • Proporciona una funcionalidad similar a los controladores y la generación de direcciones URL heredada de Razor Pages.
    • La ampliación y depuración son complejas.
    • Proporciona la implementación que usa IUrlHelper, aplicaciones auxiliares de etiquetas, aplicaciones auxiliares HTML, resultados de acciones, etc.

El papel del esquema de direcciones consiste en establecer la asociación entre la dirección y los puntos de conexión coincidentes mediante criterios arbitrarios:

  • El esquema de nombres de punto de conexión realiza una búsqueda de diccionario básica.
  • El esquema de valores de ruta tiene un subconjunto óptimo de algoritmos definidos complejo.

Valores de ambiente y valores explícitos

A partir de la solicitud actual, el enrutamiento accede a los valores de ruta del objeto HttpContext.Request.RouteValues de la solicitud actual. Los valores asociados a la solicitud actual se conocen como valores de ambiente. Para mayor claridad, en la documentación se hace referencia a los valores de ruta que se pasan a los métodos como valores explícitos.

En el ejemplo siguiente se muestran valores de ambiente y valores explícitos. Proporciona valores de ambiente de la solicitud actual y valores explícitos:

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

    // ...

El código anterior:

El código siguiente solo proporciona valores explícitos y valores sin ambiente:

var subscribePath = _linkGenerator.GetPathByAction(
    "Subscribe", "Home", new { id = 17 })!;

El método anterior devuelve /Home/Subscribe/17.

El código siguiente en WidgetController devuelve /Widget/Subscribe/17:

var subscribePath = _linkGenerator.GetPathByAction(
    HttpContext, "Subscribe", null, new { id = 17 });

En el código siguiente se proporciona el controlador a partir de los valores de ambiente de la solicitud actual y los valores explícitos:

public class GadgetController : ControllerBase
{
    public IActionResult Index() =>
        Content(Url.Action("Edit", new { id = 17 })!);
}

En el código anterior:

  • Se devuelve /Gadget/Edit/17.
  • Url obtiene el objeto IUrlHelper.
  • Action genera una dirección URL con una ruta de acceso absoluta para un método de acción. La dirección URL contiene el nombre de action especificado y los valores route.

En el código siguiente se proporcionan valores de ambiente de la solicitud actual y valores explícitos:

public class IndexModel : PageModel
{
    public void OnGet()
    {
        var editUrl = Url.Page("./Edit", new { id = 17 });

        // ...
    }
}

En el código anterior se establece url en /Edit/17 cuando la página de Razor de edición contiene la siguiente directiva de página:

@page "{id:int}"

Si la página de edición no contiene la plantilla de ruta "{id:int}", url es /Edit?id=17.

El comportamiento de IUrlHelper de MVC agrega una capa de complejidad además de las reglas descritas aquí:

  • IUrlHelper siempre proporciona los valores de ruta de la solicitud actual como valores de ambiente.
  • IUrlHelper.Action siempre copia los valores de ruta action y controller actuales como valores explícitos a menos que el desarrollador los invalide.
  • IUrlHelper.Page siempre copia el valor de ruta page actual como un valor explícito a menos que se invalide.
  • IUrlHelper.Page siempre invalida el valor de ruta handler actual con null como un valor explícito a menos que se invalide.

A los usuarios a menudo les sorprenden los detalles del comportamiento de los valores de ambiente, ya que MVC no parece seguir sus propias reglas. Por motivos históricos y de compatibilidad, algunos valores de ruta como action, controller, page y handler tienen su propio comportamiento de caso especial.

La funcionalidad equivalente proporcionada por LinkGenerator.GetPathByAction y LinkGenerator.GetPathByPage duplica estas anomalías de IUrlHelper por motivos de compatibilidad.

Proceso de generación de direcciones URL

Una vez que se encuentra el conjunto de puntos de conexión candidatos, el algoritmo de generación de direcciones URL:

  • Procesa los puntos de conexión de forma iterativa.
  • Devuelve el primer resultado correcto.

El primer paso de este proceso se denomina invalidación del valor de ruta. La invalidación del valor de ruta es el proceso por el que el enrutamiento decide qué valores de ruta de los valores de ambiente se deben usar y cuáles se deben omitir. Cada valor de ambiente se tiene en cuenta y se combina con los valores explícitos, o bien se pasa por alto.

La mejor manera de pensar en el rol de los valores de ambiente es que intentan ahorrar trabajo a los desarrolladores de aplicaciones, en algunos casos comunes. Tradicionalmente, los escenarios en los que los valores de ambiente son útiles están relacionados con MVC:

  • Al vincular a otra acción en el mismo controlador, no es necesario especificar el nombre del controlador.
  • Al vincular a otro controlador en la misma área, no es necesario especificar el nombre del área.
  • Al vincular al mismo método de acción, no es necesario especificar los valores de ruta.
  • Al vincular a otro elemento de la aplicación, no le interesa transferir valores de ruta que no tengan ningún significado en ese elemento del control de la aplicación.

Las llamadas a LinkGenerator o IUrlHelper que devuelven null se suelen deber a que no se comprende la invalidación del valor de ruta. Para solucionar problemas de invalidación del valor de ruta, especifique de forma explícita más valores de ruta para ver si eso resuelve el problema.

La invalidación del valor de ruta se basa en la suposición de que el esquema de direcciones URL de la aplicación es jerárquico, con una jerarquía formada de izquierda a derecha. Considere la posibilidad de usar la plantilla de ruta de controlador básica {controller}/{action}/{id?} para hacerse una idea intuitiva de cómo funciona esto en la práctica. Un cambio en un valor invalida todos los valores de ruta que aparecen a la derecha. Esto refleja la suposición sobre la jerarquía. Si la aplicación tiene un valor de ambiente para id y la operación especifica otro valor para controller:

  • id no se reutilizará porque {controller} está a la izquierda de {id?}.

Algunos ejemplos demuestran este principio:

  • Si los valores explícitos contienen un valor para id, se omite el valor de ambiente de id. Se pueden usar los valores de ambiente para controller y action.
  • Si los valores explícitos contienen un valor para action, se omite cualquier valor de ambiente para action. Se pueden usar los valores de ambiente para controller. Si el valor explícito para action es diferente del valor de ambiente para action, no se usará el valor de id. Si el valor explícito para action es diferente del valor de ambiente para action, se puede usar el valor de id.
  • Si los valores explícitos contienen un valor para controller, se omite cualquier valor de ambiente para controller. Si el valor explícito para controller es diferente del valor de ambiente para controller, no se usarán los valores de action y id. Si el valor explícito para controller es igual que el valor de ambiente para controller, se pueden usar los valores de action y id.

Este proceso sea complica todavía más por la existencia de rutas de atributo y rutas convencionales dedicadas. Las rutas convencionales de controlador, como {controller}/{action}/{id?}, especifican una jerarquía mediante parámetros de ruta. Para las rutas convencionales dedicadas y las rutas de atributo a controladores y Razor Pages:

  • Existe una jerarquía de valores de ruta.
  • No aparecen en la plantilla.

En estos casos, la generación de direcciones URL define el concepto de valores necesarios. Los puntos de conexión creados por controladores y Razor Pages tienen valores necesarios especificados que permiten que la invalidación del valor de ruta funcione.

El algoritmo de invalidación del valor de ruta en detalle:

  • Los nombres de valor necesarios se combinan con los parámetros de ruta y, después, se procesan de izquierda a derecha.
  • Para cada parámetro, se comparan el valor de ambiente y el valor explícito:
    • Si el valor de ambiente y el valor explícito son iguales, el proceso continúa.
    • Si el valor de ambiente está presente y el valor explícito no, se usa el valor de ambiente al generar la dirección URL.
    • Si el valor de ambiente no está presente y el valor explícito sí, rechace el valor de ambiente y todos los posteriores.
    • Si el valor de ambiente y el valor explícito están presentes, y los dos son diferentes, rechace el valor de ambiente y todos los posteriores.

En este punto, la operación de generación de direcciones URL está lista para evaluar las restricciones de ruta. El conjunto de valores aceptados se combina con los valores predeterminados de parámetro, que se proporcionan a las restricciones. Si todas las restricciones son correctas, la operación continúa.

A continuación, se pueden usar los valores aceptados para expandir la plantilla de ruta. La plantilla de ruta se procesa:

  • De izquierda a derecha.
  • En cada parámetro se sustituye su valor aceptado.
  • Con los siguientes casos especiales:
    • Si falta un valor en los valores aceptados y el parámetro tiene un valor predeterminado, se usa el valor predeterminado.
    • Si falta un valor en los valores aceptados y el parámetro es opcional, el procesamiento continúa.
    • Si un parámetro de ruta a la derecha de un parámetro opcional que falta tiene un valor, se produce un error en la operación.
    • Los parámetros con valores predeterminados contiguos y los parámetros opcionales se contraen siempre que sea posible.

Los valores que se proporcionan de forma explícita que no coinciden con un segmento de la ruta se agregan a la cadena de consulta. En la tabla siguiente se muestra el resultado cuando se usa la plantilla de ruta {controller}/{action}/{id?}.

Valores de ambiente Valores explícitos Resultado
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

Orden de los parámetros de ruta opcionales

Los parámetros de ruta opcionales deben aparecer después de todos los parámetros de ruta y literales obligatorios. En el código siguiente, los parámetros idy name deben aparecer después del parámetro 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 ?? ""}");
    }
}

Problemas con la invalidación del valor de ruta

En el código siguiente se muestra un ejemplo de un esquema de generación de direcciones URL que no es compatible con el enrutamiento:

app.MapControllerRoute(
    "default",
    "{culture}/{controller=Home}/{action=Index}/{id?}");

app.MapControllerRoute(
    "blog",
    "{culture}/{**slug}",
    new { controller = "Blog", action = "ReadPost" });

En el código anterior, se usa el parámetro de ruta culture para la localización. El objetivo es que el parámetro culture siempre se acepte como valor de ambiente. Pero el parámetro culture no se acepta como valor de ambiente debido al funcionamiento de los valores necesarios:

  • En la plantilla de ruta "default", el parámetro de ruta culture está a la izquierda de controller, por lo que los cambios en controller no invalidarán culture.
  • En la plantilla de ruta "blog", se considera que el parámetro de ruta culture está a la derecha de controller, que aparece en los valores necesarios.

Análisis de rutas de dirección URL con LinkParser

La clase LinkParser agrega compatibilidad con el análisis de una ruta de dirección URL en un conjunto de valores de ruta. El método ParsePathByEndpointName toma un nombre de punto de conexión y una ruta de dirección URL y devuelve un conjunto de valores de ruta extraídos de esta.

En el controlador de ejemplo siguiente, la acción GetProduct usa una plantilla de ruta de api/Products/{id} y tiene un valor de Name de GetProduct:

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    [HttpGet("{id}", Name = nameof(GetProduct))]
    public IActionResult GetProduct(string id)
    {
        // ...

En la misma clase de controlador, la acción AddRelatedProduct espera una ruta de dirección URL, pathToRelatedProduct, que se puede proporcionar como parámetro de cadena de consulta:

[HttpPost("{id}/Related")]
public IActionResult AddRelatedProduct(
    string id, string pathToRelatedProduct, [FromServices] LinkParser linkParser)
{
    var routeValues = linkParser.ParsePathByEndpointName(
        nameof(GetProduct), pathToRelatedProduct);
    var relatedProductId = routeValues?["id"];

    // ...

En el ejemplo anterior, la acción AddRelatedProduct extrae el valor de ruta id de la ruta de dirección URL. Por ejemplo, con una ruta de dirección URL de /api/Products/1, el valor de relatedProductId se establece en 1. Este enfoque permite a los clientes de la API usar rutas de dirección URL al hacer referencia a recursos, sin necesidad de conocer cómo se estructura dicha dirección URL.

Configuración de metadatos de punto de conexión

Los vínculos siguientes proporcionan información sobre cómo configurar los metadatos del punto de conexión:

Comparación de host en rutas con RequireHost

RequireHost aplica una restricción a la ruta que requiere el host especificado. El parámetro RequireHost o [Host] puede ser:

  • Host: www.domain.com, compara www.domain.com con cualquier puerto.
  • Host con carácter comodín: *.domain.com, coincide con www.domain.com, subdomain.domain.com o www.subdomain.domain.com en cualquier puerto.
  • Puerto: *:5000, coincide con el puerto 5000 con cualquier host.
  • Host y puerto: www.domain.com:5000 o *.domain.com:5000, coincide con el host y el puerto.

Se pueden especificar varios parámetros mediante RequireHost o [Host]. La restricción coincide con los hosts válidos para cualquiera de los parámetros. Por ejemplo, [Host("domain.com", "*.domain.com")] coincide con domain.com, www.domain.com y subdomain.domain.com.

En el código siguiente se usa RequireHost para requerir el host especificado en la ruta:

app.MapGet("/", () => "Contoso").RequireHost("contoso.com");
app.MapGet("/", () => "AdventureWorks").RequireHost("adventure-works.com");

app.MapHealthChecks("/healthz").RequireHost("*:8080");

En el código siguiente se usa el atributo [Host] en el controlador para requerir cualquiera de los hosts especificados:

[Host("contoso.com", "adventure-works.com")]
public class HostsController : Controller
{
    public IActionResult Index() =>
        View();

    [Host("example.com")]
    public IActionResult Example() =>
        View();
}

Cuando el atributo [Host] se aplica al método de acción y al controlador:

  • Se usa el atributo en la acción.
  • Se omite el atributo del controlador.

Advertencia

Las API que se basan en el encabezado host, como HttpRequest.Host y RequireHost, están sujetas a una posible suplantación de identidad por parte de los clientes.

Para evitar la suplantación de identidad de host y puerto, use uno de los métodos siguientes:

Grupos de rutas

El método de extensión MapGroup ayuda a organizar grupos de puntos de conexión con un prefijo común. Reduce el código repetitivo y permite personalizar grupos completos de puntos de conexión con una sola llamada a métodos como RequireAuthorization y WithMetadata, que agregan metadatos de punto de conexión.

Por ejemplo, el código siguiente crea dos grupos similares de puntos de conexión:

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;
}

En este escenario, puede usar una dirección relativa para el encabezado Location en el resultado 201 Created:

public static async Task<Created<Todo>> CreateTodo(Todo todo, TodoDb database)
{
    await database.AddAsync(todo);
    await database.SaveChangesAsync();

    return TypedResults.Created($"{todo.Id}", todo);
}

El primer grupo de puntos de conexión solo coincidirá con las solicitudes con el prefijo /public/todos y que sean accesibles sin autenticación. El segundo grupo de puntos de conexión solo coincidirá con las solicitudes con el prefijo /private/todos y que requieran autenticación.

La QueryPrivateTodosfábrica de filtros de punto de conexión es una función local que modifica los parámetros TodoDb del controlador para permitir el acceso y almacenar datos privados de tareas pendientes.

Los grupos de rutas también admiten grupos anidados y patrones de prefijo complejos con parámetros y restricciones de ruta. En el ejemplo siguiente, y el controlador de rutas asignado al grupo user puede capturar los parámetros de ruta {org} y {group} definidos en los prefijos del grupo externo.

El prefijo también puede estar vacío. Esto puede ser útil para agregar metadatos de punto de conexión o filtros a un grupo de puntos de conexión sin cambiar el patrón de ruta.

var all = app.MapGroup("").WithOpenApi();
var org = all.MapGroup("{org}");
var user = org.MapGroup("{user}");
user.MapGet("", (string org, string user) => $"{org}/{user}");

La adición de filtros o metadatos a un grupo se comporta del mismo modo que la adición individual a cada punto de conexión antes de agregar filtros o metadatos adicionales que quizás se hayan agregado a un grupo interno o a un punto de conexión específico.

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

En el ejemplo anterior, el filtro externo registrará la solicitud entrante antes que el filtro interno aunque se haya agregado en segundo lugar. Dado que los filtros se aplicaron a diferentes grupos, el orden en que se agregaron el uno con respecto al otro no es importante. El orden en que se agregan los filtros es importante si se aplican al mismo grupo o punto de conexión específico.

Una solicitud a /outer/inner/ registrará lo siguiente:

/outer group filter
/inner group filter
MapGet filter

Instrucciones de rendimiento para el enrutamiento

Cuando una aplicación tiene problemas de rendimiento, a menudo se sospecha que el enrutamiento es el problema. El motivo es que marcos como los controladores y Razor Pages notifican la cantidad de tiempo empleado en el marco de trabajo en sus mensajes de registro. Cuando hay una diferencia significativa entre el tiempo notificado por los controladores y el tiempo total de la solicitud:

  • Los desarrolladores eliminan el código de la aplicación como origen del problema.
  • Es habitual asumir que el enrutamiento es la causa.

El rendimiento del enrutamiento se prueba mediante miles de puntos de conexión. No es probable que una aplicación típica detecte un problema de rendimiento simplemente por ser demasiado grande. La causa raíz más común del rendimiento lento del enrutamiento suele ser middleware personalizado con un comportamiento incorrecto.

En el ejemplo de código siguiente se muestra una técnica básica para limitar el origen del retraso:

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.");

Para controlar el tiempo del enrutamiento:

  • Intercale cada middleware con una copia del middleware de tiempo que se muestra en el código anterior.
  • Agregue un identificador único para poner en correlación los datos de control de tiempo con el código.

Se trata de una forma básica de reducir el retraso cuando es significativo, por ejemplo, de más de 10ms. Al restar Time 2 de Time 1 se notifica el tiempo invertido dentro del middleware UseRouting.

En el código siguiente se usa un enfoque más compacto del código de control de tiempo anterior:

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.");

Características de enrutamiento potencialmente costosas

En la lista siguiente se proporciona información sobre las características de enrutamiento que son relativamente costosas en comparación con las plantillas de ruta básicas:

  • Expresiones regulares: Se pueden escribir expresiones regulares que sean complejas o que tengan un tiempo de ejecución de larga duración con una pequeña cantidad de entradas.
  • Segmentos complejos ({x}-{y}-{z}):
    • Son mucho más costosos que analizar un segmento de ruta de dirección URL convencional.
    • El resultado es que se asignan muchas más subcadenas.
  • Acceso a datos sincrónicos: muchas aplicaciones complejas tienen acceso a bases de datos como parte de su enrutamiento. Los puntos de extensibilidad como MatcherPolicy y EndpointSelectorContext son asincrónicos.

Guía para tablas de enrutamiento de gran tamaño

De forma predeterminada, ASP.NET Core utiliza un algoritmo de enrutamiento que compara la memoria con el tiempo de CPU. Esto genera un buen resultado por el hecho de que el tiempo de coincidencia de enrutamiento solo depende de la longitud de la ruta de acceso con la que debe coincidir y no del número de rutas. Sin embargo, este enfoque puede ser potencialmente problemático en algunos casos, cuando la aplicación tiene un gran número de rutas (miles) y hay una gran cantidad de prefijos de variable en las rutas. Por ejemplo, si las rutas tienen parámetros en los primeros segmentos de la ruta, como {parameter}/some/literal.

Es poco probable que una aplicación se ejecute en una situación en la que esto sea un problema, a menos que:

  • Haya un gran número de rutas en la aplicación que usen este patrón.
  • Haya un gran número de rutas en la aplicación.

Forma de determinar si una aplicación se está ejecutando con el problema de tablas de enrutamiento de gran tamaño

  • Hay dos síntomas que buscar:
    • La aplicación tarda en iniciarse en la primera solicitud.
      • Tenga en cuenta que esto es necesario, pero no suficiente. Hay muchos otros problemas no relacionados con el enrutamiento que pueden provocar que la aplicación se inicie lentamente. Compruebe la condición siguiente para determinar con precisión que la aplicación se está ejecutando en esta situación.
    • La aplicación consume mucha memoria durante el inicio y un volcado de memoria muestra un gran número de instancias de Microsoft.AspNetCore.Routing.Matching.DfaNode.

Forma de solucionar este problema

Hay varias técnicas y optimizaciones que se pueden aplicar a las rutas y que mejorarán en gran medida este escenario:

  • Aplique restricciones de ruta a los parámetros, por ejemplo, {parameter:int}, {parameter:guid}, {parameter:regex(\\d+)}, etc., siempre que sea posible.
    • Esto permite que el algoritmo de enrutamiento optimice internamente las estructuras usadas para buscar coincidencias y reducir drásticamente la memoria usada.
    • En la gran mayoría de los casos, esto será suficiente para volver a un comportamiento aceptable.
  • Cambie las rutas para mover parámetros a segmentos posteriores de la plantilla.
    • Esto reduce el número de posibles "rutas de acceso" para que coincidan con un punto de conexión en una ruta de acceso determinada.
  • Use una ruta dinámica y realice la asignación a un controlador o página dinámicamente.
    • Esto puede realizarse mediante MapDynamicControllerRoute y MapDynamicPageRoute.

Cortocircuito de middleware después del enrutamiento

Cuando el enrutamiento coincide con un punto de conexión, normalmente permite que el resto de la canalización de middleware se ejecute antes de invocar la lógica del punto de conexión. Los servicios pueden reducir el uso de recursos filtrando las solicitudes conocidas al principio de la canalización. Use el método de extensión ShortCircuit para hacer que el enrutamiento invoque la lógica del punto de conexión inmediatamente y, a continuación, finalice la solicitud. Por ejemplo, es posible que una ruta determinada no tenga que pasar por la autenticación o el middleware CORS. En el ejemplo siguiente se cortocircuitan las solicitudes que coinciden con la ruta /short-circuit:

app.MapGet("/short-circuit", () => "Short circuiting!").ShortCircuit();

Opcionalmente, el método ShortCircuit(IEndpointConventionBuilder, Nullable<Int32>) puede tomar un código de estado.

Use el método MapShortCircuit para configurar el cortocircuito para varias rutas a la vez, pasando a ella una matriz de parámetros de prefijos de dirección URL. Por ejemplo, los exploradores y los bots suelen sondear servidores para rutas de acceso conocidas como robots.txt y favicon.ico. Si la aplicación no tiene esos archivos, una línea de código puede configurar ambas rutas:

app.MapShortCircuit(404, "robots.txt", "favicon.ico");

MapShortCircuit devuelve IEndpointConventionBuilder para que se puedan agregar restricciones de ruta adicionales, como el filtrado de host.

Los métodos ShortCircuit y MapShortCircuit no afectan al middleware colocado antes de UseRouting. Al intentar usar estos métodos con puntos de conexión que también tienen los metadatos [Authorize] o [RequireCors], se producirá un error en las solicitudes con InvalidOperationException. Estos metadatos se aplican por los atributos [Authorize] o [EnableCors], o por los métodos RequireCors o RequireAuthorization.

Para ver el efecto de cortocircuitar el middleware, establezca la categoría de registro "Microsoft" en "Información" en appsettings.Development.json:

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

Ejecute el código siguiente:

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();

El ejemplo siguiente procede de los registros de consola generados mediante la ejecución del punto de conexión /. Incluye la salida del middleware de registro:

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

En el ejemplo siguiente se ejecuta el punto de conexión /short-circuit. No tiene nada del middleware de registro porque el middleware estaba cortocircuitado:

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.

Instrucciones para los autores de bibliotecas

Esta sección contiene instrucciones para los autores de bibliotecas que realizan la compilación sobre el enrutamiento. Estos detalles están diseñados para garantizar que los desarrolladores de aplicaciones tengan una buena experiencia en el uso de bibliotecas y marcos que amplían el enrutamiento.

Definición de puntos de conexión

Para crear un marco que use el enrutamiento para la coincidencia de direcciones URL, empiece por definir una experiencia de usuario que se base en UseEndpoints.

REALICE la compilación sobre IEndpointRouteBuilder. Esto permite a los usuarios crear el marco de trabajo con otras características de ASP.NET Core sin confusión. Todas las plantillas de ASP.NET Core incluyen el enrutamiento. Asuma que el enrutamiento está presente y es familiar para los usuarios.

// Your framework
app.MapMyFramework(...);

app.MapHealthChecks("/healthz");

DEVUELVA un tipo concreto sellado a partir de una llamada a MapMyFramework(...) que implemente IEndpointConventionBuilder. La mayoría de los métodos Map... del marco siguen este patrón. La interfaz IEndpointConventionBuilder:

  • Permite la composición de metadatos.
  • Es el destino de diversos métodos de extensión.

La declaración de un tipo propio permite agregar funcionalidad específica del marco propia al generador. Es correcto encapsular un generador declarado por el marco y reenviarle llamadas.

// Your framework
app.MapMyFramework(...)
    .RequireAuthorization()
    .WithMyFrameworkFeature(awesome: true);

app.MapHealthChecks("/healthz");

CONSIDERE LA POSIBILIDAD de escribir un objeto EndpointDataSource propio. EndpointDataSource es la primitiva de bajo nivel para declarar y actualizar una colección de puntos de conexión. EndpointDataSource es una API eficaz que usan los controladores y Razor Pages. Para más información, vea Enrutamiento dinámico de puntos de conexión.

Las pruebas de enrutamiento tienen un ejemplo básico de un origen de datos que no es de actualización.

CONSIDERE la posibilidad de implementar GetGroupedEndpoints. Esto proporciona control completo sobre la ejecución de convenciones de grupo y los metadatos finales en los puntos de conexión agrupados. Por ejemplo, esto permite que las implementaciones personalizadas de EndpointDataSource ejecuten filtros de punto de conexión agregados a grupos.

NO intente registrar un objeto EndpointDataSource de forma predeterminada. Exija a los usuarios que registren el marco en UseEndpoints. La filosofía del enrutamiento es que nada se incluye de forma predeterminada y que UseEndpoints es el lugar donde se registran los puntos de conexión.

Creación de middleware con enrutamiento integrada

CONSIDERE LA POSIBILIDAD de definir tipos de metadatos como una interfaz.

PERMITA el uso de los tipos de metadatos como atributo en clases y métodos.

public interface ICoolMetadata
{
    bool IsCool { get; }
}

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class CoolMetadataAttribute : Attribute, ICoolMetadata
{
    public bool IsCool => true;
}

Los marcos como los controladores y Razor Pages admiten la aplicación de atributos de metadatos a tipos y métodos. Si declara tipos de metadatos:

  • Haga que sean accesibles como atributos.
  • La mayoría de los usuarios están familiarizados con la aplicación de atributos.

La declaración de un tipo de metadatos como una interfaz agrega otro nivel de flexibilidad:

  • Las interfaces admiten composición.
  • Los desarrolladores pueden declarar tipos propios que combinen varias directivas.

PERMITA que los metadatos se puedan invalidar, como se muestra en el ejemplo siguiente:

[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() { }
}

La mejor manera de seguir estas instrucciones es evitar la definición de metadatos de marcador:

  • No busque solo la presencia de un tipo de metadatos.
  • Defina una propiedad en los metadatos y compruébela.

La colección de metadatos está ordenada y admite la invalidación por prioridad. En el caso de los controladores, los metadatos del método de acción son más específicos.

PERMITA que el middleware sea útil con y sin el enrutamiento:

app.UseAuthorization(new AuthorizationPolicy() { ... });

// Your framework
app.MapMyFramework(...).RequireAuthorization();

Como ejemplo de esta instrucción, considere la posibilidad de usar el middleware UseAuthorization. El middleware de autorización permite pasar una directiva de reserva. La directiva de reserva, si se especifica, se aplica a:

  • Puntos de conexión sin una directiva especificada.
  • Solicitudes que no coinciden con un punto de conexión.

Esto hace que el middleware de autorización sea útil fuera del contexto del enrutamiento. El middleware de autorización se puede usar para la programación de middleware tradicional.

Diagnóstico de depuración

Para ver la salida detallada del diagnóstico de cálculo de ruta, establezca Logging:LogLevel:Microsoft en Debug. En el entorno de desarrollo, establezca el nivel de registro en appsettings.Development.json:

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

Recursos adicionales

El enrutamiento es responsable de hacer coincidir las solicitudes HTTP entrantes y de enviarlas a los puntos de conexión ejecutables de la aplicación. Los puntos de conexión son las unidades de código de control de solicitudes ejecutable de la aplicación. Se definen en la aplicación y se configuran al iniciarla. El proceso de búsqueda de coincidencias de puntos de conexión puede extraer valores de la dirección URL de la solicitud y proporcionarlos para el procesamiento de la solicitud. Con la información de los puntos de conexión de la aplicación, el enrutamiento también puede generar direcciones URL que se asignan a los puntos de conexión.

Las aplicaciones pueden configurar el enrutamiento mediante:

  • Controladores
  • Razor Pages
  • SignalR
  • Servicios gRPC
  • Middleware habilitado para puntos de conexión, como las comprobaciones de estado.
  • Delegados y expresiones lambda registrados con el enrutamiento.

En este artículo se describen los detalles de bajo nivel del enrutamiento de ASP.NET Core. Para obtener información sobre la configuración del enrutamiento:

Fundamentos del enrutamiento

En el código siguiente se muestra un ejemplo básico de enrutamiento:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();

En el ejemplo anterior se incluye un único punto de conexión que usa el método MapGet:

  • Al enviar una solicitud HTTP GET a la dirección URL raíz /:
    • Se ejecuta el delegado de la solicitud.
    • Se escribe Hello World! en la respuesta HTTP.
  • Si el método de solicitud no es GET o la dirección URL raíz no es /, no se detecta ninguna ruta y se devuelve HTTP 404.

El enrutamiento usa un par de middleware, registrado por UseRouting y UseEndpoints:

  • UseRouting agrega coincidencia de rutas a la canalización de middleware. Este middleware examina el conjunto de puntos de conexión definidos en la aplicación y selecciona la mejor coincidencia en función de la solicitud.
  • UseEndpoints agrega la ejecución del punto de conexión a la canalización de middleware. Ejecuta el delegado asociado al punto de conexión seleccionado.

Normalmente, las aplicaciones no necesitan llamar a UseRouting ni a UseEndpoints. WebApplicationBuilder configura una canalización de middleware que encapsula el middleware agregado en Program.cs con UseRouting y UseEndpoints. Sin embargo, las aplicaciones pueden cambiar el orden en que se ejecutan UseRouting y UseEndpoints llamando a estos métodos explícitamente. Por ejemplo, el código siguiente realiza una llamada explícita a UseRouting:

app.Use(async (context, next) =>
{
    // ...
    await next(context);
});

app.UseRouting();

app.MapGet("/", () => "Hello World!");

En el código anterior:

  • La llamada a app.Use registra un middleware personalizado que se ejecuta al principio de la canalización.
  • La llamada a UseRouting configura el middleware de coincidencia de rutas para que se ejecute después del middleware personalizado.
  • El punto de conexión registrado con MapGet se ejecuta al final de la canalización.

Si el ejemplo anterior no incluyese una llamada a UseRouting, el middleware personalizado se ejecutaría después del middleware de coincidencia de rutas.

Puntos de conexión

El método MapGet se usa para definir un punto de conexión. Un punto de conexión es algo que se puede:

  • Seleccionar, si se hacen coincidir la dirección URL y el método HTTP.
  • Ejecutar, mediante la ejecución del delegado.

Los puntos de conexión que la aplicación puede ejecutar y hacer coincidir se configuran en UseEndpoints. Por ejemplo, MapGet, MapPost y métodos similares conectan delegados de solicitud al sistema de enrutamiento. Se pueden usar métodos adicionales para conectar características del marco ASP.NET Core al sistema de enrutamiento:

En el ejemplo siguiente se muestra el enrutamiento con una plantilla de ruta más sofisticada:

app.MapGet("/hello/{name:alpha}", (string name) => $"Hello {name}!");

La cadena /hello/{name:alpha} es una plantilla de ruta. Se usa una plantilla de ruta para configurar la coincidencia del punto de conexión. En este caso, la plantilla coincide con:

  • Una dirección URL como /hello/Docs.
  • Cualquier ruta de dirección URL que comience por /hello/, seguido de una secuencia de caracteres alfabéticos. :alpha aplica una restricción de ruta que solo coincide con caracteres alfabéticos. Las restricciones de ruta se explican más adelante en este artículo.

El segundo segmento de la ruta de dirección URL, {name:alpha}:

En el ejemplo siguiente se muestra el enrutamiento con comprobaciones de estado y autorización:

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

app.MapHealthChecks("/healthz").RequireAuthorization();
app.MapGet("/", () => "Hello World!");

En el ejemplo anterior se muestra cómo:

  • El middleware de autorización se puede usar con el enrutamiento.
  • Los puntos de conexión se pueden usar para configurar el comportamiento de la autorización.

La llamada a MapHealthChecks agrega un punto de conexión de comprobación de estado. Al encadenar RequireAuthorization a esta llamada, se adjunta una directiva de autorización al punto de conexión.

La llamada a UseAuthentication y UseAuthorization agrega el middleware de autenticación y autorización. Estos middleware se colocan entre UseRouting y UseEndpoints para que puedan:

  • Vea qué punto de conexión ha seleccionado UseRouting.
  • Aplique una directiva de autorización antes de que UseEndpoints envíe al punto de conexión.

Metadatos de punto de conexión

En el ejemplo anterior, hay dos puntos de conexión, pero solo el de comprobación de estado tiene una directiva de autorización adjunta. Si la solicitud coincide con el punto de conexión de comprobación de estado, /healthz, se realiza una comprobación de autorización. Esto demuestra que los puntos de conexión pueden tener datos adicionales adjuntos. Estos datos adicionales se denominan metadatos de punto de conexión:

  • Los metadatos pueden ser procesados mediante middleware compatible con el enrutamiento.
  • Los metadatos pueden ser de cualquier tipo de .NET.

Conceptos de enrutamiento

El sistema de enrutamiento se basa en la canalización de middleware mediante la adición del eficaz concepto de punto de conexión. Los puntos de conexión representan unidades de la funcionalidad de la aplicación que son diferentes entre sí en cuanto al enrutamiento, la autorización y cualquier número de sistemas de ASP.NET Core.

Definición de punto de conexión de ASP.NET Core

Un punto de conexión de ASP.NET Core es:

En el código siguiente se muestra cómo recuperar e inspeccionar el punto de conexión que coincide con la solicitud actual:

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.");

El punto de conexión, si se selecciona, se puede recuperar de HttpContext. Se pueden inspeccionar sus propiedades. Los objetos de punto de conexión son inmutables y no se pueden modificar después de crearlos. El tipo más común de punto de conexión es RouteEndpoint. RouteEndpoint incluye información que permite que el sistema de enrutamiento lo seleccione.

En el código anterior, app.Use configura un middleware insertado.

En el código siguiente se muestra que, en función de dónde se llame a app.Use en la canalización, es posible que no haya un punto de conexión:

// 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);
});

En el ejemplo anterior se agregan instrucciones Console.WriteLine que muestran si se ha seleccionado un punto de conexión o no. Para mayor claridad, en el ejemplo se asigna un nombre para mostrar al punto de conexión / proporcionado.

El ejemplo anterior también incluye llamadas a UseRouting y UseEndpoints para controlar exactamente cuándo se ejecuta este middleware dentro de la canalización.

Al ejecutar este código con una dirección URL de / se muestra lo siguiente:

1. Endpoint: (null)
2. Endpoint: Hello
3. Endpoint: Hello

Al ejecutar este código con otra dirección URL se muestra lo siguiente:

1. Endpoint: (null)
2. Endpoint: (null)
4. Endpoint: (null)

Este resultado muestra que:

  • El punto de conexión siempre es NULL antes de que se llame a UseRouting.
  • Si se encuentra una coincidencia, el extremo no es NULL entre UseRouting y UseEndpoints.
  • El middleware UseEndpoints es terminal cuando se encuentra una coincidencia. El middleware de terminal se define más adelante en este artículo.
  • El middleware después de UseEndpoints solo se ejecuta cuando no se encuentra ninguna coincidencia.

El middleware UseRouting usa el método SetEndpoint para asociar el punto de conexión al contexto actual. Se puede reemplazar el middleware UseRouting con lógica personalizada y seguir aprovechando las ventajas del uso de puntos de conexión. Los puntos de conexión son una primitiva de bajo nivel como middleware y no están unidos a la implementación de enrutamiento. La mayoría de las aplicaciones no necesitan reemplazar UseRouting por lógica personalizada.

El middleware UseEndpoints está diseñado para usarse junto con el middleware UseRouting. La lógica básica para ejecutar un punto de conexión no es complicada. Use GetEndpoint para recuperar el punto de conexión y, después, invoque su propiedad RequestDelegate.

En el código siguiente se muestra cómo el middleware puede influir en el enrutamiento o reaccionar ante este:

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 { }

En el ejemplo anterior se muestran dos conceptos importantes:

  • El middleware se puede ejecutar antes de UseRouting para modificar los datos sobre los que funciona el enrutamiento.
  • El middleware se puede ejecutar entre UseRouting y UseEndpoints para procesar los resultados del enrutamiento antes de que se ejecute el punto de conexión.
    • El middleware que se ejecuta entre UseRouting y UseEndpoints:
      • Normalmente inspecciona los metadatos para entender los puntos de conexión.
      • A menudo toma decisiones de seguridad, como UseAuthorization y UseCors.
    • La combinación de middleware y metadatos permite configurar directivas por punto de conexión.

En el código anterior se muestra un ejemplo de middleware personalizado que admite directivas por punto de conexión. El middleware escribe un registro de auditoría de acceso a datos confidenciales en la consola. El middleware se puede configurar para auditar un punto de conexión con los metadatos de RequiresAuditAttribute. En este ejemplo se muestra un patrón opcional en el que solo se auditan los puntos de conexión marcados como confidenciales. Esta lógica se puede definir en orden inverso, para auditar todo lo que no esté marcado como seguro, por ejemplo. El sistema de metadatos de punto de conexión es flexible. Esta lógica se puede diseñar de la manera que mejor se adapte al caso de uso.

El código del ejemplo anterior está diseñado para mostrar los conceptos básicos de los puntos de conexión. No está pensado para su uso en producción. Una versión más completa de un middleware de registro de auditoría:

  • Realizaría el registro en un archivo o una base de datos.
  • Incluiría detalles como el usuario, la dirección IP, el nombre del punto de conexión confidencial, etc.

El valor RequiresAuditAttribute de metadatos de directiva de auditoría se define como Attribute para facilitar su uso con marcos basados en clases como los controladores y SignalR. Cuando se usa de ruta a código:

  • Los metadatos se asocian con una API de generador.
  • Los marcos basados en clases incluyen todos los atributos en el método y la clase correspondientes al crear los puntos de conexión.

Los procedimientos recomendados para los tipos de metadatos son definirlos como interfaces o atributos. Las interfaces y los atributos permiten la reutilización del código. El sistema de metadatos es flexible y no impone ninguna limitación.

Comparación del middleware de terminal con el enrutamiento

En el ejemplo siguiente se muestra el middleware de terminal y el enrutamiento:

// 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.");

El estilo de middleware que se muestra con Approach 1: es middleware de terminal. Se denomina middleware de terminal porque realiza una operación de búsqueda de coincidencias:

  • La operación de búsqueda de coincidencias en el ejemplo anterior es Path == "/" para el middleware y Path == "/Routing" para el enrutamiento.
  • Cuando una coincidencia es correcta, ejecuta alguna funcionalidad y devuelve un valor, en lugar de invocar el middleware next.

Se denomina middleware de terminal porque finaliza la búsqueda, ejecuta alguna funcionalidad y, después, devuelve un valor.

En la lista siguiente se compara el middleware de terminal con el enrutamiento:

  • Los dos enfoques permiten terminar la canalización de procesamiento:
    • El middleware finaliza la canalización mediante la devolución de un valor en lugar de invocar next.
    • Los puntos de conexión siempre son de terminal.
  • El middleware de terminal permite colocar el middleware en un lugar arbitrario de la canalización:
    • Los puntos de conexión se ejecutan en la posición de UseEndpoints.
  • El middleware de terminal permite que el código arbitrario determine cuándo coincide el middleware:
    • El código personalizado de búsqueda de coincidencia de rutas puede ser detallado y difícil de escribir correctamente.
    • El enrutamiento proporciona soluciones sencillas para las aplicaciones típicas. La mayoría de las aplicaciones no requieren código personalizado de búsqueda de coincidencia de rutas.
  • Los puntos de conexión interactúan con middleware como UseAuthorization y UseCors.
    • Para usar un middleware de terminal con UseAuthorization o UseCors se necesita interactuar de forma manual con el sistema de autorización.

Un punto de conexión define:

  • Un delegado para procesar solicitudes.
  • Una colección de metadatos arbitrarios. Los metadatos se usan para implementar cuestiones transversales según las directivas y la configuración asociada a cada punto de conexión.

El middleware de terminal puede ser una herramienta eficaz, pero puede requerir:

  • Una cantidad significativa de código y pruebas.
  • La integración manual con otros sistemas para lograr el nivel deseado de flexibilidad.

Considere la posibilidad de realizar la integración con el enrutamiento antes de escribir middleware de terminal.

El middleware de terminal existente que se integra con Map o MapWhen normalmente se puede convertir en un punto de conexión compatible con el enrutamiento. MapHealthChecks muestra el patrón para enrutadores:

  • Escriba un método de extensión en IEndpointRouteBuilder.
  • Cree una canalización de middleware anidada mediante CreateApplicationBuilder.
  • Adjunte el middleware a la nueva canalización. En este caso, UseHealthChecks.
  • Aplique Build a la canalización de middleware en un objeto RequestDelegate.
  • Llame a Map y proporcione la nueva canalización de middleware.
  • Devuelva el objeto de generador proporcionado por Map desde el método de extensión.

En el código siguiente se muestra el uso de MapHealthChecks:

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

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

En el ejemplo anterior se muestra la importancia de devolver el objeto de generador. Al devolver el objeto de generador, el desarrollador de aplicaciones puede configurar directivas como la autorización para el punto de conexión. En este ejemplo, el middleware de comprobaciones de estado no tiene una integración directa con el sistema de autorización.

El sistema de metadatos se ha creado como respuesta a los problemas detectados por los autores de extensibilidad mediante el middleware de terminal. El problema de cada middleware es implementar su propia integración con el sistema de autorización.

Coincidencia de dirección URL

  • Es el proceso por el cual el enrutamiento hace coincidir una solicitud entrante a un punto de conexión.
  • Se basa en los datos de la ruta de acceso y los encabezados de la dirección URL.
  • Se puede extender para tener en cuenta los datos de la solicitud.

Cuando se ejecuta un middleware de enrutamiento, se establece un objeto Endpoint y se enrutan los valores a una característica de solicitud en el objeto HttpContext desde la solicitud actual:

  • La llamada a HttpContext.GetEndpoint obtiene el punto de conexión.
  • HttpRequest.RouteValues obtiene la colección de valores de ruta.

El middleware que se ejecuta después del middleware de enrutamiento puede inspeccionar el punto de conexión y tomar medidas. Por ejemplo, un middleware de autorización puede consultar la colección de metadatos del punto de conexión de una directiva de autorización. Después de que se ejecuta todo el middleware en la canalización de procesamiento de solicitudes, se invoca al delegado del punto de conexión seleccionado.

El sistema de enrutamiento en el enrutamiento de punto de conexión es responsable de todas las decisiones relativas al envío. Como el middleware aplica directivas en función del punto de conexión seleccionado, es importante que:

  • Cualquier decisión que pueda afectar al envío o a la aplicación de directivas de seguridad se realice dentro del sistema de enrutamiento.

Advertencia

En cuanto a la compatibilidad con versiones anteriores, cuando se ejecuta un delegado del punto de conexión de controlador o Razor Pages, las propiedades de RouteContext.RouteData se establecen en los valores adecuados en función del procesamiento de solicitudes realizado hasta el momento.

El tipo RouteContext se marcará como obsoleto en una versión futura:

  • Migre RouteData.Values a HttpRequest.RouteValues.
  • Migre RouteData.DataTokens para recuperar IDataTokensMetadata de los metadatos del punto de conexión.

La coincidencia de direcciones URL funciona en un conjunto configurable de fases. En cada fase, la salida es un conjunto de coincidencias. El conjunto de coincidencias se puede reducir más en la fase siguiente. La implementación de enrutamiento no garantiza un orden de procesamiento para los puntos de conexión coincidentes. Todas las coincidencias posibles se procesan a la vez. Las fases de coincidencia de direcciones URL se producen en el orden siguiente. ASP.NET Core:

  1. Procesa la ruta de dirección URL con el conjunto de puntos de conexión y sus plantillas de ruta, y se recopilan todas las coincidencias.
  2. Toma la lista anterior y quita las coincidencias en las que se produce un error con restricciones de ruta aplicadas.
  3. Toma la lista anterior y quita las coincidencias en las que se produce un error en el conjunto de instancias de MatcherPolicy.
  4. Usa EndpointSelector para tomar una decisión final de la lista anterior.

La lista de puntos de conexión se prioriza según:

Todos los puntos de conexión coincidentes se procesan en cada fase hasta que se alcanza EndpointSelector. EndpointSelector es la fase final. Elige el punto de conexión de prioridad más alta entre las coincidencias como la mejor coincidencia. Si hay otras coincidencias con la misma prioridad que la mejor, se inicia una excepción de coincidencia ambigua.

La prioridad de ruta se calcula en función de una plantilla de ruta más específica a la que se le asigna una prioridad más alta. Por ejemplo, considere las plantillas /hello y /{message}:

  • Las dos coinciden con la ruta de dirección URL /hello.
  • /hello es más específica y, por tanto, tiene mayor prioridad.

Por lo general, la precedencia de rutas realiza un buen trabajo de elegir la mejor coincidencia para los tipos de esquemas de dirección URL que se usan en la práctica. Use Order solo cuando sea necesario para evitar una ambigüedad.

Debido a los tipos de extensibilidad que proporciona el enrutamiento, el sistema de enrutamiento no puede calcular las rutas ambiguas por adelantado. Considere un ejemplo como las plantillas de ruta /{message:alpha} y /{message:int}:

  • La restricción alpha solo coincide con caracteres alfabéticos.
  • La restricción int solo coincide con números.
  • Estas plantillas tienen la misma prioridad de ruta, pero no hay ninguna dirección URL única con la que coincidan.
  • Si el sistema de enrutamiento ha notificado un error de ambigüedad al iniciarse, bloquearía este caso de uso válido.

Advertencia

El orden de las operaciones dentro de UseEndpoints no influye en el comportamiento del enrutamiento, con una excepción. MapControllerRoute y MapAreaRoute asignan de forma automática un valor de orden a sus puntos de conexión en función del orden en el que se hayan invocado. Esto simula el comportamiento a largo plazo de los controladores sin que el sistema de enrutamiento proporcione las mismas garantías que las implementaciones de enrutamiento anteriores.

Enrutamiento de puntos de conexión en ASP.NET Core:

  • No tiene el concepto de rutas.
  • No proporciona garantías de ordenación. Todos los puntos de conexión se procesan a la vez.

Prioridad de la plantilla de ruta y orden de selección de los puntos de conexión

La prioridad de la plantilla de ruta es un sistema que asigna a cada plantilla de ruta un valor en función de su especificidad. Precedencia de la plantilla de ruta:

  • Evita la necesidad de ajustar el orden de los puntos de conexión en casos comunes.
  • Intenta hacer coincidir las expectativas comunes del comportamiento del enrutamiento.

Por ejemplo, considere las plantillas /Products/List y /Products/{id}. Sería razonable suponer que /Products/List es una mejor coincidencia que /Products/{id} para la ruta de dirección URL /Products/List. Funciona porque el segmento literal /List se considera que tiene una mayor prioridad que el segmento de parámetro /{id}.

Los detalles de cómo funciona la precedencia están vinculados a cómo se definen las plantillas de ruta:

  • Las plantillas con más segmentos se consideran más específicas.
  • Un segmento con texto literal se considera más específico que un segmento de parámetro.
  • Un segmento de parámetro con una restricción se considera más específico que uno que no la tenga.
  • Un segmento complejo se considera igual de específico que un segmento de parámetro con una restricción.
  • Los parámetros comodín son los menos específicos. Vea comodín en la sección Plantillas de ruta para obtener información importante sobre las rutas comodín.

Conceptos de generación de direcciones URL

Generación de direcciones URL:

  • Es el proceso por el cual el enrutamiento puede crear una ruta de dirección URL en función de un conjunto de valores de ruta.
  • Permite una separación lógica entre los puntos de conexión y las direcciones URL que acceden a ellos.

El enrutamiento de punto de conexión incluye la API LinkGenerator. LinkGenerator es un servicio singleton disponible desde la DI. La API LinkGenerator se puede usar fuera del contexto de una solicitud en ejecución. Mvc.IUrlHelper y los escenarios que dependen de IUrlHelper, como los asistentes de etiquetas, los de HTML y los resultados de acción, usan de forma interna la API LinkGenerator para proporcionar funciones de generación de vínculos.

El generador de vínculos está respaldado por el concepto de una dirección y esquemas de direcciones. Un esquema de direcciones es una manera de determinar los puntos de conexión que se deben tener en cuenta para la generación de vínculos. Por ejemplo, los escenarios de nombre y valores de ruta de controladores y Razor Pages con los que muchos usuarios están familiarizados se implementan como un esquema de direcciones.

El generador de vínculos puede vincular a controladores y Razor Pages a través de los métodos de extensión siguientes:

Las sobrecargas de estos métodos aceptan argumentos que incluyan HttpContext. Estos métodos son funcionalmente equivalentes a Url.Action y Url.Page, pero ofrecen flexibilidad y opciones adicionales.

Los métodos GetPath* son más similares a Url.Action y Url.Page, dado que generan un URI que contiene una ruta de acceso absoluta. Los métodos GetUri* siempre generan un URI absoluto que contiene un esquema y un host. Los métodos que aceptan HttpContext generan un URI en el contexto de la solicitud que se ejecuta. A menos que se reemplacen, se usan los valores de ruta de ambiente, la ruta de acceso base de la dirección URL, el esquema y el host de la solicitud en ejecución.

Se llama a LinkGenerator con una dirección. La generación de un URI se produce en dos pasos:

  1. Se enlaza una dirección a una lista de puntos de conexión que coincidan con la dirección.
  2. Se evalúa el elemento RoutePattern de cada punto de conexión hasta que se encuentra un patrón de ruta que coincida con los valores proporcionados. La salida resultante se combina con otras partes del URI proporcionadas al generador de vínculos y devueltas.

Los métodos proporcionados por LinkGenerator admiten funciones estándar de generación de vínculos para cualquier tipo de dirección. La forma más práctica de usar el generador de vínculos es a través de métodos de extensión que realicen operaciones para un tipo de dirección específica:

Método de extensión Descripción
GetPathByAddress Genera un URI con una ruta de acceso absoluta en función de los valores proporcionados.
GetUriByAddress Genera un URI absoluto en función de los valores proporcionados.

Advertencia

Preste atención a las consecuencias siguientes de llamar a los métodos LinkGenerator:

  • Use los métodos de extensión GetUri* con precaución en una configuración de aplicación en la que no se valide el encabezado Host de las solicitudes entrantes. Si no se valida el encabezado Host de las solicitudes entrantes, la entrada de la solicitud que no sea de confianza se puede devolver al cliente en los URI de una página o vista. Se recomienda que todas las aplicaciones de producción configuren su servidor para validar el encabezado Host en función de valores válidos conocidos.

  • Use LinkGenerator con precaución en el middleware junto con Map o MapWhen. Map* cambia la ruta de acceso base de la solicitud que se ejecuta, lo que afecta a la salida de la generación de vínculos. Todas las API de LinkGenerator permiten especificar una ruta de acceso base. Especifique una ruta de acceso base vacía para deshacer el efecto de Map* en la generación de vínculos.

Ejemplo de middleware

En el ejemplo siguiente, un middleware usa la API LinkGenerator para crear un vínculo a un método de acción que enumera los productos de la tienda. El uso del generador de vínculos mediante su inserción en una clase y la llamada a GenerateLink está disponible para cualquier clase de una aplicación:

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

Plantillas de ruta

Los tokens de {} definen parámetros de ruta que se enlazan si se encuentran coincidencias con la ruta. Se puede definir más de un parámetro de ruta en un segmento de ruta, pero deben estar separados por un valor literal. Por ejemplo:

{controller=Home}{action=Index}

No es una ruta válida, ya que no hay ningún valor literal entre {controller} y {action}. Los parámetros de ruta deben tener un nombre y, opcionalmente, atributos adicionales especificados.

El texto literal diferente de los parámetros de ruta (por ejemplo, {id}) y el separador de ruta / deben coincidir con el texto de la dirección URL. La coincidencia de texto no distingue mayúsculas de minúsculas y se basa en la representación descodificada de la ruta de las direcciones URL. Para que el delimitador de parámetro de ruta literal { o } coincida, repita el carácter para aplicar escape al carácter. Por ejemplo, {{ o }}.

Asterisco * o asterisco doble **:

  • Se puede usar como prefijo de un parámetro de ruta para enlazar con el resto del URI.
  • Se denominan parámetros comodín. Por ejemplo, blog/{**slug}:
    • Coincide con cualquier URI que empiece por blog/ y después tenga cualquier valor.
    • El valor que aparece detrás de blog/ se asigna al valor de ruta slug.

Advertencia

Un parámetro catch-all puede relacionar rutas de forma incorrecta debido a un error en el enrutamiento. Las aplicaciones afectadas por este error tienen las características siguientes:

  • Una ruta catch-all (por ejemplo, {**slug}")
  • La ruta catch-all causa un error al relacionar solicitudes que sí que debería relacionar.
  • Al quitar otras rutas, la ruta catch-all empieza a funcionar.

Para ver casos de ejemplo relacionados con este error, consulte los errores 18677 y 16579 en GitHub.

Se incluye una corrección de participación para este error en el SDK de .NET Core 3.1.301 y versiones posteriores. En el código que hay a continuación se establece un cambio interno que corrige este error:

public static void Main(string[] args)
{
   AppContext.SetSwitch("Microsoft.AspNetCore.Routing.UseCorrectCatchAllBehavior", 
                         true);
   CreateHostBuilder(args).Build().Run();
}
// Remaining code removed for brevity.

Los parámetros comodín también pueden coincidir con una cadena vacía.

El parámetro comodín inserta los caracteres de escape correspondientes cuando se usa la ruta para generar una dirección URL, incluidos los caracteres / de separación de ruta de acceso. Por ejemplo, la ruta foo/{*path} con valores de ruta { path = "my/path" } genera foo/my%2Fpath. Tenga en cuenta la barra diagonal de escape. Para los caracteres separadores de ruta de acceso de ida y vuelta, use el prefijo de parámetro de ruta **. La ruta foo/{**path} con { path = "my/path" } genera foo/my/path.

Los patrones de dirección URL que intentan capturar un nombre de archivo con una extensión de archivo opcional tienen consideraciones adicionales. Por ejemplo, considere la plantilla files/{filename}.{ext?}. Cuando existen valores para filename y ext, los dos valores se rellenan. Si solo existe un valor para filename en la dirección URL, la ruta coincide porque el carácter . final es opcional. Las direcciones URL siguientes coinciden con esta ruta:

  • /files/myFile.txt
  • /files/myFile

Los parámetros de ruta pueden tener valores predeterminados designados mediante la especificación del valor predeterminado después del nombre de parámetro, separado por un signo igual (=). Por ejemplo, {controller=Home} define Home como el valor predeterminado de controller. El valor predeterminado se usa si no hay ningún valor en la dirección URL para el parámetro. Los parámetros de ruta se pueden convertir en opcionales si se anexa un signo de interrogación (?) al final del nombre del parámetro. Por ejemplo: id?. La diferencia entre los valores opcionales y los parámetros de ruta predeterminados es:

  • Un parámetro de ruta con un valor predeterminado siempre produce un valor.
  • Un parámetro opcional solo tiene un valor cuando la dirección URL de la solicitud proporciona un valor.

Los parámetros de ruta pueden tener restricciones que deben coincidir con el valor de ruta enlazado desde la dirección URL. Al agregar : y un nombre de restricción después del nombre del parámetro de ruta, se especifica una restricción insertada en un parámetro de ruta. Si la restricción requiere argumentos, se incluyen entre paréntesis (...) después del nombre de restricción. Se pueden especificar varias restricciones insertadas si se anexa otro carácter : y un nombre de restricción.

El nombre de restricción y los argumentos se pasan al servicio IInlineConstraintResolver para crear una instancia de IRouteConstraint para su uso en el procesamiento de direcciones URL. Por ejemplo, la plantilla de ruta blog/{article:minlength(10)} especifica una restricción minlength con el argumento 10. Para obtener más información sobre las restricciones de ruta y una lista de las restricciones proporcionadas por el marco, vea la sección Restricciones de ruta.

Los parámetros de ruta también pueden tener transformadores de parámetros. Los transformadores de parámetros transforman el valor de un parámetro al generar vínculos y hacer coincidir acciones y páginas con direcciones URL. Como sucede con las restricciones, los transformadores de parámetros se pueden agregar en línea a un parámetro de ruta mediante la incorporación un carácter : y un nombre de transformador después del nombre del parámetro de ruta. Por ejemplo, la plantilla de ruta blog/{article:slugify} especifica un transformador slugify. Para obtener más información sobre los transformadores de parámetros, vea la sección Transformadores de parámetros.

En la tabla siguiente se muestran plantillas de ruta de ejemplo y su comportamiento:

Plantilla de ruta URI coincidente de ejemplo El URI de la solicitud...
hello /hello Solo coincide con la ruta de acceso única /hello.
{Page=Home} / Coincide y establece Page en Home.
{Page=Home} /Contact Coincide y establece Page en Contact.
{controller}/{action}/{id?} /Products/List Se asigna al controlador Products y la acción List.
{controller}/{action}/{id?} /Products/Details/123 Se asigna al controlador Products y la acción Details con id establecido en 123.
{controller=Home}/{action=Index}/{id?} / Se asigna al controlador Home y al método Index. id se pasa por alto.
{controller=Home}/{action=Index}/{id?} /Products Se asigna al controlador Products y al método Index. id se pasa por alto.

El uso de una plantilla suele ser el método de enrutamiento más sencillo. Las restricciones y los valores predeterminados también se pueden especificar fuera de la plantilla de ruta.

Segmentos complejos

Los segmentos complejos se procesan mediante la búsqueda de coincidencias de delimitadores literales de derecha a izquierda de un modo no expansivo. Por ejemplo, [Route("/a{b}c{d}")] es un segmento complejo. Los segmentos complejos funcionan de una manera determinada que se debe entender para usarlos correctamente. En el ejemplo de esta sección se muestra por qué los segmentos complejos solo funcionan bien cuando el texto del delimitador no aparece dentro de los valores de los parámetros. En casos más complejos es necesario usar una expresión regular y extraer los valores de forma manual.

Advertencia

Cuando se usa System.Text.RegularExpressions para procesar entradas que no son de confianza, pase un tiempo de expiración. Un usuario malintencionado puede proporcionar entradas a RegularExpressions y provocar un ataque por denegación de servicio. Las API del marco ASP.NET Core en las que se usa RegularExpressions pasan un tiempo de expiración.

Este es un resumen de los pasos que realiza el enrutamiento con la plantilla /a{b}c{d} y la ruta de dirección URL /abcd. | se usa para ayudar a visualizar cómo funciona el algoritmo:

  • El primer literal, de derecha a izquierda, es c. Por tanto, se busca en /abcd desde la derecha y se encuentra /ab|c|d.
  • Todo lo que se encuentra a la derecha (d) coincide ahora con el parámetro de ruta {d}.
  • El siguiente literal, de derecha a izquierda, es a. Por tanto, se busca en /ab|c|d a partir de donde se ha parado antes, después a y se encuentra /|a|b|c|d.
  • El valor situado a la derecha (b) coincide ahora con el parámetro de ruta {b}.
  • No queda ningún texto ni ninguna plantilla de ruta, por lo que se trata de una coincidencia.

Este es un ejemplo de un caso negativo en el que se usa la misma plantilla /a{b}c{d} y la ruta de dirección URL /aabcd. | se usa para ayudar a visualizar cómo funciona el algoritmo. Este caso no es una coincidencia, que se explica mediante el mismo algoritmo:

  • El primer literal, de derecha a izquierda, es c. Por tanto, se busca en /aabcd desde la derecha y se encuentra /aab|c|d.
  • Todo lo que se encuentra a la derecha (d) coincide ahora con el parámetro de ruta {d}.
  • El siguiente literal, de derecha a izquierda, es a. Por tanto, se busca en /aab|c|d a partir de donde se ha parado antes, después a y se encuentra /a|a|b|c|d.
  • El valor situado a la derecha (b) coincide ahora con el parámetro de ruta {b}.
  • En este momento, todavía hay texto a, pero el algoritmo se ha quedado sin plantilla de ruta para analizar, por lo que no es una coincidencia.

Como el algoritmo de búsqueda de coincidencias es no expansivo:

  • Coincide con la menor cantidad de texto posible en cada paso.
  • Cualquier caso en el que el valor de delimitador aparezca dentro de los valores de parámetro provoca que no coincida.

Las expresiones regulares proporcionan un mayor control sobre el comportamiento de búsqueda de coincidencias.

La coincidencia expansiva, también conocida como coincidencia diferida, coincide con la cadena más grande posible. La búsqueda no expansiva coincide con la cadena más pequeña posible.

Enrutamiento con caracteres especiales

El enrutamiento con caracteres especiales puede dar lugar a resultados inesperados. Por ejemplo, considere un controlador con el siguiente método de acción:

[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;
}

Cuando string id contiene los siguientes valores codificados, pueden producirse resultados inesperados:

ASCII Encoded
/ %2F
+

Los parámetros de ruta no siempre están descodificados por URL. Este problema puede abordarse en el futuro. Para obtener más información, vea esta incidencia de GitHub.

Restricciones de ruta

Las restricciones de ruta se ejecutan cuando se ha producido una coincidencia con la dirección URL entrante y la ruta de dirección URL se convierte en tokens en valores de ruta. En general, las restricciones de ruta inspeccionan el valor de ruta asociado a través de la plantilla de ruta y deciden si el valor es aceptable o no. Algunas restricciones de ruta usan datos ajenos al valor de ruta para decidir si la solicitud se puede enrutar. Por ejemplo, HttpMethodRouteConstraint puede aceptar o rechazar una solicitud basada en su verbo HTTP. Las restricciones se usan en las solicitudes de enrutamiento y la generación de vínculos.

Advertencia

No use las restricciones para la validación de entradas. Si se usan restricciones para la validación de entradas, las que no sean válidas generan una respuesta 404 No encontrado. Una entrada no válida debería generar 400 Solicitud incorrecta con un mensaje de error adecuado. Las restricciones de ruta se usan para eliminar la ambigüedad entre rutas similares, no para validar las entradas de una ruta determinada.

En la tabla siguiente se muestran restricciones de ruta de ejemplo y su comportamiento esperado:

restricción Ejemplo Coincidencias de ejemplo Notas
int {id:int} 123456789, -123456789 Coincide con cualquier entero
bool {active:bool} true, FALSE Coincide con true o false. No distingue mayúsculas de minúsculas
datetime {dob:datetime} 2016-12-31, 2016-12-31 7:32pm Coincide con un valor DateTime válido en la referencia cultural invariable. Vea la advertencia anterior.
decimal {price:decimal} 49.99, -1,000.01 Coincide con un valor decimal válido en la referencia cultural invariable. Vea la advertencia anterior.
double {weight:double} 1.234, -1,001.01e8 Coincide con un valor double válido en la referencia cultural invariable. Vea la advertencia anterior.
float {weight:float} 1.234, -1,001.01e8 Coincide con un valor float válido en la referencia cultural invariable. Vea la advertencia anterior.
guid {id:guid} CD2C1638-1638-72D5-1638-DEADBEEF1638 Coincide con un valor Guid válido
long {ticks:long} 123456789, -123456789 Coincide con un valor long válido
minlength(value) {username:minlength(4)} Rick La cadena debe tener al menos cuatro caracteres
maxlength(value) {filename:maxlength(8)} MyFile La cadena no debe tener más de ocho caracteres
length(length) {filename:length(12)} somefile.txt La cadena debe tener una longitud de exactamente 12 caracteres
length(min,max) {filename:length(8,16)} somefile.txt La cadena debe tener una longitud como mínimo de ocho caracteres y como máximo de 16
min(value) {age:min(18)} 19 El valor entero debe ser como mínimo 18
max(value) {age:max(120)} 91 El valor entero debe ser como máximo 120
range(min,max) {age:range(18,120)} 91 El valor entero debe ser como mínimo 18 y máximo 120
alpha {name:alpha} Rick La cadena debe constar de uno o más caracteres alfabéticos, a-z y no distinguir mayúsculas de minúsculas.
regex(expression) {ssn:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)} 123-45-6789 La cadena debe coincidir con la expresión regular. Vea las sugerencias sobre cómo definir una expresión regular.
required {name:required} Rick Se usa para exigir que un valor que no es de parámetro esté presente durante la generación de dirección URL

Advertencia

Cuando se usa System.Text.RegularExpressions para procesar entradas que no son de confianza, pase un tiempo de expiración. Un usuario malintencionado puede proporcionar entradas a RegularExpressions y provocar un ataque por denegación de servicio. Las API del marco ASP.NET Core en las que se usa RegularExpressions pasan un tiempo de expiración.

Es posible aplicar varias restricciones delimitadas por dos puntos a un único parámetro. Por ejemplo, la siguiente restricción permite limitar un parámetro a un valor entero de 1 o superior:

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

Advertencia

Las restricciones de ruta que comprueban la dirección URL y que se convierten en un tipo CLR siempre usan la referencia cultural invariable. Por ejemplo, la conversión al tipo int o DateTime de CLR. Estas restricciones dan por supuesto que la dirección URL no es localizable. Las restricciones de ruta proporcionadas por el marco de trabajo no modifican los valores almacenados en los valores de ruta. Todos los valores de ruta analizados desde la dirección URL se almacenan como cadenas. Por ejemplo, la restricción float intenta convertir el valor de ruta en un valor Float, pero el valor convertido se usa exclusivamente para comprobar que se puede convertir en Float.

Expresiones regulares en restricciones

Advertencia

Cuando se usa System.Text.RegularExpressions para procesar entradas que no son de confianza, pase un tiempo de expiración. Un usuario malintencionado puede proporcionar entradas a RegularExpressions y provocar un ataque por denegación de servicio. Las API del marco ASP.NET Core en las que se usa RegularExpressions pasan un tiempo de expiración.

Las expresiones regulares se pueden especificar como restricciones insertadas mediante la restricción de ruta regex(...). Los métodos de la familia MapControllerRoute también aceptan un literal de objeto de restricciones. Si se usa ese formato, los valores de cadena se interpretan como expresiones regulares.

En el código siguiente se usa una restricción de expresión regular insertada:

app.MapGet("{message:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)}",
    () => "Inline Regex Constraint Matched");

En el código siguiente se usa un literal de objeto para especificar una restricción de expresión regular:

app.MapControllerRoute(
    name: "people",
    pattern: "people/{ssn}",
    constraints: new { ssn = "^\\d{3}-\\d{2}-\\d{4}$", },
    defaults: new { controller = "People", action = "List" });

El marco de trabajo de ASP.NET Core agrega RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant al constructor de expresiones regulares. Vea RegexOptions para obtener una descripción de estos miembros.

Las expresiones regulares usan delimitadores y tokens similares a los que usan el enrutamiento y el lenguaje C#. Es necesario usar secuencias de escape con los tokens de expresiones regulares. Para usar la expresión regular ^\d{3}-\d{2}-\d{4}$ en una restricción insertada, utilice una de las opciones siguientes:

  • Reemplace los caracteres \ proporcionados en la cadena como caracteres \\ en el archivo de código fuente de C# para aplicar secuencias de escape al carácter de escape de cadena \.
  • Literales de cadena textual.

Para aplicar secuencias de escape a los caracteres delimitadores de parámetro de enrutamiento ({, }, [ y ]), duplique los caracteres en la expresión, por ejemplo {{, }}, [[ y ]]. En la tabla siguiente se muestra una expresión regular y su versión con la secuencia de escape:

Expresión regular Expresión regular con secuencia de escape
^\d{3}-\d{2}-\d{4}$ ^\\d{{3}}-\\d{{2}}-\\d{{4}}$
^[a-z]{2}$ ^[[a-z]]{{2}}$

Las expresiones regulares que se usan en el enrutamiento suelen empezar con el carácter ^ y coincidir con la posición inicial de la cadena. Las expresiones suelen terminar con el carácter $ y coincidir con el final de la cadena. Los caracteres ^ y $ garantizan que la expresión regular coincide con el valor completo del parámetro de ruta. Sin los caracteres ^ y $, la expresión regular coincide con cualquier subcadena de la cadena, lo que normalmente no es deseable. En la tabla siguiente se proporcionan ejemplos y se explica por qué coinciden o no:

Expresión String Coincidir con Comentario
[a-z]{2} hello Coincidencias de subcadenas
[a-z]{2} 123abc456 Coincidencias de subcadenas
[a-z]{2} mz Coincide con la expresión
[a-z]{2} MZ No distingue mayúsculas de minúsculas
^[a-z]{2}$ hello No Vea ^ y $ más arriba
^[a-z]{2}$ 123abc456 No Vea ^ y $ más arriba

Para obtener más información sobre la sintaxis de expresiones regulares, vea Expresiones regulares de .NET Framework.

Para restringir un parámetro a un conjunto conocido de valores posibles, use una expresión regular. Por ejemplo, {action:regex(^(list|get|create)$)} solo hace coincidir el valor de ruta action con list, get o create. Si se pasa al diccionario de restricciones, la cadena ^(list|get|create)$ es equivalente. Las restricciones que se pasan al diccionario de restricciones que no coinciden con una de las conocidas también se tratan como expresiones regulares. Las restricciones que se pasan en una plantilla y que no coinciden con una de las conocidas no se tratan como expresiones regulares.

Restricciones de ruta personalizadas

Se pueden crear restricciones de ruta personalizadas mediante la implementación de la interfaz IRouteConstraint. La interfaz IRouteConstraint contiene Match, que devuelve true si se cumple la restricción, y false en caso contrario.

Las restricciones de ruta personalizadas rara vez son necesarias. Antes de implementar una restricción de ruta personalizada, considere alternativas, como el enlace de modelos.

En la carpeta Constraints de ASP.NET Core se proporcionan buenos ejemplos de creación de restricciones. Por ejemplo, GuidRouteConstraint.

Para usar una restricción IRouteConstraint personalizada, el tipo de restricción de ruta se debe registrar con el parámetro ConstraintMap de la aplicación en el contenedor de servicios. ConstraintMap es un diccionario que asigna claves de restricciones de ruta a implementaciones de IRouteConstraint que validen esas restricciones. El parámetro ConstraintMap de una aplicación puede actualizarse en Program.cs como parte de una llamada a AddRouting o configurando RouteOptions directamente con builder.Services.Configure<RouteOptions>. Por ejemplo:

builder.Services.AddRouting(options =>
    options.ConstraintMap.Add("noZeroes", typeof(NoZeroesRouteConstraint)));

La restricción anterior se aplica en el código siguiente:

[ApiController]
[Route("api/[controller]")]
public class NoZeroesController : ControllerBase
{
    [HttpGet("{id:noZeroes}")]
    public IActionResult Get(string id) =>
        Content(id);
}

La implementación de NoZeroesRouteConstraint impide que 0 se use en un parámetro de ruta:

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

Advertencia

Cuando se usa System.Text.RegularExpressions para procesar entradas que no son de confianza, pase un tiempo de expiración. Un usuario malintencionado puede proporcionar entradas a RegularExpressions y provocar un ataque por denegación de servicio. Las API del marco ASP.NET Core en las que se usa RegularExpressions pasan un tiempo de expiración.

El código anterior:

  • Impide 0 en el segmento {id} de la ruta.
  • Se muestra para proporcionar un ejemplo básico de implementación de una restricción personalizada. No se debe usar en una aplicación de producción.

El código siguiente es un enfoque mejor para impedir que se procese un valor id que contenga 0:

[HttpGet("{id}")]
public IActionResult Get(string id)
{
    if (id.Contains('0'))
    {
        return StatusCode(StatusCodes.Status406NotAcceptable);
    }

    return Content(id);
}

El código anterior tiene las ventajas siguientes con respecto al enfoque de NoZeroesRouteConstraint:

  • No requiere una restricción personalizada.
  • Devuelve un error más descriptivo cuando el parámetro de ruta incluye 0.

Transformadores de parámetros

Transformadores de parámetros:

Por ejemplo, un transformador de parámetros personalizado slugify en el patrón de ruta blog\{article:slugify} con Url.Action(new { article = "MyTestArticle" }) genera blog\my-test-article.

Considere la siguiente implementación de 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();
    }
}

Para usar un transformador de parámetros en un patrón de ruta, configúrelo con ConstraintMap en Program.cs:

builder.Services.AddRouting(options =>
    options.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer));

El marco ASP.NET Core usa los transformadores de parámetros para transformar el URI en el que se resuelve un punto de conexión. Por ejemplo, los transformadores de parámetros transforman los valores de ruta que se usan para hacer coincidir objetos area, controller, action y page:

app.MapControllerRoute(
    name: "default",
    pattern: "{controller:slugify=Home}/{action:slugify=Index}/{id?}");

Con la plantilla de ruta anterior, la acción SubscriptionManagementController.GetAll coincide con el URI /subscription-management/get-all. Un transformador de parámetros no cambia los valores de ruta usados para generar un vínculo. Por ejemplo, Url.Action("GetAll", "SubscriptionManagement") genera /subscription-management/get-all.

ASP.NET Core proporciona convenciones de API para usar transformadores de parámetros con rutas generadas:

Referencia de generación de direcciones URL

Esta sección contiene una referencia para el algoritmo implementado por la generación de direcciones URL. En la práctica, los ejemplos más complejos de generación de direcciones URL usan controladores o Razor Pages. Vea Enrutamiento en controladores para obtener información adicional.

El proceso de generación de direcciones URL comienza con una llamada a LinkGenerator.GetPathByAddress o un método similar. Al método se le proporciona una dirección, un conjunto de valores de ruta y, opcionalmente, información sobre la solicitud actual de HttpContext.

El primer paso consiste en usar la dirección para resolver un conjunto de puntos de conexión candidatos con una instancia de IEndpointAddressScheme<TAddress> que coincide con el tipo de la dirección.

Una vez que el esquema de direcciones encuentra el conjunto de candidatos, los puntos de conexión se ordenan y procesan de forma iterativa hasta que se realiza correctamente una operación de generación de direcciones URL. La generación de direcciones URL no comprueba si hay ambigüedades; el primer resultado devuelto es el resultado final.

Solución de problemas de generación de direcciones URL con registro

El primer paso para solucionar problemas de generación de direcciones URL consiste en establecer el nivel de registro de Microsoft.AspNetCore.Routing en TRACE. LinkGenerator registra muchos detalles sobre su procesamiento que pueden ser útiles para solucionar problemas.

Vea Referencia de generación de direcciones URL para obtener más información sobre la generación de direcciones URL.

Direcciones

Las direcciones son el concepto de la generación de direcciones URL que se usa para enlazar una llamada al generador de vínculos a un conjunto de puntos de conexión candidatos.

Las direcciones son un concepto extensible que incluyen dos implementaciones de forma predeterminada:

  • Con el nombre del punto de conexión (string) como dirección:
    • Proporciona una funcionalidad similar al nombre de ruta de MVC.
    • Usa el tipo de metadatos de IEndpointNameMetadata.
    • Resuelve la cadena proporcionada con los metadatos de todos los puntos de conexión registrados.
    • Inicia una excepción durante el inicio si varios puntos de conexión usan el mismo nombre.
    • Se recomienda para uso general fuera de los controladores y Razor Pages.
  • Con los valores de ruta (RouteValuesAddress) como dirección:
    • Proporciona una funcionalidad similar a los controladores y la generación de direcciones URL heredada de Razor Pages.
    • La ampliación y depuración son complejas.
    • Proporciona la implementación que usa IUrlHelper, aplicaciones auxiliares de etiquetas, aplicaciones auxiliares HTML, resultados de acciones, etc.

El papel del esquema de direcciones consiste en establecer la asociación entre la dirección y los puntos de conexión coincidentes mediante criterios arbitrarios:

  • El esquema de nombres de punto de conexión realiza una búsqueda de diccionario básica.
  • El esquema de valores de ruta tiene un subconjunto óptimo de algoritmos definidos complejo.

Valores de ambiente y valores explícitos

A partir de la solicitud actual, el enrutamiento accede a los valores de ruta del objeto HttpContext.Request.RouteValues de la solicitud actual. Los valores asociados a la solicitud actual se conocen como valores de ambiente. Para mayor claridad, en la documentación se hace referencia a los valores de ruta que se pasan a los métodos como valores explícitos.

En el ejemplo siguiente se muestran valores de ambiente y valores explícitos. Proporciona valores de ambiente de la solicitud actual y valores explícitos:

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

    // ...

El código anterior:

El código siguiente solo proporciona valores explícitos y valores sin ambiente:

var subscribePath = _linkGenerator.GetPathByAction(
    "Subscribe", "Home", new { id = 17 })!;

El método anterior devuelve /Home/Subscribe/17.

El código siguiente en WidgetController devuelve /Widget/Subscribe/17:

var subscribePath = _linkGenerator.GetPathByAction(
    HttpContext, "Subscribe", null, new { id = 17 });

En el código siguiente se proporciona el controlador a partir de los valores de ambiente de la solicitud actual y los valores explícitos:

public class GadgetController : ControllerBase
{
    public IActionResult Index() =>
        Content(Url.Action("Edit", new { id = 17 })!);
}

En el código anterior:

  • Se devuelve /Gadget/Edit/17.
  • Url obtiene el objeto IUrlHelper.
  • Action genera una dirección URL con una ruta de acceso absoluta para un método de acción. La dirección URL contiene el nombre de action especificado y los valores route.

En el código siguiente se proporcionan valores de ambiente de la solicitud actual y valores explícitos:

public class IndexModel : PageModel
{
    public void OnGet()
    {
        var editUrl = Url.Page("./Edit", new { id = 17 });

        // ...
    }
}

En el código anterior se establece url en /Edit/17 cuando la página de Razor de edición contiene la siguiente directiva de página:

@page "{id:int}"

Si la página de edición no contiene la plantilla de ruta "{id:int}", url es /Edit?id=17.

El comportamiento de IUrlHelper de MVC agrega una capa de complejidad además de las reglas descritas aquí:

  • IUrlHelper siempre proporciona los valores de ruta de la solicitud actual como valores de ambiente.
  • IUrlHelper.Action siempre copia los valores de ruta action y controller actuales como valores explícitos a menos que el desarrollador los invalide.
  • IUrlHelper.Page siempre copia el valor de ruta page actual como un valor explícito a menos que se invalide.
  • IUrlHelper.Page siempre invalida el valor de ruta handler actual con null como un valor explícito a menos que se invalide.

A los usuarios a menudo les sorprenden los detalles del comportamiento de los valores de ambiente, ya que MVC no parece seguir sus propias reglas. Por motivos históricos y de compatibilidad, algunos valores de ruta como action, controller, page y handler tienen su propio comportamiento de caso especial.

La funcionalidad equivalente proporcionada por LinkGenerator.GetPathByAction y LinkGenerator.GetPathByPage duplica estas anomalías de IUrlHelper por motivos de compatibilidad.

Proceso de generación de direcciones URL

Una vez que se encuentra el conjunto de puntos de conexión candidatos, el algoritmo de generación de direcciones URL:

  • Procesa los puntos de conexión de forma iterativa.
  • Devuelve el primer resultado correcto.

El primer paso de este proceso se denomina invalidación del valor de ruta. La invalidación del valor de ruta es el proceso por el que el enrutamiento decide qué valores de ruta de los valores de ambiente se deben usar y cuáles se deben omitir. Cada valor de ambiente se tiene en cuenta y se combina con los valores explícitos, o bien se pasa por alto.

La mejor manera de pensar en el rol de los valores de ambiente es que intentan ahorrar trabajo a los desarrolladores de aplicaciones, en algunos casos comunes. Tradicionalmente, los escenarios en los que los valores de ambiente son útiles están relacionados con MVC:

  • Al vincular a otra acción en el mismo controlador, no es necesario especificar el nombre del controlador.
  • Al vincular a otro controlador en la misma área, no es necesario especificar el nombre del área.
  • Al vincular al mismo método de acción, no es necesario especificar los valores de ruta.
  • Al vincular a otro elemento de la aplicación, no le interesa transferir valores de ruta que no tengan ningún significado en ese elemento del control de la aplicación.

Las llamadas a LinkGenerator o IUrlHelper que devuelven null se suelen deber a que no se comprende la invalidación del valor de ruta. Para solucionar problemas de invalidación del valor de ruta, especifique de forma explícita más valores de ruta para ver si eso resuelve el problema.

La invalidación del valor de ruta se basa en la suposición de que el esquema de direcciones URL de la aplicación es jerárquico, con una jerarquía formada de izquierda a derecha. Considere la posibilidad de usar la plantilla de ruta de controlador básica {controller}/{action}/{id?} para hacerse una idea intuitiva de cómo funciona esto en la práctica. Un cambio en un valor invalida todos los valores de ruta que aparecen a la derecha. Esto refleja la suposición sobre la jerarquía. Si la aplicación tiene un valor de ambiente para id y la operación especifica otro valor para controller:

  • id no se reutilizará porque {controller} está a la izquierda de {id?}.

Algunos ejemplos demuestran este principio:

  • Si los valores explícitos contienen un valor para id, se omite el valor de ambiente de id. Se pueden usar los valores de ambiente para controller y action.
  • Si los valores explícitos contienen un valor para action, se omite cualquier valor de ambiente para action. Se pueden usar los valores de ambiente para controller. Si el valor explícito para action es diferente del valor de ambiente para action, no se usará el valor de id. Si el valor explícito para action es diferente del valor de ambiente para action, se puede usar el valor de id.
  • Si los valores explícitos contienen un valor para controller, se omite cualquier valor de ambiente para controller. Si el valor explícito para controller es diferente del valor de ambiente para controller, no se usarán los valores de action y id. Si el valor explícito para controller es igual que el valor de ambiente para controller, se pueden usar los valores de action y id.

Este proceso sea complica todavía más por la existencia de rutas de atributo y rutas convencionales dedicadas. Las rutas convencionales de controlador, como {controller}/{action}/{id?}, especifican una jerarquía mediante parámetros de ruta. Para las rutas convencionales dedicadas y las rutas de atributo a controladores y Razor Pages:

  • Existe una jerarquía de valores de ruta.
  • No aparecen en la plantilla.

En estos casos, la generación de direcciones URL define el concepto de valores necesarios. Los puntos de conexión creados por controladores y Razor Pages tienen valores necesarios especificados que permiten que la invalidación del valor de ruta funcione.

El algoritmo de invalidación del valor de ruta en detalle:

  • Los nombres de valor necesarios se combinan con los parámetros de ruta y, después, se procesan de izquierda a derecha.
  • Para cada parámetro, se comparan el valor de ambiente y el valor explícito:
    • Si el valor de ambiente y el valor explícito son iguales, el proceso continúa.
    • Si el valor de ambiente está presente y el valor explícito no, se usa el valor de ambiente al generar la dirección URL.
    • Si el valor de ambiente no está presente y el valor explícito sí, rechace el valor de ambiente y todos los posteriores.
    • Si el valor de ambiente y el valor explícito están presentes, y los dos son diferentes, rechace el valor de ambiente y todos los posteriores.

En este punto, la operación de generación de direcciones URL está lista para evaluar las restricciones de ruta. El conjunto de valores aceptados se combina con los valores predeterminados de parámetro, que se proporcionan a las restricciones. Si todas las restricciones son correctas, la operación continúa.

A continuación, se pueden usar los valores aceptados para expandir la plantilla de ruta. La plantilla de ruta se procesa:

  • De izquierda a derecha.
  • En cada parámetro se sustituye su valor aceptado.
  • Con los siguientes casos especiales:
    • Si falta un valor en los valores aceptados y el parámetro tiene un valor predeterminado, se usa el valor predeterminado.
    • Si falta un valor en los valores aceptados y el parámetro es opcional, el procesamiento continúa.
    • Si un parámetro de ruta a la derecha de un parámetro opcional que falta tiene un valor, se produce un error en la operación.
    • Los parámetros con valores predeterminados contiguos y los parámetros opcionales se contraen siempre que sea posible.

Los valores que se proporcionan de forma explícita que no coinciden con un segmento de la ruta se agregan a la cadena de consulta. En la tabla siguiente se muestra el resultado cuando se usa la plantilla de ruta {controller}/{action}/{id?}.

Valores de ambiente Valores explícitos Resultado
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

Orden de los parámetros de ruta opcionales

Los parámetros de ruta opcionales deben aparecer después de todos los parámetros de ruta obligatorios. En el código siguiente, los parámetros idy name deben aparecer después del parámetro 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 ?? ""}");
    }
}

Problemas con la invalidación del valor de ruta

En el código siguiente se muestra un ejemplo de un esquema de generación de direcciones URL que no es compatible con el enrutamiento:

app.MapControllerRoute(
    "default",
    "{culture}/{controller=Home}/{action=Index}/{id?}");

app.MapControllerRoute(
    "blog",
    "{culture}/{**slug}",
    new { controller = "Blog", action = "ReadPost" });

En el código anterior, se usa el parámetro de ruta culture para la localización. El objetivo es que el parámetro culture siempre se acepte como valor de ambiente. Pero el parámetro culture no se acepta como valor de ambiente debido al funcionamiento de los valores necesarios:

  • En la plantilla de ruta "default", el parámetro de ruta culture está a la izquierda de controller, por lo que los cambios en controller no invalidarán culture.
  • En la plantilla de ruta "blog", se considera que el parámetro de ruta culture está a la derecha de controller, que aparece en los valores necesarios.

Análisis de rutas de dirección URL con LinkParser

La clase LinkParser agrega compatibilidad con el análisis de una ruta de dirección URL en un conjunto de valores de ruta. El método ParsePathByEndpointName toma un nombre de punto de conexión y una ruta de dirección URL y devuelve un conjunto de valores de ruta extraídos de esta.

En el controlador de ejemplo siguiente, la acción GetProduct usa una plantilla de ruta de api/Products/{id} y tiene un valor de Name de GetProduct:

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    [HttpGet("{id}", Name = nameof(GetProduct))]
    public IActionResult GetProduct(string id)
    {
        // ...

En la misma clase de controlador, la acción AddRelatedProduct espera una ruta de dirección URL, pathToRelatedProduct, que se puede proporcionar como parámetro de cadena de consulta:

[HttpPost("{id}/Related")]
public IActionResult AddRelatedProduct(
    string id, string pathToRelatedProduct, [FromServices] LinkParser linkParser)
{
    var routeValues = linkParser.ParsePathByEndpointName(
        nameof(GetProduct), pathToRelatedProduct);
    var relatedProductId = routeValues?["id"];

    // ...

En el ejemplo anterior, la acción AddRelatedProduct extrae el valor de ruta id de la ruta de dirección URL. Por ejemplo, con una ruta de dirección URL de /api/Products/1, el valor de relatedProductId se establece en 1. Este enfoque permite a los clientes de la API usar rutas de dirección URL al hacer referencia a recursos, sin necesidad de conocer cómo se estructura dicha dirección URL.

Configuración de metadatos de punto de conexión

Los vínculos siguientes proporcionan información sobre cómo configurar los metadatos del punto de conexión:

Comparación de host en rutas con RequireHost

RequireHost aplica una restricción a la ruta que requiere el host especificado. El parámetro RequireHost o [Host] puede ser:

  • Host: www.domain.com, compara www.domain.com con cualquier puerto.
  • Host con carácter comodín: *.domain.com, coincide con www.domain.com, subdomain.domain.com o www.subdomain.domain.com en cualquier puerto.
  • Puerto: *:5000, coincide con el puerto 5000 con cualquier host.
  • Host y puerto: www.domain.com:5000 o *.domain.com:5000, coincide con el host y el puerto.

Se pueden especificar varios parámetros mediante RequireHost o [Host]. La restricción coincide con los hosts válidos para cualquiera de los parámetros. Por ejemplo, [Host("domain.com", "*.domain.com")] coincide con domain.com, www.domain.com y subdomain.domain.com.

En el código siguiente se usa RequireHost para requerir el host especificado en la ruta:

app.MapGet("/", () => "Contoso").RequireHost("contoso.com");
app.MapGet("/", () => "AdventureWorks").RequireHost("adventure-works.com");

app.MapHealthChecks("/healthz").RequireHost("*:8080");

En el código siguiente se usa el atributo [Host] en el controlador para requerir cualquiera de los hosts especificados:

[Host("contoso.com", "adventure-works.com")]
public class HostsController : Controller
{
    public IActionResult Index() =>
        View();

    [Host("example.com")]
    public IActionResult Example() =>
        View();
}

Cuando el atributo [Host] se aplica al método de acción y al controlador:

  • Se usa el atributo en la acción.
  • Se omite el atributo del controlador.

Grupos de rutas

El método de extensión MapGroup ayuda a organizar grupos de puntos de conexión con un prefijo común. Reduce el código repetitivo y permite personalizar grupos completos de puntos de conexión con una sola llamada a métodos como RequireAuthorization y WithMetadata, que agregan metadatos de punto de conexión.

Por ejemplo, el código siguiente crea dos grupos similares de puntos de conexión:

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;
}

En este escenario, puede usar una dirección relativa para el encabezado Location en el resultado 201 Created:

public static async Task<Created<Todo>> CreateTodo(Todo todo, TodoDb database)
{
    await database.AddAsync(todo);
    await database.SaveChangesAsync();

    return TypedResults.Created($"{todo.Id}", todo);
}

El primer grupo de puntos de conexión solo coincidirá con las solicitudes con el prefijo /public/todos y que sean accesibles sin autenticación. El segundo grupo de puntos de conexión solo coincidirá con las solicitudes con el prefijo /private/todos y que requieran autenticación.

La QueryPrivateTodosfábrica de filtros de punto de conexión es una función local que modifica los parámetros TodoDb del controlador para permitir el acceso y almacenar datos privados de tareas pendientes.

Los grupos de rutas también admiten grupos anidados y patrones de prefijo complejos con parámetros y restricciones de ruta. En el ejemplo siguiente, y el controlador de rutas asignado al grupo user puede capturar los parámetros de ruta {org} y {group} definidos en los prefijos del grupo externo.

El prefijo también puede estar vacío. Esto puede ser útil para agregar metadatos de punto de conexión o filtros a un grupo de puntos de conexión sin cambiar el patrón de ruta.

var all = app.MapGroup("").WithOpenApi();
var org = all.MapGroup("{org}");
var user = org.MapGroup("{user}");
user.MapGet("", (string org, string user) => $"{org}/{user}");

La adición de filtros o metadatos a un grupo se comporta del mismo modo que la adición individual a cada punto de conexión antes de agregar filtros o metadatos adicionales que quizás se hayan agregado a un grupo interno o a un punto de conexión específico.

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

En el ejemplo anterior, el filtro externo registrará la solicitud entrante antes que el filtro interno aunque se haya agregado en segundo lugar. Dado que los filtros se aplicaron a diferentes grupos, el orden en que se agregaron el uno con respecto al otro no es importante. El orden en que se agregan los filtros es importante si se aplican al mismo grupo o punto de conexión específico.

Una solicitud a /outer/inner/ registrará lo siguiente:

/outer group filter
/inner group filter
MapGet filter

Instrucciones de rendimiento para el enrutamiento

Cuando una aplicación tiene problemas de rendimiento, a menudo se sospecha que el enrutamiento es el problema. El motivo es que marcos como los controladores y Razor Pages notifican la cantidad de tiempo empleado en el marco de trabajo en sus mensajes de registro. Cuando hay una diferencia significativa entre el tiempo notificado por los controladores y el tiempo total de la solicitud:

  • Los desarrolladores eliminan el código de la aplicación como origen del problema.
  • Es habitual asumir que el enrutamiento es la causa.

El rendimiento del enrutamiento se prueba mediante miles de puntos de conexión. No es probable que una aplicación típica detecte un problema de rendimiento simplemente por ser demasiado grande. La causa raíz más común del rendimiento lento del enrutamiento suele ser middleware personalizado con un comportamiento incorrecto.

En el ejemplo de código siguiente se muestra una técnica básica para limitar el origen del retraso:

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.");

Para controlar el tiempo del enrutamiento:

  • Intercale cada middleware con una copia del middleware de tiempo que se muestra en el código anterior.
  • Agregue un identificador único para poner en correlación los datos de control de tiempo con el código.

Se trata de una forma básica de reducir el retraso cuando es significativo, por ejemplo, de más de 10ms. Al restar Time 2 de Time 1 se notifica el tiempo invertido dentro del middleware UseRouting.

En el código siguiente se usa un enfoque más compacto del código de control de tiempo anterior:

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.");

Características de enrutamiento potencialmente costosas

En la lista siguiente se proporciona información sobre las características de enrutamiento que son relativamente costosas en comparación con las plantillas de ruta básicas:

  • Expresiones regulares: Se pueden escribir expresiones regulares que sean complejas o que tengan un tiempo de ejecución de larga duración con una pequeña cantidad de entradas.
  • Segmentos complejos ({x}-{y}-{z}):
    • Son mucho más costosos que analizar un segmento de ruta de dirección URL convencional.
    • El resultado es que se asignan muchas más subcadenas.
  • Acceso a datos sincrónicos: muchas aplicaciones complejas tienen acceso a bases de datos como parte de su enrutamiento. Los puntos de extensibilidad como MatcherPolicy y EndpointSelectorContext son asincrónicos.

Guía para tablas de enrutamiento de gran tamaño

De forma predeterminada, ASP.NET Core utiliza un algoritmo de enrutamiento que compara la memoria con el tiempo de CPU. Esto genera un buen resultado por el hecho de que el tiempo de coincidencia de enrutamiento solo depende de la longitud de la ruta de acceso con la que debe coincidir y no del número de rutas. Sin embargo, este enfoque puede ser potencialmente problemático en algunos casos, cuando la aplicación tiene un gran número de rutas (miles) y hay una gran cantidad de prefijos de variable en las rutas. Por ejemplo, si las rutas tienen parámetros en los primeros segmentos de la ruta, como {parameter}/some/literal.

Es poco probable que una aplicación se ejecute en una situación en la que esto sea un problema, a menos que:

  • Haya un gran número de rutas en la aplicación que usen este patrón.
  • Haya un gran número de rutas en la aplicación.

Forma de determinar si una aplicación se está ejecutando con el problema de tablas de enrutamiento de gran tamaño

  • Hay dos síntomas que buscar:
    • La aplicación tarda en iniciarse en la primera solicitud.
      • Tenga en cuenta que esto es necesario, pero no suficiente. Hay muchos otros problemas no relacionados con el enrutamiento que pueden provocar que la aplicación se inicie lentamente. Compruebe la condición siguiente para determinar con precisión que la aplicación se está ejecutando en esta situación.
    • La aplicación consume mucha memoria durante el inicio y un volcado de memoria muestra un gran número de instancias de Microsoft.AspNetCore.Routing.Matching.DfaNode.

Forma de solucionar este problema

Hay varias técnicas y optimizaciones que se pueden aplicar a las rutas que mejorarán en gran medida este escenario:

  • Aplique restricciones de ruta a los parámetros, por ejemplo, {parameter:int}, {parameter:guid}, {parameter:regex(\\d+)}, etc., siempre que sea posible.
    • Esto permite que el algoritmo de enrutamiento optimice internamente las estructuras usadas para buscar coincidencias y reducir drásticamente la memoria usada.
    • En la gran mayoría de los casos, esto será suficiente para volver a un comportamiento aceptable.
  • Cambie las rutas para mover parámetros a segmentos posteriores de la plantilla.
    • Esto reduce el número de posibles "rutas de acceso" para que coincidan con un punto de conexión en una ruta de acceso determinada.
  • Use una ruta dinámica y realice la asignación a un controlador o página dinámicamente.
    • Esto puede realizarse mediante MapDynamicControllerRoute y MapDynamicPageRoute.

Instrucciones para los autores de bibliotecas

Esta sección contiene instrucciones para los autores de bibliotecas que realizan la compilación sobre el enrutamiento. Estos detalles están diseñados para garantizar que los desarrolladores de aplicaciones tengan una buena experiencia en el uso de bibliotecas y marcos que amplían el enrutamiento.

Definición de puntos de conexión

Para crear un marco que use el enrutamiento para la coincidencia de direcciones URL, empiece por definir una experiencia de usuario que se base en UseEndpoints.

REALICE la compilación sobre IEndpointRouteBuilder. Esto permite a los usuarios crear el marco de trabajo con otras características de ASP.NET Core sin confusión. Todas las plantillas de ASP.NET Core incluyen el enrutamiento. Asuma que el enrutamiento está presente y es familiar para los usuarios.

// Your framework
app.MapMyFramework(...);

app.MapHealthChecks("/healthz");

DEVUELVA un tipo concreto sellado a partir de una llamada a MapMyFramework(...) que implemente IEndpointConventionBuilder. La mayoría de los métodos Map... del marco siguen este patrón. La interfaz IEndpointConventionBuilder:

  • Permite la composición de metadatos.
  • Es el destino de diversos métodos de extensión.

La declaración de un tipo propio permite agregar funcionalidad específica del marco propia al generador. Es correcto encapsular un generador declarado por el marco y reenviarle llamadas.

// Your framework
app.MapMyFramework(...)
    .RequireAuthorization()
    .WithMyFrameworkFeature(awesome: true);

app.MapHealthChecks("/healthz");

CONSIDERE LA POSIBILIDAD de escribir un objeto EndpointDataSource propio. EndpointDataSource es la primitiva de bajo nivel para declarar y actualizar una colección de puntos de conexión. EndpointDataSource es una API eficaz que usan los controladores y Razor Pages.

Las pruebas de enrutamiento tienen un ejemplo básico de un origen de datos que no es de actualización.

CONSIDERE la posibilidad de implementar GetGroupedEndpoints. Esto proporciona control completo sobre la ejecución de convenciones de grupo y los metadatos finales en los puntos de conexión agrupados. Por ejemplo, esto permite que las implementaciones personalizadas de EndpointDataSource ejecuten filtros de punto de conexión agregados a grupos.

NO intente registrar un objeto EndpointDataSource de forma predeterminada. Exija a los usuarios que registren el marco en UseEndpoints. La filosofía del enrutamiento es que nada se incluye de forma predeterminada y que UseEndpoints es el lugar donde se registran los puntos de conexión.

Creación de middleware con enrutamiento integrada

CONSIDERE LA POSIBILIDAD de definir tipos de metadatos como una interfaz.

PERMITA el uso de los tipos de metadatos como atributo en clases y métodos.

public interface ICoolMetadata
{
    bool IsCool { get; }
}

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class CoolMetadataAttribute : Attribute, ICoolMetadata
{
    public bool IsCool => true;
}

Los marcos como los controladores y Razor Pages admiten la aplicación de atributos de metadatos a tipos y métodos. Si declara tipos de metadatos:

  • Haga que sean accesibles como atributos.
  • La mayoría de los usuarios están familiarizados con la aplicación de atributos.

La declaración de un tipo de metadatos como una interfaz agrega otro nivel de flexibilidad:

  • Las interfaces admiten composición.
  • Los desarrolladores pueden declarar tipos propios que combinen varias directivas.

PERMITA que los metadatos se puedan invalidar, como se muestra en el ejemplo siguiente:

[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() { }
}

La mejor manera de seguir estas instrucciones es evitar la definición de metadatos de marcador:

  • No busque solo la presencia de un tipo de metadatos.
  • Defina una propiedad en los metadatos y compruébela.

La colección de metadatos está ordenada y admite la invalidación por prioridad. En el caso de los controladores, los metadatos del método de acción son más específicos.

PERMITA que el middleware sea útil con y sin el enrutamiento:

app.UseAuthorization(new AuthorizationPolicy() { ... });

// Your framework
app.MapMyFramework(...).RequireAuthorization();

Como ejemplo de esta instrucción, considere la posibilidad de usar el middleware UseAuthorization. El middleware de autorización permite pasar una directiva de reserva. La directiva de reserva, si se especifica, se aplica a:

  • Puntos de conexión sin una directiva especificada.
  • Solicitudes que no coinciden con un punto de conexión.

Esto hace que el middleware de autorización sea útil fuera del contexto del enrutamiento. El middleware de autorización se puede usar para la programación de middleware tradicional.

Diagnóstico de depuración

Para ver la salida detallada del diagnóstico de cálculo de ruta, establezca Logging:LogLevel:Microsoft en Debug. En el entorno de desarrollo, establezca el nivel de registro en appsettings.Development.json:

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

Recursos adicionales

El enrutamiento es responsable de hacer coincidir las solicitudes HTTP entrantes y de enviarlas a los puntos de conexión ejecutables de la aplicación. Los puntos de conexión son las unidades de código de control de solicitudes ejecutable de la aplicación. Se definen en la aplicación y se configuran al iniciarla. El proceso de búsqueda de coincidencias de puntos de conexión puede extraer valores de la dirección URL de la solicitud y proporcionarlos para el procesamiento de la solicitud. Con la información de los puntos de conexión de la aplicación, el enrutamiento también puede generar direcciones URL que se asignan a los puntos de conexión.

Las aplicaciones pueden configurar el enrutamiento mediante:

  • Controladores
  • Razor Pages
  • SignalR
  • Servicios gRPC
  • Middleware habilitado para puntos de conexión, como las comprobaciones de estado.
  • Delegados y expresiones lambda registrados con el enrutamiento.

En este artículo se describen los detalles de bajo nivel del enrutamiento de ASP.NET Core. Para obtener información sobre la configuración del enrutamiento:

Fundamentos del enrutamiento

En el código siguiente se muestra un ejemplo básico de enrutamiento:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();

En el ejemplo anterior se incluye un único punto de conexión que usa el método MapGet:

  • Al enviar una solicitud HTTP GET a la dirección URL raíz /:
    • Se ejecuta el delegado de la solicitud.
    • Se escribe Hello World! en la respuesta HTTP.
  • Si el método de solicitud no es GET o la dirección URL raíz no es /, no se detecta ninguna ruta y se devuelve HTTP 404.

El enrutamiento usa un par de middleware, registrado por UseRouting y UseEndpoints:

  • UseRouting agrega coincidencia de rutas a la canalización de middleware. Este middleware examina el conjunto de puntos de conexión definidos en la aplicación y selecciona la mejor coincidencia en función de la solicitud.
  • UseEndpoints agrega la ejecución del punto de conexión a la canalización de middleware. Ejecuta el delegado asociado al punto de conexión seleccionado.

Normalmente, las aplicaciones no necesitan llamar a UseRouting ni a UseEndpoints. WebApplicationBuilder configura una canalización de middleware que encapsula el middleware agregado en Program.cs con UseRouting y UseEndpoints. Sin embargo, las aplicaciones pueden cambiar el orden en que se ejecutan UseRouting y UseEndpoints llamando a estos métodos explícitamente. Por ejemplo, el código siguiente realiza una llamada explícita a UseRouting:

app.Use(async (context, next) =>
{
    // ...
    await next(context);
});

app.UseRouting();

app.MapGet("/", () => "Hello World!");

En el código anterior:

  • La llamada a app.Use registra un middleware personalizado que se ejecuta al principio de la canalización.
  • La llamada a UseRouting configura el middleware de coincidencia de rutas para que se ejecute después del middleware personalizado.
  • El punto de conexión registrado con MapGet se ejecuta al final de la canalización.

Si el ejemplo anterior no incluyese una llamada a UseRouting, el middleware personalizado se ejecutaría después del middleware de coincidencia de rutas.

Puntos de conexión

El método MapGet se usa para definir un punto de conexión. Un punto de conexión es algo que se puede:

  • Seleccionar, si se hacen coincidir la dirección URL y el método HTTP.
  • Ejecutar, mediante la ejecución del delegado.

Los puntos de conexión que la aplicación puede ejecutar y hacer coincidir se configuran en UseEndpoints. Por ejemplo, MapGet, MapPost y métodos similares conectan delegados de solicitud al sistema de enrutamiento. Se pueden usar métodos adicionales para conectar características del marco ASP.NET Core al sistema de enrutamiento:

En el ejemplo siguiente se muestra el enrutamiento con una plantilla de ruta más sofisticada:

app.MapGet("/hello/{name:alpha}", (string name) => $"Hello {name}!");

La cadena /hello/{name:alpha} es una plantilla de ruta. Se usa una plantilla de ruta para configurar la coincidencia del punto de conexión. En este caso, la plantilla coincide con:

  • Una dirección URL como /hello/Docs.
  • Cualquier ruta de dirección URL que comience por /hello/, seguido de una secuencia de caracteres alfabéticos. :alpha aplica una restricción de ruta que solo coincide con caracteres alfabéticos. Las restricciones de ruta se explican más adelante en este artículo.

El segundo segmento de la ruta de dirección URL, {name:alpha}:

En el ejemplo siguiente se muestra el enrutamiento con comprobaciones de estado y autorización:

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

app.MapHealthChecks("/healthz").RequireAuthorization();
app.MapGet("/", () => "Hello World!");

En el ejemplo anterior se muestra cómo:

  • El middleware de autorización se puede usar con el enrutamiento.
  • Los puntos de conexión se pueden usar para configurar el comportamiento de la autorización.

La llamada a MapHealthChecks agrega un punto de conexión de comprobación de estado. Al encadenar RequireAuthorization a esta llamada, se adjunta una directiva de autorización al punto de conexión.

La llamada a UseAuthentication y UseAuthorization agrega el middleware de autenticación y autorización. Estos middleware se colocan entre UseRouting y UseEndpoints para que puedan:

  • Vea qué punto de conexión ha seleccionado UseRouting.
  • Aplique una directiva de autorización antes de que UseEndpoints envíe al punto de conexión.

Metadatos de punto de conexión

En el ejemplo anterior, hay dos puntos de conexión, pero solo el de comprobación de estado tiene una directiva de autorización adjunta. Si la solicitud coincide con el punto de conexión de comprobación de estado, /healthz, se realiza una comprobación de autorización. Esto demuestra que los puntos de conexión pueden tener datos adicionales adjuntos. Estos datos adicionales se denominan metadatos de punto de conexión:

  • Los metadatos pueden ser procesados mediante middleware compatible con el enrutamiento.
  • Los metadatos pueden ser de cualquier tipo de .NET.

Conceptos de enrutamiento

El sistema de enrutamiento se basa en la canalización de middleware mediante la adición del eficaz concepto de punto de conexión. Los puntos de conexión representan unidades de la funcionalidad de la aplicación que son diferentes entre sí en cuanto al enrutamiento, la autorización y cualquier número de sistemas de ASP.NET Core.

Definición de punto de conexión de ASP.NET Core

Un punto de conexión de ASP.NET Core es:

En el código siguiente se muestra cómo recuperar e inspeccionar el punto de conexión que coincide con la solicitud actual:

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.");

El punto de conexión, si se selecciona, se puede recuperar de HttpContext. Se pueden inspeccionar sus propiedades. Los objetos de punto de conexión son inmutables y no se pueden modificar después de crearlos. El tipo más común de punto de conexión es RouteEndpoint. RouteEndpoint incluye información que permite que el sistema de enrutamiento lo seleccione.

En el código anterior, app.Use configura un middleware insertado.

En el código siguiente se muestra que, en función de dónde se llame a app.Use en la canalización, es posible que no haya un punto de conexión:

// 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);
});

En el ejemplo anterior se agregan instrucciones Console.WriteLine que muestran si se ha seleccionado un punto de conexión o no. Para mayor claridad, en el ejemplo se asigna un nombre para mostrar al punto de conexión / proporcionado.

El ejemplo anterior también incluye llamadas a UseRouting y UseEndpoints para controlar exactamente cuándo se ejecuta este middleware dentro de la canalización.

Al ejecutar este código con una dirección URL de / se muestra lo siguiente:

1. Endpoint: (null)
2. Endpoint: Hello
3. Endpoint: Hello

Al ejecutar este código con otra dirección URL se muestra lo siguiente:

1. Endpoint: (null)
2. Endpoint: (null)
4. Endpoint: (null)

Este resultado muestra que:

  • El punto de conexión siempre es NULL antes de que se llame a UseRouting.
  • Si se encuentra una coincidencia, el extremo no es NULL entre UseRouting y UseEndpoints.
  • El middleware UseEndpoints es terminal cuando se encuentra una coincidencia. El middleware de terminal se define más adelante en este artículo.
  • El middleware después de UseEndpoints solo se ejecuta cuando no se encuentra ninguna coincidencia.

El middleware UseRouting usa el método SetEndpoint para asociar el punto de conexión al contexto actual. Se puede reemplazar el middleware UseRouting con lógica personalizada y seguir aprovechando las ventajas del uso de puntos de conexión. Los puntos de conexión son una primitiva de bajo nivel como middleware y no están unidos a la implementación de enrutamiento. La mayoría de las aplicaciones no necesitan reemplazar UseRouting por lógica personalizada.

El middleware UseEndpoints está diseñado para usarse junto con el middleware UseRouting. La lógica básica para ejecutar un punto de conexión no es complicada. Use GetEndpoint para recuperar el punto de conexión y, después, invoque su propiedad RequestDelegate.

En el código siguiente se muestra cómo el middleware puede influir en el enrutamiento o reaccionar ante este:

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 { }

En el ejemplo anterior se muestran dos conceptos importantes:

  • El middleware se puede ejecutar antes de UseRouting para modificar los datos sobre los que funciona el enrutamiento.
  • El middleware se puede ejecutar entre UseRouting y UseEndpoints para procesar los resultados del enrutamiento antes de que se ejecute el punto de conexión.
    • El middleware que se ejecuta entre UseRouting y UseEndpoints:
      • Normalmente inspecciona los metadatos para entender los puntos de conexión.
      • A menudo toma decisiones de seguridad, como UseAuthorization y UseCors.
    • La combinación de middleware y metadatos permite configurar directivas por punto de conexión.

En el código anterior se muestra un ejemplo de middleware personalizado que admite directivas por punto de conexión. El middleware escribe un registro de auditoría de acceso a datos confidenciales en la consola. El middleware se puede configurar para auditar un punto de conexión con los metadatos de RequiresAuditAttribute. En este ejemplo se muestra un patrón opcional en el que solo se auditan los puntos de conexión marcados como confidenciales. Esta lógica se puede definir en orden inverso, para auditar todo lo que no esté marcado como seguro, por ejemplo. El sistema de metadatos de punto de conexión es flexible. Esta lógica se puede diseñar de la manera que mejor se adapte al caso de uso.

El código del ejemplo anterior está diseñado para mostrar los conceptos básicos de los puntos de conexión. No está pensado para su uso en producción. Una versión más completa de un middleware de registro de auditoría:

  • Realizaría el registro en un archivo o una base de datos.
  • Incluiría detalles como el usuario, la dirección IP, el nombre del punto de conexión confidencial, etc.

El valor RequiresAuditAttribute de metadatos de directiva de auditoría se define como Attribute para facilitar su uso con marcos basados en clases como los controladores y SignalR. Cuando se usa de ruta a código:

  • Los metadatos se asocian con una API de generador.
  • Los marcos basados en clases incluyen todos los atributos en el método y la clase correspondientes al crear los puntos de conexión.

Los procedimientos recomendados para los tipos de metadatos son definirlos como interfaces o atributos. Las interfaces y los atributos permiten la reutilización del código. El sistema de metadatos es flexible y no impone ninguna limitación.

Comparación del middleware de terminal con el enrutamiento

En el ejemplo siguiente se muestra el middleware de terminal y el enrutamiento:

// 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.");

El estilo de middleware que se muestra con Approach 1: es middleware de terminal. Se denomina middleware de terminal porque realiza una operación de búsqueda de coincidencias:

  • La operación de búsqueda de coincidencias en el ejemplo anterior es Path == "/" para el middleware y Path == "/Routing" para el enrutamiento.
  • Cuando una coincidencia es correcta, ejecuta alguna funcionalidad y devuelve un valor, en lugar de invocar el middleware next.

Se denomina middleware de terminal porque finaliza la búsqueda, ejecuta alguna funcionalidad y, después, devuelve un valor.

En la lista siguiente se compara el middleware de terminal con el enrutamiento:

  • Los dos enfoques permiten terminar la canalización de procesamiento:
    • El middleware finaliza la canalización mediante la devolución de un valor en lugar de invocar next.
    • Los puntos de conexión siempre son de terminal.
  • El middleware de terminal permite colocar el middleware en un lugar arbitrario de la canalización:
    • Los puntos de conexión se ejecutan en la posición de UseEndpoints.
  • El middleware de terminal permite que el código arbitrario determine cuándo coincide el middleware:
    • El código personalizado de búsqueda de coincidencia de rutas puede ser detallado y difícil de escribir correctamente.
    • El enrutamiento proporciona soluciones sencillas para las aplicaciones típicas. La mayoría de las aplicaciones no requieren código personalizado de búsqueda de coincidencia de rutas.
  • Los puntos de conexión interactúan con middleware como UseAuthorization y UseCors.
    • Para usar un middleware de terminal con UseAuthorization o UseCors se necesita interactuar de forma manual con el sistema de autorización.

Un punto de conexión define:

  • Un delegado para procesar solicitudes.
  • Una colección de metadatos arbitrarios. Los metadatos se usan para implementar cuestiones transversales según las directivas y la configuración asociada a cada punto de conexión.

El middleware de terminal puede ser una herramienta eficaz, pero puede requerir:

  • Una cantidad significativa de código y pruebas.
  • La integración manual con otros sistemas para lograr el nivel deseado de flexibilidad.

Considere la posibilidad de realizar la integración con el enrutamiento antes de escribir middleware de terminal.

El middleware de terminal existente que se integra con Map o MapWhen normalmente se puede convertir en un punto de conexión compatible con el enrutamiento. MapHealthChecks muestra el patrón para enrutadores:

  • Escriba un método de extensión en IEndpointRouteBuilder.
  • Cree una canalización de middleware anidada mediante CreateApplicationBuilder.
  • Adjunte el middleware a la nueva canalización. En este caso, UseHealthChecks.
  • Aplique Build a la canalización de middleware en un objeto RequestDelegate.
  • Llame a Map y proporcione la nueva canalización de middleware.
  • Devuelva el objeto de generador proporcionado por Map desde el método de extensión.

En el código siguiente se muestra el uso de MapHealthChecks:

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

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

En el ejemplo anterior se muestra la importancia de devolver el objeto de generador. Al devolver el objeto de generador, el desarrollador de aplicaciones puede configurar directivas como la autorización para el punto de conexión. En este ejemplo, el middleware de comprobaciones de estado no tiene una integración directa con el sistema de autorización.

El sistema de metadatos se ha creado como respuesta a los problemas detectados por los autores de extensibilidad mediante el middleware de terminal. El problema de cada middleware es implementar su propia integración con el sistema de autorización.

Coincidencia de dirección URL

  • Es el proceso por el cual el enrutamiento hace coincidir una solicitud entrante a un punto de conexión.
  • Se basa en los datos de la ruta de acceso y los encabezados de la dirección URL.
  • Se puede extender para tener en cuenta los datos de la solicitud.

Cuando se ejecuta un middleware de enrutamiento, se establece un objeto Endpoint y se enrutan los valores a una característica de solicitud en el objeto HttpContext desde la solicitud actual:

  • La llamada a HttpContext.GetEndpoint obtiene el punto de conexión.
  • HttpRequest.RouteValues obtiene la colección de valores de ruta.

El middleware que se ejecuta después del middleware de enrutamiento puede inspeccionar el punto de conexión y tomar medidas. Por ejemplo, un middleware de autorización puede consultar la colección de metadatos del punto de conexión de una directiva de autorización. Después de que se ejecuta todo el middleware en la canalización de procesamiento de solicitudes, se invoca al delegado del punto de conexión seleccionado.

El sistema de enrutamiento en el enrutamiento de punto de conexión es responsable de todas las decisiones relativas al envío. Como el middleware aplica directivas en función del punto de conexión seleccionado, es importante que:

  • Cualquier decisión que pueda afectar al envío o a la aplicación de directivas de seguridad se realice dentro del sistema de enrutamiento.

Advertencia

En cuanto a la compatibilidad con versiones anteriores, cuando se ejecuta un delegado del punto de conexión de controlador o Razor Pages, las propiedades de RouteContext.RouteData se establecen en los valores adecuados en función del procesamiento de solicitudes realizado hasta el momento.

El tipo RouteContext se marcará como obsoleto en una versión futura:

  • Migre RouteData.Values a HttpRequest.RouteValues.
  • Migre RouteData.DataTokens para recuperar IDataTokensMetadata de los metadatos del punto de conexión.

La coincidencia de direcciones URL funciona en un conjunto configurable de fases. En cada fase, la salida es un conjunto de coincidencias. El conjunto de coincidencias se puede reducir más en la fase siguiente. La implementación de enrutamiento no garantiza un orden de procesamiento para los puntos de conexión coincidentes. Todas las coincidencias posibles se procesan a la vez. Las fases de coincidencia de direcciones URL se producen en el orden siguiente. ASP.NET Core:

  1. Procesa la ruta de dirección URL con el conjunto de puntos de conexión y sus plantillas de ruta, y se recopilan todas las coincidencias.
  2. Toma la lista anterior y quita las coincidencias en las que se produce un error con restricciones de ruta aplicadas.
  3. Toma la lista anterior y quita las coincidencias en las que se produce un error en el conjunto de instancias de MatcherPolicy.
  4. Usa EndpointSelector para tomar una decisión final de la lista anterior.

La lista de puntos de conexión se prioriza según:

Todos los puntos de conexión coincidentes se procesan en cada fase hasta que se alcanza EndpointSelector. EndpointSelector es la fase final. Elige el punto de conexión de prioridad más alta entre las coincidencias como la mejor coincidencia. Si hay otras coincidencias con la misma prioridad que la mejor, se inicia una excepción de coincidencia ambigua.

La prioridad de ruta se calcula en función de una plantilla de ruta más específica a la que se le asigna una prioridad más alta. Por ejemplo, considere las plantillas /hello y /{message}:

  • Las dos coinciden con la ruta de dirección URL /hello.
  • /hello es más específica y, por tanto, tiene mayor prioridad.

Por lo general, la precedencia de rutas realiza un buen trabajo de elegir la mejor coincidencia para los tipos de esquemas de dirección URL que se usan en la práctica. Use Order solo cuando sea necesario para evitar una ambigüedad.

Debido a los tipos de extensibilidad que proporciona el enrutamiento, el sistema de enrutamiento no puede calcular las rutas ambiguas por adelantado. Considere un ejemplo como las plantillas de ruta /{message:alpha} y /{message:int}:

  • La restricción alpha solo coincide con caracteres alfabéticos.
  • La restricción int solo coincide con números.
  • Estas plantillas tienen la misma prioridad de ruta, pero no hay ninguna dirección URL única con la que coincidan.
  • Si el sistema de enrutamiento ha notificado un error de ambigüedad al iniciarse, bloquearía este caso de uso válido.

Advertencia

El orden de las operaciones dentro de UseEndpoints no influye en el comportamiento del enrutamiento, con una excepción. MapControllerRoute y MapAreaRoute asignan de forma automática un valor de orden a sus puntos de conexión en función del orden en el que se hayan invocado. Esto simula el comportamiento a largo plazo de los controladores sin que el sistema de enrutamiento proporcione las mismas garantías que las implementaciones de enrutamiento anteriores.

Enrutamiento de puntos de conexión en ASP.NET Core:

  • No tiene el concepto de rutas.
  • No proporciona garantías de ordenación. Todos los puntos de conexión se procesan a la vez.

Prioridad de la plantilla de ruta y orden de selección de los puntos de conexión

La prioridad de la plantilla de ruta es un sistema que asigna a cada plantilla de ruta un valor en función de su especificidad. Precedencia de la plantilla de ruta:

  • Evita la necesidad de ajustar el orden de los puntos de conexión en casos comunes.
  • Intenta hacer coincidir las expectativas comunes del comportamiento del enrutamiento.

Por ejemplo, considere las plantillas /Products/List y /Products/{id}. Sería razonable suponer que /Products/List es una mejor coincidencia que /Products/{id} para la ruta de dirección URL /Products/List. Funciona porque el segmento literal /List se considera que tiene una mayor prioridad que el segmento de parámetro /{id}.

Los detalles de cómo funciona la precedencia están vinculados a cómo se definen las plantillas de ruta:

  • Las plantillas con más segmentos se consideran más específicas.
  • Un segmento con texto literal se considera más específico que un segmento de parámetro.
  • Un segmento de parámetro con una restricción se considera más específico que uno que no la tenga.
  • Un segmento complejo se considera igual de específico que un segmento de parámetro con una restricción.
  • Los parámetros comodín son los menos específicos. Vea comodín en la sección Plantillas de ruta para obtener información importante sobre las rutas comodín.

Conceptos de generación de direcciones URL

Generación de direcciones URL:

  • Es el proceso por el cual el enrutamiento puede crear una ruta de dirección URL en función de un conjunto de valores de ruta.
  • Permite una separación lógica entre los puntos de conexión y las direcciones URL que acceden a ellos.

El enrutamiento de punto de conexión incluye la API LinkGenerator. LinkGenerator es un servicio singleton disponible desde la DI. La API LinkGenerator se puede usar fuera del contexto de una solicitud en ejecución. Mvc.IUrlHelper y los escenarios que dependen de IUrlHelper, como los asistentes de etiquetas, los de HTML y los resultados de acción, usan de forma interna la API LinkGenerator para proporcionar funciones de generación de vínculos.

El generador de vínculos está respaldado por el concepto de una dirección y esquemas de direcciones. Un esquema de direcciones es una manera de determinar los puntos de conexión que se deben tener en cuenta para la generación de vínculos. Por ejemplo, los escenarios de nombre y valores de ruta de controladores y Razor Pages con los que muchos usuarios están familiarizados se implementan como un esquema de direcciones.

El generador de vínculos puede vincular a controladores y Razor Pages a través de los métodos de extensión siguientes:

Las sobrecargas de estos métodos aceptan argumentos que incluyan HttpContext. Estos métodos son funcionalmente equivalentes a Url.Action y Url.Page, pero ofrecen flexibilidad y opciones adicionales.

Los métodos GetPath* son más similares a Url.Action y Url.Page, dado que generan un URI que contiene una ruta de acceso absoluta. Los métodos GetUri* siempre generan un URI absoluto que contiene un esquema y un host. Los métodos que aceptan HttpContext generan un URI en el contexto de la solicitud que se ejecuta. A menos que se reemplacen, se usan los valores de ruta de ambiente, la ruta de acceso base de la dirección URL, el esquema y el host de la solicitud en ejecución.

Se llama a LinkGenerator con una dirección. La generación de un URI se produce en dos pasos:

  1. Se enlaza una dirección a una lista de puntos de conexión que coincidan con la dirección.
  2. Se evalúa el elemento RoutePattern de cada punto de conexión hasta que se encuentra un patrón de ruta que coincida con los valores proporcionados. La salida resultante se combina con otras partes del URI proporcionadas al generador de vínculos y devueltas.

Los métodos proporcionados por LinkGenerator admiten funciones estándar de generación de vínculos para cualquier tipo de dirección. La forma más práctica de usar el generador de vínculos es a través de métodos de extensión que realicen operaciones para un tipo de dirección específica:

Método de extensión Descripción
GetPathByAddress Genera un URI con una ruta de acceso absoluta en función de los valores proporcionados.
GetUriByAddress Genera un URI absoluto en función de los valores proporcionados.

Advertencia

Preste atención a las consecuencias siguientes de llamar a los métodos LinkGenerator:

  • Use los métodos de extensión GetUri* con precaución en una configuración de aplicación en la que no se valide el encabezado Host de las solicitudes entrantes. Si no se valida el encabezado Host de las solicitudes entrantes, la entrada de la solicitud que no sea de confianza se puede devolver al cliente en los URI de una página o vista. Se recomienda que todas las aplicaciones de producción configuren su servidor para validar el encabezado Host en función de valores válidos conocidos.

  • Use LinkGenerator con precaución en el middleware junto con Map o MapWhen. Map* cambia la ruta de acceso base de la solicitud que se ejecuta, lo que afecta a la salida de la generación de vínculos. Todas las API de LinkGenerator permiten especificar una ruta de acceso base. Especifique una ruta de acceso base vacía para deshacer el efecto de Map* en la generación de vínculos.

Ejemplo de middleware

En el ejemplo siguiente, un middleware usa la API LinkGenerator para crear un vínculo a un método de acción que enumera los productos de la tienda. El uso del generador de vínculos mediante su inserción en una clase y la llamada a GenerateLink está disponible para cualquier clase de una aplicación:

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

Plantillas de ruta

Los tokens de {} definen parámetros de ruta que se enlazan si se encuentran coincidencias con la ruta. Se puede definir más de un parámetro de ruta en un segmento de ruta, pero deben estar separados por un valor literal. Por ejemplo:

{controller=Home}{action=Index}

No es una ruta válida, ya que no hay ningún valor literal entre {controller} y {action}. Los parámetros de ruta deben tener un nombre y, opcionalmente, atributos adicionales especificados.

El texto literal diferente de los parámetros de ruta (por ejemplo, {id}) y el separador de ruta / deben coincidir con el texto de la dirección URL. La coincidencia de texto no distingue mayúsculas de minúsculas y se basa en la representación descodificada de la ruta de las direcciones URL. Para que el delimitador de parámetro de ruta literal { o } coincida, repita el carácter para aplicar escape al carácter. Por ejemplo, {{ o }}.

Asterisco * o asterisco doble **:

  • Se puede usar como prefijo de un parámetro de ruta para enlazar con el resto del URI.
  • Se denominan parámetros comodín. Por ejemplo, blog/{**slug}:
    • Coincide con cualquier URI que empiece por blog/ y después tenga cualquier valor.
    • El valor que aparece detrás de blog/ se asigna al valor de ruta slug.

Advertencia

Un parámetro catch-all puede relacionar rutas de forma incorrecta debido a un error en el enrutamiento. Las aplicaciones afectadas por este error tienen las características siguientes:

  • Una ruta catch-all (por ejemplo, {**slug}")
  • La ruta catch-all causa un error al relacionar solicitudes que sí que debería relacionar.
  • Al quitar otras rutas, la ruta catch-all empieza a funcionar.

Para ver casos de ejemplo relacionados con este error, consulte los errores 18677 y 16579 en GitHub.

Se incluye una corrección de participación para este error en el SDK de .NET Core 3.1.301 y versiones posteriores. En el código que hay a continuación se establece un cambio interno que corrige este error:

public static void Main(string[] args)
{
   AppContext.SetSwitch("Microsoft.AspNetCore.Routing.UseCorrectCatchAllBehavior", 
                         true);
   CreateHostBuilder(args).Build().Run();
}
// Remaining code removed for brevity.

Los parámetros comodín también pueden coincidir con una cadena vacía.

El parámetro comodín inserta los caracteres de escape correspondientes cuando se usa la ruta para generar una dirección URL, incluidos los caracteres / de separación de ruta de acceso. Por ejemplo, la ruta foo/{*path} con valores de ruta { path = "my/path" } genera foo/my%2Fpath. Tenga en cuenta la barra diagonal de escape. Para los caracteres separadores de ruta de acceso de ida y vuelta, use el prefijo de parámetro de ruta **. La ruta foo/{**path} con { path = "my/path" } genera foo/my/path.

Los patrones de dirección URL que intentan capturar un nombre de archivo con una extensión de archivo opcional tienen consideraciones adicionales. Por ejemplo, considere la plantilla files/{filename}.{ext?}. Cuando existen valores para filename y ext, los dos valores se rellenan. Si solo existe un valor para filename en la dirección URL, la ruta coincide porque el carácter . final es opcional. Las direcciones URL siguientes coinciden con esta ruta:

  • /files/myFile.txt
  • /files/myFile

Los parámetros de ruta pueden tener valores predeterminados designados mediante la especificación del valor predeterminado después del nombre de parámetro, separado por un signo igual (=). Por ejemplo, {controller=Home} define Home como el valor predeterminado de controller. El valor predeterminado se usa si no hay ningún valor en la dirección URL para el parámetro. Los parámetros de ruta se pueden convertir en opcionales si se anexa un signo de interrogación (?) al final del nombre del parámetro. Por ejemplo: id?. La diferencia entre los valores opcionales y los parámetros de ruta predeterminados es:

  • Un parámetro de ruta con un valor predeterminado siempre produce un valor.
  • Un parámetro opcional solo tiene un valor cuando la dirección URL de la solicitud proporciona un valor.

Los parámetros de ruta pueden tener restricciones que deben coincidir con el valor de ruta enlazado desde la dirección URL. Al agregar : y un nombre de restricción después del nombre del parámetro de ruta, se especifica una restricción insertada en un parámetro de ruta. Si la restricción requiere argumentos, se incluyen entre paréntesis (...) después del nombre de restricción. Se pueden especificar varias restricciones insertadas si se anexa otro carácter : y un nombre de restricción.

El nombre de restricción y los argumentos se pasan al servicio IInlineConstraintResolver para crear una instancia de IRouteConstraint para su uso en el procesamiento de direcciones URL. Por ejemplo, la plantilla de ruta blog/{article:minlength(10)} especifica una restricción minlength con el argumento 10. Para obtener más información sobre las restricciones de ruta y una lista de las restricciones proporcionadas por el marco, vea la sección Restricciones de ruta.

Los parámetros de ruta también pueden tener transformadores de parámetros. Los transformadores de parámetros transforman el valor de un parámetro al generar vínculos y hacer coincidir acciones y páginas con direcciones URL. Como sucede con las restricciones, los transformadores de parámetros se pueden agregar en línea a un parámetro de ruta mediante la incorporación un carácter : y un nombre de transformador después del nombre del parámetro de ruta. Por ejemplo, la plantilla de ruta blog/{article:slugify} especifica un transformador slugify. Para obtener más información sobre los transformadores de parámetros, vea la sección Transformadores de parámetros.

En la tabla siguiente se muestran plantillas de ruta de ejemplo y su comportamiento:

Plantilla de ruta URI coincidente de ejemplo El URI de la solicitud...
hello /hello Solo coincide con la ruta de acceso única /hello.
{Page=Home} / Coincide y establece Page en Home.
{Page=Home} /Contact Coincide y establece Page en Contact.
{controller}/{action}/{id?} /Products/List Se asigna al controlador Products y la acción List.
{controller}/{action}/{id?} /Products/Details/123 Se asigna al controlador Products y la acción Details con id establecido en 123.
{controller=Home}/{action=Index}/{id?} / Se asigna al controlador Home y al método Index. id se pasa por alto.
{controller=Home}/{action=Index}/{id?} /Products Se asigna al controlador Products y al método Index. id se pasa por alto.

El uso de una plantilla suele ser el método de enrutamiento más sencillo. Las restricciones y los valores predeterminados también se pueden especificar fuera de la plantilla de ruta.

Segmentos complejos

Los segmentos complejos se procesan mediante la búsqueda de coincidencias de delimitadores literales de derecha a izquierda de un modo no expansivo. Por ejemplo, [Route("/a{b}c{d}")] es un segmento complejo. Los segmentos complejos funcionan de una manera determinada que se debe entender para usarlos correctamente. En el ejemplo de esta sección se muestra por qué los segmentos complejos solo funcionan bien cuando el texto del delimitador no aparece dentro de los valores de los parámetros. En casos más complejos es necesario usar una expresión regular y extraer los valores de forma manual.

Advertencia

Cuando se usa System.Text.RegularExpressions para procesar entradas que no son de confianza, pase un tiempo de expiración. Un usuario malintencionado puede proporcionar entradas a RegularExpressions y provocar un ataque por denegación de servicio. Las API del marco ASP.NET Core en las que se usa RegularExpressions pasan un tiempo de expiración.

Este es un resumen de los pasos que realiza el enrutamiento con la plantilla /a{b}c{d} y la ruta de dirección URL /abcd. | se usa para ayudar a visualizar cómo funciona el algoritmo:

  • El primer literal, de derecha a izquierda, es c. Por tanto, se busca en /abcd desde la derecha y se encuentra /ab|c|d.
  • Todo lo que se encuentra a la derecha (d) coincide ahora con el parámetro de ruta {d}.
  • El siguiente literal, de derecha a izquierda, es a. Por tanto, se busca en /ab|c|d a partir de donde se ha parado antes, después a y se encuentra /|a|b|c|d.
  • El valor situado a la derecha (b) coincide ahora con el parámetro de ruta {b}.
  • No queda ningún texto ni ninguna plantilla de ruta, por lo que se trata de una coincidencia.

Este es un ejemplo de un caso negativo en el que se usa la misma plantilla /a{b}c{d} y la ruta de dirección URL /aabcd. | se usa para ayudar a visualizar cómo funciona el algoritmo. Este caso no es una coincidencia, que se explica mediante el mismo algoritmo:

  • El primer literal, de derecha a izquierda, es c. Por tanto, se busca en /aabcd desde la derecha y se encuentra /aab|c|d.
  • Todo lo que se encuentra a la derecha (d) coincide ahora con el parámetro de ruta {d}.
  • El siguiente literal, de derecha a izquierda, es a. Por tanto, se busca en /aab|c|d a partir de donde se ha parado antes, después a y se encuentra /a|a|b|c|d.
  • El valor situado a la derecha (b) coincide ahora con el parámetro de ruta {b}.
  • En este momento, todavía hay texto a, pero el algoritmo se ha quedado sin plantilla de ruta para analizar, por lo que no es una coincidencia.

Como el algoritmo de búsqueda de coincidencias es no expansivo:

  • Coincide con la menor cantidad de texto posible en cada paso.
  • Cualquier caso en el que el valor de delimitador aparezca dentro de los valores de parámetro provoca que no coincida.

Las expresiones regulares proporcionan un mayor control sobre el comportamiento de búsqueda de coincidencias.

La coincidencia expansiva, también conocida como coincidencia diferida, coincide con la cadena más grande posible. La búsqueda no expansiva coincide con la cadena más pequeña posible.

Enrutamiento con caracteres especiales

El enrutamiento con caracteres especiales puede dar lugar a resultados inesperados. Por ejemplo, considere un controlador con el siguiente método de acción:

[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;
}

Cuando string id contiene los siguientes valores codificados, pueden producirse resultados inesperados:

ASCII Encoded
/ %2F
+

Los parámetros de ruta no siempre están descodificados por URL. Este problema puede abordarse en el futuro. Para obtener más información, vea esta incidencia de GitHub.

Restricciones de ruta

Las restricciones de ruta se ejecutan cuando se ha producido una coincidencia con la dirección URL entrante y la ruta de dirección URL se convierte en tokens en valores de ruta. En general, las restricciones de ruta inspeccionan el valor de ruta asociado a través de la plantilla de ruta y deciden si el valor es aceptable o no. Algunas restricciones de ruta usan datos ajenos al valor de ruta para decidir si la solicitud se puede enrutar. Por ejemplo, HttpMethodRouteConstraint puede aceptar o rechazar una solicitud basada en su verbo HTTP. Las restricciones se usan en las solicitudes de enrutamiento y la generación de vínculos.

Advertencia

No use las restricciones para la validación de entradas. Si se usan restricciones para la validación de entradas, las que no sean válidas generan una respuesta 404 No encontrado. Una entrada no válida debería generar 400 Solicitud incorrecta con un mensaje de error adecuado. Las restricciones de ruta se usan para eliminar la ambigüedad entre rutas similares, no para validar las entradas de una ruta determinada.

En la tabla siguiente se muestran restricciones de ruta de ejemplo y su comportamiento esperado:

restricción Ejemplo Coincidencias de ejemplo Notas
int {id:int} 123456789, -123456789 Coincide con cualquier entero
bool {active:bool} true, FALSE Coincide con true o false. No distingue mayúsculas de minúsculas
datetime {dob:datetime} 2016-12-31, 2016-12-31 7:32pm Coincide con un valor DateTime válido en la referencia cultural invariable. Vea la advertencia anterior.
decimal {price:decimal} 49.99, -1,000.01 Coincide con un valor decimal válido en la referencia cultural invariable. Vea la advertencia anterior.
double {weight:double} 1.234, -1,001.01e8 Coincide con un valor double válido en la referencia cultural invariable. Vea la advertencia anterior.
float {weight:float} 1.234, -1,001.01e8 Coincide con un valor float válido en la referencia cultural invariable. Vea la advertencia anterior.
guid {id:guid} CD2C1638-1638-72D5-1638-DEADBEEF1638 Coincide con un valor Guid válido
long {ticks:long} 123456789, -123456789 Coincide con un valor long válido
minlength(value) {username:minlength(4)} Rick La cadena debe tener al menos cuatro caracteres
maxlength(value) {filename:maxlength(8)} MyFile La cadena no debe tener más de ocho caracteres
length(length) {filename:length(12)} somefile.txt La cadena debe tener una longitud de exactamente 12 caracteres
length(min,max) {filename:length(8,16)} somefile.txt La cadena debe tener una longitud como mínimo de ocho caracteres y como máximo de 16
min(value) {age:min(18)} 19 El valor entero debe ser como mínimo 18
max(value) {age:max(120)} 91 El valor entero debe ser como máximo 120
range(min,max) {age:range(18,120)} 91 El valor entero debe ser como mínimo 18 y máximo 120
alpha {name:alpha} Rick La cadena debe constar de uno o más caracteres alfabéticos, a-z y no distinguir mayúsculas de minúsculas.
regex(expression) {ssn:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)} 123-45-6789 La cadena debe coincidir con la expresión regular. Vea las sugerencias sobre cómo definir una expresión regular.
required {name:required} Rick Se usa para exigir que un valor que no es de parámetro esté presente durante la generación de dirección URL

Advertencia

Cuando se usa System.Text.RegularExpressions para procesar entradas que no son de confianza, pase un tiempo de expiración. Un usuario malintencionado puede proporcionar entradas a RegularExpressions y provocar un ataque por denegación de servicio. Las API del marco ASP.NET Core en las que se usa RegularExpressions pasan un tiempo de expiración.

Es posible aplicar varias restricciones delimitadas por dos puntos a un único parámetro. Por ejemplo, la siguiente restricción permite limitar un parámetro a un valor entero de 1 o superior:

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

Advertencia

Las restricciones de ruta que comprueban la dirección URL y que se convierten en un tipo CLR siempre usan la referencia cultural invariable. Por ejemplo, la conversión al tipo int o DateTime de CLR. Estas restricciones dan por supuesto que la dirección URL no es localizable. Las restricciones de ruta proporcionadas por el marco de trabajo no modifican los valores almacenados en los valores de ruta. Todos los valores de ruta analizados desde la dirección URL se almacenan como cadenas. Por ejemplo, la restricción float intenta convertir el valor de ruta en un valor Float, pero el valor convertido se usa exclusivamente para comprobar que se puede convertir en Float.

Expresiones regulares en restricciones

Advertencia

Cuando se usa System.Text.RegularExpressions para procesar entradas que no son de confianza, pase un tiempo de expiración. Un usuario malintencionado puede proporcionar entradas a RegularExpressions y provocar un ataque por denegación de servicio. Las API del marco ASP.NET Core en las que se usa RegularExpressions pasan un tiempo de expiración.

Las expresiones regulares se pueden especificar como restricciones insertadas mediante la restricción de ruta regex(...). Los métodos de la familia MapControllerRoute también aceptan un literal de objeto de restricciones. Si se usa ese formato, los valores de cadena se interpretan como expresiones regulares.

En el código siguiente se usa una restricción de expresión regular insertada:

app.MapGet("{message:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)}",
    () => "Inline Regex Constraint Matched");

En el código siguiente se usa un literal de objeto para especificar una restricción de expresión regular:

app.MapControllerRoute(
    name: "people",
    pattern: "people/{ssn}",
    constraints: new { ssn = "^\\d{3}-\\d{2}-\\d{4}$", },
    defaults: new { controller = "People", action = "List" });

El marco de trabajo de ASP.NET Core agrega RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant al constructor de expresiones regulares. Vea RegexOptions para obtener una descripción de estos miembros.

Las expresiones regulares usan delimitadores y tokens similares a los que usan el enrutamiento y el lenguaje C#. Es necesario usar secuencias de escape con los tokens de expresiones regulares. Para usar la expresión regular ^\d{3}-\d{2}-\d{4}$ en una restricción insertada, utilice una de las opciones siguientes:

  • Reemplace los caracteres \ proporcionados en la cadena como caracteres \\ en el archivo de código fuente de C# para aplicar secuencias de escape al carácter de escape de cadena \.
  • Literales de cadena textual.

Para aplicar secuencias de escape a los caracteres delimitadores de parámetro de enrutamiento ({, }, [ y ]), duplique los caracteres en la expresión, por ejemplo {{, }}, [[ y ]]. En la tabla siguiente se muestra una expresión regular y su versión con la secuencia de escape:

Expresión regular Expresión regular con secuencia de escape
^\d{3}-\d{2}-\d{4}$ ^\\d{{3}}-\\d{{2}}-\\d{{4}}$
^[a-z]{2}$ ^[[a-z]]{{2}}$

Las expresiones regulares que se usan en el enrutamiento suelen empezar con el carácter ^ y coincidir con la posición inicial de la cadena. Las expresiones suelen terminar con el carácter $ y coincidir con el final de la cadena. Los caracteres ^ y $ garantizan que la expresión regular coincide con el valor completo del parámetro de ruta. Sin los caracteres ^ y $, la expresión regular coincide con cualquier subcadena de la cadena, lo que normalmente no es deseable. En la tabla siguiente se proporcionan ejemplos y se explica por qué coinciden o no:

Expresión String Coincidir con Comentario
[a-z]{2} hello Coincidencias de subcadenas
[a-z]{2} 123abc456 Coincidencias de subcadenas
[a-z]{2} mz Coincide con la expresión
[a-z]{2} MZ No distingue mayúsculas de minúsculas
^[a-z]{2}$ hello No Vea ^ y $ más arriba
^[a-z]{2}$ 123abc456 No Vea ^ y $ más arriba

Para obtener más información sobre la sintaxis de expresiones regulares, vea Expresiones regulares de .NET Framework.

Para restringir un parámetro a un conjunto conocido de valores posibles, use una expresión regular. Por ejemplo, {action:regex(^(list|get|create)$)} solo hace coincidir el valor de ruta action con list, get o create. Si se pasa al diccionario de restricciones, la cadena ^(list|get|create)$ es equivalente. Las restricciones que se pasan al diccionario de restricciones que no coinciden con una de las conocidas también se tratan como expresiones regulares. Las restricciones que se pasan en una plantilla y que no coinciden con una de las conocidas no se tratan como expresiones regulares.

Restricciones de ruta personalizadas

Se pueden crear restricciones de ruta personalizadas mediante la implementación de la interfaz IRouteConstraint. La interfaz IRouteConstraint contiene Match, que devuelve true si se cumple la restricción, y false en caso contrario.

Las restricciones de ruta personalizadas rara vez son necesarias. Antes de implementar una restricción de ruta personalizada, considere alternativas, como el enlace de modelos.

En la carpeta Constraints de ASP.NET Core se proporcionan buenos ejemplos de creación de restricciones. Por ejemplo, GuidRouteConstraint.

Para usar una restricción IRouteConstraint personalizada, el tipo de restricción de ruta se debe registrar con el parámetro ConstraintMap de la aplicación en el contenedor de servicios. ConstraintMap es un diccionario que asigna claves de restricciones de ruta a implementaciones de IRouteConstraint que validen esas restricciones. El parámetro ConstraintMap de una aplicación puede actualizarse en Program.cs como parte de una llamada a AddRouting o configurando RouteOptions directamente con builder.Services.Configure<RouteOptions>. Por ejemplo:

builder.Services.AddRouting(options =>
    options.ConstraintMap.Add("noZeroes", typeof(NoZeroesRouteConstraint)));

La restricción anterior se aplica en el código siguiente:

[ApiController]
[Route("api/[controller]")]
public class NoZeroesController : ControllerBase
{
    [HttpGet("{id:noZeroes}")]
    public IActionResult Get(string id) =>
        Content(id);
}

La implementación de NoZeroesRouteConstraint impide que 0 se use en un parámetro de ruta:

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

Advertencia

Cuando se usa System.Text.RegularExpressions para procesar entradas que no son de confianza, pase un tiempo de expiración. Un usuario malintencionado puede proporcionar entradas a RegularExpressions y provocar un ataque por denegación de servicio. Las API del marco ASP.NET Core en las que se usa RegularExpressions pasan un tiempo de expiración.

El código anterior:

  • Impide 0 en el segmento {id} de la ruta.
  • Se muestra para proporcionar un ejemplo básico de implementación de una restricción personalizada. No se debe usar en una aplicación de producción.

El código siguiente es un enfoque mejor para impedir que se procese un valor id que contenga 0:

[HttpGet("{id}")]
public IActionResult Get(string id)
{
    if (id.Contains('0'))
    {
        return StatusCode(StatusCodes.Status406NotAcceptable);
    }

    return Content(id);
}

El código anterior tiene las ventajas siguientes con respecto al enfoque de NoZeroesRouteConstraint:

  • No requiere una restricción personalizada.
  • Devuelve un error más descriptivo cuando el parámetro de ruta incluye 0.

Transformadores de parámetros

Transformadores de parámetros:

Por ejemplo, un transformador de parámetros personalizado slugify en el patrón de ruta blog\{article:slugify} con Url.Action(new { article = "MyTestArticle" }) genera blog\my-test-article.

Considere la siguiente implementación de 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();
    }
}

Para usar un transformador de parámetros en un patrón de ruta, configúrelo con ConstraintMap en Program.cs:

builder.Services.AddRouting(options =>
    options.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer));

El marco ASP.NET Core usa los transformadores de parámetros para transformar el URI en el que se resuelve un punto de conexión. Por ejemplo, los transformadores de parámetros transforman los valores de ruta que se usan para hacer coincidir objetos area, controller, action y page:

app.MapControllerRoute(
    name: "default",
    pattern: "{controller:slugify=Home}/{action:slugify=Index}/{id?}");

Con la plantilla de ruta anterior, la acción SubscriptionManagementController.GetAll coincide con el URI /subscription-management/get-all. Un transformador de parámetros no cambia los valores de ruta usados para generar un vínculo. Por ejemplo, Url.Action("GetAll", "SubscriptionManagement") genera /subscription-management/get-all.

ASP.NET Core proporciona convenciones de API para usar transformadores de parámetros con rutas generadas:

Referencia de generación de direcciones URL

Esta sección contiene una referencia para el algoritmo implementado por la generación de direcciones URL. En la práctica, los ejemplos más complejos de generación de direcciones URL usan controladores o Razor Pages. Vea Enrutamiento en controladores para obtener información adicional.

El proceso de generación de direcciones URL comienza con una llamada a LinkGenerator.GetPathByAddress o un método similar. Al método se le proporciona una dirección, un conjunto de valores de ruta y, opcionalmente, información sobre la solicitud actual de HttpContext.

El primer paso consiste en usar la dirección para resolver un conjunto de puntos de conexión candidatos con una instancia de IEndpointAddressScheme<TAddress> que coincide con el tipo de la dirección.

Una vez que el esquema de direcciones encuentra el conjunto de candidatos, los puntos de conexión se ordenan y procesan de forma iterativa hasta que se realiza correctamente una operación de generación de direcciones URL. La generación de direcciones URL no comprueba si hay ambigüedades; el primer resultado devuelto es el resultado final.

Solución de problemas de generación de direcciones URL con registro

El primer paso para solucionar problemas de generación de direcciones URL consiste en establecer el nivel de registro de Microsoft.AspNetCore.Routing en TRACE. LinkGenerator registra muchos detalles sobre su procesamiento que pueden ser útiles para solucionar problemas.

Vea Referencia de generación de direcciones URL para obtener más información sobre la generación de direcciones URL.

Direcciones

Las direcciones son el concepto de la generación de direcciones URL que se usa para enlazar una llamada al generador de vínculos a un conjunto de puntos de conexión candidatos.

Las direcciones son un concepto extensible que incluyen dos implementaciones de forma predeterminada:

  • Con el nombre del punto de conexión (string) como dirección:
    • Proporciona una funcionalidad similar al nombre de ruta de MVC.
    • Usa el tipo de metadatos de IEndpointNameMetadata.
    • Resuelve la cadena proporcionada con los metadatos de todos los puntos de conexión registrados.
    • Inicia una excepción durante el inicio si varios puntos de conexión usan el mismo nombre.
    • Se recomienda para uso general fuera de los controladores y Razor Pages.
  • Con los valores de ruta (RouteValuesAddress) como dirección:
    • Proporciona una funcionalidad similar a los controladores y la generación de direcciones URL heredada de Razor Pages.
    • La ampliación y depuración son complejas.
    • Proporciona la implementación que usa IUrlHelper, aplicaciones auxiliares de etiquetas, aplicaciones auxiliares HTML, resultados de acciones, etc.

El papel del esquema de direcciones consiste en establecer la asociación entre la dirección y los puntos de conexión coincidentes mediante criterios arbitrarios:

  • El esquema de nombres de punto de conexión realiza una búsqueda de diccionario básica.
  • El esquema de valores de ruta tiene un subconjunto óptimo de algoritmos definidos complejo.

Valores de ambiente y valores explícitos

A partir de la solicitud actual, el enrutamiento accede a los valores de ruta del objeto HttpContext.Request.RouteValues de la solicitud actual. Los valores asociados a la solicitud actual se conocen como valores de ambiente. Para mayor claridad, en la documentación se hace referencia a los valores de ruta que se pasan a los métodos como valores explícitos.

En el ejemplo siguiente se muestran valores de ambiente y valores explícitos. Proporciona valores de ambiente de la solicitud actual y valores explícitos:

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

    // ...

El código anterior:

El código siguiente solo proporciona valores explícitos y valores sin ambiente:

var subscribePath = _linkGenerator.GetPathByAction(
    "Subscribe", "Home", new { id = 17 })!;

El método anterior devuelve /Home/Subscribe/17.

El código siguiente en WidgetController devuelve /Widget/Subscribe/17:

var subscribePath = _linkGenerator.GetPathByAction(
    HttpContext, "Subscribe", null, new { id = 17 });

En el código siguiente se proporciona el controlador a partir de los valores de ambiente de la solicitud actual y los valores explícitos:

public class GadgetController : ControllerBase
{
    public IActionResult Index() =>
        Content(Url.Action("Edit", new { id = 17 })!);
}

En el código anterior:

  • Se devuelve /Gadget/Edit/17.
  • Url obtiene el objeto IUrlHelper.
  • Action genera una dirección URL con una ruta de acceso absoluta para un método de acción. La dirección URL contiene el nombre de action especificado y los valores route.

En el código siguiente se proporcionan valores de ambiente de la solicitud actual y valores explícitos:

public class IndexModel : PageModel
{
    public void OnGet()
    {
        var editUrl = Url.Page("./Edit", new { id = 17 });

        // ...
    }
}

En el código anterior se establece url en /Edit/17 cuando la página de Razor de edición contiene la siguiente directiva de página:

@page "{id:int}"

Si la página de edición no contiene la plantilla de ruta "{id:int}", url es /Edit?id=17.

El comportamiento de IUrlHelper de MVC agrega una capa de complejidad además de las reglas descritas aquí:

  • IUrlHelper siempre proporciona los valores de ruta de la solicitud actual como valores de ambiente.
  • IUrlHelper.Action siempre copia los valores de ruta action y controller actuales como valores explícitos a menos que el desarrollador los invalide.
  • IUrlHelper.Page siempre copia el valor de ruta page actual como un valor explícito a menos que se invalide.
  • IUrlHelper.Page siempre invalida el valor de ruta handler actual con null como un valor explícito a menos que se invalide.

A los usuarios a menudo les sorprenden los detalles del comportamiento de los valores de ambiente, ya que MVC no parece seguir sus propias reglas. Por motivos históricos y de compatibilidad, algunos valores de ruta como action, controller, page y handler tienen su propio comportamiento de caso especial.

La funcionalidad equivalente proporcionada por LinkGenerator.GetPathByAction y LinkGenerator.GetPathByPage duplica estas anomalías de IUrlHelper por motivos de compatibilidad.

Proceso de generación de direcciones URL

Una vez que se encuentra el conjunto de puntos de conexión candidatos, el algoritmo de generación de direcciones URL:

  • Procesa los puntos de conexión de forma iterativa.
  • Devuelve el primer resultado correcto.

El primer paso de este proceso se denomina invalidación del valor de ruta. La invalidación del valor de ruta es el proceso por el que el enrutamiento decide qué valores de ruta de los valores de ambiente se deben usar y cuáles se deben omitir. Cada valor de ambiente se tiene en cuenta y se combina con los valores explícitos, o bien se pasa por alto.

La mejor manera de pensar en el rol de los valores de ambiente es que intentan ahorrar trabajo a los desarrolladores de aplicaciones, en algunos casos comunes. Tradicionalmente, los escenarios en los que los valores de ambiente son útiles están relacionados con MVC:

  • Al vincular a otra acción en el mismo controlador, no es necesario especificar el nombre del controlador.
  • Al vincular a otro controlador en la misma área, no es necesario especificar el nombre del área.
  • Al vincular al mismo método de acción, no es necesario especificar los valores de ruta.
  • Al vincular a otro elemento de la aplicación, no le interesa transferir valores de ruta que no tengan ningún significado en ese elemento del control de la aplicación.

Las llamadas a LinkGenerator o IUrlHelper que devuelven null se suelen deber a que no se comprende la invalidación del valor de ruta. Para solucionar problemas de invalidación del valor de ruta, especifique de forma explícita más valores de ruta para ver si eso resuelve el problema.

La invalidación del valor de ruta se basa en la suposición de que el esquema de direcciones URL de la aplicación es jerárquico, con una jerarquía formada de izquierda a derecha. Considere la posibilidad de usar la plantilla de ruta de controlador básica {controller}/{action}/{id?} para hacerse una idea intuitiva de cómo funciona esto en la práctica. Un cambio en un valor invalida todos los valores de ruta que aparecen a la derecha. Esto refleja la suposición sobre la jerarquía. Si la aplicación tiene un valor de ambiente para id y la operación especifica otro valor para controller:

  • id no se reutilizará porque {controller} está a la izquierda de {id?}.

Algunos ejemplos demuestran este principio:

  • Si los valores explícitos contienen un valor para id, se omite el valor de ambiente de id. Se pueden usar los valores de ambiente para controller y action.
  • Si los valores explícitos contienen un valor para action, se omite cualquier valor de ambiente para action. Se pueden usar los valores de ambiente para controller. Si el valor explícito para action es diferente del valor de ambiente para action, no se usará el valor de id. Si el valor explícito para action es diferente del valor de ambiente para action, se puede usar el valor de id.
  • Si los valores explícitos contienen un valor para controller, se omite cualquier valor de ambiente para controller. Si el valor explícito para controller es diferente del valor de ambiente para controller, no se usarán los valores de action y id. Si el valor explícito para controller es igual que el valor de ambiente para controller, se pueden usar los valores de action y id.

Este proceso sea complica todavía más por la existencia de rutas de atributo y rutas convencionales dedicadas. Las rutas convencionales de controlador, como {controller}/{action}/{id?}, especifican una jerarquía mediante parámetros de ruta. Para las rutas convencionales dedicadas y las rutas de atributo a controladores y Razor Pages:

  • Existe una jerarquía de valores de ruta.
  • No aparecen en la plantilla.

En estos casos, la generación de direcciones URL define el concepto de valores necesarios. Los puntos de conexión creados por controladores y Razor Pages tienen valores necesarios especificados que permiten que la invalidación del valor de ruta funcione.

El algoritmo de invalidación del valor de ruta en detalle:

  • Los nombres de valor necesarios se combinan con los parámetros de ruta y, después, se procesan de izquierda a derecha.
  • Para cada parámetro, se comparan el valor de ambiente y el valor explícito:
    • Si el valor de ambiente y el valor explícito son iguales, el proceso continúa.
    • Si el valor de ambiente está presente y el valor explícito no, se usa el valor de ambiente al generar la dirección URL.
    • Si el valor de ambiente no está presente y el valor explícito sí, rechace el valor de ambiente y todos los posteriores.
    • Si el valor de ambiente y el valor explícito están presentes, y los dos son diferentes, rechace el valor de ambiente y todos los posteriores.

En este punto, la operación de generación de direcciones URL está lista para evaluar las restricciones de ruta. El conjunto de valores aceptados se combina con los valores predeterminados de parámetro, que se proporcionan a las restricciones. Si todas las restricciones son correctas, la operación continúa.

A continuación, se pueden usar los valores aceptados para expandir la plantilla de ruta. La plantilla de ruta se procesa:

  • De izquierda a derecha.
  • En cada parámetro se sustituye su valor aceptado.
  • Con los siguientes casos especiales:
    • Si falta un valor en los valores aceptados y el parámetro tiene un valor predeterminado, se usa el valor predeterminado.
    • Si falta un valor en los valores aceptados y el parámetro es opcional, el procesamiento continúa.
    • Si un parámetro de ruta a la derecha de un parámetro opcional que falta tiene un valor, se produce un error en la operación.
    • Los parámetros con valores predeterminados contiguos y los parámetros opcionales se contraen siempre que sea posible.

Los valores que se proporcionan de forma explícita que no coinciden con un segmento de la ruta se agregan a la cadena de consulta. En la tabla siguiente se muestra el resultado cuando se usa la plantilla de ruta {controller}/{action}/{id?}.

Valores de ambiente Valores explícitos Resultado
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

Problemas con la invalidación del valor de ruta

En el código siguiente se muestra un ejemplo de un esquema de generación de direcciones URL que no es compatible con el enrutamiento:

app.MapControllerRoute(
    "default",
    "{culture}/{controller=Home}/{action=Index}/{id?}");

app.MapControllerRoute(
    "blog",
    "{culture}/{**slug}",
    new { controller = "Blog", action = "ReadPost" });

En el código anterior, se usa el parámetro de ruta culture para la localización. El objetivo es que el parámetro culture siempre se acepte como valor de ambiente. Pero el parámetro culture no se acepta como valor de ambiente debido al funcionamiento de los valores necesarios:

  • En la plantilla de ruta "default", el parámetro de ruta culture está a la izquierda de controller, por lo que los cambios en controller no invalidarán culture.
  • En la plantilla de ruta "blog", se considera que el parámetro de ruta culture está a la derecha de controller, que aparece en los valores necesarios.

Análisis de rutas de dirección URL con LinkParser

La clase LinkParser agrega compatibilidad con el análisis de una ruta de dirección URL en un conjunto de valores de ruta. El método ParsePathByEndpointName toma un nombre de punto de conexión y una ruta de dirección URL y devuelve un conjunto de valores de ruta extraídos de esta.

En el controlador de ejemplo siguiente, la acción GetProduct usa una plantilla de ruta de api/Products/{id} y tiene un valor de Name de GetProduct:

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    [HttpGet("{id}", Name = nameof(GetProduct))]
    public IActionResult GetProduct(string id)
    {
        // ...

En la misma clase de controlador, la acción AddRelatedProduct espera una ruta de dirección URL, pathToRelatedProduct, que se puede proporcionar como parámetro de cadena de consulta:

[HttpPost("{id}/Related")]
public IActionResult AddRelatedProduct(
    string id, string pathToRelatedProduct, [FromServices] LinkParser linkParser)
{
    var routeValues = linkParser.ParsePathByEndpointName(
        nameof(GetProduct), pathToRelatedProduct);
    var relatedProductId = routeValues?["id"];

    // ...

En el ejemplo anterior, la acción AddRelatedProduct extrae el valor de ruta id de la ruta de dirección URL. Por ejemplo, con una ruta de dirección URL de /api/Products/1, el valor de relatedProductId se establece en 1. Este enfoque permite a los clientes de la API usar rutas de dirección URL al hacer referencia a recursos, sin necesidad de conocer cómo se estructura dicha dirección URL.

Configuración de metadatos de punto de conexión

Los vínculos siguientes proporcionan información sobre cómo configurar los metadatos del punto de conexión:

Comparación de host en rutas con RequireHost

RequireHost aplica una restricción a la ruta que requiere el host especificado. El parámetro RequireHost o [Host] puede ser:

  • Host: www.domain.com, compara www.domain.com con cualquier puerto.
  • Host con carácter comodín: *.domain.com, coincide con www.domain.com, subdomain.domain.com o www.subdomain.domain.com en cualquier puerto.
  • Puerto: *:5000, coincide con el puerto 5000 con cualquier host.
  • Host y puerto: www.domain.com:5000 o *.domain.com:5000, coincide con el host y el puerto.

Se pueden especificar varios parámetros mediante RequireHost o [Host]. La restricción coincide con los hosts válidos para cualquiera de los parámetros. Por ejemplo, [Host("domain.com", "*.domain.com")] coincide con domain.com, www.domain.com y subdomain.domain.com.

En el código siguiente se usa RequireHost para requerir el host especificado en la ruta:

app.MapGet("/", () => "Contoso").RequireHost("contoso.com");
app.MapGet("/", () => "AdventureWorks").RequireHost("adventure-works.com");

app.MapHealthChecks("/healthz").RequireHost("*:8080");

En el código siguiente se usa el atributo [Host] en el controlador para requerir cualquiera de los hosts especificados:

[Host("contoso.com", "adventure-works.com")]
public class HostsController : Controller
{
    public IActionResult Index() =>
        View();

    [Host("example.com")]
    public IActionResult Example() =>
        View();
}

Cuando el atributo [Host] se aplica al método de acción y al controlador:

  • Se usa el atributo en la acción.
  • Se omite el atributo del controlador.

Instrucciones de rendimiento para el enrutamiento

Cuando una aplicación tiene problemas de rendimiento, a menudo se sospecha que el enrutamiento es el problema. El motivo es que marcos como los controladores y Razor Pages notifican la cantidad de tiempo empleado en el marco de trabajo en sus mensajes de registro. Cuando hay una diferencia significativa entre el tiempo notificado por los controladores y el tiempo total de la solicitud:

  • Los desarrolladores eliminan el código de la aplicación como origen del problema.
  • Es habitual asumir que el enrutamiento es la causa.

El rendimiento del enrutamiento se prueba mediante miles de puntos de conexión. No es probable que una aplicación típica detecte un problema de rendimiento simplemente por ser demasiado grande. La causa raíz más común del rendimiento lento del enrutamiento suele ser middleware personalizado con un comportamiento incorrecto.

En el ejemplo de código siguiente se muestra una técnica básica para limitar el origen del retraso:

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.");

Para controlar el tiempo del enrutamiento:

  • Intercale cada middleware con una copia del middleware de tiempo que se muestra en el código anterior.
  • Agregue un identificador único para poner en correlación los datos de control de tiempo con el código.

Se trata de una forma básica de reducir el retraso cuando es significativo, por ejemplo, de más de 10ms. Al restar Time 2 de Time 1 se notifica el tiempo invertido dentro del middleware UseRouting.

En el código siguiente se usa un enfoque más compacto del código de control de tiempo anterior:

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.");

Características de enrutamiento potencialmente costosas

En la lista siguiente se proporciona información sobre las características de enrutamiento que son relativamente costosas en comparación con las plantillas de ruta básicas:

  • Expresiones regulares: Se pueden escribir expresiones regulares que sean complejas o que tengan un tiempo de ejecución de larga duración con una pequeña cantidad de entradas.
  • Segmentos complejos ({x}-{y}-{z}):
    • Son mucho más costosos que analizar un segmento de ruta de dirección URL convencional.
    • El resultado es que se asignan muchas más subcadenas.
  • Acceso a datos sincrónicos: muchas aplicaciones complejas tienen acceso a bases de datos como parte de su enrutamiento. Los puntos de extensibilidad como MatcherPolicy y EndpointSelectorContext son asincrónicos.

Guía para tablas de enrutamiento de gran tamaño

De forma predeterminada, ASP.NET Core utiliza un algoritmo de enrutamiento que compara la memoria con el tiempo de CPU. Esto genera un buen resultado por el hecho de que el tiempo de coincidencia de enrutamiento solo depende de la longitud de la ruta de acceso con la que debe coincidir y no del número de rutas. Sin embargo, este enfoque puede ser potencialmente problemático en algunos casos, cuando la aplicación tiene un gran número de rutas (miles) y hay una gran cantidad de prefijos de variable en las rutas. Por ejemplo, si las rutas tienen parámetros en los primeros segmentos de la ruta, como {parameter}/some/literal.

Es poco probable que una aplicación se ejecute en una situación en la que esto sea un problema, a menos que:

  • Haya un gran número de rutas en la aplicación que usen este patrón.
  • Haya un gran número de rutas en la aplicación.

Forma de determinar si una aplicación se está ejecutando con el problema de tablas de enrutamiento de gran tamaño

  • Hay dos síntomas que buscar:
    • La aplicación tarda en iniciarse en la primera solicitud.
      • Tenga en cuenta que esto es necesario, pero no suficiente. Hay muchos otros problemas no relacionados con el enrutamiento que pueden provocar que la aplicación se inicie lentamente. Compruebe la condición siguiente para determinar con precisión que la aplicación se está ejecutando en esta situación.
    • La aplicación consume mucha memoria durante el inicio y un volcado de memoria muestra un gran número de instancias de Microsoft.AspNetCore.Routing.Matching.DfaNode.

Forma de solucionar este problema

Hay varias técnicas y optimizaciones que se pueden aplicar a las rutas que mejorarán en gran medida este escenario:

  • Aplique restricciones de ruta a los parámetros, por ejemplo, {parameter:int}, {parameter:guid}, {parameter:regex(\\d+)}, etc., siempre que sea posible.
    • Esto permite que el algoritmo de enrutamiento optimice internamente las estructuras usadas para buscar coincidencias y reducir drásticamente la memoria usada.
    • En la gran mayoría de los casos, esto será suficiente para volver a un comportamiento aceptable.
  • Cambie las rutas para mover parámetros a segmentos posteriores de la plantilla.
    • Esto reduce el número de posibles "rutas de acceso" para que coincidan con un punto de conexión en una ruta de acceso determinada.
  • Use una ruta dinámica y realice la asignación a un controlador o página dinámicamente.
    • Esto puede realizarse mediante MapDynamicControllerRoute y MapDynamicPageRoute.

Instrucciones para los autores de bibliotecas

Esta sección contiene instrucciones para los autores de bibliotecas que realizan la compilación sobre el enrutamiento. Estos detalles están diseñados para garantizar que los desarrolladores de aplicaciones tengan una buena experiencia en el uso de bibliotecas y marcos que amplían el enrutamiento.

Definición de puntos de conexión

Para crear un marco que use el enrutamiento para la coincidencia de direcciones URL, empiece por definir una experiencia de usuario que se base en UseEndpoints.

REALICE la compilación sobre IEndpointRouteBuilder. Esto permite a los usuarios crear el marco de trabajo con otras características de ASP.NET Core sin confusión. Todas las plantillas de ASP.NET Core incluyen el enrutamiento. Asuma que el enrutamiento está presente y es familiar para los usuarios.

// Your framework
app.MapMyFramework(...);

app.MapHealthChecks("/healthz");

DEVUELVA un tipo concreto sellado a partir de una llamada a MapMyFramework(...) que implemente IEndpointConventionBuilder. La mayoría de los métodos Map... del marco siguen este patrón. La interfaz IEndpointConventionBuilder:

  • Permite la composición de metadatos.
  • Es el destino de diversos métodos de extensión.

La declaración de un tipo propio permite agregar funcionalidad específica del marco propia al generador. Es correcto encapsular un generador declarado por el marco y reenviarle llamadas.

// Your framework
app.MapMyFramework(...)
    .RequireAuthorization()
    .WithMyFrameworkFeature(awesome: true);

app.MapHealthChecks("/healthz");

CONSIDERE LA POSIBILIDAD de escribir un objeto EndpointDataSource propio. EndpointDataSource es la primitiva de bajo nivel para declarar y actualizar una colección de puntos de conexión. EndpointDataSource es una API eficaz que usan los controladores y Razor Pages.

Las pruebas de enrutamiento tienen un ejemplo básico de un origen de datos que no es de actualización.

NO intente registrar un objeto EndpointDataSource de forma predeterminada. Exija a los usuarios que registren el marco en UseEndpoints. La filosofía del enrutamiento es que nada se incluye de forma predeterminada y que UseEndpoints es el lugar donde se registran los puntos de conexión.

Creación de middleware con enrutamiento integrada

CONSIDERE LA POSIBILIDAD de definir tipos de metadatos como una interfaz.

PERMITA el uso de los tipos de metadatos como atributo en clases y métodos.

public interface ICoolMetadata
{
    bool IsCool { get; }
}

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class CoolMetadataAttribute : Attribute, ICoolMetadata
{
    public bool IsCool => true;
}

Los marcos como los controladores y Razor Pages admiten la aplicación de atributos de metadatos a tipos y métodos. Si declara tipos de metadatos:

  • Haga que sean accesibles como atributos.
  • La mayoría de los usuarios están familiarizados con la aplicación de atributos.

La declaración de un tipo de metadatos como una interfaz agrega otro nivel de flexibilidad:

  • Las interfaces admiten composición.
  • Los desarrolladores pueden declarar tipos propios que combinen varias directivas.

PERMITA que los metadatos se puedan invalidar, como se muestra en el ejemplo siguiente:

[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() { }
}

La mejor manera de seguir estas instrucciones es evitar la definición de metadatos de marcador:

  • No busque solo la presencia de un tipo de metadatos.
  • Defina una propiedad en los metadatos y compruébela.

La colección de metadatos está ordenada y admite la invalidación por prioridad. En el caso de los controladores, los metadatos del método de acción son más específicos.

PERMITA que el middleware sea útil con y sin el enrutamiento:

app.UseAuthorization(new AuthorizationPolicy() { ... });

// Your framework
app.MapMyFramework(...).RequireAuthorization();

Como ejemplo de esta instrucción, considere la posibilidad de usar el middleware UseAuthorization. El middleware de autorización permite pasar una directiva de reserva. La directiva de reserva, si se especifica, se aplica a:

  • Puntos de conexión sin una directiva especificada.
  • Solicitudes que no coinciden con un punto de conexión.

Esto hace que el middleware de autorización sea útil fuera del contexto del enrutamiento. El middleware de autorización se puede usar para la programación de middleware tradicional.

Diagnóstico de depuración

Para ver la salida detallada del diagnóstico de cálculo de ruta, establezca Logging:LogLevel:Microsoft en Debug. En el entorno de desarrollo, establezca el nivel de registro en appsettings.Development.json:

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

Recursos adicionales

El enrutamiento es responsable de hacer coincidir las solicitudes HTTP entrantes y de enviarlas a los puntos de conexión ejecutables de la aplicación. Los puntos de conexión son las unidades de código de control de solicitudes ejecutable de la aplicación. Se definen en la aplicación y se configuran al iniciarla. El proceso de búsqueda de coincidencias de puntos de conexión puede extraer valores de la dirección URL de la solicitud y proporcionarlos para el procesamiento de la solicitud. Con la información de los puntos de conexión de la aplicación, el enrutamiento también puede generar direcciones URL que se asignan a los puntos de conexión.

Las aplicaciones pueden configurar el enrutamiento mediante:

  • Controladores
  • Razor Pages
  • SignalR
  • Servicios gRPC
  • Middleware habilitado para puntos de conexión, como las comprobaciones de estado.
  • Delegados y expresiones lambda registrados con el enrutamiento.

En este documento se describen los detalles de bajo nivel del enrutamiento de ASP.NET Core. Para obtener información sobre la configuración del enrutamiento:

El sistema de enrutamiento de puntos de conexión descrito en este documento se aplica a ASP.NET Core 3.0 y versiones posteriores. Para obtener información sobre el sistema de enrutamiento anterior basado en IRouter, seleccione la versión ASP.NET Core 2.1 mediante uno de los enfoques siguientes:

Vea o descargue el código de ejemplo (cómo descargarlo)

Los ejemplos de descarga para este documento están habilitados por una clase Startup específica. Para ejecutar un ejemplo concreto, modifique Program.cs para llamar a la clase Startup deseada.

Fundamentos del enrutamiento

Todas las plantillas de ASP.NET Core incluyen el enrutamiento en el código generado. El enrutamiento se registra en la canalización de middleware en Startup.Configure.

En el código siguiente se muestra un ejemplo básico de enrutamiento:

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

El enrutamiento usa un par de middleware, registrado por UseRouting y UseEndpoints:

  • UseRouting agrega coincidencia de rutas a la canalización de middleware. Este middleware examina el conjunto de puntos de conexión definidos en la aplicación y selecciona la mejor coincidencia en función de la solicitud.
  • UseEndpoints agrega la ejecución del punto de conexión a la canalización de middleware. Ejecuta el delegado asociado al punto de conexión seleccionado.

En el ejemplo anterior se incluye un único punto de conexión de ruta a código a través del método MapGet:

  • Al enviar una solicitud HTTP GET a la dirección URL raíz /:
    • Se ejecuta el delegado de solicitud mostrado.
    • Se escribe Hello World! en la respuesta HTTP. De forma predeterminada, la dirección URL raíz / es https://localhost:5001/.
  • Si el método de solicitud no es GET o la dirección URL raíz no es /, no se detecta ninguna ruta y se devuelve HTTP 404.

punto de conexión

El método MapGet se usa para definir un punto de conexión. Un punto de conexión es algo que se puede:

  • Seleccionar, si se hacen coincidir la dirección URL y el método HTTP.
  • Ejecutar, mediante la ejecución del delegado.

Los puntos de conexión que la aplicación puede ejecutar y hacer coincidir se configuran en UseEndpoints. Por ejemplo, MapGet, MapPost y métodos similares conectan delegados de solicitud al sistema de enrutamiento. Se pueden usar métodos adicionales para conectar características del marco ASP.NET Core al sistema de enrutamiento:

En el ejemplo siguiente se muestra el enrutamiento con una plantilla de ruta más sofisticada:

app.UseEndpoints(endpoints =>
{
    endpoints.MapGet("/hello/{name:alpha}", async context =>
    {
        var name = context.Request.RouteValues["name"];
        await context.Response.WriteAsync($"Hello {name}!");
    });
});

La cadena /hello/{name:alpha} es una plantilla de ruta. Se usa para configurar cómo se hace coincidir el punto de conexión. En este caso, la plantilla coincide con:

  • Una dirección URL como /hello/Ryan.
  • Cualquier ruta de dirección URL que comience por /hello/, seguido de una secuencia de caracteres alfabéticos. :alpha aplica una restricción de ruta que solo coincide con caracteres alfabéticos. Las restricciones de ruta se explican más adelante en este documento.

El segundo segmento de la ruta de dirección URL, {name:alpha}:

El sistema de enrutamiento de puntos de conexión descrito en este documento es nuevo desde ASP.NET Core 3.0. Sin embargo, todas las versiones de ASP.NET Core admiten el mismo conjunto de características de plantilla de ruta y restricciones de ruta.

En el ejemplo siguiente se muestra el enrutamiento con comprobaciones de estado y autorización:

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

Si quiere que los comentarios de código se traduzcan en más idiomas además del inglés, háganoslo saber en este problema de debate de GitHub.

En el ejemplo anterior se muestra cómo:

  • El middleware de autorización se puede usar con el enrutamiento.
  • Los puntos de conexión se pueden usar para configurar el comportamiento de la autorización.

La llamada a MapHealthChecks agrega un punto de conexión de comprobación de estado. Al encadenar RequireAuthorization a esta llamada, se adjunta una directiva de autorización al punto de conexión.

La llamada a UseAuthentication y UseAuthorization agrega el middleware de autenticación y autorización. Estos middleware se colocan entre UseRouting y UseEndpoints para que puedan:

  • Vea qué punto de conexión ha seleccionado UseRouting.
  • Aplique una directiva de autorización antes de que UseEndpoints envíe al punto de conexión.

Metadatos de punto de conexión

En el ejemplo anterior, hay dos puntos de conexión, pero solo el de comprobación de estado tiene una directiva de autorización adjunta. Si la solicitud coincide con el punto de conexión de comprobación de estado, /healthz, se realiza una comprobación de autorización. Esto demuestra que los puntos de conexión pueden tener datos adicionales adjuntos. Estos datos adicionales se denominan metadatos de punto de conexión:

  • Los metadatos pueden ser procesados mediante middleware compatible con el enrutamiento.
  • Los metadatos pueden ser de cualquier tipo de .NET.

Conceptos de enrutamiento

El sistema de enrutamiento se basa en la canalización de middleware mediante la adición del eficaz concepto de punto de conexión. Los puntos de conexión representan unidades de la funcionalidad de la aplicación que son diferentes entre sí en cuanto al enrutamiento, la autorización y cualquier número de sistemas de ASP.NET Core.

Definición de punto de conexión de ASP.NET Core

Un punto de conexión de ASP.NET Core es:

En el código siguiente se muestra cómo recuperar e inspeccionar el punto de conexión que coincide con la solicitud actual:

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

El punto de conexión, si se selecciona, se puede recuperar de HttpContext. Se pueden inspeccionar sus propiedades. Los objetos de punto de conexión son inmutables y no se pueden modificar después de crearlos. El tipo más común de punto de conexión es RouteEndpoint. RouteEndpoint incluye información que permite que el sistema de enrutamiento lo seleccione.

En el código anterior, app.Use configura un middleware insertado.

En el código siguiente se muestra que, en función de dónde se llame a app.Use en la canalización, es posible que no haya un punto de conexión:

// 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);
});

En este ejemplo se agregan instrucciones Console.WriteLine que muestran si se ha seleccionado un punto de conexión o no. Para mayor claridad, en el ejemplo se asigna un nombre para mostrar al punto de conexión / proporcionado.

Al ejecutar este código con una dirección URL de / se muestra lo siguiente:

1. Endpoint: (null)
2. Endpoint: Hello
3. Endpoint: Hello

Al ejecutar este código con otra dirección URL se muestra lo siguiente:

1. Endpoint: (null)
2. Endpoint: (null)
4. Endpoint: (null)

Este resultado muestra que:

  • El punto de conexión siempre es NULL antes de que se llame a UseRouting.
  • Si se encuentra una coincidencia, el extremo no es NULL entre UseRouting y UseEndpoints.
  • El middleware UseEndpoints es terminal cuando se encuentra una coincidencia. El middleware de terminal se define más adelante en este documento.
  • El middleware después de UseEndpoints solo se ejecuta cuando no se encuentra ninguna coincidencia.

El middleware UseRouting usa el método SetEndpoint para asociar el punto de conexión al contexto actual. Se puede reemplazar el middleware UseRouting con lógica personalizada y seguir aprovechando las ventajas del uso de puntos de conexión. Los puntos de conexión son una primitiva de bajo nivel como middleware y no están unidos a la implementación de enrutamiento. La mayoría de las aplicaciones no necesitan reemplazar UseRouting por lógica personalizada.

El middleware UseEndpoints está diseñado para usarse junto con el middleware UseRouting. La lógica básica para ejecutar un punto de conexión no es complicada. Use GetEndpoint para recuperar el punto de conexión y, después, invoque su propiedad RequestDelegate.

En el código siguiente se muestra cómo el middleware puede influir en el enrutamiento o reaccionar ante este:

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; }
}

En el ejemplo anterior se muestran dos conceptos importantes:

  • El middleware se puede ejecutar antes de UseRouting para modificar los datos sobre los que funciona el enrutamiento.
  • El middleware se puede ejecutar entre UseRouting y UseEndpoints para procesar los resultados del enrutamiento antes de que se ejecute el punto de conexión.
    • El middleware que se ejecuta entre UseRouting y UseEndpoints:
      • Normalmente inspecciona los metadatos para entender los puntos de conexión.
      • A menudo toma decisiones de seguridad, como UseAuthorization y UseCors.
    • La combinación de middleware y metadatos permite configurar directivas por punto de conexión.

En el código anterior se muestra un ejemplo de middleware personalizado que admite directivas por punto de conexión. El middleware escribe un registro de auditoría de acceso a datos confidenciales en la consola. El middleware se puede configurar para auditar un punto de conexión con los metadatos de AuditPolicyAttribute. En este ejemplo se muestra un patrón opcional en el que solo se auditan los puntos de conexión marcados como confidenciales. Esta lógica se puede definir en orden inverso, para auditar todo lo que no esté marcado como seguro, por ejemplo. El sistema de metadatos de punto de conexión es flexible. Esta lógica se puede diseñar de la manera que mejor se adapte al caso de uso.

El código del ejemplo anterior está diseñado para mostrar los conceptos básicos de los puntos de conexión. No está pensado para su uso en producción. Una versión más completa de un middleware de registro de auditoría:

  • Realizaría el registro en un archivo o una base de datos.
  • Incluiría detalles como el usuario, la dirección IP, el nombre del punto de conexión confidencial, etc.

El valor AuditPolicyAttribute de metadatos de directiva de auditoría se define como Attribute para facilitar su uso con marcos basados en clases como los controladores y SignalR. Cuando se usa de ruta a código:

  • Los metadatos se asocian con una API de generador.
  • Los marcos basados en clases incluyen todos los atributos en el método y la clase correspondientes al crear los puntos de conexión.

Los procedimientos recomendados para los tipos de metadatos son definirlos como interfaces o atributos. Las interfaces y los atributos permiten la reutilización del código. El sistema de metadatos es flexible y no impone ninguna limitación.

Comparación entre un middleware de terminal y el enrutamiento

En el ejemplo de código siguiente se compara el uso de middleware con el del enrutamiento:

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

El estilo de middleware que se muestra con Approach 1: es middleware de terminal. Se denomina middleware de terminal porque realiza una operación de búsqueda de coincidencias:

  • La operación de búsqueda de coincidencias en el ejemplo anterior es Path == "/" para el middleware y Path == "/Movie" para el enrutamiento.
  • Cuando una coincidencia es correcta, ejecuta alguna funcionalidad y devuelve un valor, en lugar de invocar el middleware next.

Se denomina middleware de terminal porque finaliza la búsqueda, ejecuta alguna funcionalidad y, después, devuelve un valor.

Comparación entre un middleware de terminal y el enrutamiento:

  • Los dos enfoques permiten terminar la canalización de procesamiento:
    • El middleware finaliza la canalización mediante la devolución de un valor en lugar de invocar next.
    • Los puntos de conexión siempre son de terminal.
  • El middleware de terminal permite colocar el middleware en un lugar arbitrario de la canalización:
    • Los puntos de conexión se ejecutan en la posición de UseEndpoints.
  • El middleware de terminal permite que el código arbitrario determine cuándo coincide el middleware:
    • El código personalizado de búsqueda de coincidencia de rutas puede ser detallado y difícil de escribir correctamente.
    • El enrutamiento proporciona soluciones sencillas para las aplicaciones típicas. La mayoría de las aplicaciones no requieren código personalizado de búsqueda de coincidencia de rutas.
  • Los puntos de conexión interactúan con middleware como UseAuthorization y UseCors.
    • Para usar un middleware de terminal con UseAuthorization o UseCors se necesita interactuar de forma manual con el sistema de autorización.

Un punto de conexión define:

  • Un delegado para procesar solicitudes.
  • Una colección de metadatos arbitrarios. Los metadatos se usan para implementar cuestiones transversales según las directivas y la configuración asociada a cada punto de conexión.

El middleware de terminal puede ser una herramienta eficaz, pero puede requerir:

  • Una cantidad significativa de código y pruebas.
  • La integración manual con otros sistemas para lograr el nivel deseado de flexibilidad.

Considere la posibilidad de realizar la integración con el enrutamiento antes de escribir middleware de terminal.

El middleware de terminal existente que se integra con Map o MapWhen normalmente se puede convertir en un punto de conexión compatible con el enrutamiento. MapHealthChecks muestra el patrón para enrutadores:

  • Escriba un método de extensión en IEndpointRouteBuilder.
  • Cree una canalización de middleware anidada mediante CreateApplicationBuilder.
  • Adjunte el middleware a la nueva canalización. En este caso, UseHealthChecks.
  • Aplique Build a la canalización de middleware en un objeto RequestDelegate.
  • Llame a Map y proporcione la nueva canalización de middleware.
  • Devuelva el objeto de generador proporcionado por Map desde el método de extensión.

En el código siguiente se muestra el uso de 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!");
        });
    });
}

En el ejemplo anterior se muestra la importancia de devolver el objeto de generador. Al devolver el objeto de generador, el desarrollador de aplicaciones puede configurar directivas como la autorización para el punto de conexión. En este ejemplo, el middleware de comprobaciones de estado no tiene una integración directa con el sistema de autorización.

El sistema de metadatos se ha creado como respuesta a los problemas detectados por los autores de extensibilidad mediante el middleware de terminal. El problema de cada middleware es implementar su propia integración con el sistema de autorización.

Coincidencia de dirección URL

  • Es el proceso por el cual el enrutamiento hace coincidir una solicitud entrante a un punto de conexión.
  • Se basa en los datos de la ruta de acceso y los encabezados de la dirección URL.
  • Se puede extender para tener en cuenta los datos de la solicitud.

Cuando se ejecuta un middleware de enrutamiento, se establece un objeto Endpoint y se enrutan los valores a una característica de solicitud en el objeto HttpContext desde la solicitud actual:

  • La llamada a HttpContext.GetEndpoint obtiene el punto de conexión.
  • HttpRequest.RouteValues obtiene la colección de valores de ruta.

El middleware que se ejecuta después del middleware de enrutamiento puede inspeccionar el punto de conexión y tomar medidas. Por ejemplo, un middleware de autorización puede consultar la colección de metadatos del punto de conexión de una directiva de autorización. Después de que se ejecuta todo el middleware en la canalización de procesamiento de solicitudes, se invoca al delegado del punto de conexión seleccionado.

El sistema de enrutamiento en el enrutamiento de punto de conexión es responsable de todas las decisiones relativas al envío. Como el middleware aplica directivas en función del punto de conexión seleccionado, es importante que:

  • Cualquier decisión que pueda afectar al envío o a la aplicación de directivas de seguridad se realice dentro del sistema de enrutamiento.

Advertencia

En cuanto a la compatibilidad con versiones anteriores, cuando se ejecuta un delegado del punto de conexión de controlador o Razor Pages, las propiedades de RouteContext.RouteData se establecen en los valores adecuados en función del procesamiento de solicitudes realizado hasta el momento.

El tipo RouteContext se marcará como obsoleto en una versión futura:

  • Migre RouteData.Values a HttpRequest.RouteValues.
  • Migre RouteData.DataTokens para recuperar IDataTokensMetadata de los metadatos del punto de conexión.

La coincidencia de direcciones URL funciona en un conjunto configurable de fases. En cada fase, la salida es un conjunto de coincidencias. El conjunto de coincidencias se puede reducir más en la fase siguiente. La implementación de enrutamiento no garantiza un orden de procesamiento para los puntos de conexión coincidentes. Todas las coincidencias posibles se procesan a la vez. Las fases de coincidencia de direcciones URL se producen en el orden siguiente. ASP.NET Core:

  1. Procesa la ruta de dirección URL con el conjunto de puntos de conexión y sus plantillas de ruta, y se recopilan todas las coincidencias.
  2. Toma la lista anterior y quita las coincidencias en las que se produce un error con restricciones de ruta aplicadas.
  3. Toma la lista anterior y quita las coincidencias en las que se produce un error en el conjunto de instancias de MatcherPolicy.
  4. Usa EndpointSelector para tomar una decisión final de la lista anterior.

La lista de puntos de conexión se prioriza según:

Todos los puntos de conexión coincidentes se procesan en cada fase hasta que se alcanza EndpointSelector. EndpointSelector es la fase final. Elige el punto de conexión de prioridad más alta entre las coincidencias como la mejor coincidencia. Si hay otras coincidencias con la misma prioridad que la mejor, se inicia una excepción de coincidencia ambigua.

La prioridad de ruta se calcula en función de una plantilla de ruta más específica a la que se le asigna una prioridad más alta. Por ejemplo, considere las plantillas /hello y /{message}:

  • Las dos coinciden con la ruta de dirección URL /hello.
  • /hello es más específica y, por tanto, tiene mayor prioridad.

Por lo general, la precedencia de rutas realiza un buen trabajo de elegir la mejor coincidencia para los tipos de esquemas de dirección URL que se usan en la práctica. Use Order solo cuando sea necesario para evitar una ambigüedad.

Debido a los tipos de extensibilidad que proporciona el enrutamiento, el sistema de enrutamiento no puede calcular las rutas ambiguas por adelantado. Considere un ejemplo como las plantillas de ruta /{message:alpha} y /{message:int}:

  • La restricción alpha solo coincide con caracteres alfabéticos.
  • La restricción int solo coincide con números.
  • Estas plantillas tienen la misma prioridad de ruta, pero no hay ninguna dirección URL única con la que coincidan.
  • Si el sistema de enrutamiento ha notificado un error de ambigüedad al iniciarse, bloquearía este caso de uso válido.

Advertencia

El orden de las operaciones dentro de UseEndpoints no influye en el comportamiento del enrutamiento, con una excepción. MapControllerRoute y MapAreaRoute asignan de forma automática un valor de orden a sus puntos de conexión en función del orden en el que se hayan invocado. Esto simula el comportamiento a largo plazo de los controladores sin que el sistema de enrutamiento proporcione las mismas garantías que las implementaciones de enrutamiento anteriores.

En la implementación heredada de enrutamiento, es posible implementar la extensibilidad de enrutamiento que tiene una dependencia en el orden de procesamiento de las rutas. Enrutamiento de puntos de conexión en ASP.NET Core 3.0 y versiones posteriores:

  • No tiene un concepto de rutas.
  • No proporciona garantías de ordenación. Todos los puntos de conexión se procesan a la vez.

Prioridad de la plantilla de ruta y orden de selección de los puntos de conexión

La prioridad de la plantilla de ruta es un sistema que asigna a cada plantilla de ruta un valor en función de su especificidad. Precedencia de la plantilla de ruta:

  • Evita la necesidad de ajustar el orden de los puntos de conexión en casos comunes.
  • Intenta hacer coincidir las expectativas comunes del comportamiento del enrutamiento.

Por ejemplo, considere las plantillas /Products/List y /Products/{id}. Sería razonable suponer que /Products/List es una mejor coincidencia que /Products/{id} para la ruta de dirección URL /Products/List. Funciona porque el segmento literal /List se considera que tiene una mayor prioridad que el segmento de parámetro /{id}.

Los detalles de cómo funciona la precedencia están vinculados a cómo se definen las plantillas de ruta:

  • Las plantillas con más segmentos se consideran más específicas.
  • Un segmento con texto literal se considera más específico que un segmento de parámetro.
  • Un segmento de parámetro con una restricción se considera más específico que uno que no la tenga.
  • Un segmento complejo se considera igual de específico que un segmento de parámetro con una restricción.
  • Los parámetros comodín son los menos específicos. Vea comodín en Referencia de plantilla de ruta para obtener información importante sobre las rutas comodín.

Vea el código fuente en GitHub para obtener una referencia de los valores exactos.

Conceptos de generación de direcciones URL

Generación de direcciones URL:

  • Es el proceso por el cual el enrutamiento puede crear una ruta de dirección URL en función de un conjunto de valores de ruta.
  • Permite una separación lógica entre los puntos de conexión y las direcciones URL que acceden a ellos.

El enrutamiento de punto de conexión incluye la API LinkGenerator. LinkGenerator es un servicio singleton disponible desde la DI. La API LinkGenerator se puede usar fuera del contexto de una solicitud en ejecución. Mvc.IUrlHelper y los escenarios que dependen de IUrlHelper, como los asistentes de etiquetas, los de HTML y los resultados de acción, usan de forma interna la API LinkGenerator para proporcionar funciones de generación de vínculos.

El generador de vínculos está respaldado por el concepto de una dirección y esquemas de direcciones. Un esquema de direcciones es una manera de determinar los puntos de conexión que se deben tener en cuenta para la generación de vínculos. Por ejemplo, los escenarios de nombre y valores de ruta de controladores y Razor Pages con los que muchos usuarios están familiarizados se implementan como un esquema de direcciones.

El generador de vínculos puede vincular a controladores y Razor Pages a través de los métodos de extensión siguientes:

Las sobrecargas de estos métodos aceptan argumentos que incluyan HttpContext. Estos métodos son funcionalmente equivalentes a Url.Action y Url.Page, pero ofrecen flexibilidad y opciones adicionales.

Los métodos GetPath* son más similares a Url.Action y Url.Page, dado que generan un URI que contiene una ruta de acceso absoluta. Los métodos GetUri* siempre generan un URI absoluto que contiene un esquema y un host. Los métodos que aceptan HttpContext generan un URI en el contexto de la solicitud que se ejecuta. A menos que se reemplacen, se usan los valores de ruta de ambiente, la ruta de acceso base de la dirección URL, el esquema y el host de la solicitud en ejecución.

Se llama a LinkGenerator con una dirección. La generación de un URI se produce en dos pasos:

  1. Se enlaza una dirección a una lista de puntos de conexión que coincidan con la dirección.
  2. Se evalúa el elemento RoutePattern de cada punto de conexión hasta que se encuentra un patrón de ruta que coincida con los valores proporcionados. La salida resultante se combina con otras partes del URI proporcionadas al generador de vínculos y devueltas.

Los métodos proporcionados por LinkGenerator admiten funciones estándar de generación de vínculos para cualquier tipo de dirección. La forma más práctica de usar el generador de vínculos es a través de métodos de extensión que realicen operaciones para un tipo de dirección específica:

Método de extensión Descripción
GetPathByAddress Genera un URI con una ruta de acceso absoluta en función de los valores proporcionados.
GetUriByAddress Genera un URI absoluto en función de los valores proporcionados.

Advertencia

Preste atención a las consecuencias siguientes de llamar a los métodos LinkGenerator:

  • Use los métodos de extensión GetUri* con precaución en una configuración de aplicación en la que no se valide el encabezado Host de las solicitudes entrantes. Si no se valida el encabezado Host de las solicitudes entrantes, la entrada de la solicitud que no sea de confianza se puede devolver al cliente en los URI de una página o vista. Se recomienda que todas las aplicaciones de producción configuren su servidor para validar el encabezado Host en función de valores válidos conocidos.

  • Use LinkGenerator con precaución en el middleware junto con Map o MapWhen. Map* cambia la ruta de acceso base de la solicitud que se ejecuta, lo que afecta a la salida de la generación de vínculos. Todas las API de LinkGenerator permiten especificar una ruta de acceso base. Especifique una ruta de acceso base vacía para deshacer el efecto de Map* en la generación de vínculos.

Ejemplo de middleware

En el ejemplo siguiente, un middleware usa la API LinkGenerator para crear un vínculo a un método de acción que enumera los productos de la tienda. El uso del generador de vínculos mediante su inserción en una clase y la llamada a GenerateLink está disponible para cualquier clase de una aplicación:

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

Referencia de plantilla de ruta

Los tokens de {} definen parámetros de ruta que se enlazan si se encuentran coincidencias con la ruta. Se puede definir más de un parámetro de ruta en un segmento de ruta, pero deben estar separados por un valor literal. Por ejemplo, {controller=Home}{action=Index} no es una ruta válida, ya que no hay ningún valor literal entre {controller} y {action}. Los parámetros de ruta deben tener un nombre y, opcionalmente, atributos adicionales especificados.

El texto literal diferente de los parámetros de ruta (por ejemplo, {id}) y el separador de ruta / deben coincidir con el texto de la dirección URL. La coincidencia de texto no distingue mayúsculas de minúsculas y se basa en la representación descodificada de la ruta de las direcciones URL. Para que el delimitador de parámetro de ruta literal { o } coincida, repita el carácter para aplicar escape al carácter. Por ejemplo, {{ o }}.

Asterisco * o asterisco doble **:

  • Se puede usar como prefijo de un parámetro de ruta para enlazar con el resto del URI.
  • Se denominan parámetros comodín. Por ejemplo, blog/{**slug}:
    • Coincide con cualquier URI que empiece por /blog y después tenga cualquier valor.
    • El valor que aparece detrás de /blog se asigna al valor de ruta slug.

Advertencia

Un parámetro catch-all puede relacionar rutas de forma incorrecta debido a un error en el enrutamiento. Las aplicaciones afectadas por este error tienen las características siguientes:

  • Una ruta catch-all (por ejemplo, {**slug}")
  • La ruta catch-all causa un error al relacionar solicitudes que sí que debería relacionar.
  • Al quitar otras rutas, la ruta catch-all empieza a funcionar.

Para ver casos de ejemplo relacionados con este error, consulte los errores 18677 y 16579 en GitHub.

Se incluye una corrección de participación para este error en el SDK de .NET Core 3.1.301 y versiones posteriores. En el código que hay a continuación se establece un cambio interno que corrige este error:

public static void Main(string[] args)
{
   AppContext.SetSwitch("Microsoft.AspNetCore.Routing.UseCorrectCatchAllBehavior", 
                         true);
   CreateHostBuilder(args).Build().Run();
}
// Remaining code removed for brevity.

Los parámetros comodín también pueden coincidir con una cadena vacía.

El parámetro comodín inserta los caracteres de escape correspondientes cuando se usa la ruta para generar una dirección URL, incluidos los caracteres / de separación de ruta de acceso. Por ejemplo, la ruta foo/{*path} con valores de ruta { path = "my/path" } genera foo/my%2Fpath. Tenga en cuenta la barra diagonal de escape. Para los caracteres separadores de ruta de acceso de ida y vuelta, use el prefijo de parámetro de ruta **. La ruta foo/{**path} con { path = "my/path" } genera foo/my/path.

Los patrones de dirección URL que intentan capturar un nombre de archivo con una extensión de archivo opcional tienen consideraciones adicionales. Por ejemplo, considere la plantilla files/{filename}.{ext?}. Cuando existen valores para filename y ext, los dos valores se rellenan. Si solo existe un valor para filename en la dirección URL, la ruta coincide porque el carácter . final es opcional. Las direcciones URL siguientes coinciden con esta ruta:

  • /files/myFile.txt
  • /files/myFile

Los parámetros de ruta pueden tener valores predeterminados designados mediante la especificación del valor predeterminado después del nombre de parámetro, separado por un signo igual (=). Por ejemplo, {controller=Home} define Home como el valor predeterminado de controller. El valor predeterminado se usa si no hay ningún valor en la dirección URL para el parámetro. Los parámetros de ruta se pueden convertir en opcionales si se anexa un signo de interrogación (?) al final del nombre del parámetro. Por ejemplo: id?. La diferencia entre los valores opcionales y los parámetros de ruta predeterminados es:

  • Un parámetro de ruta con un valor predeterminado siempre produce un valor.
  • Un parámetro opcional solo tiene un valor cuando la dirección URL de la solicitud proporciona un valor.

Los parámetros de ruta pueden tener restricciones que deben coincidir con el valor de ruta enlazado desde la dirección URL. Al agregar : y un nombre de restricción después del nombre del parámetro de ruta, se especifica una restricción insertada en un parámetro de ruta. Si la restricción requiere argumentos, se incluyen entre paréntesis (...) después del nombre de restricción. Se pueden especificar varias restricciones insertadas si se anexa otro carácter : y un nombre de restricción.

El nombre de restricción y los argumentos se pasan al servicio IInlineConstraintResolver para crear una instancia de IRouteConstraint para su uso en el procesamiento de direcciones URL. Por ejemplo, la plantilla de ruta blog/{article:minlength(10)} especifica una restricción minlength con el argumento 10. Para obtener más información sobre las restricciones de ruta y una lista de las restricciones proporcionadas por el marco de trabajo, vea la sección Referencia de restricciones de ruta.

Los parámetros de ruta también pueden tener transformadores de parámetros. Los transformadores de parámetros transforman el valor de un parámetro al generar vínculos y hacer coincidir acciones y páginas con direcciones URL. Como sucede con las restricciones, los transformadores de parámetros se pueden agregar en línea a un parámetro de ruta mediante la incorporación un carácter : y un nombre de transformador después del nombre del parámetro de ruta. Por ejemplo, la plantilla de ruta blog/{article:slugify} especifica un transformador slugify. Para obtener más información sobre los transformadores de parámetros, vea la sección Referencia de transformadores de parámetros.

En la tabla siguiente se muestran plantillas de ruta de ejemplo y su comportamiento:

Plantilla de ruta URI coincidente de ejemplo El URI de la solicitud...
hello /hello Solo coincide con la ruta de acceso única /hello.
{Page=Home} / Coincide y establece Page en Home.
{Page=Home} /Contact Coincide y establece Page en Contact.
{controller}/{action}/{id?} /Products/List Se asigna al controlador Products y la acción List.
{controller}/{action}/{id?} /Products/Details/123 Se asigna al controlador Products y la acción Details con id establecido en 123.
{controller=Home}/{action=Index}/{id?} / Se asigna al controlador Home y al método Index. id se pasa por alto.
{controller=Home}/{action=Index}/{id?} /Products Se asigna al controlador Products y al método Index. id se pasa por alto.

El uso de una plantilla suele ser el método de enrutamiento más sencillo. Las restricciones y los valores predeterminados también se pueden especificar fuera de la plantilla de ruta.

Segmentos complejos

Los segmentos complejos se procesan mediante la búsqueda de coincidencias de delimitadores literales de derecha a izquierda de un modo no expansivo. Por ejemplo, [Route("/a{b}c{d}")] es un segmento complejo. Los segmentos complejos funcionan de una manera determinada que se debe entender para usarlos correctamente. En el ejemplo de esta sección se muestra por qué los segmentos complejos solo funcionan bien cuando el texto del delimitador no aparece dentro de los valores de los parámetros. En casos más complejos es necesario usar una expresión regular y extraer los valores de forma manual.

Advertencia

Cuando se usa System.Text.RegularExpressions para procesar entradas que no son de confianza, pase un tiempo de expiración. Un usuario malintencionado puede proporcionar entradas a RegularExpressions y provocar un ataque por denegación de servicio. Las API del marco ASP.NET Core en las que se usa RegularExpressions pasan un tiempo de expiración.

Este es un resumen de los pasos que realiza el enrutamiento con la plantilla /a{b}c{d} y la ruta de dirección URL /abcd. | se usa para ayudar a visualizar cómo funciona el algoritmo:

  • El primer literal, de derecha a izquierda, es c. Por tanto, se busca en /abcd desde la derecha y se encuentra /ab|c|d.
  • Todo lo que se encuentra a la derecha (d) coincide ahora con el parámetro de ruta {d}.
  • El siguiente literal, de derecha a izquierda, es a. Por tanto, se busca en /ab|c|d a partir de donde se ha parado antes, después a y se encuentra /|a|b|c|d.
  • El valor situado a la derecha (b) coincide ahora con el parámetro de ruta {b}.
  • No queda ningún texto ni ninguna plantilla de ruta, por lo que se trata de una coincidencia.

Este es un ejemplo de un caso negativo en el que se usa la misma plantilla /a{b}c{d} y la ruta de dirección URL /aabcd. | se usa para ayudar a visualizar cómo funciona el algoritmo. Este caso no es una coincidencia, que se explica mediante el mismo algoritmo:

  • El primer literal, de derecha a izquierda, es c. Por tanto, se busca en /aabcd desde la derecha y se encuentra /aab|c|d.
  • Todo lo que se encuentra a la derecha (d) coincide ahora con el parámetro de ruta {d}.
  • El siguiente literal, de derecha a izquierda, es a. Por tanto, se busca en /aab|c|d a partir de donde se ha parado antes, después a y se encuentra /a|a|b|c|d.
  • El valor situado a la derecha (b) coincide ahora con el parámetro de ruta {b}.
  • En este momento, todavía hay texto a, pero el algoritmo se ha quedado sin plantilla de ruta para analizar, por lo que no es una coincidencia.

Como el algoritmo de búsqueda de coincidencias es no expansivo:

  • Coincide con la menor cantidad de texto posible en cada paso.
  • Cualquier caso en el que el valor de delimitador aparezca dentro de los valores de parámetro provoca que no coincida.

Las expresiones regulares proporcionan un mayor control sobre el comportamiento de búsqueda de coincidencias.

La coincidencia expansiva, también conocida como coincidencia diferida, coincide con la cadena más grande posible. La búsqueda no expansiva coincide con la cadena más pequeña posible.

Referencia de restricción de ruta

Las restricciones de ruta se ejecutan cuando se ha producido una coincidencia con la dirección URL entrante y la ruta de dirección URL se convierte en tokens en valores de ruta. En general, las restricciones de ruta inspeccionan el valor de ruta asociado a través de la plantilla de ruta y deciden si el valor es aceptable o no. Algunas restricciones de ruta usan datos ajenos al valor de ruta para decidir si la solicitud se puede enrutar. Por ejemplo, HttpMethodRouteConstraint puede aceptar o rechazar una solicitud basada en su verbo HTTP. Las restricciones se usan en las solicitudes de enrutamiento y la generación de vínculos.

Advertencia

No use las restricciones para la validación de entradas. Si se usan restricciones para la validación de entradas, las que no sean válidas generan una respuesta 404 No encontrado. Una entrada no válida debería generar 400 Solicitud incorrecta con un mensaje de error adecuado. Las restricciones de ruta se usan para eliminar la ambigüedad entre rutas similares, no para validar las entradas de una ruta determinada.

En la tabla siguiente se muestran restricciones de ruta de ejemplo y su comportamiento esperado:

restricción Ejemplo Coincidencias de ejemplo Notas
int {id:int} 123456789, -123456789 Coincide con cualquier entero
bool {active:bool} true, FALSE Coincide con true o false. No distingue mayúsculas de minúsculas
datetime {dob:datetime} 2016-12-31, 2016-12-31 7:32pm Coincide con un valor DateTime válido en la referencia cultural invariable. Vea la advertencia anterior.
decimal {price:decimal} 49.99, -1,000.01 Coincide con un valor decimal válido en la referencia cultural invariable. Vea la advertencia anterior.
double {weight:double} 1.234, -1,001.01e8 Coincide con un valor double válido en la referencia cultural invariable. Vea la advertencia anterior.
float {weight:float} 1.234, -1,001.01e8 Coincide con un valor float válido en la referencia cultural invariable. Vea la advertencia anterior.
guid {id:guid} CD2C1638-1638-72D5-1638-DEADBEEF1638 Coincide con un valor Guid válido
long {ticks:long} 123456789, -123456789 Coincide con un valor long válido
minlength(value) {username:minlength(4)} Rick La cadena debe tener al menos cuatro caracteres
maxlength(value) {filename:maxlength(8)} MyFile La cadena no debe tener más de ocho caracteres
length(length) {filename:length(12)} somefile.txt La cadena debe tener una longitud de exactamente 12 caracteres
length(min,max) {filename:length(8,16)} somefile.txt La cadena debe tener una longitud como mínimo de ocho caracteres y como máximo de 16
min(value) {age:min(18)} 19 El valor entero debe ser como mínimo 18
max(value) {age:max(120)} 91 El valor entero debe ser como máximo 120
range(min,max) {age:range(18,120)} 91 El valor entero debe ser como mínimo 18 y máximo 120
alpha {name:alpha} Rick La cadena debe constar de uno o más caracteres alfabéticos, a-z y no distinguir mayúsculas de minúsculas.
regex(expression) {ssn:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)} 123-45-6789 La cadena debe coincidir con la expresión regular. Vea las sugerencias sobre cómo definir una expresión regular.
required {name:required} Rick Se usa para exigir que un valor que no es de parámetro esté presente durante la generación de dirección URL

Advertencia

Cuando se usa System.Text.RegularExpressions para procesar entradas que no son de confianza, pase un tiempo de expiración. Un usuario malintencionado puede proporcionar entradas a RegularExpressions y provocar un ataque por denegación de servicio. Las API del marco ASP.NET Core en las que se usa RegularExpressions pasan un tiempo de expiración.

Es posible aplicar varias restricciones delimitadas por dos puntos a un único parámetro. Por ejemplo, la siguiente restricción permite limitar un parámetro a un valor entero de 1 o superior:

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

Advertencia

Las restricciones de ruta que comprueban la dirección URL y que se convierten en un tipo CLR siempre usan la referencia cultural invariable. Por ejemplo, la conversión al tipo int o DateTime de CLR. Estas restricciones dan por supuesto que la dirección URL no es localizable. Las restricciones de ruta proporcionadas por el marco de trabajo no modifican los valores almacenados en los valores de ruta. Todos los valores de ruta analizados desde la dirección URL se almacenan como cadenas. Por ejemplo, la restricción float intenta convertir el valor de ruta en un valor Float, pero el valor convertido se usa exclusivamente para comprobar que se puede convertir en Float.

Expresiones regulares en restricciones

Advertencia

Cuando se usa System.Text.RegularExpressions para procesar entradas que no son de confianza, pase un tiempo de expiración. Un usuario malintencionado puede proporcionar entradas a RegularExpressions y provocar un ataque por denegación de servicio. Las API del marco ASP.NET Core en las que se usa RegularExpressions pasan un tiempo de expiración.

Las expresiones regulares se pueden especificar como restricciones insertadas mediante la restricción de ruta regex(...). Los métodos de la familia MapControllerRoute también aceptan un literal de objeto de restricciones. Si se usa ese formato, los valores de cadena se interpretan como expresiones regulares.

En el código siguiente se usa una restricción de expresión regular insertada:

app.UseEndpoints(endpoints =>
{
    endpoints.MapGet("{message:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)}",
        context => 
        {
            return context.Response.WriteAsync("inline-constraint match");
        });
 });

En el código siguiente se usa un literal de objeto para especificar una restricción de expresión regular:

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

El marco de trabajo de ASP.NET Core agrega RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant al constructor de expresiones regulares. Vea RegexOptions para obtener una descripción de estos miembros.

Las expresiones regulares usan delimitadores y tokens similares a los que usan el enrutamiento y el lenguaje C#. Es necesario usar secuencias de escape con los tokens de expresiones regulares. Para usar la expresión regular ^\d{3}-\d{2}-\d{4}$ en una restricción insertada, utilice una de las opciones siguientes:

  • Reemplace los caracteres \ proporcionados en la cadena como caracteres \\ en el archivo de código fuente de C# para aplicar secuencias de escape al carácter de escape de cadena \.
  • Literales de cadena textual.

Para aplicar secuencias de escape a los caracteres delimitadores de parámetro de enrutamiento ({, }, [ y ]), duplique los caracteres en la expresión, por ejemplo {{, }}, [[ y ]]. En la tabla siguiente se muestra una expresión regular y su versión con la secuencia de escape:

Expresión regular Expresión regular con secuencia de escape
^\d{3}-\d{2}-\d{4}$ ^\\d{{3}}-\\d{{2}}-\\d{{4}}$
^[a-z]{2}$ ^[[a-z]]{{2}}$

Las expresiones regulares que se usan en el enrutamiento suelen empezar con el carácter ^ y coincidir con la posición inicial de la cadena. Las expresiones suelen terminar con el carácter $ y coincidir con el final de la cadena. Los caracteres ^ y $ garantizan que la expresión regular coincide con el valor completo del parámetro de ruta. Sin los caracteres ^ y $, la expresión regular coincide con cualquier subcadena de la cadena, lo que normalmente no es deseable. En la tabla siguiente se proporcionan ejemplos y se explica por qué coinciden o no:

Expresión String Coincidir con Comentario
[a-z]{2} hello Coincidencias de subcadenas
[a-z]{2} 123abc456 Coincidencias de subcadenas
[a-z]{2} mz Coincide con la expresión
[a-z]{2} MZ No distingue mayúsculas de minúsculas
^[a-z]{2}$ hello No Vea ^ y $ más arriba
^[a-z]{2}$ 123abc456 No Vea ^ y $ más arriba

Para obtener más información sobre la sintaxis de expresiones regulares, vea Expresiones regulares de .NET Framework.

Para restringir un parámetro a un conjunto conocido de valores posibles, use una expresión regular. Por ejemplo, {action:regex(^(list|get|create)$)} solo hace coincidir el valor de ruta action con list, get o create. Si se pasa al diccionario de restricciones, la cadena ^(list|get|create)$ es equivalente. Las restricciones que se pasan al diccionario de restricciones que no coinciden con una de las conocidas también se tratan como expresiones regulares. Las restricciones que se pasan en una plantilla y que no coinciden con una de las conocidas no se tratan como expresiones regulares.

Restricciones de ruta personalizadas

Se pueden crear restricciones de ruta personalizadas mediante la implementación de la interfaz IRouteConstraint. La interfaz IRouteConstraint contiene Match, que devuelve true si se cumple la restricción, y false en caso contrario.

Las restricciones de ruta personalizadas rara vez son necesarias. Antes de implementar una restricción de ruta personalizada, considere alternativas, como el enlace de modelos.

En la carpeta Constraints de ASP.NET Core se proporcionan buenos ejemplos de creación de restricciones. Por ejemplo, GuidRouteConstraint.

Para usar una restricción IRouteConstraint personalizada, el tipo de restricción de ruta se debe registrar con el parámetro ConstraintMap de la aplicación en el contenedor de servicios. ConstraintMap es un diccionario que asigna claves de restricciones de ruta a implementaciones de IRouteConstraint que validen esas restricciones. El parámetro ConstraintMap de una aplicación puede actualizarse en Startup.ConfigureServices como parte de una llamada a services.AddRouting o configurando RouteOptions directamente con services.Configure<RouteOptions>. Por ejemplo:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();

    services.AddRouting(options =>
    {
        options.ConstraintMap.Add("customName", typeof(MyCustomConstraint));
    });
}

La restricción anterior se aplica en el código siguiente:

[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);
    }
}

El paquete de NuGet Rick.Docs.Samples.RouteInfo proporciona MyDisplayRouteInfo y se muestra la información de ruta.

La implementación de MyCustomConstraint impide que 0 se aplique a un parámetro de ruta:

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;
    }
}

Advertencia

Cuando se usa System.Text.RegularExpressions para procesar entradas que no son de confianza, pase un tiempo de expiración. Un usuario malintencionado puede proporcionar entradas a RegularExpressions y provocar un ataque por denegación de servicio. Las API del marco ASP.NET Core en las que se usa RegularExpressions pasan un tiempo de expiración.

El código anterior:

  • Impide 0 en el segmento {id} de la ruta.
  • Se muestra para proporcionar un ejemplo básico de implementación de una restricción personalizada. No se debe usar en una aplicación de producción.

El código siguiente es un enfoque mejor para impedir que se procese un valor id que contenga 0:

[HttpGet("{id}")]
public IActionResult Get(string id)
{
    if (id.Contains('0'))
    {
        return StatusCode(StatusCodes.Status406NotAcceptable);
    }

    return ControllerContext.MyDisplayRouteInfo(id);
}

El código anterior tiene las ventajas siguientes con respecto al enfoque de MyCustomConstraint:

  • No requiere una restricción personalizada.
  • Devuelve un error más descriptivo cuando el parámetro de ruta incluye 0.

Referencia de transformadores de parámetros

Transformadores de parámetros:

Por ejemplo, un transformador de parámetros personalizado slugify en el patrón de ruta blog\{article:slugify} con Url.Action(new { article = "MyTestArticle" }) genera blog\my-test-article.

Considere la siguiente implementación de 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();
    }
}

Para usar un transformador de parámetros en un patrón de ruta, configúrelo con ConstraintMap en Startup.ConfigureServices:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();

    services.AddRouting(options =>
    {
        options.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer);
    });
}

El marco ASP.NET Core usa los transformadores de parámetros para transformar el URI en el que se resuelve un punto de conexión. Por ejemplo, los transformadores de parámetros transforman los valores de ruta que se usan para hacer coincidir objetos area, controller, action y page.

routes.MapControllerRoute(
    name: "default",
    template: "{controller:slugify=Home}/{action:slugify=Index}/{id?}");

Con la plantilla de ruta anterior, la acción SubscriptionManagementController.GetAll coincide con el URI /subscription-management/get-all. Un transformador de parámetros no cambia los valores de ruta usados para generar un vínculo. Por ejemplo, Url.Action("GetAll", "SubscriptionManagement") genera /subscription-management/get-all.

ASP.NET Core proporciona convenciones de API para usar transformadores de parámetros con rutas generadas:

Referencia de generación de direcciones URL

Esta sección contiene una referencia para el algoritmo implementado por la generación de direcciones URL. En la práctica, los ejemplos más complejos de generación de direcciones URL usan controladores o Razor Pages. Vea Enrutamiento en controladores para obtener información adicional.

El proceso de generación de direcciones URL comienza con una llamada a LinkGenerator.GetPathByAddress o un método similar. Al método se le proporciona una dirección, un conjunto de valores de ruta y, opcionalmente, información sobre la solicitud actual de HttpContext.

El primer paso consiste en usar la dirección para resolver un conjunto de puntos de conexión candidatos con una instancia de IEndpointAddressScheme<TAddress> que coincide con el tipo de la dirección.

Una vez que el esquema de direcciones encuentra el conjunto de candidatos, los puntos de conexión se ordenan y procesan de forma iterativa hasta que se realiza correctamente una operación de generación de direcciones URL. La generación de direcciones URL no comprueba si hay ambigüedades; el primer resultado devuelto es el resultado final.

Solución de problemas de generación de direcciones URL con registro

El primer paso para solucionar problemas de generación de direcciones URL consiste en establecer el nivel de registro de Microsoft.AspNetCore.Routing en TRACE. LinkGenerator registra muchos detalles sobre su procesamiento que pueden ser útiles para solucionar problemas.

Vea Referencia de generación de direcciones URL para obtener más información sobre la generación de direcciones URL.

Direcciones

Las direcciones son el concepto de la generación de direcciones URL que se usa para enlazar una llamada al generador de vínculos a un conjunto de puntos de conexión candidatos.

Las direcciones son un concepto extensible que incluyen dos implementaciones de forma predeterminada:

  • Con el nombre del punto de conexión (string) como dirección:
    • Proporciona una funcionalidad similar al nombre de ruta de MVC.
    • Usa el tipo de metadatos de IEndpointNameMetadata.
    • Resuelve la cadena proporcionada con los metadatos de todos los puntos de conexión registrados.
    • Inicia una excepción durante el inicio si varios puntos de conexión usan el mismo nombre.
    • Se recomienda para uso general fuera de los controladores y Razor Pages.
  • Con los valores de ruta (RouteValuesAddress) como dirección:
    • Proporciona una funcionalidad similar a los controladores y la generación de direcciones URL heredada de Razor Pages.
    • La ampliación y depuración son complejas.
    • Proporciona la implementación que usa IUrlHelper, aplicaciones auxiliares de etiquetas, aplicaciones auxiliares HTML, resultados de acciones, etc.

El papel del esquema de direcciones consiste en establecer la asociación entre la dirección y los puntos de conexión coincidentes mediante criterios arbitrarios:

  • El esquema de nombres de punto de conexión realiza una búsqueda de diccionario básica.
  • El esquema de valores de ruta tiene un subconjunto óptimo de algoritmos definidos complejo.

Valores de ambiente y valores explícitos

A partir de la solicitud actual, el enrutamiento accede a los valores de ruta del objeto HttpContext.Request.RouteValues de la solicitud actual. Los valores asociados a la solicitud actual se conocen como valores de ambiente. Para mayor claridad, en la documentación se hace referencia a los valores de ruta que se pasan a los métodos como valores explícitos.

En el ejemplo siguiente se muestran valores de ambiente y valores explícitos. Proporciona valores de ambiente de la solicitud actual y valores explícitos, { 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);
    }

El código anterior:

En el código siguiente no se proporcionan valores de ambiente y valores explícitos, { controller = "Home", action = "Subscribe", id = 17, }:

public IActionResult Index2()
{
    var url = _linkGenerator.GetPathByAction("Subscribe", "Home",
                                             new { id = 17, });
    return Content(url);
}

El método anterior devuelve /Home/Subscribe/17.

El código siguiente en WidgetController devuelve /Widget/Subscribe/17:

var url = _linkGenerator.GetPathByAction("Subscribe", null,
                                         new { id = 17, });

En el código siguiente se proporciona el controlador a partir de los valores de ambiente de la solicitud actual y los valores explícitos, { action = "Edit", id = 17, }:

public class GadgetController : Controller
{
    public IActionResult Index()
    {
        var url = Url.Action("Edit", new { id = 17, });
        return Content(url);
    }

En el código anterior:

  • Se devuelve /Gadget/Edit/17.
  • Url obtiene el objeto IUrlHelper.
  • Action genera una dirección URL con una ruta de acceso absoluta para un método de acción. La dirección URL contiene el nombre de action especificado y los valores route.

En el código siguiente se proporcionan valores de ambiente de la solicitud actual y valores explícitos: { page = "./Edit, id = 17, }:

public class IndexModel : PageModel
{
    public void OnGet()
    {
        var url = Url.Page("./Edit", new { id = 17, });
        ViewData["URL"] = url;
    }
}

En el código anterior se establece url en /Edit/17 cuando la página de Razor de edición contiene la siguiente directiva de página:

@page "{id:int}"

Si la página de edición no contiene la plantilla de ruta "{id:int}", url es /Edit?id=17.

El comportamiento de IUrlHelper de MVC agrega una capa de complejidad además de las reglas descritas aquí:

  • IUrlHelper siempre proporciona los valores de ruta de la solicitud actual como valores de ambiente.
  • IUrlHelper.Action siempre copia los valores de ruta action y controller actuales como valores explícitos a menos que el desarrollador los invalide.
  • IUrlHelper.Page siempre copia el valor de ruta page actual como un valor explícito a menos que se invalide.
  • IUrlHelper.Page siempre invalida el valor de ruta handler actual con null como un valor explícito a menos que se invalide.

A los usuarios a menudo les sorprenden los detalles del comportamiento de los valores de ambiente, ya que MVC no parece seguir sus propias reglas. Por motivos históricos y de compatibilidad, algunos valores de ruta como action, controller, page y handler tienen su propio comportamiento de caso especial.

La funcionalidad equivalente proporcionada por LinkGenerator.GetPathByAction y LinkGenerator.GetPathByPage duplica estas anomalías de IUrlHelper por motivos de compatibilidad.

Proceso de generación de direcciones URL

Una vez que se encuentra el conjunto de puntos de conexión candidatos, el algoritmo de generación de direcciones URL:

  • Procesa los puntos de conexión de forma iterativa.
  • Devuelve el primer resultado correcto.

El primer paso de este proceso se denomina invalidación del valor de ruta. La invalidación del valor de ruta es el proceso por el que el enrutamiento decide qué valores de ruta de los valores de ambiente se deben usar y cuáles se deben omitir. Cada valor de ambiente se tiene en cuenta y se combina con los valores explícitos, o bien se pasa por alto.

La mejor manera de pensar en el rol de los valores de ambiente es que intentan ahorrar trabajo a los desarrolladores de aplicaciones, en algunos casos comunes. Tradicionalmente, los escenarios en los que los valores de ambiente son útiles están relacionados con MVC:

  • Al vincular a otra acción en el mismo controlador, no es necesario especificar el nombre del controlador.
  • Al vincular a otro controlador en la misma área, no es necesario especificar el nombre del área.
  • Al vincular al mismo método de acción, no es necesario especificar los valores de ruta.
  • Al vincular a otro elemento de la aplicación, no le interesa transferir valores de ruta que no tengan ningún significado en ese elemento del control de la aplicación.

Las llamadas a LinkGenerator o IUrlHelper que devuelven null se suelen deber a que no se comprende la invalidación del valor de ruta. Para solucionar problemas de invalidación del valor de ruta, especifique de forma explícita más valores de ruta para ver si eso resuelve el problema.

La invalidación del valor de ruta se basa en la suposición de que el esquema de direcciones URL de la aplicación es jerárquico, con una jerarquía formada de izquierda a derecha. Considere la posibilidad de usar la plantilla de ruta de controlador básica {controller}/{action}/{id?} para hacerse una idea intuitiva de cómo funciona esto en la práctica. Un cambio en un valor invalida todos los valores de ruta que aparecen a la derecha. Esto refleja la suposición sobre la jerarquía. Si la aplicación tiene un valor de ambiente para id y la operación especifica otro valor para controller:

  • id no se reutilizará porque {controller} está a la izquierda de {id?}.

Algunos ejemplos demuestran este principio:

  • Si los valores explícitos contienen un valor para id, se omite el valor de ambiente de id. Se pueden usar los valores de ambiente para controller y action.
  • Si los valores explícitos contienen un valor para action, se omite cualquier valor de ambiente para action. Se pueden usar los valores de ambiente para controller. Si el valor explícito para action es diferente del valor de ambiente para action, no se usará el valor de id. Si el valor explícito para action es diferente del valor de ambiente para action, se puede usar el valor de id.
  • Si los valores explícitos contienen un valor para controller, se omite cualquier valor de ambiente para controller. Si el valor explícito para controller es diferente del valor de ambiente para controller, no se usarán los valores de action y id. Si el valor explícito para controller es igual que el valor de ambiente para controller, se pueden usar los valores de action y id.

Este proceso sea complica todavía más por la existencia de rutas de atributo y rutas convencionales dedicadas. Las rutas convencionales de controlador, como {controller}/{action}/{id?}, especifican una jerarquía mediante parámetros de ruta. Para las rutas convencionales dedicadas y las rutas de atributo a controladores y Razor Pages:

  • Existe una jerarquía de valores de ruta.
  • No aparecen en la plantilla.

En estos casos, la generación de direcciones URL define el concepto de valores necesarios. Los puntos de conexión creados por controladores y Razor Pages tienen valores necesarios especificados que permiten que la invalidación del valor de ruta funcione.

El algoritmo de invalidación del valor de ruta en detalle:

  • Los nombres de valor necesarios se combinan con los parámetros de ruta y, después, se procesan de izquierda a derecha.
  • Para cada parámetro, se comparan el valor de ambiente y el valor explícito:
    • Si el valor de ambiente y el valor explícito son iguales, el proceso continúa.
    • Si el valor de ambiente está presente y el valor explícito no, se usa el valor de ambiente al generar la dirección URL.
    • Si el valor de ambiente no está presente y el valor explícito sí, rechace el valor de ambiente y todos los posteriores.
    • Si el valor de ambiente y el valor explícito están presentes, y los dos son diferentes, rechace el valor de ambiente y todos los posteriores.

En este punto, la operación de generación de direcciones URL está lista para evaluar las restricciones de ruta. El conjunto de valores aceptados se combina con los valores predeterminados de parámetro, que se proporcionan a las restricciones. Si todas las restricciones son correctas, la operación continúa.

A continuación, se pueden usar los valores aceptados para expandir la plantilla de ruta. La plantilla de ruta se procesa:

  • De izquierda a derecha.
  • En cada parámetro se sustituye su valor aceptado.
  • Con los siguientes casos especiales:
    • Si falta un valor en los valores aceptados y el parámetro tiene un valor predeterminado, se usa el valor predeterminado.
    • Si falta un valor en los valores aceptados y el parámetro es opcional, el procesamiento continúa.
    • Si un parámetro de ruta a la derecha de un parámetro opcional que falta tiene un valor, se produce un error en la operación.
    • Los parámetros con valores predeterminados contiguos y los parámetros opcionales se contraen siempre que sea posible.

Los valores que se proporcionan de forma explícita que no coinciden con un segmento de la ruta se agregan a la cadena de consulta. En la tabla siguiente se muestra el resultado cuando se usa la plantilla de ruta {controller}/{action}/{id?}.

Valores de ambiente Valores explícitos Resultado
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

Problemas con la invalidación del valor de ruta

A partir de ASP.NET Core 3.0, algunos esquemas de generación de direcciones URL que se usaban en versiones anteriores de ASP.NET Core no funcionan bien con la generación de direcciones URL. El equipo de ASP.NET Core planea agregar características para abordar estas necesidades en una versión futura. Por ahora, la mejor solución consiste en usar el enrutamiento heredado.

En el código siguiente se muestra un ejemplo de un esquema de generación de direcciones URL que no es compatible con el enrutamiento.

app.UseEndpoints(endpoints =>
{
    endpoints.MapControllerRoute("default", 
                                     "{culture}/{controller=Home}/{action=Index}/{id?}");
    endpoints.MapControllerRoute("blog", "{culture}/{**slug}", 
                                      new { controller = "Blog", action = "ReadPost", });
});

En el código anterior, se usa el parámetro de ruta culture para la localización. El objetivo es que el parámetro culture siempre se acepte como valor de ambiente. Pero el parámetro culture no se acepta como valor de ambiente debido al funcionamiento de los valores necesarios:

  • En la plantilla de ruta "default", el parámetro de ruta culture está a la izquierda de controller, por lo que los cambios en controller no invalidarán culture.
  • En la plantilla de ruta "blog", se considera que el parámetro de ruta culture está a la derecha de controller, que aparece en los valores necesarios.

Configuración de metadatos de punto de conexión

Los vínculos siguientes proporcionan información sobre la configuración de metadatos de punto de conexión:

Comparación de host en rutas con RequireHost

RequireHost aplica una restricción a la ruta que requiere el host especificado. El parámetro RequireHost o [Host] puede ser:

  • Host: www.domain.com, compara www.domain.com con cualquier puerto.
  • Host con carácter comodín: *.domain.com, coincide con www.domain.com, subdomain.domain.com o www.subdomain.domain.com en cualquier puerto.
  • Puerto: *:5000, coincide con el puerto 5000 con cualquier host.
  • Host y puerto: www.domain.com:5000 o *.domain.com:5000, coincide con el host y el puerto.

Se pueden especificar varios parámetros mediante RequireHost o [Host]. La restricción coincide con los hosts válidos para cualquiera de los parámetros. Por ejemplo, [Host("domain.com", "*.domain.com")] coincide con domain.com, www.domain.com y subdomain.domain.com.

En el código siguiente se usa RequireHost para requerir el host especificado en la ruta:

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

En el código siguiente se usa el atributo [Host] en el controlador para requerir cualquiera de los hosts especificados:

[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();
    }
}

Cuando el atributo [Host] se aplica al método de acción y al controlador:

  • Se usa el atributo en la acción.
  • Se omite el atributo del controlador.

Instrucciones de rendimiento para el enrutamiento

La mayor parte del enrutamiento se ha actualizado en ASP.NET Core 3.0 para aumentar el rendimiento.

Cuando una aplicación tiene problemas de rendimiento, a menudo se sospecha que el enrutamiento es el problema. El motivo es que marcos como los controladores y Razor Pages notifican la cantidad de tiempo empleado en el marco de trabajo en sus mensajes de registro. Cuando hay una diferencia significativa entre el tiempo notificado por los controladores y el tiempo total de la solicitud:

  • Los desarrolladores eliminan el código de la aplicación como origen del problema.
  • Es habitual asumir que el enrutamiento es la causa.

El rendimiento del enrutamiento se prueba mediante miles de puntos de conexión. No es probable que una aplicación típica detecte un problema de rendimiento simplemente por ser demasiado grande. La causa raíz más común del rendimiento lento del enrutamiento suele ser middleware personalizado con un comportamiento incorrecto.

En el ejemplo de código siguiente se muestra una técnica básica para limitar el origen del retraso:

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

Para controlar el tiempo del enrutamiento:

  • Intercale cada middleware con una copia del middleware de tiempo que se muestra en el código anterior.
  • Agregue un identificador único para poner en correlación los datos de control de tiempo con el código.

Se trata de una forma básica de reducir el retraso cuando es significativo, por ejemplo, de más de 10ms. Al restar Time 2 de Time 1 se notifica el tiempo invertido dentro del middleware UseRouting.

En el código siguiente se usa un enfoque más compacto del código de control de tiempo anterior:

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

Características de enrutamiento potencialmente costosas

En la lista siguiente se proporciona información sobre las características de enrutamiento que son relativamente costosas en comparación con las plantillas de ruta básicas:

  • Expresiones regulares: Se pueden escribir expresiones regulares que sean complejas o que tengan un tiempo de ejecución de larga duración con una pequeña cantidad de entradas.
  • Segmentos complejos ({x}-{y}-{z}):
    • Son mucho más costosos que analizar un segmento de ruta de dirección URL convencional.
    • El resultado es que se asignan muchas más subcadenas.
    • La lógica de segmentos complejos no se ha actualizado en la actualización de rendimiento de enrutamiento de ASP.NET Core 3.0.
  • Acceso a datos sincrónicos: muchas aplicaciones complejas tienen acceso a bases de datos como parte de su enrutamiento. Es posible que en ASP.NET Core 2.2 y versiones anteriores el enrutamiento no proporcionara los puntos de extensibilidad correctos para admitir el enrutamiento de acceso a bases de datos. Por ejemplo, IRouteConstraint y IActionConstraint son sincrónicos. Los puntos de extensibilidad como MatcherPolicy y EndpointSelectorContext son asincrónicos.

Instrucciones para los autores de bibliotecas

Esta sección contiene instrucciones para los autores de bibliotecas que realizan la compilación sobre el enrutamiento. Estos detalles están diseñados para garantizar que los desarrolladores de aplicaciones tengan una buena experiencia en el uso de bibliotecas y marcos que amplían el enrutamiento.

Definición de puntos de conexión

Para crear un marco que use el enrutamiento para la coincidencia de direcciones URL, empiece por definir una experiencia de usuario que se base en UseEndpoints.

REALICE la compilación sobre IEndpointRouteBuilder. Esto permite a los usuarios crear el marco de trabajo con otras características de ASP.NET Core sin confusión. Todas las plantillas de ASP.NET Core incluyen el enrutamiento. Asuma que el enrutamiento está presente y es familiar para los usuarios.

app.UseEndpoints(endpoints =>
{
    // Your framework
    endpoints.MapMyFramework(...);

    endpoints.MapHealthChecks("/healthz");
});

DEVUELVA un tipo concreto sellado a partir de una llamada a MapMyFramework(...) que implemente IEndpointConventionBuilder. La mayoría de los métodos Map... del marco siguen este patrón. La interfaz IEndpointConventionBuilder:

  • Permite la composición de metadatos.
  • Es el destino de diversos métodos de extensión.

La declaración de un tipo propio permite agregar funcionalidad específica del marco propia al generador. Es correcto encapsular un generador declarado por el marco y reenviarle llamadas.

app.UseEndpoints(endpoints =>
{
    // Your framework
    endpoints.MapMyFramework(...).RequireAuthorization()
                                 .WithMyFrameworkFeature(awesome: true);

    endpoints.MapHealthChecks("/healthz");
});

CONSIDERE LA POSIBILIDAD de escribir un objeto EndpointDataSource propio. EndpointDataSource es la primitiva de bajo nivel para declarar y actualizar una colección de puntos de conexión. EndpointDataSource es una API eficaz que usan los controladores y Razor Pages.

Las pruebas de enrutamiento tienen un ejemplo básico de un origen de datos que no es de actualización.

NO intente registrar un objeto EndpointDataSource de forma predeterminada. Exija a los usuarios que registren el marco en UseEndpoints. La filosofía del enrutamiento es que nada se incluye de forma predeterminada y que UseEndpoints es el lugar donde se registran los puntos de conexión.

Creación de middleware con enrutamiento integrada

CONSIDERE LA POSIBILIDAD de definir tipos de metadatos como una interfaz.

PERMITA el uso de los tipos de metadatos como atributo en clases y métodos.

public interface ICoolMetadata
{
    bool IsCool { get; }
}

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class CoolMetadataAttribute : Attribute, ICoolMetadata
{
    public bool IsCool => true;
}

Los marcos como los controladores y Razor Pages admiten la aplicación de atributos de metadatos a tipos y métodos. Si declara tipos de metadatos:

  • Haga que sean accesibles como atributos.
  • La mayoría de los usuarios están familiarizados con la aplicación de atributos.

La declaración de un tipo de metadatos como una interfaz agrega otro nivel de flexibilidad:

  • Las interfaces admiten composición.
  • Los desarrolladores pueden declarar tipos propios que combinen varias directivas.

PERMITA que los metadatos se puedan invalidar, como se muestra en el ejemplo siguiente:

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() { }
}

La mejor manera de seguir estas instrucciones es evitar la definición de metadatos de marcador:

  • No busque solo la presencia de un tipo de metadatos.
  • Defina una propiedad en los metadatos y compruébela.

La colección de metadatos está ordenada y admite la invalidación por prioridad. En el caso de los controladores, los metadatos del método de acción son más específicos.

PERMITA que el middleware sea útil con y sin el enrutamiento.

app.UseRouting();

app.UseAuthorization(new AuthorizationPolicy() { ... });

app.UseEndpoints(endpoints =>
{
    // Your framework
    endpoints.MapMyFramework(...).RequireAuthorization();
});

Como ejemplo de esta instrucción, considere la posibilidad de usar el middleware UseAuthorization. El middleware de autorización permite pasar una directiva de reserva. La directiva de reserva, si se especifica, se aplica a:

  • Puntos de conexión sin una directiva especificada.
  • Solicitudes que no coinciden con un punto de conexión.

Esto hace que el middleware de autorización sea útil fuera del contexto del enrutamiento. El middleware de autorización se puede usar para la programación de middleware tradicional.

Diagnóstico de depuración

Para ver la salida detallada del diagnóstico de cálculo de ruta, establezca Logging:LogLevel:Microsoft en Debug. En el entorno de desarrollo, establezca el nivel de registro en appsettings.Development.json:

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