Partager via


Routage dans ASP.NET Core

Par Ryan Nowak, Kirk Larkinet Rick Anderson

Notes

Ceci n’est pas la dernière version de cet article. Pour la version actuelle, consultez la version .NET 8 de cet article.

Avertissement

Cette version d’ASP.NET Core n’est plus prise en charge. Pour plus d’informations, consultez la Stratégie de prise en charge de .NET et .NET Core. Pour la version actuelle, consultez la version .NET 8 de cet article.

Important

Ces informations portent sur la préversion du produit, qui est susceptible d’être en grande partie modifié avant sa commercialisation. Microsoft n’offre aucune garantie, expresse ou implicite, concernant les informations fournies ici.

Pour la version actuelle, consultez la version .NET 8 de cet article.

Le routage est responsable de la correspondance des requêtes HTTP entrantes et de la distribution de ces requêtes aux points de terminaison exécutables de l’application. Les points de terminaison sont les unités de code de gestion des requêtes exécutables de l’application. Les points de terminaison sont définies dans l’application et configurées au démarrage de l’application. Le processus de correspondance de point de terminaison peut extraire des valeurs de l’URL de la requête et fournir ces valeurs pour le traitement des demandes. Avec les informations de point de terminaison fournies par l’application, le routage peut également générer des URL qui mappent vers des points de terminaison.

Les applications peuvent configurer le routage à l’aide des éléments suivants :

Cet article décrit les détails de bas niveau du routage ASP.NET Core. Pour plus d’informations sur la configuration du routage :

Concepts de base du routage

Le code suivant illustre un exemple de routage de base :

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

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

app.Run();

L’exemple précédent inclut un point de terminaison unique à l’aide de la méthode MapGet :

  • Lorsqu’une requête http GET est envoyée à l’URL racine / :
    • Le délégué de requête s’exécute.
    • Hello World! est écrit dans la réponse HTTP.
  • Si la méthode de requête n’est pas GET ou si l’URL racine n’est pas /, aucun routage ne correspond et un HTTP 404 est retourné.

Le routage utilise une paire d’intergiciels, inscrite par UseRouting et UseEndpoints :

  • UseRouting ajoute la correspondance de routage au pipeline d’intergiciels. Cet intergiciel examine l’ensemble des points de terminaison définis dans l’application et sélectionne la meilleure correspondance en fonction de la requête.
  • UseEndpoints ajoute l’exécution du point de terminaison au pipeline de l’intergiciel. Il exécute le délégué associé au point de terminaison sélectionné.

Les applications n’ont généralement pas besoin d’appeler UseRouting ou UseEndpoints. WebApplicationBuilder configure un pipeline d’intergiciels qui encapsule l’intergiciel ajouté dans Program.cs avec UseRouting et UseEndpoints. Toutefois, les applications peuvent modifier l’ordre dans lequel UseRouting et UseEndpoints s’exécutent en appelant ces méthodes explicitement. Par exemple, le code suivant effectue un appel explicite à UseRouting :

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

app.UseRouting();

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

Dans le code précédent :

  • L’appel à app.Use inscrit un intergiciel personnalisé qui s’exécute au début du pipeline.
  • L’appel à UseRouting configure l’intergiciel de correspondance de routage à exécuter après l’intergiciel personnalisé.
  • Le point de terminaison inscrit avec MapGet s’exécute à la fin du pipeline.

Si l’exemple précédent n’incluait pas d’appel à UseRouting, l’intergiciel personnalisé s’exécuterait après l’intergiciel de correspondance de routage.

Remarque : Itinéraires ajoutés directement à l’WebApplication exécution à la fin du pipeline.

Points de terminaison

La méthode MapGet est utilisée pour définir un point de terminaison. Un point de terminaison peut être :

  • Sélectionné, en correspondant à l’URL et à la méthode HTTP.
  • Exécuté, en exécutant le délégué.

Les points de terminaison qui peuvent être mis en correspondance et exécutés par l’application sont configurés dans UseEndpoints. Par exemple, MapGet, MapPost et des méthodes similaires connectent des délégués de requête au système de routage. Des méthodes supplémentaires peuvent être utilisées pour connecter les fonctionnalités d’infrastructure ASP.NET Core au système de routage :

L’exemple suivant montre le routage avec un modèle de routage plus sophistiqué :

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

La chaîne /hello/{name:alpha} est un modèle de routage. Un modèle de routage est utilisé pour configurer la mise en correspondance du point de terminaison. Dans ce cas, le modèle correspond à :

  • Un URL comme /hello/Docs
  • Tout chemin d’URL qui commence par /hello/ suivi d’une séquence de caractères alphabétiques. :alpha applique une contrainte de routage qui fait correspondre uniquement les caractères alphabétiques. Les contraintes de routage sont expliquées plus loin dans cet article.

Deuxième segment du chemin d’URL, {name:alpha} :

L’exemple suivant montre le routage avec les contrôles d’intégrité et l’autorisation :

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

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

L’exemple précédent montre comment :

  • L’intergiciel d’autorisation peut être utilisé avec le routage.
  • Les points de terminaison peuvent être utilisés pour configurer le comportement d’autorisation.

L’appel MapHealthChecks ajoute un point de terminaison de contrôle d’intégrité. Le chaînage RequireAuthorization sur cet appel attache une stratégie d’autorisation au point de terminaison.

Appeler UseAuthentication et UseAuthorization ajoute l’intergiciel d’authentification et d’autorisation. Ces intergiciels sont placés entre UseRouting et UseEndpoints afin qu’ils puissent :

  • Voir le point de terminaison sélectionné par UseRouting.
  • Appliquez une stratégie d’autorisation avant que UseEndpoints les distribue au point de terminaison.

Métadonnées de point de terminaison

Dans l’exemple précédent, il existe deux points de terminaison, mais seul le point de terminaison de contrôle d’intégrité a une stratégie d’autorisation attachée. Si la demande correspond au point de terminaison de contrôle d’intégrité, /healthz, une vérification d’autorisation est effectuée. Cela montre que les points de terminaison peuvent avoir des données supplémentaires attachées. Ces données supplémentaires sont appelées métadonnées de point de terminaison :

  • Les métadonnées peuvent être traitées par un intergiciel prenant en charge le routage.
  • Les métadonnées peuvent être de n’importe quel type .NET.

Concepts de routage

Le système de routage s’appuie sur le pipeline d’intergiciels en ajoutant le concept de point de terminaison puissant. Les points de terminaison représentent des unités des fonctionnalités de l’application qui sont distinctes les unes des autres en termes de routage, d’autorisation et de n’importe quel nombre de systèmes ASP.NET Core.

Définition de point de terminaison ASP.NET Core

Un point de terminaison ASP.NET Core est :

Le code suivant montre comment récupérer et inspecter le point de terminaison correspondant à la requête actuelle :

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

Le point de terminaison, s’il est sélectionné, peut être récupéré à partir de HttpContext. Ses propriétés peuvent être inspectées. Les objets de point de terminaison sont immuables et ne peuvent pas être modifiés après la création. Le type de point de terminaison le plus courant est RouteEndpoint. RouteEndpoint inclut des informations qui lui permettent d’être sélectionné par le système de routage.

Dans le code précédent, app.Use configure un intergiciel inclus.

Le code suivant montre que, selon l’endroit où app.Use est appelé dans le pipeline, il se peut qu’il n’y ait pas de point de terminaison :

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

L’exemple précédent ajoute des instructions Console.WriteLine qui indiquent si un point de terminaison a été sélectionné ou non. Pour plus de clarté, l’exemple affecte un nom complet au point de terminaison / fourni.

L’exemple précédent inclut également des appels vers UseRouting et UseEndpoints pour contrôler exactement quand ces intergiciels s’exécutent dans le pipeline.

L’exécution de ce code avec une URL / affiche :

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

L’exécution de ce code avec toute autre URL affiche :

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

Cette sortie montre que :

  • Le point de terminaison est toujours null avant que soit UseRouting appelé.
  • Si une correspondance est trouvée, le point de terminaison n’est pas null entre UseRouting et UseEndpoints.
  • L’intergiciel UseEndpoints est terminal lorsqu’une correspondance est trouvée. L’intergiciel terminal est défini plus loin dans cet article.
  • L’intergiciel après UseEndpoints s’exécute uniquement lorsqu’aucune correspondance n’est trouvée.

L’intergiciel UseRouting utilise la méthode SetEndpoint pour attacher le point de terminaison au contexte actuel. Il est possible de remplacer l’intergiciel UseRouting par une logique personnalisée et d’obtenir les avantages de l’utilisation de points de terminaison. Les points de terminaison sont une primitive de bas niveau comme l’intergiciel et ne sont pas couplés à l’implémentation du routage. La plupart des applications n’ont pas besoin de remplacer UseRouting par une logique personnalisée.

L’intergiciel UseEndpoints est conçu pour être utilisé en tandem avec l’intergiciel UseRouting. La logique principale pour exécuter un point de terminaison n’est pas compliquée. Utilisez GetEndpoint pour récupérer le point de terminaison, puis appelez sa propriété RequestDelegate.

Le code suivant montre comment l’intergiciel peut influencer ou réagir au routage :

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

L’exemple précédent illustre deux concepts importants :

  • L’intergiciel peut s’exécuter avant UseRouting pour modifier les données sur lesquelles le routage fonctionne.
  • L’intergiciel peut s’exécuter entre UseRouting et UseEndpoints pour traiter les résultats du routage avant l’exécution du point de terminaison.
    • Intergiciel qui s’exécute entre UseRouting et UseEndpoints :
      • Inspecte généralement les métadonnées pour comprendre les points de terminaison.
      • Prend souvent des décisions de sécurité, comme le font UseAuthorization et UseCors.
    • La combinaison d’intergiciels et de métadonnées permet de configurer des stratégies par point de terminaison.

Le code précédent montre un exemple d’intergiciel personnalisé qui prend en charge les stratégies par point de terminaison. L’intergiciel écrit un journal d’audit de l’accès aux données sensibles dans la console. L’intergiciel peut être configuré pour auditer un point de terminaison avec les métadonnées RequiresAuditAttribute. Cet exemple illustre un modèle d’activation dans lequel seuls les points de terminaison marqués comme sensibles sont audités. Il est possible de définir l’inverse de cette logique, en auditant tout ce qui n’est pas marqué comme sécurisé, par exemple. Le système de métadonnées de point de terminaison est flexible. Cette logique peut être conçue de quelque manière que ce soit en fonction du cas d’usage.

L’exemple de code précédent est destiné à illustrer les concepts de base des points de terminaison. L’exemple n’est pas destiné à une utilisation en production. Une version plus complète d’un intergiciel de journal d’audit :

  • Se connecterais à un fichier ou une base de données.
  • Inclurais des détails tels que l’utilisateur, l’adresse IP, le nom du point de terminaison sensible, etc.

Les métadonnées de stratégie d’audit RequiresAuditAttribute sont définies en tant que Attribute pour faciliter l’utilisation avec des infrastructures basées sur des classes telles que des contrôleurs et SignalR. Lors de l’utilisation de route vers le code :

  • Les métadonnées sont attachées à une API de générateur.
  • Les infrastructure basées sur des classes incluent tous les attributs sur la méthode et la classe correspondantes lors de la création de points de terminaison.

Les meilleures pratiques pour les types de métadonnées sont de les définir en tant qu’interfaces ou attributs. Les interfaces et les attributs autorisent la réutilisation du code. Le système de métadonnées est flexible et n’impose aucune limitation.

Comparer l’intergiciel terminal avec le routage

L’exemple suivant illustre à la fois l’intergiciel terminal et le routage :

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

Le style d’intergiciel indiqué avec Approach 1: est l’intergiciel terminal. Il est appelé intergiciel terminal, car il effectue une opération de correspondance :

  • L’opération de correspondance dans l’exemple précédent est Path == "/" pour l’intergiciel et Path == "/Routing" pour le routage.
  • Lorsqu’une correspondance réussit, elle exécute certaines fonctionnalités et retourne, plutôt que d’appeler l’intergiciel next.

Il est appelé intergiciel de terminal, car il met fin à la recherche, exécute certaines fonctionnalités, puis retourne.

La liste suivante compare les intergiciels de terminal avec le routage :

  • Les deux approches permettent de terminer le pipeline de traitement :
    • L’intergiciel met fin au pipeline en retournant plutôt qu’en appelant next.
    • Les points de terminaison sont toujours terminaux.
  • L’intergiciel terminal permet de positionner l’intergiciel à un emplacement arbitraire dans le pipeline :
    • Les points de terminaison s’exécutent à la position de UseEndpoints.
  • L’intergiciel de terminal permet au code arbitraire de déterminer quand l’intergiciel fait correspondre :
    • Le code de correspondance de routage personnalisé peut être détaillé et difficile à écrire correctement.
    • Le routage fournit des solutions simples pour les applications classiques. La plupart des applications ne nécessitent pas de code de correspondance de routage personnalisé.
  • L’interface des points de terminaison avec un intergiciel tel que UseAuthorization et UseCors.
    • L’utilisation d’un intergiciel terminal avec UseAuthorization ou UseCors nécessite une interaction manuelle avec le système d’autorisation.

Un point de terminaison définit les :

  • Le délégué pour traiter les demandes.
  • La collection de métadonnées arbitraires. Les métadonnées sont utilisées pour implémenter des problèmes transversaux basés sur des stratégies et une configuration attachées à chaque point de terminaison.

L’intergiciel terminal peut être un outil efficace, mais peut nécessiter :

  • Une quantité importante de codage et de test.
  • L’intégration manuelle avec d’autres systèmes pour atteindre le niveau de flexibilité souhaité.

Envisagez d’intégrer le routage avant d’écrire un intergiciel terminal.

Les intergiciels terminaux existants qui s’intègrent à Map ou MapWhen peuvent généralement être transformés en point de terminaison prenant en charge le routage. MapHealthChecks illustre le modèle de routeur-ware :

  • Écrire une méthode d’extension sur IEndpointRouteBuilder.
  • Créer un pipeline d’intergiciels imbriqués à l’aide de CreateApplicationBuilder.
  • Attacher l’intergiciel au nouveau pipeline. Dans ce cas, UseHealthChecks.
  • Build le pipeline d’intergiciel dans un RequestDelegate.
  • Appeler Map et fournir le nouveau pipeline d’intergiciels.
  • Retourner l’objet générateur fourni par Map à partir de la méthode d’extension.

Le code suivant montre l’utilisation de MapHealthChecks :

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

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

L’exemple précédent montre pourquoi le retour de l’objet générateur est important. Le renvoi de l’objet générateur permet au développeur d’applications de configurer des stratégies telles que l’autorisation pour le point de terminaison. Dans cet exemple, l’intergiciel de contrôle d’intégrité n’a pas d’intégration directe avec le système d’autorisation.

Le système de métadonnées a été créé en réponse aux problèmes rencontrés par les auteurs d’extensibilité à l’aide de l’intergiciel terminal. Il est problématique pour chaque intergiciel d’implémenter sa propre intégration avec le système d’autorisation.

Correspondance d’URL

  • La correspondance d’URL est le processus par lequel le routage distribue une requête entrante à un point de terminaison.
  • Est basé sur des données dans le chemin d’URL et les en-têtes.
  • Peut être étendu pour prendre en compte toutes les données de la demande.

Lorsqu’un intergiciel de routage s’exécute, il définit les valeurs de Endpoint et de routage et vers une fonctionnalité de requête sur HttpContext à partir de la requête actuelle :

  • L’appel de HttpContext.GetEndpoint obtient le point de terminaison.
  • HttpRequest.RouteValues récupère la collection de valeurs d’itinéraire.

L’intergiciel qui s’exécute après que l’intergiciel de routage puisse inspecter le point de terminaison et prendre des mesures. Par exemple, un intergiciel d’autorisation peut interroger la collection de métadonnées du point de terminaison pour une stratégie d’autorisation. Une fois que tous les intergiciels dans le pipeline de traitement de requêtes sont exécutés, le délégué du point de terminaison sélectionné est appelé.

Le système de routage dans le routage de point de terminaison est responsable de toutes les décisions de distribution. Étant donné que l’intergiciel applique des stratégies basées sur le point de terminaison sélectionné, il est important que :

  • Toute décision susceptible d’affecter la répartition ou l’application de stratégies de sécurité soit prise à l’intérieur du système de routage.

Avertissement

Pour une compatibilité descendante, lorsqu’un délégué de point de terminaison Contrôleur ou Razor Pages est exécuté, les propriétés de RouteContext.RouteData sont définies sur des valeurs appropriées en fonction du traitement des requêtes effectué jusqu’à présent.

Le type RouteContext sera marqué comme obsolète dans une version ultérieure :

  • Migrez RouteData.Values vers HttpRequest.RouteValues.
  • Migrez RouteData.DataTokens pour récupérer IDataTokensMetadata à partir des métadonnées du point de terminaison.

La correspondance d’URL fonctionne dans un ensemble configurable de phases. Dans chaque phase, la sortie est un ensemble de correspondances. L’ensemble de correspondances peut être réduit plus loin par la phase suivante. L’implémentation du routage ne garantit pas un ordre de traitement pour les points de terminaison correspondants. Toutes les correspondances possibles sont traitées simultanément. Les phases de correspondance d’URL se produisent dans l’ordre suivant. ASP.NET Core :

  1. Traite le chemin d’URL par rapport à l’ensemble de points de terminaison et à leurs modèles de routage, en collectant toutes les correspondances.
  2. Prend la liste précédente et supprime les correspondances qui échouent avec les contraintes de routage appliquées.
  3. Prend la liste précédente et supprime les correspondances qui échouent au jeu d’instances MatcherPolicy.
  4. Utilise EndpointSelector pour prendre une décision finale à partir de la liste précédente.

La liste des points de terminaison est hiérarchisée en fonction des éléments suivants :

Tous les points de terminaison correspondants sont traités dans chaque phase jusqu’à ce que EndpointSelector soit atteint. EndpointSelector est la phase finale. Il choisit le point de terminaison avec la priorité la plus élevée parmi les correspondances comme correspondance optimale. S’il existe d’autres correspondances avec la même priorité que la meilleure correspondance, une exception de correspondance ambiguë est levée.

La priorité du routage est calculée en fonction d’un modèle de routage plus spécifique qui reçoit une priorité plus élevée. Par exemple, considérez les modèles /hello et /{message} :

  • Les deux correspondent au chemin d’URL /hello.
  • /hello est plus spécifique et, par conséquent, a une priorité plus élevée.

En général, la priorité des routages permet de choisir la meilleure correspondance pour les types de schémas d’URL utilisés dans la pratique. Utilisez Order uniquement si nécessaire pour éviter une ambiguïté.

En raison des types d’extensibilité fournis par le routage, il n’est pas possible que le système de routage calcule à l’avance les routages ambigus. Prenons un exemple tel que les modèles de routage /{message:alpha} et /{message:int} :

  • La contrainte alpha ne fait correspondre que les caractères alphabétiques.
  • La contrainte int ne fait correspondre que les nombres.
  • Ces modèles ont la même priorité de routage, mais il n’existe aucune URL à laquelle ils correspondent.
  • Si le système de routage a signalé une erreur d’ambiguïté au démarrage, il bloque ce cas d’usage valide.

Avertissement

L’ordre des opérations à l’intérieur de UseEndpoints n’influence pas le comportement du routage, à une exception près. MapControllerRoute et MapAreaRoute attribuent automatiquement une valeur de commande à leurs points de terminaison en fonction de l’ordre qu’ils appellent. Cela simule le comportement long des contrôleurs sans le système de routage fournissant les mêmes garanties que les implémentations de routage plus anciennes.

Routage des points de terminaison dans ASP.NET Core :

  • N’a pas le concept de routages.
  • Ne fournit pas de garanties de commande. Tous les points de terminaison sont traités simultanément.

Priorité du modèle de routage et ordre de sélection du point de terminaison

La priorité du modèle de routage est un système qui attribue à chaque modèle de routage une valeur en fonction de sa spécificité. La priorité du modèle de routage :

  • Évite la nécessité d’ajuster l’ordre des points de terminaison dans les cas courants.
  • Tente de faire correspondre les attentes courantes du comportement de routage.

Par exemple, envisagez des modèles /Products/List et /Products/{id}. Il serait raisonnable de supposer que /Products/List est une meilleure correspondance que /Products/{id} pour le chemin d’URL /Products/List. Cela fonctionne parce que le segment littéral /List est considéré comme ayant une meilleure priorité que le segment de paramètre /{id}.

Les détails du fonctionnement de la priorité sont couplés à la façon dont les modèles de routage sont définis :

  • Les modèles avec plus de segments sont considérés comme plus spécifiques.
  • Un segment avec du texte littéral est considéré comme plus spécifique qu’un segment de paramètre.
  • Un segment de paramètre avec une contrainte est considéré comme plus spécifique qu’un segment sans.
  • Un segment complexe est considéré aussi spécifique qu’un segment de paramètre avec une contrainte.
  • Les paramètres catch-all sont les moins spécifiques. Consultez catch-all dans la section Modèles de routage pour obtenir des informations importantes sur les routages catch-all.

Concepts de génération d’URL

La génération des URL :

  • Est le processus par lequel le routage peut créer un chemin d’URL basé sur un ensemble de valeurs de route.
  • Permet une séparation logique entre les points de terminaison et les URL qui y accèdent.

Le routage des points de terminaison inclut l’API LinkGenerator. LinkGenerator est un service singleton disponible à partir de DI. L’API LinkGenerator peut être utilisée en dehors du contexte d’une requête en cours d’exécution. Mvc.IUrlHelper et les scénarios qui s’appuient sur IUrlHelper, comme l’Assistance des balises, l’assistance HTML et les résultats d’action, utilisent l’API LinkGenerator pour fournir les fonctionnalités de création de liens.

Le générateur de liens est basé sur le concept d’une adresse et de schémas d’adresse. Un schéma d’adresse est un moyen de déterminer les points de terminaison à prendre en compte pour la génération de liens. Par exemple, les scénarios de nom de route et de valeurs de route que de nombreux utilisateurs connaissent bien dans les contrôleurs et Razor Pages sont implémentés en tant que schémas d’adresse.

Le générateur de liens peut lier à des contrôleurs et Razor Pages via les méthodes d’extension suivantes :

Une surcharge de ces méthodes accepte des arguments qui incluent HttpContext. Ces méthodes sont fonctionnellement équivalentes à Url.Action et à Url.Page, mais elles offrent davantage de flexibilité et d’options.

Les méthodes GetPath* sont les plus similaires à Url.Action et Url.Page, car elles génèrent un URI contenant un chemin d’accès absolu. Les méthodes GetUri* génèrent toujours un URI absolu contenant un schéma et un hôte. Les méthodes qui acceptent un HttpContext génèrent un URI dans le contexte de la requête en cours d’exécution. Les valeurs de route ambiante, le chemin de base d’URL, le schéma et l’hôte de la requête en cours d’exécution sont utilisés, sauf s’ils sont remplacés.

LinkGenerator est appelé avec une adresse. La génération d’un URI se fait en deux étapes :

  1. Une adresse est liée à une liste de points de terminaison qui correspondent à l’adresse.
  2. Le RoutePattern de chaque point de terminaison est évalué jusqu’à ce qu’un modèle de route correspondant aux valeurs fournies soit trouvé. Le résultat obtenu est combiné avec d’autres parties de l’URI fournies par le générateur de liens, puis il est retourné.

Les méthodes fournies par LinkGenerator prennent en charge des fonctionnalités de génération de liens standard pour n’importe quel type d’adresse. La façon la plus pratique d’utiliser le générateur de liens est de le faire via des méthodes d’extension qui effectuent des opérations pour un type d’adresse spécifique :

Méthode d’extension Description
GetPathByAddress Génère un URI avec un chemin absolu basé sur les valeurs fournies.
GetUriByAddress Génère un URI absolu basé sur les valeurs fournies.

Avertissement

Faites attention aux implications suivantes de l’appel de méthodes LinkGenerator :

  • Utilisez les méthodes d’extension GetUri* avec précaution dans une configuration d’application qui ne valide pas l’en-tête Host des requêtes entrantes. Si l’en-tête Host des requêtes entrantes n’est pas validé, l’entrée de requête non approuvée peut être renvoyée au client dans les URI d’une page ou d’une vue. Nous recommandons que toutes les applications de production configurent leur serveur pour qu’il valide l’en-tête Host par rapport à des valeurs valides connues.

  • Utilisez LinkGenerator avec précaution dans le middleware en combinaison avec Map ou MapWhen. Map* modifie le chemin de base de la requête en cours d’exécution, ce qui affecte la sortie de la génération de liens. Toutes les API LinkGenerator permettent la spécification d’un chemin de base. Spécifiez un chemin de base vide pour annuler l’effet de Map* sur la génération de liens.

Exemple de middleware

Dans l’exemple suivant, un intergiciel utilise l’API LinkGenerator pour créer un lien vers une méthode d’action qui liste les produits d’un magasin. L’utilisation du générateur de liens en l’injectant dans une classe et en appelant GenerateLink est disponible pour n’importe quelle classe dans une application :

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

Modèles de route

Les jetons dans {} définissent les paramètres de routage liés si le routage est mis en correspondance. Plusieurs paramètres de routage peuvent être définis dans un segment de routage, mais les paramètres de routage doivent être séparés par une valeur littérale. Par exemple :

{controller=Home}{action=Index}

n’est pas un routage valide, car il n’y a pas de valeur littérale entre {controller} et {action}. Les paramètres de routage doivent avoir un nom, et ils autorisent la spécification d’attributs supplémentaires.

Un texte littéral autre que les paramètres de routage (par exemple, {id}) et le séparateur de chemin / doit correspondre au texte présent dans l’URL. La correspondance de texte ne respecte pas la casse et est basée sur la représentation décodée du chemin des URL. Pour mettre en correspondance un délimiteur de paramètre de route littéral { ou }, placez-le dans une séquence d’échappement en répétant le caractère. Par exemple {{ ou }}.

Astérisque * ou astérisque double ** :

  • Peut être utilisé comme préfixe pour un paramètre de routage pour établir une liaison au rest de l’URI.
  • Ils sont appelés des paramètres catch-all. Par exemple, blog/{**slug} :
    • Correspond à n’importe quel URI qui commence par blog/ et a n’importe quelle valeur qui suit.
    • La valeur suivant blog/ est affectée à la valeur de routage slug.

Avertissement

Un paramètre catch-all peut faire correspondre les mauvais routages en raison d’un bogue dans le routage. Les applications affectées par ce bogue présentent les caractéristiques suivantes :

  • Un routage catch-all, par exemple, {**slug}"
  • Le routage catch-all ne fait pas correspondre les demandes qu’il doit faire correspondre.
  • La suppression d’autres routes fait que la route catch-all commence à fonctionner.

Consultez les bogues GitHub 18677 et 16579, par exemple les cas qui ont rencontré ce bogue.

Un correctif d’opt-in pour ce bogue est contenu dans le Kit de développement logiciel (SDK) .NET Core 3.1.301 et versions ultérieures. Le code suivant définit un commutateur interne qui corrige ce bogue :

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

Les paramètres fourre-tout peuvent également établir une correspondance avec la chaîne vide.

Le paramètre catch-all place les caractères appropriés dans une séquence d’échappement lorsque la route est utilisée pour générer une URL, y compris les caractères de séparation de chemin /. Par exemple, la route foo/{*path} avec les valeurs de route { path = "my/path" } génère foo/my%2Fpath. Notez la barre oblique d’échappement. Pour les séparateurs de chemin aller-retour, utilisez le préfixe de paramètre de routage **. La route foo/{**path} avec { path = "my/path" } génère foo/my/path.

Les modèles d’URL qui tentent de capturer un nom de fichier avec une extension de fichier facultative doivent faire l’objet de considérations supplémentaires. Prenez par exemple le modèle files/{filename}.{ext?}. Quand des valeurs existent à la fois pour filename et pour ext, les deux valeurs sont renseignées. Si seule une valeur existe pour filename dans l’URL, une correspondance est trouvée pour la route, car le . de fin est facultatif. Les URL suivantes correspondent à cette route :

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

Les paramètres de route peuvent avoir des valeurs par défaut, désignées en spécifiant la valeur par défaut après le nom du paramètre, séparée par un signe égal (=). Par exemple, {controller=Home} définit Home comme valeur par défaut de controller. La valeur par défaut est utilisée si aucune valeur n’est présente dans l’URL pour le paramètre. Vous pouvez rendre facultatifs les paramètres de route en ajoutant un point d’interrogation (?) à la fin du nom du paramètre. Par exemple, id? La différence entre les valeurs facultatives et les paramètres de routage par défaut est la suivante :

  • Un paramètre de routage avec une valeur par défaut produit toujours une valeur.
  • Un paramètre facultatif a une valeur uniquement lorsqu’une valeur est fournie par l’URL de la requête.

Les paramètres de route peuvent avoir des contraintes, qui doivent correspondre à la valeur de route liée à partir de l’URL. L’ajout de : et d’un nom de contrainte après le nom du paramètre de routage spécifie une contrainte inline sur un paramètre de routage. Si la contrainte exige des arguments, ils sont fournis entre parenthèses (...) après le nom de la contrainte. Il est possible de spécifier plusieurs contraintes inline en ajoutant un autre : et le nom d’une autre contrainte.

Le nom de la contrainte et les arguments sont passés au service IInlineConstraintResolver pour créer une instance de IRouteConstraint à utiliser dans le traitement des URL. Par exemple, le modèle de routage blog/{article:minlength(10)} spécifie une contrainte minlength avec l’argument 10. Pour plus d’informations sur les contraintes de route et pour obtenir la liste des contraintes fournies par le framework, consultez la section Contraintes de route.

Les paramètres de route peuvent également avoir des transformateurs de paramètres. Les transformateurs de paramètres transforment la valeur d’un paramètre lors de la génération de liens et d’actions et de pages correspondantes en URL. À l’instar des contraintes, les transformateurs de paramètre peuvent être ajoutés inline à un paramètre de routage en ajoutant un : et le nom du transformateur après le nom du paramètre de routage. Par exemple, le modèle de routage blog/{article:slugify} spécifie un transformateur slugify. Pour plus d’informations sur les transformateurs de paramètre, consultez la section Transformateurs de paramètre.

Le tableau suivant montre des exemples de modèles de route et leur comportement.

Modèle de routage Exemple d’URI en correspondance URI de requête
hello /hello Correspond seulement au chemin unique /hello.
{Page=Home} / Correspond à Page et le définit sur Home.
{Page=Home} /Contact Correspond à Page et le définit sur Contact.
{controller}/{action}/{id?} /Products/List Mappe au contrôleur Products et à l’action List.
{controller}/{action}/{id?} /Products/Details/123 Mappe au contrôleur Products et à l’action Details avec id défini sur 123).
{controller=Home}/{action=Index}/{id?} / Mappe au contrôleur Home et à l’action Index. id est ignoré.
{controller=Home}/{action=Index}/{id?} /Products Mappe au contrôleur Products et à la méthode Index. id est ignoré.

L’utilisation d’un modèle est généralement l’approche la plus simple pour le routage. Il est également possible de spécifier des contraintes et des valeurs par défaut hors du modèle de routage.

Segments complexes

Les segments complexes sont traités en faisant correspondre les délimiteurs littéraux de droite à gauche de manière non gourmande. Par exemple, [Route("/a{b}c{d}")] est un segment complexe. Les segments complexes fonctionnent d’une manière particulière qui doit être comprise pour les utiliser correctement. L’exemple de cette section montre pourquoi les segments complexes ne fonctionnent vraiment bien que lorsque le texte du délimiteur n’apparaît pas dans les valeurs des paramètres. L’utilisation d’un regex, puis l’extraction manuelle des valeurs est nécessaire pour des cas plus complexes.

Avertissement

Lorsque vous utilisez System.Text.RegularExpressions pour traiter une entrée non approuvée, passez un délai d’expiration. Un utilisateur malveillant peut fournir une entrée à RegularExpressions, provoquant une attaque par déni de service. Les API d’infrastructure ASP.NET Core qui utilisent RegularExpressions passent un délai d’expiration.

Il s’agit d’un résumé des étapes effectuées par le routage avec le modèle /a{b}c{d} et le chemin d’URL /abcd. Un | est utilisé pour vous aider à visualiser le fonctionnement de l’algorithme :

  • Le premier littéral, de droite à gauche, est c. Donc /abcd est recherché à partir de la droite et trouve /ab|c|d.
  • Tout ce qui se trouve à droite (d) est désormais mis en correspondance avec le paramètre de routage {d}.
  • Le littéral suivant, de droite à gauche, est a. Donc /ab|c|d est recherché à partir de là où nous sommes partis, puis a est trouvé /|a|b|c|d.
  • La valeur à droite (b) est désormais associée au paramètre de routage {b}.
  • Il n’y a pas de texte restant et aucun modèle de routage restant. Il s’agit donc d’une correspondance.

Voici un exemple de cas négatif utilisant le même modèle /a{b}c{d} et le chemin d’URL /aabcd. Un | est utilisé pour vous aider à visualiser le fonctionnement de l’algorithme. Ce cas n’est pas une correspondance, qui est expliquée par le même algorithme :

  • Le premier littéral, de droite à gauche, est c. Donc /aabcd est recherché à partir de la droite et trouve /aab|c|d.
  • Tout ce qui se trouve à droite (d) est désormais mis en correspondance avec le paramètre de routage {d}.
  • Le littéral suivant, de droite à gauche, est a. Donc /aab|c|d est recherché à partir de là où nous sommes partis, puis a est trouvé /a|a|b|c|d.
  • La valeur à droite (b) est désormais associée au paramètre de routage {b}.
  • À ce stade, il reste du texte a, mais l’algorithme n’a plus de modèle de routage à analyser. Il ne s’agit donc pas d’une correspondance.

Étant donné que l’algorithme correspondant n’est pas gourmand :

  • Il correspond à la plus petite quantité de texte possible dans chaque étape.
  • Si la valeur de délimiteur apparaît à l’intérieur des valeurs de paramètre, elle ne correspond pas.

Les expressions régulières fournissent beaucoup plus de contrôle sur leur comportement de correspondance.

Correspondance gourmande, également appelée correspondance maximale tente de trouver la correspondance la plus longue possible dans le texte d’entrée qui satisfait au modèle d’expression régulière. La correspondance non gourmande, également appelée correspondance paresseuse, recherche la correspondance la plus courte possible dans le texte d’entrée qui satisfait au modèle d’expression régulière.

Routage avec des caractères spéciaux

Le routage avec des caractères spéciaux peut entraîner des résultats inattendus. Par exemple, considérez un contrôleur avec la méthode d’action suivante :

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

Lorsque string id contient les valeurs encodées suivantes, des résultats inattendus peuvent se produire :

ASCII Encoded
/ %2F
+

Les paramètres de routage ne sont pas toujours décodés par URL. Ce problème peut être résolu à l’avenir. Pour plus d’informations, consultez ce problème GitHub ;

Contraintes d'itinéraire

Les contraintes de route s’exécutent quand une correspondance s’est produite pour l’URL entrante, et le chemin de l’URL est tokenisé en valeurs de route. En général, les contraintes de routage inspectent la valeur de route associée par le biais du modèle de routage, et créent une décision true ou false indiquant si la valeur est acceptable. Certaines contraintes de routage utilisent des données hors de la valeur de route pour déterminer si la requête peut être routée. Par exemple, HttpMethodRouteConstraint peut accepter ou rejeter une requête en fonction de son verbe HTTP. Les contraintes sont utilisées dans le routage des requêtes et la génération des liens.

Avertissement

N’utilisez pas de contraintes pour la validation des entrées. Si des contraintes sont utilisées pour la validation d’entrée, une entrée non valide génère une réponse introuvable 404. Une entrée non valide doit produire une demande incorrecte 400 avec un message d’erreur approprié. Les contraintes de route sont utilisées pour lever l’ambiguïté entre des routes similaires, et non pas pour valider les entrées d’une route particulière.

Le tableau suivant montre des exemples de contrainte de route et leur comportement attendu :

contrainte Exemple Exemples de correspondances Notes
int {id:int} 123456789, -123456789 Correspond à n’importe quel entier
bool {active:bool} true, FALSE Correspond à true ou false. Non-respect de la casse
datetime {dob:datetime} 2016-12-31, 2016-12-31 7:32pm Correspond à une valeur valide DateTime dans la culture invariante. Voir l’avertissement précédent.
decimal {price:decimal} 49.99, -1,000.01 Correspond à une valeur valide decimal dans la culture invariante. Voir l’avertissement précédent.
double {weight:double} 1.234, -1,001.01e8 Correspond à une valeur valide double dans la culture invariante. Voir l’avertissement précédent.
float {weight:float} 1.234, -1,001.01e8 Correspond à une valeur valide float dans la culture invariante. Voir l’avertissement précédent.
guid {id:guid} CD2C1638-1638-72D5-1638-DEADBEEF1638 Correspond à une valeur Guid valide
long {ticks:long} 123456789, -123456789 Correspond à une valeur long valide
minlength(value) {username:minlength(4)} Rick La chaîne doit comporter au moins 4 caractères
maxlength(value) {filename:maxlength(8)} MyFile La chaîne ne doit pas comporter plus de 8 caractères
length(length) {filename:length(12)} somefile.txt La chaîne doit comporter exactement 12 caractères
length(min,max) {filename:length(8,16)} somefile.txt La chaîne doit comporter au moins 8 caractères et pas plus de 16 caractères
min(value) {age:min(18)} 19 La valeur entière doit être au moins égale à 18
max(value) {age:max(120)} 91 La valeur entière ne doit pas être supérieure à 120
range(min,max) {age:range(18,120)} 91 La valeur entière doit être au moins égale à 18 mais ne doit pas être supérieure à 120
alpha {name:alpha} Rick La chaîne doit se composer d’un ou de plusieurs caractères alphabétiques (a-z, non-respect de la casse).
regex(expression) {ssn:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)} 123-45-6789 La chaîne doit correspondre à l’expression régulière. Consultez des conseils sur la définition d’une expression régulière.
required {name:required} Rick Utilisé pour garantir qu’une valeur autre qu’un paramètre est présente pendant la génération de l’URL

Avertissement

Lorsque vous utilisez System.Text.RegularExpressions pour traiter une entrée non approuvée, passez un délai d’expiration. Un utilisateur malveillant peut fournir une entrée à RegularExpressions, provoquant une attaque par déni de service. Les API d’infrastructure ASP.NET Core qui utilisent RegularExpressions passent un délai d’expiration.

Il est possible d’appliquer plusieurs contraintes séparées par un point-virgule à un même paramètre. Par exemple, la contrainte suivante limite un paramètre à une valeur entière supérieure ou égale à 1 :

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

Avertissement

Les contraintes de routage qui vérifient que l’URL peut être convertie en type CLR utilisent toujours la culture invariant. Par exemple, conversion en type CLR int ou DateTime. Ces contraintes partent du principe que l’URL ne peut pas être localisé. Les contraintes de routage fournies par le framework ne modifient pas les valeurs stockées dans les valeurs de route. Toutes les valeurs de route analysées à partir de l’URL sont stockées sous forme de chaînes. Par exemple, la contrainte float tente de convertir la valeur de route en valeur float, mais la valeur convertie est utilisée uniquement pour vérifier qu’elle peut être convertie en valeur float.

Expressions régulières dans les contraintes

Avertissement

Lorsque vous utilisez System.Text.RegularExpressions pour traiter une entrée non approuvée, passez un délai d’expiration. Un utilisateur malveillant peut fournir une entrée à RegularExpressions, provoquant une attaque par déni de service. Les API d’infrastructure ASP.NET Core qui utilisent RegularExpressions passent un délai d’expiration.

Les expressions régulières peuvent être spécifiées en tant que contraintes inline à l’aide de la contrainte de routage regex(...). Les méthodes de la famille MapControllerRoute acceptent également un littéral d’objet de contraintes. Si ce formulaire est utilisé, les valeurs de chaîne sont interprétées comme des expressions régulières.

Le code suivant utilise une contrainte d’expression régulière inline :

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

Le code suivant utilise un littéral d’objet pour spécifier une contrainte d’expression régulière :

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

Le framework ASP.NET Core ajoute RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant au constructeur d’expression régulière. Pour obtenir une description de ces membres, consultez RegexOptions.

Les expressions régulières utilisent les délimiteurs et des jetons semblables à ceux utilisés par le service de routage et le langage C#. Les jetons d’expression régulière doivent être placés dans une séquence d’échappement. Pour utiliser l’expression régulière ^\d{3}-\d{2}-\d{4}$ dans une contrainte inline, utilisez l’une des options suivantes :

  • Remplacez les caractères \ fournis dans la chaîne en tant que caractères \\ dans le fichier source C# afin d’échapper au caractère \ d’échappement de chaîne.
  • Littéraux de chaîne verbatim.

Pour placer en échappement les caractères de délimiteur de paramètre de route {, }, [, ], doublez les caractères dans l’expression, par exemple {{, }}, [[, ]]. Le tableau suivant montre une expression régulière et la version placée en échappement :

Expression régulière Expression régulière en échappement
^\d{3}-\d{2}-\d{4}$ ^\\d{{3}}-\\d{{2}}-\\d{{4}}$
^[a-z]{2}$ ^[[a-z]]{{2}}$

Les expressions régulières utilisées dans le routage commencent souvent par le caractère ^ et correspondent à la position de début de la chaîne. Les expressions se terminent souvent par le caractère $ et correspondent à la fin de la chaîne. Les caractères ^ et $ garantissent que l’expression régulière établit une correspondance avec la totalité de la valeur du paramètre de route. Sans les caractères ^ et $, l’expression régulière peut correspondre à n’importe quelle sous-chaîne dans la chaîne, ce qui est souvent indésirable. Le tableau suivant contient des exemples et explique pourquoi ils établissent ou non une correspondance :

Expression String Correspond Commentaire
[a-z]{2} hello Oui Correspondances de sous-chaînes
[a-z]{2} 123abc456 Oui Correspondances de sous-chaînes
[a-z]{2} mz Oui Correspondance avec l’expression
[a-z]{2} MZ Oui Non-respect de la casse
^[a-z]{2}$ hello Non Voir ^ et $ ci-dessus
^[a-z]{2}$ 123abc456 Non Voir ^ et $ ci-dessus

Pour plus d’informations sur la syntaxe des expressions régulières, consultez Expressions régulières du .NET Framework.

Pour contraindre un paramètre à un ensemble connu de valeurs possibles, utilisez une expression régulière. Par exemple, {action:regex(^(list|get|create)$)} établit une correspondance avec la valeur de route action uniquement pour list, get ou create. Si elle est passée dans le dictionnaire de contraintes, la chaîne ^(list|get|create)$ est équivalente. Les contraintes passées dans le dictionnaire de contraintes qui ne correspondent pas à l’une des contraintes connues sont également traitées comme des expressions régulières. Les contraintes passées dans un modèle qui ne correspondent pas à l’une des contraintes connues ne sont pas traitées comme des expressions régulières.

Contraintes de routage personnalisées

Les contraintes de routage personnalisées peuvent être créées en implémentant l’interface IRouteConstraint. L’interface IRouteConstraint contient une méthode unique, Match, qui retourne true si la contrainte est satisfaite et false dans le cas contraire.

Les contraintes de routage personnalisées sont rarement nécessaires. Avant d’implémenter une contrainte de routage personnalisée, envisagez des alternatives, telles que la liaison de modèle.

Le dossier ASP.NET Core Contraintes fournit de bons exemples de création de contraintes. Par exemple, GuidRouteConstraint.

Pour utiliser un IRouteConstraint personnalisé, le type de contrainte de routage doit être inscrit avec le ConstraintMap de l’application dans le conteneur de service de l’application. Un ConstraintMap est un dictionnaire qui mappe les clés de contrainte d’itinéraire aux implémentations IRouteConstraint qui valident ces contraintes. Le ConstraintMap d’une application peut être mis à jour dans Program.cs en tant qu’appel AddRouting ou en configurant RouteOptions directement avec builder.Services.Configure<RouteOptions>. Par exemple :

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

La contrainte précédente est appliquée dans le code suivant :

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

L’implémentation de NoZeroesRouteConstraint empêche l’utilisation de 0 dans un paramètre de routage :

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

Avertissement

Lorsque vous utilisez System.Text.RegularExpressions pour traiter une entrée non approuvée, passez un délai d’expiration. Un utilisateur malveillant peut fournir une entrée à RegularExpressions, provoquant une attaque par déni de service. Les API d’infrastructure ASP.NET Core qui utilisent RegularExpressions passent un délai d’expiration.

Le code précédent :

  • Empêche 0 dans le segment {id} de la route.
  • S’affiche pour fournir un exemple de base d’implémentation d’une contrainte personnalisée. Il ne doit pas être utilisé dans une application de production.

Le code suivant est une meilleure approche pour empêcher un id contenant un 0 d’être traité :

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

    return Content(id);
}

Le code précédent présente les avantages suivants sur l’approche NoZeroesRouteConstraint :

  • Il ne nécessite pas de contrainte personnalisée.
  • Il retourne une erreur plus descriptive lorsque le paramètre de routage inclut 0.

Les transformateurs de paramètres

Transformateurs de paramètre :

Par exemple, un transformateur de paramètre slugify personnalisé dans le modèle d’itinéraire blog\{article:slugify} avec Url.Action(new { article = "MyTestArticle" }) génère blog\my-test-article.

Examinez l’implémentation suivante 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();
    }
}

Pour utiliser un transformateur de paramètre dans un modèle d’itinéraire, configurez-le d’abord en utilisant ConstraintMap dans Program.cs :

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

L’infrastructure ASP.NET Core utilise des transformateurs de paramètres pour transformer l’URI où un point de terminaison est résolu. Par exemple, les transformateurs de paramètres transforment les valeurs de routage utilisées pour faire correspondre un area, controller, actionet page :

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

Avec le modèle de routage précédent, l’action SubscriptionManagementController.GetAll est mise en correspondance avec l’URI /subscription-management/get-all. Un transformateur de paramètre ne modifie pas les valeurs de routage utilisées pour générer un lien. Par exemple, Url.Action("GetAll", "SubscriptionManagement") produit /subscription-management/get-all.

ASP.NET Core fournit des conventions d’API pour l’utilisation des transformateurs de paramètre avec des routages générés :

Informations de référence sur la génération d’URL

Cette section contient une référence pour l’algorithme implémenté par génération d’URL. Dans la pratique, les exemples les plus complexes de génération d’URL utilisent des contrôleurs ou Razor Pages. Pour plus d’informations, consultez Routage dans les contrôleurs.

Le processus de génération d’URL commence par un appel à LinkGenerator.GetPathByAddress ou une méthode similaire. La méthode est fournie avec une adresse, un ensemble de valeurs de routage et éventuellement des informations sur la requête actuelle de HttpContext.

La première étape consiste à utiliser l’adresse pour résoudre un ensemble de points de terminaison candidats à l’aide d’un IEndpointAddressScheme<TAddress> correspondant au type de l’adresse.

Une fois que l’ensemble de candidats est trouvé par le schéma d’adresses, les points de terminaison sont classés et traités de manière itérative jusqu’à ce qu’une opération de génération d’URL réussisse. La génération d’URL ne vérifie pas les ambiguïtés, le premier résultat retourné est le résultat final.

Résolution des problèmes de génération d’URL avec la journalisation

La première étape de la résolution des problèmes de génération d’URL consiste à définir le niveau de journalisation de Microsoft.AspNetCore.Routing sur TRACE. LinkGenerator enregistre de nombreux détails sur son traitement, ce qui peut être utile pour résoudre les problèmes.

Consultez Référence de génération d’URL pour plus d’informations sur la génération d’URL.

Adresses

Les adresses sont le concept de génération d’URL utilisé pour lier un appel au générateur de liens à un ensemble de points de terminaison candidats.

Les adresses sont un concept extensible qui comprend deux implémentations par défaut :

  • Utilisation du nom du point de terminaison (string) comme adresse :
    • Fournit des fonctionnalités similaires au nom du routage de MVC.
    • Utilise le type de métadonnées IEndpointNameMetadata.
    • Résout la chaîne fournie par rapport aux métadonnées de tous les points de terminaison inscrits.
    • Lève une exception au démarrage si plusieurs points de terminaison utilisent le même nom.
    • Recommandé pour une utilisation à usage général en dehors des contrôleurs et de Razor Pages.
  • Utilisation des valeurs de route (RouteValuesAddress) comme adresse :
    • Fournit des fonctionnalités similaires à la génération d’URL hérité des contrôleurs et de Razor Pages.
    • Très complexe à étendre et à déboguer.
    • Fournit l’implémentation utilisée par IUrlHelper, l’assistance des balises, l’assistance HTML , Résultats d’action, etc.

Le rôle du schéma d’adresses consiste à faire l’association entre l’adresse et les points de terminaison correspondants selon des critères arbitraires :

  • Le schéma de noms de point de terminaison effectue une recherche de dictionnaire de base.
  • Le schéma de valeurs de route a un sous-ensemble complexe de l’algorithme défini.

Valeurs ambiantes et valeurs explicites

À partir de la requête actuelle, le routage accède aux valeurs de routage de la requête HttpContext.Request.RouteValuesactuelle. Les valeurs associées à la requête actuelle sont appelées valeurs ambiantes. À des fins de clarté, la documentation fait référence aux valeurs de routage transmises aux méthodes en tant que valeurs explicites.

L’exemple suivant montre les valeurs ambiantes et les valeurs explicites. Il fournit des valeurs ambiantes à partir de la requête actuelle et des valeurs explicites :

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

    // ...

Le code précédent :

Le code suivant fournit uniquement des valeurs explicites et aucune valeur ambiante :

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

La méthode précédente retourne /Home/Subscribe/17

Le code suivant dans le WidgetController retourne /Widget/Subscribe/17 :

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

Le code suivant fournit au contrôleur des valeurs ambiantes dans la requête actuelle et des valeurs explicites :

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

Dans le code précédent :

  • /Gadget/Edit/17 est retourné.
  • Url obtient IUrlHelper.
  • Action génère une URL avec un chemin absolu pour une méthode d’action. L’URL contient le nom de action spécifié et les valeurs route.

Le code suivant fournit des valeurs ambiantes à partir de la requête actuelle et des valeurs explicites :

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

        // ...
    }
}

Le code précédent définit url sur /Edit/17 lorsque la page Razor Modifier contient la directive de page suivante :

@page "{id:int}"

Si la page Modifier ne contient pas le modèle de route "{id:int}", url est /Edit?id=17.

Le comportement de l'IUrlHelper de MVC ajoute une couche de complexité en plus des règles décrites ici :

  • IUrlHelper fournit toujours les valeurs de routage de la requête actuelle en tant que valeurs ambiantes.
  • IUrlHelper.Action copie toujours les valeurs actuelles action et controller de routage en tant que valeurs explicites, sauf substitution par le développeur.
  • IUrlHelper.Page copie toujours la valeur de routage actuelle page en tant que valeur explicite, sauf si elle est remplacée.
  • IUrlHelper.Page remplace toujours la valeur de route handler actuelle par null comme valeurs explicites, sauf substitution.

Les utilisateurs sont souvent surpris par les détails comportementaux des valeurs ambiantes, car MVC ne semble pas suivre ses propres règles. Pour des raisons historiques et de compatibilité, certaines valeurs de routage telles que action, controller, page et handler ont leur propre comportement de cas spécial.

La fonctionnalité équivalente fournie par LinkGenerator.GetPathByAction et LinkGenerator.GetPathByPage duplique ces anomalies de IUrlHelper pour la compatibilité.

Processus de génération d’URL

Une fois l’ensemble de points de terminaison candidats trouvés, l’algorithme de génération d’URL :

  • Traite les points de terminaison de manière itérative.
  • Retourne le premier résultat réussi.

La première étape de ce processus est appelée invalidation des valeurs de routage. L’invalidation des valeurs de routage est le processus par lequel le routage détermine les valeurs de routage des valeurs ambiantes à utiliser et qui doivent être ignorées. Chaque valeur ambiante est considérée et combinée aux valeurs explicites ou ignorée.

La meilleure façon de penser au rôle des valeurs ambiantes est qu’elles tentent d’enregistrer la saisie par les développeurs d’applications, dans certains cas courants. Traditionnellement, les scénarios où les valeurs ambiantes sont utiles sont liés à MVC :

  • Lors de la liaison à une autre action dans le même contrôleur, le nom du contrôleur n’a pas besoin d’être spécifié.
  • Lors de la liaison à un autre contrôleur dans la même zone, le nom de la zone n’a pas besoin d’être spécifié.
  • Lors de la liaison à la même méthode d’action, les valeurs de routage n’ont pas besoin d’être spécifiées.
  • Lors de la liaison à une autre partie de l’application, vous ne souhaitez pas transporter les valeurs de routage qui n’ont aucune signification dans cette partie de l’application.

Les appels à ou LinkGenerator qui retournent IUrlHelper sont généralement dus à null une non-compréhension de l’invalidation de la valeur de route. Résolvez les problèmes d’invalidation des valeurs de routage en spécifiant explicitement davantage de valeurs de routage pour voir si cela résout le problème.

L’invalidation de la valeur de routage repose sur l’hypothèse que le schéma d’URL de l’application est hiérarchique, avec une hiérarchie formée de gauche à droite. Considérez le modèle de route de contrôleur de base {controller}/{action}/{id?} pour avoir un sens intuitif de la façon dont cela fonctionne dans la pratique. Une modification apportée à une valeur invalide toutes les valeurs de routage qui apparaissent à droite. Cela reflète l’hypothèse sur la hiérarchie. Si l’application a une valeur ambiante pour id, et que l’opération spécifie une valeur différente pour controller :

  • id ne sera pas réutilisée, car {controller} est à gauche de {id?}.

Voici quelques exemples illustrant ce principe :

  • Si les valeurs explicites contiennent une valeur pour id, la valeur ambiante de id est ignorée. Les valeurs ambiantes de controller et action peuvent être utilisées.
  • Si les valeurs explicites contiennent une valeur pour action, toute valeur ambiante de action est ignorée. Les valeurs ambiantes de controller peuvent être utilisées. Si la valeur explicite de action est différente de la valeur ambiante de action, la valeur id ne sera pas utilisée. Si la valeur explicite de action est identique à la valeur ambiante de action, la valeur id peut être utilisée.
  • Si les valeurs explicites contiennent une valeur de controller, toute valeur ambiante de controller est ignorée. Si la valeur explicite de controller est différente de la valeur ambiante de controller, les valeurs action et id ne seront pas utilisées. Si la valeur explicite de controller est identique à la valeur ambiante de controller, les valeurs action et id peuvent être utilisées.

Ce processus est encore plus compliqué à cause de l’existence de routes d’attributs et de routes conventionnelles dédiées. Les routes conventionnelles de contrôleur tels que {controller}/{action}/{id?} spécifient une hiérarchie à l’aide de paramètres de routage. Pour les routages conventionnels dédiés et les routes d’attribut aux contrôleurs et à Razor Pages :

  • Il existe une hiérarchie de valeurs de routage.
  • Elles n’apparaissent pas dans le modèle.

Dans ce cas, la génération d’URL définit le concept de valeurs requises. Les points de terminaison créés par les contrôleurs et Razor Pages ont des valeurs requises spécifiées qui autorisent l’invalidation de la valeur de routage à fonctionner.

Algorithme d’invalidation de valeur de routage en détail :

  • Les noms de valeurs requis sont combinés avec les paramètres de routage, puis traités de gauche à droite.
  • Pour chaque paramètre, la valeur ambiante et la valeur explicite sont comparées :
    • Si la valeur ambiante et la valeur explicite sont identiques, le processus continue.
    • Si la valeur ambiante est présente et que la valeur explicite ne l’est pas, la valeur ambiante est utilisée lors de la génération de l’URL.
    • Si la valeur ambiante n’est pas présente et que la valeur explicite l’est, rejetez la valeur ambiante et toutes les valeurs ambiantes suivantes.
    • Si la valeur ambiante et la valeur explicite sont présentes et que les deux valeurs sont différentes, rejetez la valeur ambiante et toutes les valeurs ambiantes suivantes.

À ce stade, l’opération de génération d’URL est prête à évaluer les contraintes de routage. L’ensemble de valeurs acceptées est combiné aux valeurs par défaut des paramètres, qui sont fournies aux contraintes. Si les contraintes passent toutes, l’opération se poursuit.

Ensuite, les valeurs acceptées peuvent être utilisées pour développer le modèle de routage. Le modèle de routage est traité :

  • De gauche à droite.
  • Chaque paramètre a sa valeur acceptée remplacée.
  • Avec les cas spéciaux suivants :
    • S’il manque une valeur aux valeurs acceptées et que le paramètre a une valeur par défaut, la valeur par défaut est utilisée.
    • S’il manque une valeur aux valeurs acceptées et que le paramètre est facultatif, le traitement se poursuit.
    • Si un paramètre de routage à droite d’un paramètre facultatif manquant a une valeur, l’opération échoue.
    • Les paramètres par défaut contigus et les paramètres facultatifs sont réduits si possible.

Les valeurs fournies explicitement mais qui n’ont pas de correspondance avec un segment de la route sont ajoutées à la chaîne de requête. Le tableau suivant présente le résultat en cas d’utilisation du modèle de routage {controller}/{action}/{id?}.

Valeurs ambiantes Valeurs explicites Résultat
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

Ordre des paramètres d’itinéraire facultatif

Les paramètres d’itinéraire facultatifs doivent être fournis après tous les littéraux et paramètres d’itinéraire requis. Dans le code suivant, les paramètres id et name doivent venir après le paramètre 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 ?? ""}");
    }
}

Problèmes liés à l’invalidation des valeurs de routage

Le code suivant montre un exemple de schéma de génération d’URL qui n’est pas pris en charge par le routage :

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

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

Dans le code précédent, le paramètre de routage culture est utilisé pour la localisation. On veut que le paramètre culture soit toujours accepté comme valeur ambiante. Toutefois, le paramètre culture n’est pas accepté comme valeur ambiante en raison de la façon dont les valeurs requises fonctionnent :

  • Dans le modèle de routage "default", le paramètre de routage culture est à gauche de controller. Les modifications apportées à controller n’invalident donc pas culture.
  • Dans le modèle de routage "blog", le paramètre de routage culture est considéré comme à droite de controller, qui apparaît dans les valeurs requises.

Analyser les chemins d’URL avec LinkParser

La classe LinkParser ajoute la prise en charge de l’analyse d’un chemin d’URL dans un ensemble de valeurs de routage. La méthode ParsePathByEndpointName prend un nom de point de terminaison et un chemin d’URL et retourne un ensemble de valeurs de routage extraites du chemin d’URL.

Dans l’exemple de contrôleur suivant, l’action GetProduct utilise un modèle de routage de api/Products/{id} et a un Name de GetProduct :

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

Dans la même classe de contrôleur, l’action AddRelatedProduct attend un chemin d’URL, pathToRelatedProduct, qui peut être fourni en tant que paramètre de chaîne de requête :

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

    // ...

Dans l’exemple précédent, l’action AddRelatedProduct extrait la valeur de route id du chemin d’URL. Par exemple, avec un chemin d’URL de /api/Products/1, la valeur relatedProductId est définie sur 1. Cette approche permet aux clients de l’API d’utiliser des chemins d’URL lorsque vous faites référence à des ressources, sans avoir à connaître la façon dont cette URL est structurée.

Configurer les métadonnées de point de terminaison

Les liens suivants fournissent des informations sur la configuration des métadonnées de point de terminaison :

Correspondance de l’hôte dans les routages avec RequireHost

RequireHost applique une contrainte au routage qui nécessite l’hôte spécifié. Le paramètre RequireHost ou [Host] peut être un :

  • Hôte : www.domain.com, fait correspondre www.domain.com à n’importe quel port.
  • Hôte avec caractère générique : *.domain.com, fait correspondre www.domain.com, subdomain.domain.comou www.subdomain.domain.com sur n’importe quel port.
  • Port : *:5000, fait correspondre le port 5000 avec n’importe quel hôte.
  • Hôte et port : www.domain.com:5000 ou *.domain.com:5000, fait correspondre l’hôte et le port.

Plusieurs paramètres peuvent être spécifiés à l’aide RequireHost ou [Host]. La contrainte fait correspondre les hôtes valides pour l’un des paramètres. Par exemples, [Host("domain.com", "*.domain.com")] fait correspondre domain.com, www.domain.com et subdomain.domain.com.

Le code suivant utilise RequireHost pour exiger l’hôte spécifié sur le routage :

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

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

Le code suivant utilise l’attribut [Host] sur le contrôleur pour exiger l’un des hôtes spécifiés :

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

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

Lorsque l’attribut [Host] est appliqué à la fois au contrôleur et à la méthode d’action :

  • L’attribut de l’action est utilisé.
  • L’attribut du contrôleur est ignoré.

Avertissement

L’API qui s’appuie sur l’en-tête d’hôte, comme HttpRequest.Host et RequireHost, est soumise à une usurpation potentielle par les clients.

Pour éviter l’usurpation d’hôte ou de port, utilisez l’une des approches suivantes :

Groupes de routes

La méthode d’extension MapGroup permet d’organiser des groupes de points de terminaison avec un préfixe commun. Cela réduit le code répétitif et permet de personnaliser des groupes entiers de points de terminaison avec un seul appel à des méthodes comme RequireAuthorization et WithMetadata, qui ajoutent des métadonnées de point de terminaison.

Par exemple, le code suivant crée deux groupes de points de terminaison similaires :

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

Dans ce scénario, vous pouvez utiliser une adresse relative pour l’en-tête Location dans le résultat 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);
}

Le premier groupe de points de terminaison correspond uniquement aux requêtes précédées de /public/todos, accessibles sans authentification. Le second groupe de points de terminaison correspond uniquement aux requêtes préfixées par /private/todos, qui nécessitent une authentification.

La QueryPrivateTodosfabrique de filtre de point de terminaison est une fonction locale qui modifie les paramètres TodoDb du gestionnaire d’itinéraires pour permettre l’accès et le stockage de données todo privées.

Les groupes de routage prennent également en charge les groupes imbriqués et les modèles de préfixe complexes avec des contraintes et des paramètres de routage. Dans l’exemple suivant, un gestionnaire de routage mappé au groupe user peut capturer les paramètres de routage {org} et {group} définis dans les préfixes de groupe externe.

Le préfixe peut également être vide. Cela peut être utile pour ajouter des métadonnées ou des filtres de point de terminaison à un groupe de points de terminaison sans modifier le modèle de routage.

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

L’ajout de filtres ou de métadonnées à un groupe se comporte de la même façon que si vous les ajoutiez individuellement à chaque point de terminaison avant d’ajouter des filtres ou des métadonnées supplémentaires qui ont pu être ajoutés à un groupe interne ou à un point de terminaison spécifique.

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

Dans l’exemple ci-dessus, le filtre externe enregistre la requête entrante avant le filtre interne, même si elle a été ajoutée en deuxième. Étant donné que les filtres ont été appliqués à différents groupes, l’ordre dans lequel ils ont été ajoutés les uns par rapport aux autres n’a pas d’importance. Les filtres d’ordre ajoutés sont importants s’ils sont appliqués au même groupe ou au même point de terminaison spécifique.

Une requête sur /outer/inner/ journalisera les éléments suivants :

/outer group filter
/inner group filter
MapGet filter

Conseils sur les performances pour le routage

Lorsqu’une application rencontre des problèmes de performances, le routage est souvent soupçonné comme étant le problème. La raison pour laquelle le routage est soupçonné est que les infrastructures telles que les contrôleurs et Razor Pages signalent le temps passé à l’intérieur de l’infrastructure dans leurs messages de journalisation. En cas de différence significative entre le temps signalé par les contrôleurs et le temps total de la requête :

  • Les développeurs éliminent leur code d’application comme source du problème.
  • Il est courant de supposer que le routage est la cause.

Les performances du routage est testé à l’aide de milliers de points de terminaison. Il est peu probable qu’une application classique rencontre un problème de performances simplement en étant trop volumineuse. La cause racine la plus courante des performances de routage lentes est généralement un intergiciel personnalisé qui se comporte mal.

Cet exemple de code suivant illustre une technique de base pour affiner la source de délai :

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

Pour le routage temporel :

  • Entrelacez chaque intergiciel avec une copie de l’intergiciel de minutage indiqué dans le code précédent.
  • Ajoutez un identificateur unique pour mettre en corrélation les données de minutage avec le code.

Il s’agit d’un moyen de base de limiter le délai lorsqu’il est significatif, par exemple, plus que 10ms. Soustraire Time 2 de Time 1 signale le temps passé à l’intérieur de l’intergiciel UseRouting.

Le code suivant utilise une approche plus compacte du code de minutage précédent :

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

Fonctionnalités de routage potentiellement coûteuses

La liste suivante fournit un aperçu des fonctionnalités de routage relativement coûteuses par rapport aux modèles de routage de base :

  • Expressions régulières : il est possible d’écrire des expressions régulières qui sont complexes ou qui ont un temps d’exécution long avec une petite quantité d’entrée.
  • Segments complexes ({x}-{y}-{z}) :
    • Sont beaucoup plus coûteux que l’analyse d’un segment de chemin d’URL standard.
    • Entraînent l’allocation d’un grand nombre de sous-chaînes.
  • Accès aux données synchrones : de nombreuses applications complexes disposent d’un accès à la base de données dans le cadre de leur routage. Utilisez des points d’extensibilité tels que MatcherPolicy et EndpointSelectorContext, qui sont asynchrones.

Conseils pour les tables de routage volumineuses

Par défaut, ASP.NET Core utilise un algorithme de routage qui échange la mémoire pour le temps processeur. Cela a l’effet intéressant que le temps de correspondance de la route dépend uniquement de la longueur du chemin d’accès à mettre en correspondance et non du nombre de routes. Toutefois, cette approche peut être potentiellement problématique dans certains cas, lorsque l’application a un grand nombre de routes (dans les milliers) et qu’il existe un grand nombre de préfixes variables dans les routes. Par exemple, si les routes ont des paramètres dans les premiers segments de la route, comme {parameter}/some/literal.

Il est peu probable qu’une application rencontre un problème, sauf si :

  • Il existe un nombre élevé de routes dans l’application avec ce modèle.
  • Il existe un grand nombre de routes dans l’application.

Comment déterminer si une application s’exécute dans le problème de la table de routage volumineuse

  • Il existe deux symptômes à rechercher :
    • L’application est lente à démarrer sur la première requête.
      • Notez que cela est requis, mais pas suffisant. Il existe de nombreux autres problèmes non liés au routage qui peuvent entraîner un démarrage d’application lent. Vérifiez la condition ci-dessous pour déterminer avec précision que l’application se trouve dans cette situation.
    • L’application consomme beaucoup de mémoire au démarrage et un vidage de la mémoire affiche un grand nombre d’instances Microsoft.AspNetCore.Routing.Matching.DfaNode.

Comment résoudre ce problème

Il existe plusieurs techniques et optimisations qui peuvent être appliquées aux routes afin d’améliorer sensiblement ce scénario :

  • Appliquez des contraintes de routage à vos paramètres, par exemple {parameter:int}, {parameter:guid}, {parameter:regex(\\d+)}, etc. si possible.
    • Cela permet à l’algorithme de routage d’optimiser en interne les structures utilisées pour la correspondance et de réduire considérablement la mémoire utilisée.
    • Dans la grande majorité des cas, cela suffit pour revenir à un comportement acceptable.
  • Modifiez les routes pour déplacer des paramètres vers des segments ultérieurs dans le modèle.
    • Cela réduit le nombre de « chemins » possibles pour correspondre à un point de terminaison donné un chemin d’accès.
  • Utilisez une route dynamique et effectuez le mappage sur un contrôleur/page dynamiquement.
    • Pour ce faire, vous pouvez utiliser MapDynamicControllerRoute et MapDynamicPageRoute.

Intergiciel de court-circuit après routage

Lorsque le routage trouve un point de terminaison, il laisse généralement le rest du pipeline d’intergiciels s’exécuter avant d’appeler la logique de point de terminaison. Les services peuvent réduire l’utilisation des ressources en filtrant les requêtes connues tôt dans le pipeline. Utilisez la méthode d’extension ShortCircuit pour laisser le routage appeler immédiatement la logique de point de terminaison, puis mettre fin à la requête. Par exemple, un itinéraire donné n’a peut-être pas besoin de passer par l’authentification ou l’intergiciel CORS. L’exemple suivant montre comment court-circuiter les requêtes qui correspondent à l’itinéraire /short-circuit :

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

La méthode ShortCircuit(IEndpointConventionBuilder, Nullable<Int32>) peut éventuellement prendre un code d’état.

Utilisez la méthode MapShortCircuit pour configurer le court-circuit pour plusieurs itinéraires à la fois, en lui transmettant un tableau de paramètres ou des préfixes d’URL. Par exemple, les navigateurs et les bots sondent souvent des serveurs pour trouver des chemins connus comme robots.txt et favicon.ico. Si l’application n’a pas ces fichiers, une ligne de code peut configurer les deux itinéraires :

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

MapShortCircuit retourne IEndpointConventionBuilder afin que des contraintes de routage supplémentaires telles que le filtrage d’hôte puissent y être ajoutées.

Les méthodes ShortCircuit et MapShortCircuit n’affectent pas l’intergiciel placé avant UseRouting. La tentative d’utilisation de ces méthodes avec des points de terminaison qui ont des métadonnées [Authorize] ou [RequireCors] entraîne l’échec des requêtes avec un InvalidOperationException. Ces métadonnées sont appliquées par ou par les attributs [Authorize] ou [EnableCors] ou par les méthodes RequireCors ou RequireAuthorization.

Pour voir l’effet d’un court-circuit de l’intergiciel, définissez la catégorie de journalisation « Microsoft » sur « Informations » dans appsettings.Development.json :

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

Exécutez le code ci-dessous :

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

L’exemple suivant provient des journaux de console générés en exécutant le point de terminaison /. Il inclut la sortie de l’intergiciel de journalisation :

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

L’exemple suivant provient de l’exécution du point de terminaison /short-circuit. Il ne contient aucun élément provenant de l’intergiciel de journalisation, car l’intergiciel a été court-circuité :

info: Microsoft.AspNetCore.Routing.EndpointRoutingMiddleware[4]
      The endpoint 'HTTP: GET /short-circuit' is being executed without running additional middleware.
info: Microsoft.AspNetCore.Routing.EndpointRoutingMiddleware[5]
      The endpoint 'HTTP: GET /short-circuit' has been executed without running additional middleware.

Conseils pour les auteurs de bibliothèques

Cette section contient des conseils pour les auteurs de bibliothèques qui s’appuient sur le routage. Ces détails sont destinés à garantir que les développeurs d’applications ont une bonne expérience à l’aide de bibliothèques et d’infrastructures qui étendent le routage.

Définir des points de terminaison

Pour créer une infrastructure qui utilise le routage pour la correspondance d’URL, commencez par définir une expérience utilisateur qui s’appuie sur UseEndpoints.

GÉNÉREZ sur IEndpointRouteBuilder. Cela permet aux utilisateurs de composer votre infrastructure avec d’autres fonctionnalités ASP.NET Core sans confusion. Chaque modèle ASP.NET Core inclut le routage. Supposons que le routage est présent et familier pour les utilisateurs.

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

app.MapHealthChecks("/healthz");

RETOURNEZ un type concret scellé à partir d’un appel à MapMyFramework(...) qui implémente IEndpointConventionBuilder. La plupart des méthodes d’infrastructure Map... suivent ce modèle. L'interface IEndpointConventionBuilder :

  • Permet la composition des métadonnées.
  • Est ciblée par diverses méthodes d’extension.

La déclaration de votre propre type vous permet d’ajouter vos propres fonctionnalités spécifiques à l’infrastructure au générateur. Vous pouvez encapsuler un générateur déclaré par l’infrastructure et lui transférer les appels.

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

app.MapHealthChecks("/healthz");

ENVISAGEZ d’écrire votre propre EndpointDataSource. EndpointDataSource est la primitive de bas niveau permettant de déclarer et de mettre à jour une collection de points de terminaison. EndpointDataSource est une API puissante utilisée par les contrôleurs et Razor Pages. Pour plus d’informations, consultez Routage des points de terminaison dynamiques.

Les tests de routage ont un exemple de base d’une source de données sans mise à jour.

ENVISAGEZ d’implémenter GetGroupedEndpoints. Cela donne un contrôle total sur les conventions de groupe en cours d’exécution et les métadonnées finales sur les points de terminaison groupés. Par exemple, cela permet aux implémentations personnalisées EndpointDataSource d’exécuter des filtres de point de terminaisons ajoutés aux groupes.

NE TENTEZ PAS d’inscrire un EndpointDataSource par défaut. Demandez aux utilisateurs d’inscrire votre infrastructure dans UseEndpoints. La philosophie du routage est que rien n’est inclus par défaut et que UseEndpoints est l’endroit où inscrire des points de terminaison.

Création d’un intergiciel intégré au routage

ENVISAGEZ de définir des types de métadonnées en tant qu’interface.

FAITES EN SORTE qu’il soit possible d’utiliser des types de métadonnées en tant qu’attribut sur des classes et des méthodes.

public interface ICoolMetadata
{
    bool IsCool { get; }
}

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

Les frameworks tels que les contrôleurs et Razor Pages prennent en charge l’application d’attributs de métadonnées aux types et méthodes. Si vous déclarez des types de métadonnées :

  • Rendez-les accessibles en tant qu’attributs.
  • La plupart des utilisateurs sont familiarisés avec l’application d’attributs.

La déclaration d’un type de métadonnées en tant qu’interface ajoute une autre couche de flexibilité :

  • Les interfaces sont composables.
  • Les développeurs peuvent déclarer leurs propres types qui combinent plusieurs stratégies.

FAITES EN SORTE qu’il soit possible de remplacer les métadonnées, comme illustré dans l’exemple suivant :

[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 meilleure façon de suivre ces instructions consiste à éviter de définir des métadonnées de marqueur :

  • Ne recherchez pas simplement la présence d’un type de métadonnées.
  • Définissez une propriété sur les métadonnées et vérifiez la propriété.

La collection de métadonnées est triée et prend en charge la substitution par priorité. Dans le cas des contrôleurs, les métadonnées sur la méthode d’action sont les plus spécifiques.

FAITES EN SORTE que l’intergiciel soit utile avec et sans routage :

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

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

À titre d’exemple de cette recommandation, considérez l’intergiciel UseAuthorization. L’intergiciel d’autorisation vous permet de passer une stratégie de secours. La stratégie de secours, si elle est spécifiée, s’applique aux :

  • Points de terminaison sans stratégie spécifiée.
  • Requêtes qui ne correspondent pas à un point de terminaison.

Cela rend l’intergiciel d’autorisation utile en dehors du contexte du routage. L’intergiciel d’autorisation peut être utilisé pour la programmation d’intergiciels traditionnels.

Déboguer les diagnostics

Pour obtenir une sortie de diagnostic de routage détaillée, définissez Logging:LogLevel:Microsoft sur Debug. Dans l’environnement de développement, définissez le niveau de journal dans appsettings.Development.json :

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

Ressources supplémentaires

Le routage est responsable de la correspondance des requêtes HTTP entrantes et de la distribution de ces requêtes aux points de terminaison exécutables de l’application. Les points de terminaison sont les unités de code de gestion des requêtes exécutables de l’application. Les points de terminaison sont définies dans l’application et configurées au démarrage de l’application. Le processus de correspondance de point de terminaison peut extraire des valeurs de l’URL de la requête et fournir ces valeurs pour le traitement des demandes. Avec les informations de point de terminaison fournies par l’application, le routage peut également générer des URL qui mappent vers des points de terminaison.

Les applications peuvent configurer le routage à l’aide des éléments suivants :

Cet article décrit les détails de bas niveau du routage ASP.NET Core. Pour plus d’informations sur la configuration du routage :

Concepts de base du routage

Le code suivant illustre un exemple de routage de base :

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

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

app.Run();

L’exemple précédent inclut un point de terminaison unique à l’aide de la méthode MapGet :

  • Lorsqu’une requête http GET est envoyée à l’URL racine / :
    • Le délégué de requête s’exécute.
    • Hello World! est écrit dans la réponse HTTP.
  • Si la méthode de requête n’est pas GET ou si l’URL racine n’est pas /, aucun routage ne correspond et un HTTP 404 est retourné.

Le routage utilise une paire d’intergiciels, inscrite par UseRouting et UseEndpoints :

  • UseRouting ajoute la correspondance de routage au pipeline d’intergiciels. Cet intergiciel examine l’ensemble des points de terminaison définis dans l’application et sélectionne la meilleure correspondance en fonction de la requête.
  • UseEndpoints ajoute l’exécution du point de terminaison au pipeline de l’intergiciel. Il exécute le délégué associé au point de terminaison sélectionné.

Les applications n’ont généralement pas besoin d’appeler UseRouting ou UseEndpoints. WebApplicationBuilder configure un pipeline d’intergiciels qui encapsule l’intergiciel ajouté dans Program.cs avec UseRouting et UseEndpoints. Toutefois, les applications peuvent modifier l’ordre dans lequel UseRouting et UseEndpoints s’exécutent en appelant ces méthodes explicitement. Par exemple, le code suivant effectue un appel explicite à UseRouting :

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

app.UseRouting();

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

Dans le code précédent :

  • L’appel à app.Use inscrit un intergiciel personnalisé qui s’exécute au début du pipeline.
  • L’appel à UseRouting configure l’intergiciel de correspondance de routage à exécuter après l’intergiciel personnalisé.
  • Le point de terminaison inscrit avec MapGet s’exécute à la fin du pipeline.

Si l’exemple précédent n’incluait pas d’appel à UseRouting, l’intergiciel personnalisé s’exécuterait après l’intergiciel de correspondance de routage.

Points de terminaison

La méthode MapGet est utilisée pour définir un point de terminaison. Un point de terminaison peut être :

  • Sélectionné, en correspondant à l’URL et à la méthode HTTP.
  • Exécuté, en exécutant le délégué.

Les points de terminaison qui peuvent être mis en correspondance et exécutés par l’application sont configurés dans UseEndpoints. Par exemple, MapGet, MapPost et des méthodes similaires connectent des délégués de requête au système de routage. Des méthodes supplémentaires peuvent être utilisées pour connecter les fonctionnalités d’infrastructure ASP.NET Core au système de routage :

L’exemple suivant montre le routage avec un modèle de routage plus sophistiqué :

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

La chaîne /hello/{name:alpha} est un modèle de routage. Un modèle de routage est utilisé pour configurer la mise en correspondance du point de terminaison. Dans ce cas, le modèle correspond à :

  • Un URL comme /hello/Docs
  • Tout chemin d’URL qui commence par /hello/ suivi d’une séquence de caractères alphabétiques. :alpha applique une contrainte de routage qui fait correspondre uniquement les caractères alphabétiques. Les contraintes de routage sont expliquées plus loin dans cet article.

Deuxième segment du chemin d’URL, {name:alpha} :

L’exemple suivant montre le routage avec les contrôles d’intégrité et l’autorisation :

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

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

L’exemple précédent montre comment :

  • L’intergiciel d’autorisation peut être utilisé avec le routage.
  • Les points de terminaison peuvent être utilisés pour configurer le comportement d’autorisation.

L’appel MapHealthChecks ajoute un point de terminaison de contrôle d’intégrité. Le chaînage RequireAuthorization sur cet appel attache une stratégie d’autorisation au point de terminaison.

Appeler UseAuthentication et UseAuthorization ajoute l’intergiciel d’authentification et d’autorisation. Ces intergiciels sont placés entre UseRouting et UseEndpoints afin qu’ils puissent :

  • Voir le point de terminaison sélectionné par UseRouting.
  • Appliquez une stratégie d’autorisation avant que UseEndpoints les distribue au point de terminaison.

Métadonnées de point de terminaison

Dans l’exemple précédent, il existe deux points de terminaison, mais seul le point de terminaison de contrôle d’intégrité a une stratégie d’autorisation attachée. Si la demande correspond au point de terminaison de contrôle d’intégrité, /healthz, une vérification d’autorisation est effectuée. Cela montre que les points de terminaison peuvent avoir des données supplémentaires attachées. Ces données supplémentaires sont appelées métadonnées de point de terminaison :

  • Les métadonnées peuvent être traitées par un intergiciel prenant en charge le routage.
  • Les métadonnées peuvent être de n’importe quel type .NET.

Concepts de routage

Le système de routage s’appuie sur le pipeline d’intergiciels en ajoutant le concept de point de terminaison puissant. Les points de terminaison représentent des unités des fonctionnalités de l’application qui sont distinctes les unes des autres en termes de routage, d’autorisation et de n’importe quel nombre de systèmes ASP.NET Core.

Définition de point de terminaison ASP.NET Core

Un point de terminaison ASP.NET Core est :

Le code suivant montre comment récupérer et inspecter le point de terminaison correspondant à la requête actuelle :

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

Le point de terminaison, s’il est sélectionné, peut être récupéré à partir de HttpContext. Ses propriétés peuvent être inspectées. Les objets de point de terminaison sont immuables et ne peuvent pas être modifiés après la création. Le type de point de terminaison le plus courant est RouteEndpoint. RouteEndpoint inclut des informations qui lui permettent d’être sélectionné par le système de routage.

Dans le code précédent, app.Use configure un intergiciel inclus.

Le code suivant montre que, selon l’endroit où app.Use est appelé dans le pipeline, il se peut qu’il n’y ait pas de point de terminaison :

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

L’exemple précédent ajoute des instructions Console.WriteLine qui indiquent si un point de terminaison a été sélectionné ou non. Pour plus de clarté, l’exemple affecte un nom complet au point de terminaison / fourni.

L’exemple précédent inclut également des appels vers UseRouting et UseEndpoints pour contrôler exactement quand ces intergiciels s’exécutent dans le pipeline.

L’exécution de ce code avec une URL / affiche :

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

L’exécution de ce code avec toute autre URL affiche :

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

Cette sortie montre que :

  • Le point de terminaison est toujours null avant que soit UseRouting appelé.
  • Si une correspondance est trouvée, le point de terminaison n’est pas null entre UseRouting et UseEndpoints.
  • L’intergiciel UseEndpoints est terminal lorsqu’une correspondance est trouvée. L’intergiciel terminal est défini plus loin dans cet article.
  • L’intergiciel après UseEndpoints s’exécute uniquement lorsqu’aucune correspondance n’est trouvée.

L’intergiciel UseRouting utilise la méthode SetEndpoint pour attacher le point de terminaison au contexte actuel. Il est possible de remplacer l’intergiciel UseRouting par une logique personnalisée et d’obtenir les avantages de l’utilisation de points de terminaison. Les points de terminaison sont une primitive de bas niveau comme l’intergiciel et ne sont pas couplés à l’implémentation du routage. La plupart des applications n’ont pas besoin de remplacer UseRouting par une logique personnalisée.

L’intergiciel UseEndpoints est conçu pour être utilisé en tandem avec l’intergiciel UseRouting. La logique principale pour exécuter un point de terminaison n’est pas compliquée. Utilisez GetEndpoint pour récupérer le point de terminaison, puis appelez sa propriété RequestDelegate.

Le code suivant montre comment l’intergiciel peut influencer ou réagir au routage :

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

L’exemple précédent illustre deux concepts importants :

  • L’intergiciel peut s’exécuter avant UseRouting pour modifier les données sur lesquelles le routage fonctionne.
  • L’intergiciel peut s’exécuter entre UseRouting et UseEndpoints pour traiter les résultats du routage avant l’exécution du point de terminaison.
    • Intergiciel qui s’exécute entre UseRouting et UseEndpoints :
      • Inspecte généralement les métadonnées pour comprendre les points de terminaison.
      • Prend souvent des décisions de sécurité, comme le font UseAuthorization et UseCors.
    • La combinaison d’intergiciels et de métadonnées permet de configurer des stratégies par point de terminaison.

Le code précédent montre un exemple d’intergiciel personnalisé qui prend en charge les stratégies par point de terminaison. L’intergiciel écrit un journal d’audit de l’accès aux données sensibles dans la console. L’intergiciel peut être configuré pour auditer un point de terminaison avec les métadonnées RequiresAuditAttribute. Cet exemple illustre un modèle d’activation dans lequel seuls les points de terminaison marqués comme sensibles sont audités. Il est possible de définir l’inverse de cette logique, en auditant tout ce qui n’est pas marqué comme sécurisé, par exemple. Le système de métadonnées de point de terminaison est flexible. Cette logique peut être conçue de quelque manière que ce soit en fonction du cas d’usage.

L’exemple de code précédent est destiné à illustrer les concepts de base des points de terminaison. L’exemple n’est pas destiné à une utilisation en production. Une version plus complète d’un intergiciel de journal d’audit :

  • Se connecterais à un fichier ou une base de données.
  • Inclurais des détails tels que l’utilisateur, l’adresse IP, le nom du point de terminaison sensible, etc.

Les métadonnées de stratégie d’audit RequiresAuditAttribute sont définies en tant que Attribute pour faciliter l’utilisation avec des infrastructures basées sur des classes telles que des contrôleurs et SignalR. Lors de l’utilisation de route vers le code :

  • Les métadonnées sont attachées à une API de générateur.
  • Les infrastructure basées sur des classes incluent tous les attributs sur la méthode et la classe correspondantes lors de la création de points de terminaison.

Les meilleures pratiques pour les types de métadonnées sont de les définir en tant qu’interfaces ou attributs. Les interfaces et les attributs autorisent la réutilisation du code. Le système de métadonnées est flexible et n’impose aucune limitation.

Comparer l’intergiciel terminal avec le routage

L’exemple suivant illustre à la fois l’intergiciel terminal et le routage :

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

Le style d’intergiciel indiqué avec Approach 1: est l’intergiciel terminal. Il est appelé intergiciel terminal, car il effectue une opération de correspondance :

  • L’opération de correspondance dans l’exemple précédent est Path == "/" pour l’intergiciel et Path == "/Routing" pour le routage.
  • Lorsqu’une correspondance réussit, elle exécute certaines fonctionnalités et retourne, plutôt que d’appeler l’intergiciel next.

Il est appelé intergiciel de terminal, car il met fin à la recherche, exécute certaines fonctionnalités, puis retourne.

La liste suivante compare les intergiciels de terminal avec le routage :

  • Les deux approches permettent de terminer le pipeline de traitement :
    • L’intergiciel met fin au pipeline en retournant plutôt qu’en appelant next.
    • Les points de terminaison sont toujours terminaux.
  • L’intergiciel terminal permet de positionner l’intergiciel à un emplacement arbitraire dans le pipeline :
    • Les points de terminaison s’exécutent à la position de UseEndpoints.
  • L’intergiciel de terminal permet au code arbitraire de déterminer quand l’intergiciel fait correspondre :
    • Le code de correspondance de routage personnalisé peut être détaillé et difficile à écrire correctement.
    • Le routage fournit des solutions simples pour les applications classiques. La plupart des applications ne nécessitent pas de code de correspondance de routage personnalisé.
  • L’interface des points de terminaison avec un intergiciel tel que UseAuthorization et UseCors.
    • L’utilisation d’un intergiciel terminal avec UseAuthorization ou UseCors nécessite une interaction manuelle avec le système d’autorisation.

Un point de terminaison définit les :

  • Le délégué pour traiter les demandes.
  • La collection de métadonnées arbitraires. Les métadonnées sont utilisées pour implémenter des problèmes transversaux basés sur des stratégies et une configuration attachées à chaque point de terminaison.

L’intergiciel terminal peut être un outil efficace, mais peut nécessiter :

  • Une quantité importante de codage et de test.
  • L’intégration manuelle avec d’autres systèmes pour atteindre le niveau de flexibilité souhaité.

Envisagez d’intégrer le routage avant d’écrire un intergiciel terminal.

Les intergiciels terminaux existants qui s’intègrent à Map ou MapWhen peuvent généralement être transformés en point de terminaison prenant en charge le routage. MapHealthChecks illustre le modèle de routeur-ware :

  • Écrire une méthode d’extension sur IEndpointRouteBuilder.
  • Créer un pipeline d’intergiciels imbriqués à l’aide de CreateApplicationBuilder.
  • Attacher l’intergiciel au nouveau pipeline. Dans ce cas, UseHealthChecks.
  • Build le pipeline d’intergiciel dans un RequestDelegate.
  • Appeler Map et fournir le nouveau pipeline d’intergiciels.
  • Retourner l’objet générateur fourni par Map à partir de la méthode d’extension.

Le code suivant montre l’utilisation de MapHealthChecks :

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

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

L’exemple précédent montre pourquoi le retour de l’objet générateur est important. Le renvoi de l’objet générateur permet au développeur d’applications de configurer des stratégies telles que l’autorisation pour le point de terminaison. Dans cet exemple, l’intergiciel de contrôle d’intégrité n’a pas d’intégration directe avec le système d’autorisation.

Le système de métadonnées a été créé en réponse aux problèmes rencontrés par les auteurs d’extensibilité à l’aide de l’intergiciel terminal. Il est problématique pour chaque intergiciel d’implémenter sa propre intégration avec le système d’autorisation.

Correspondance d’URL

  • La correspondance d’URL est le processus par lequel le routage distribue une requête entrante à un point de terminaison.
  • Est basé sur des données dans le chemin d’URL et les en-têtes.
  • Peut être étendu pour prendre en compte toutes les données de la demande.

Lorsqu’un intergiciel de routage s’exécute, il définit les valeurs de Endpoint et de routage et vers une fonctionnalité de requête sur HttpContext à partir de la requête actuelle :

  • L’appel de HttpContext.GetEndpoint obtient le point de terminaison.
  • HttpRequest.RouteValues récupère la collection de valeurs d’itinéraire.

L’intergiciel s’exécute après que l’intergiciel de routage puisse inspecter le point de terminaison et prendre des mesures. Par exemple, un intergiciel d’autorisation peut interroger la collection de métadonnées du point de terminaison pour une stratégie d’autorisation. Une fois que tous les intergiciels dans le pipeline de traitement de requêtes sont exécutés, le délégué du point de terminaison sélectionné est appelé.

Le système de routage dans le routage de point de terminaison est responsable de toutes les décisions de distribution. Étant donné que l’intergiciel applique des stratégies basées sur le point de terminaison sélectionné, il est important que :

  • Toute décision susceptible d’affecter la répartition ou l’application de stratégies de sécurité soit prise à l’intérieur du système de routage.

Avertissement

Pour une compatibilité descendante, lorsqu’un délégué de point de terminaison Contrôleur ou Razor Pages est exécuté, les propriétés de RouteContext.RouteData sont définies sur des valeurs appropriées en fonction du traitement des requêtes effectué jusqu’à présent.

Le type RouteContext sera marqué comme obsolète dans une version ultérieure :

  • Migrez RouteData.Values vers HttpRequest.RouteValues.
  • Migrez RouteData.DataTokens pour récupérer IDataTokensMetadata à partir des métadonnées du point de terminaison.

La correspondance d’URL fonctionne dans un ensemble configurable de phases. Dans chaque phase, la sortie est un ensemble de correspondances. L’ensemble de correspondances peut être réduit plus loin par la phase suivante. L’implémentation du routage ne garantit pas un ordre de traitement pour les points de terminaison correspondants. Toutes les correspondances possibles sont traitées simultanément. Les phases de correspondance d’URL se produisent dans l’ordre suivant. ASP.NET Core :

  1. Traite le chemin d’URL par rapport à l’ensemble de points de terminaison et à leurs modèles de routage, en collectant toutes les correspondances.
  2. Prend la liste précédente et supprime les correspondances qui échouent avec les contraintes de routage appliquées.
  3. Prend la liste précédente et supprime les correspondances qui échouent au jeu d’instances MatcherPolicy.
  4. Utilise EndpointSelector pour prendre une décision finale à partir de la liste précédente.

La liste des points de terminaison est hiérarchisée en fonction des éléments suivants :

Tous les points de terminaison correspondants sont traités dans chaque phase jusqu’à ce que EndpointSelector soit atteint. EndpointSelector est la phase finale. Il choisit le point de terminaison avec la priorité la plus élevée parmi les correspondances comme correspondance optimale. S’il existe d’autres correspondances avec la même priorité que la meilleure correspondance, une exception de correspondance ambiguë est levée.

La priorité du routage est calculée en fonction d’un modèle de routage plus spécifique qui reçoit une priorité plus élevée. Par exemple, considérez les modèles /hello et /{message} :

  • Les deux correspondent au chemin d’URL /hello.
  • /hello est plus spécifique et, par conséquent, a une priorité plus élevée.

En général, la priorité des routages permet de choisir la meilleure correspondance pour les types de schémas d’URL utilisés dans la pratique. Utilisez Order uniquement si nécessaire pour éviter une ambiguïté.

En raison des types d’extensibilité fournis par le routage, il n’est pas possible que le système de routage calcule à l’avance les routages ambigus. Prenons un exemple tel que les modèles de routage /{message:alpha} et /{message:int} :

  • La contrainte alpha ne fait correspondre que les caractères alphabétiques.
  • La contrainte int ne fait correspondre que les nombres.
  • Ces modèles ont la même priorité de routage, mais il n’existe aucune URL à laquelle ils correspondent.
  • Si le système de routage a signalé une erreur d’ambiguïté au démarrage, il bloque ce cas d’usage valide.

Avertissement

L’ordre des opérations à l’intérieur de UseEndpoints n’influence pas le comportement du routage, à une exception près. MapControllerRoute et MapAreaRoute attribuent automatiquement une valeur de commande à leurs points de terminaison en fonction de l’ordre qu’ils appellent. Cela simule le comportement long des contrôleurs sans le système de routage fournissant les mêmes garanties que les implémentations de routage plus anciennes.

Routage des points de terminaison dans ASP.NET Core :

  • N’a pas le concept de routages.
  • Ne fournit pas de garanties de commande. Tous les points de terminaison sont traités simultanément.

Priorité du modèle de routage et ordre de sélection du point de terminaison

La priorité du modèle de routage est un système qui attribue à chaque modèle de routage une valeur en fonction de sa spécificité. La priorité du modèle de routage :

  • Évite la nécessité d’ajuster l’ordre des points de terminaison dans les cas courants.
  • Tente de faire correspondre les attentes courantes du comportement de routage.

Par exemple, envisagez des modèles /Products/List et /Products/{id}. Il serait raisonnable de supposer que /Products/List est une meilleure correspondance que /Products/{id} pour le chemin d’URL /Products/List. Cela fonctionne parce que le segment littéral /List est considéré comme ayant une meilleure priorité que le segment de paramètre /{id}.

Les détails du fonctionnement de la priorité sont couplés à la façon dont les modèles de routage sont définis :

  • Les modèles avec plus de segments sont considérés comme plus spécifiques.
  • Un segment avec du texte littéral est considéré comme plus spécifique qu’un segment de paramètre.
  • Un segment de paramètre avec une contrainte est considéré comme plus spécifique qu’un segment sans.
  • Un segment complexe est considéré aussi spécifique qu’un segment de paramètre avec une contrainte.
  • Les paramètres catch-all sont les moins spécifiques. Consultez catch-all dans la section Modèles de routage pour obtenir des informations importantes sur les routages catch-all.

Concepts de génération d’URL

La génération des URL :

  • Est le processus par lequel le routage peut créer un chemin d’URL basé sur un ensemble de valeurs de route.
  • Permet une séparation logique entre les points de terminaison et les URL qui y accèdent.

Le routage des points de terminaison inclut l’API LinkGenerator. LinkGenerator est un service singleton disponible à partir de DI. L’API LinkGenerator peut être utilisée en dehors du contexte d’une requête en cours d’exécution. Mvc.IUrlHelper et les scénarios qui s’appuient sur IUrlHelper, comme l’Assistance des balises, l’assistance HTML et les résultats d’action, utilisent l’API LinkGenerator pour fournir les fonctionnalités de création de liens.

Le générateur de liens est basé sur le concept d’une adresse et de schémas d’adresse. Un schéma d’adresse est un moyen de déterminer les points de terminaison à prendre en compte pour la génération de liens. Par exemple, les scénarios de nom de route et de valeurs de route que de nombreux utilisateurs connaissent bien dans les contrôleurs et Razor Pages sont implémentés en tant que schémas d’adresse.

Le générateur de liens peut lier à des contrôleurs et Razor Pages via les méthodes d’extension suivantes :

Une surcharge de ces méthodes accepte des arguments qui incluent HttpContext. Ces méthodes sont fonctionnellement équivalentes à Url.Action et à Url.Page, mais elles offrent davantage de flexibilité et d’options.

Les méthodes GetPath* sont les plus similaires à Url.Action et Url.Page, car elles génèrent un URI contenant un chemin d’accès absolu. Les méthodes GetUri* génèrent toujours un URI absolu contenant un schéma et un hôte. Les méthodes qui acceptent un HttpContext génèrent un URI dans le contexte de la requête en cours d’exécution. Les valeurs de route ambiante, le chemin de base d’URL, le schéma et l’hôte de la requête en cours d’exécution sont utilisés, sauf s’ils sont remplacés.

LinkGenerator est appelé avec une adresse. La génération d’un URI se fait en deux étapes :

  1. Une adresse est liée à une liste de points de terminaison qui correspondent à l’adresse.
  2. Le RoutePattern de chaque point de terminaison est évalué jusqu’à ce qu’un modèle de route correspondant aux valeurs fournies soit trouvé. Le résultat obtenu est combiné avec d’autres parties de l’URI fournies par le générateur de liens, puis il est retourné.

Les méthodes fournies par LinkGenerator prennent en charge des fonctionnalités de génération de liens standard pour n’importe quel type d’adresse. La façon la plus pratique d’utiliser le générateur de liens est de le faire via des méthodes d’extension qui effectuent des opérations pour un type d’adresse spécifique :

Méthode d’extension Description
GetPathByAddress Génère un URI avec un chemin absolu basé sur les valeurs fournies.
GetUriByAddress Génère un URI absolu basé sur les valeurs fournies.

Avertissement

Faites attention aux implications suivantes de l’appel de méthodes LinkGenerator :

  • Utilisez les méthodes d’extension GetUri* avec précaution dans une configuration d’application qui ne valide pas l’en-tête Host des requêtes entrantes. Si l’en-tête Host des requêtes entrantes n’est pas validé, l’entrée de requête non approuvée peut être renvoyée au client dans les URI d’une page ou d’une vue. Nous recommandons que toutes les applications de production configurent leur serveur pour qu’il valide l’en-tête Host par rapport à des valeurs valides connues.

  • Utilisez LinkGenerator avec précaution dans le middleware en combinaison avec Map ou MapWhen. Map* modifie le chemin de base de la requête en cours d’exécution, ce qui affecte la sortie de la génération de liens. Toutes les API LinkGenerator permettent la spécification d’un chemin de base. Spécifiez un chemin de base vide pour annuler l’effet de Map* sur la génération de liens.

Exemple de middleware

Dans l’exemple suivant, un intergiciel utilise l’API LinkGenerator pour créer un lien vers une méthode d’action qui liste les produits d’un magasin. L’utilisation du générateur de liens en l’injectant dans une classe et en appelant GenerateLink est disponible pour n’importe quelle classe dans une application :

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

Modèles de route

Les jetons dans {} définissent les paramètres de routage liés si le routage est mis en correspondance. Plusieurs paramètres de routage peuvent être définis dans un segment de routage, mais les paramètres de routage doivent être séparés par une valeur littérale. Par exemple :

{controller=Home}{action=Index}

n’est pas un routage valide, car il n’y a pas de valeur littérale entre {controller} et {action}. Les paramètres de routage doivent avoir un nom, et ils autorisent la spécification d’attributs supplémentaires.

Un texte littéral autre que les paramètres de routage (par exemple, {id}) et le séparateur de chemin / doit correspondre au texte présent dans l’URL. La correspondance de texte ne respecte pas la casse et est basée sur la représentation décodée du chemin des URL. Pour mettre en correspondance un délimiteur de paramètre de route littéral { ou }, placez-le dans une séquence d’échappement en répétant le caractère. Par exemple {{ ou }}.

Astérisque * ou astérisque double ** :

  • Peut être utilisé comme préfixe pour un paramètre de routage pour établir une liaison au rest de l’URI.
  • Ils sont appelés des paramètres catch-all. Par exemple, blog/{**slug} :
    • Correspond à n’importe quel URI qui commence par blog/ et a n’importe quelle valeur qui suit.
    • La valeur suivant blog/ est affectée à la valeur de routage slug.

Avertissement

Un paramètre catch-all peut faire correspondre les mauvais routages en raison d’un bogue dans le routage. Les applications affectées par ce bogue présentent les caractéristiques suivantes :

  • Un routage catch-all, par exemple, {**slug}"
  • Le routage catch-all ne fait pas correspondre les demandes qu’il doit faire correspondre.
  • La suppression d’autres routes fait que la route catch-all commence à fonctionner.

Consultez les bogues GitHub 18677 et 16579, par exemple les cas qui ont rencontré ce bogue.

Un correctif d’opt-in pour ce bogue est contenu dans le Kit de développement logiciel (SDK) .NET Core 3.1.301 et versions ultérieures. Le code suivant définit un commutateur interne qui corrige ce bogue :

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

Les paramètres fourre-tout peuvent également établir une correspondance avec la chaîne vide.

Le paramètre catch-all place les caractères appropriés dans une séquence d’échappement lorsque la route est utilisée pour générer une URL, y compris les caractères de séparation de chemin /. Par exemple, la route foo/{*path} avec les valeurs de route { path = "my/path" } génère foo/my%2Fpath. Notez la barre oblique d’échappement. Pour les séparateurs de chemin aller-retour, utilisez le préfixe de paramètre de routage **. La route foo/{**path} avec { path = "my/path" } génère foo/my/path.

Les modèles d’URL qui tentent de capturer un nom de fichier avec une extension de fichier facultative doivent faire l’objet de considérations supplémentaires. Prenez par exemple le modèle files/{filename}.{ext?}. Quand des valeurs existent à la fois pour filename et pour ext, les deux valeurs sont renseignées. Si seule une valeur existe pour filename dans l’URL, une correspondance est trouvée pour la route, car le . de fin est facultatif. Les URL suivantes correspondent à cette route :

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

Les paramètres de route peuvent avoir des valeurs par défaut, désignées en spécifiant la valeur par défaut après le nom du paramètre, séparée par un signe égal (=). Par exemple, {controller=Home} définit Home comme valeur par défaut de controller. La valeur par défaut est utilisée si aucune valeur n’est présente dans l’URL pour le paramètre. Vous pouvez rendre facultatifs les paramètres de route en ajoutant un point d’interrogation (?) à la fin du nom du paramètre. Par exemple, id? La différence entre les valeurs facultatives et les paramètres de routage par défaut est la suivante :

  • Un paramètre de routage avec une valeur par défaut produit toujours une valeur.
  • Un paramètre facultatif a une valeur uniquement lorsqu’une valeur est fournie par l’URL de la requête.

Les paramètres de route peuvent avoir des contraintes, qui doivent correspondre à la valeur de route liée à partir de l’URL. L’ajout de : et d’un nom de contrainte après le nom du paramètre de routage spécifie une contrainte inline sur un paramètre de routage. Si la contrainte exige des arguments, ils sont fournis entre parenthèses (...) après le nom de la contrainte. Il est possible de spécifier plusieurs contraintes inline en ajoutant un autre : et le nom d’une autre contrainte.

Le nom de la contrainte et les arguments sont passés au service IInlineConstraintResolver pour créer une instance de IRouteConstraint à utiliser dans le traitement des URL. Par exemple, le modèle de routage blog/{article:minlength(10)} spécifie une contrainte minlength avec l’argument 10. Pour plus d’informations sur les contraintes de route et pour obtenir la liste des contraintes fournies par le framework, consultez la section Contraintes de route.

Les paramètres de route peuvent également avoir des transformateurs de paramètres. Les transformateurs de paramètres transforment la valeur d’un paramètre lors de la génération de liens et d’actions et de pages correspondantes en URL. À l’instar des contraintes, les transformateurs de paramètre peuvent être ajoutés inline à un paramètre de routage en ajoutant un : et le nom du transformateur après le nom du paramètre de routage. Par exemple, le modèle de routage blog/{article:slugify} spécifie un transformateur slugify. Pour plus d’informations sur les transformateurs de paramètre, consultez la section Transformateurs de paramètre.

Le tableau suivant montre des exemples de modèles de route et leur comportement.

Modèle de routage Exemple d’URI en correspondance URI de requête
hello /hello Correspond seulement au chemin unique /hello.
{Page=Home} / Correspond à Page et le définit sur Home.
{Page=Home} /Contact Correspond à Page et le définit sur Contact.
{controller}/{action}/{id?} /Products/List Mappe au contrôleur Products et à l’action List.
{controller}/{action}/{id?} /Products/Details/123 Mappe au contrôleur Products et à l’action Details avec id défini sur 123).
{controller=Home}/{action=Index}/{id?} / Mappe au contrôleur Home et à l’action Index. id est ignoré.
{controller=Home}/{action=Index}/{id?} /Products Mappe au contrôleur Products et à la méthode Index. id est ignoré.

L’utilisation d’un modèle est généralement l’approche la plus simple pour le routage. Il est également possible de spécifier des contraintes et des valeurs par défaut hors du modèle de routage.

Segments complexes

Les segments complexes sont traités en faisant correspondre les délimiteurs littéraux de droite à gauche de manière non gourmande. Par exemple, [Route("/a{b}c{d}")] est un segment complexe. Les segments complexes fonctionnent d’une manière particulière qui doit être comprise pour les utiliser correctement. L’exemple de cette section montre pourquoi les segments complexes ne fonctionnent vraiment bien que lorsque le texte du délimiteur n’apparaît pas dans les valeurs des paramètres. L’utilisation d’un regex, puis l’extraction manuelle des valeurs est nécessaire pour des cas plus complexes.

Avertissement

Lorsque vous utilisez System.Text.RegularExpressions pour traiter une entrée non approuvée, passez un délai d’expiration. Un utilisateur malveillant peut fournir une entrée à RegularExpressions, provoquant une attaque par déni de service. Les API d’infrastructure ASP.NET Core qui utilisent RegularExpressions passent un délai d’expiration.

Il s’agit d’un résumé des étapes effectuées par le routage avec le modèle /a{b}c{d} et le chemin d’URL /abcd. Un | est utilisé pour vous aider à visualiser le fonctionnement de l’algorithme :

  • Le premier littéral, de droite à gauche, est c. Donc /abcd est recherché à partir de la droite et trouve /ab|c|d.
  • Tout ce qui se trouve à droite (d) est désormais mis en correspondance avec le paramètre de routage {d}.
  • Le littéral suivant, de droite à gauche, est a. Donc /ab|c|d est recherché à partir de là où nous sommes partis, puis a est trouvé /|a|b|c|d.
  • La valeur à droite (b) est désormais associée au paramètre de routage {b}.
  • Il n’y a pas de texte restant et aucun modèle de routage restant. Il s’agit donc d’une correspondance.

Voici un exemple de cas négatif utilisant le même modèle /a{b}c{d} et le chemin d’URL /aabcd. Un | est utilisé pour vous aider à visualiser le fonctionnement de l’algorithme. Ce cas n’est pas une correspondance, qui est expliquée par le même algorithme :

  • Le premier littéral, de droite à gauche, est c. Donc /aabcd est recherché à partir de la droite et trouve /aab|c|d.
  • Tout ce qui se trouve à droite (d) est désormais mis en correspondance avec le paramètre de routage {d}.
  • Le littéral suivant, de droite à gauche, est a. Donc /aab|c|d est recherché à partir de là où nous sommes partis, puis a est trouvé /a|a|b|c|d.
  • La valeur à droite (b) est désormais associée au paramètre de routage {b}.
  • À ce stade, il reste du texte a, mais l’algorithme n’a plus de modèle de routage à analyser. Il ne s’agit donc pas d’une correspondance.

Étant donné que l’algorithme correspondant n’est pas gourmand :

  • Il correspond à la plus petite quantité de texte possible dans chaque étape.
  • Si la valeur de délimiteur apparaît à l’intérieur des valeurs de paramètre, elle ne correspond pas.

Les expressions régulières fournissent beaucoup plus de contrôle sur leur comportement de correspondance.

La correspondance gourmande, également appelée correspondance paresseuse, correspond à la plus grande chaîne possible. La chaîne non gourmande correspond à la plus petite chaîne possible.

Routage avec des caractères spéciaux

Le routage avec des caractères spéciaux peut entraîner des résultats inattendus. Par exemple, considérez un contrôleur avec la méthode d’action suivante :

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

Lorsque string id contient les valeurs encodées suivantes, des résultats inattendus peuvent se produire :

ASCII Encoded
/ %2F
+

Les paramètres de routage ne sont pas toujours décodés par URL. Ce problème peut être résolu à l’avenir. Pour plus d’informations, consultez ce problème GitHub ;

Contraintes d'itinéraire

Les contraintes de route s’exécutent quand une correspondance s’est produite pour l’URL entrante, et le chemin de l’URL est tokenisé en valeurs de route. En général, les contraintes de routage inspectent la valeur de route associée par le biais du modèle de routage, et créent une décision true ou false indiquant si la valeur est acceptable. Certaines contraintes de routage utilisent des données hors de la valeur de route pour déterminer si la requête peut être routée. Par exemple, HttpMethodRouteConstraint peut accepter ou rejeter une requête en fonction de son verbe HTTP. Les contraintes sont utilisées dans le routage des requêtes et la génération des liens.

Avertissement

N’utilisez pas de contraintes pour la validation des entrées. Si des contraintes sont utilisées pour la validation d’entrée, une entrée non valide génère une réponse introuvable 404. Une entrée non valide doit produire une demande incorrecte 400 avec un message d’erreur approprié. Les contraintes de route sont utilisées pour lever l’ambiguïté entre des routes similaires, et non pas pour valider les entrées d’une route particulière.

Le tableau suivant montre des exemples de contrainte de route et leur comportement attendu :

contrainte Exemple Exemples de correspondances Notes
int {id:int} 123456789, -123456789 Correspond à n’importe quel entier
bool {active:bool} true, FALSE Correspond à true ou false. Non-respect de la casse
datetime {dob:datetime} 2016-12-31, 2016-12-31 7:32pm Correspond à une valeur valide DateTime dans la culture invariante. Voir l’avertissement précédent.
decimal {price:decimal} 49.99, -1,000.01 Correspond à une valeur valide decimal dans la culture invariante. Voir l’avertissement précédent.
double {weight:double} 1.234, -1,001.01e8 Correspond à une valeur valide double dans la culture invariante. Voir l’avertissement précédent.
float {weight:float} 1.234, -1,001.01e8 Correspond à une valeur valide float dans la culture invariante. Voir l’avertissement précédent.
guid {id:guid} CD2C1638-1638-72D5-1638-DEADBEEF1638 Correspond à une valeur Guid valide
long {ticks:long} 123456789, -123456789 Correspond à une valeur long valide
minlength(value) {username:minlength(4)} Rick La chaîne doit comporter au moins 4 caractères
maxlength(value) {filename:maxlength(8)} MyFile La chaîne ne doit pas comporter plus de 8 caractères
length(length) {filename:length(12)} somefile.txt La chaîne doit comporter exactement 12 caractères
length(min,max) {filename:length(8,16)} somefile.txt La chaîne doit comporter au moins 8 caractères et pas plus de 16 caractères
min(value) {age:min(18)} 19 La valeur entière doit être au moins égale à 18
max(value) {age:max(120)} 91 La valeur entière ne doit pas être supérieure à 120
range(min,max) {age:range(18,120)} 91 La valeur entière doit être au moins égale à 18 mais ne doit pas être supérieure à 120
alpha {name:alpha} Rick La chaîne doit se composer d’un ou de plusieurs caractères alphabétiques (a-z, non-respect de la casse).
regex(expression) {ssn:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)} 123-45-6789 La chaîne doit correspondre à l’expression régulière. Consultez des conseils sur la définition d’une expression régulière.
required {name:required} Rick Utilisé pour garantir qu’une valeur autre qu’un paramètre est présente pendant la génération de l’URL

Avertissement

Lorsque vous utilisez System.Text.RegularExpressions pour traiter une entrée non approuvée, passez un délai d’expiration. Un utilisateur malveillant peut fournir une entrée à RegularExpressions, provoquant une attaque par déni de service. Les API d’infrastructure ASP.NET Core qui utilisent RegularExpressions passent un délai d’expiration.

Il est possible d’appliquer plusieurs contraintes séparées par un point-virgule à un même paramètre. Par exemple, la contrainte suivante limite un paramètre à une valeur entière supérieure ou égale à 1 :

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

Avertissement

Les contraintes de routage qui vérifient que l’URL peut être convertie en type CLR utilisent toujours la culture invariant. Par exemple, conversion en type CLR int ou DateTime. Ces contraintes partent du principe que l’URL ne peut pas être localisé. Les contraintes de routage fournies par le framework ne modifient pas les valeurs stockées dans les valeurs de route. Toutes les valeurs de route analysées à partir de l’URL sont stockées sous forme de chaînes. Par exemple, la contrainte float tente de convertir la valeur de route en valeur float, mais la valeur convertie est utilisée uniquement pour vérifier qu’elle peut être convertie en valeur float.

Expressions régulières dans les contraintes

Avertissement

Lorsque vous utilisez System.Text.RegularExpressions pour traiter une entrée non approuvée, passez un délai d’expiration. Un utilisateur malveillant peut fournir une entrée à RegularExpressions, provoquant une attaque par déni de service. Les API d’infrastructure ASP.NET Core qui utilisent RegularExpressions passent un délai d’expiration.

Les expressions régulières peuvent être spécifiées en tant que contraintes inline à l’aide de la contrainte de routage regex(...). Les méthodes de la famille MapControllerRoute acceptent également un littéral d’objet de contraintes. Si ce formulaire est utilisé, les valeurs de chaîne sont interprétées comme des expressions régulières.

Le code suivant utilise une contrainte d’expression régulière inline :

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

Le code suivant utilise un littéral d’objet pour spécifier une contrainte d’expression régulière :

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

Le framework ASP.NET Core ajoute RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant au constructeur d’expression régulière. Pour obtenir une description de ces membres, consultez RegexOptions.

Les expressions régulières utilisent les délimiteurs et des jetons semblables à ceux utilisés par le service de routage et le langage C#. Les jetons d’expression régulière doivent être placés dans une séquence d’échappement. Pour utiliser l’expression régulière ^\d{3}-\d{2}-\d{4}$ dans une contrainte inline, utilisez l’une des options suivantes :

  • Remplacez les caractères \ fournis dans la chaîne en tant que caractères \\ dans le fichier source C# afin d’échapper au caractère \ d’échappement de chaîne.
  • Littéraux de chaîne verbatim.

Pour placer en échappement les caractères de délimiteur de paramètre de route {, }, [, ], doublez les caractères dans l’expression, par exemple {{, }}, [[, ]]. Le tableau suivant montre une expression régulière et la version placée en échappement :

Expression régulière Expression régulière en échappement
^\d{3}-\d{2}-\d{4}$ ^\\d{{3}}-\\d{{2}}-\\d{{4}}$
^[a-z]{2}$ ^[[a-z]]{{2}}$

Les expressions régulières utilisées dans le routage commencent souvent par le caractère ^ et correspondent à la position de début de la chaîne. Les expressions se terminent souvent par le caractère $ et correspondent à la fin de la chaîne. Les caractères ^ et $ garantissent que l’expression régulière établit une correspondance avec la totalité de la valeur du paramètre de route. Sans les caractères ^ et $, l’expression régulière peut correspondre à n’importe quelle sous-chaîne dans la chaîne, ce qui est souvent indésirable. Le tableau suivant contient des exemples et explique pourquoi ils établissent ou non une correspondance :

Expression String Correspond Commentaire
[a-z]{2} hello Oui Correspondances de sous-chaînes
[a-z]{2} 123abc456 Oui Correspondances de sous-chaînes
[a-z]{2} mz Oui Correspondance avec l’expression
[a-z]{2} MZ Oui Non-respect de la casse
^[a-z]{2}$ hello Non Voir ^ et $ ci-dessus
^[a-z]{2}$ 123abc456 Non Voir ^ et $ ci-dessus

Pour plus d’informations sur la syntaxe des expressions régulières, consultez Expressions régulières du .NET Framework.

Pour contraindre un paramètre à un ensemble connu de valeurs possibles, utilisez une expression régulière. Par exemple, {action:regex(^(list|get|create)$)} établit une correspondance avec la valeur de route action uniquement pour list, get ou create. Si elle est passée dans le dictionnaire de contraintes, la chaîne ^(list|get|create)$ est équivalente. Les contraintes passées dans le dictionnaire de contraintes qui ne correspondent pas à l’une des contraintes connues sont également traitées comme des expressions régulières. Les contraintes passées dans un modèle qui ne correspondent pas à l’une des contraintes connues ne sont pas traitées comme des expressions régulières.

Contraintes de routage personnalisées

Les contraintes de routage personnalisées peuvent être créées en implémentant l’interface IRouteConstraint. L’interface IRouteConstraint contient une méthode unique, Match, qui retourne true si la contrainte est satisfaite et false dans le cas contraire.

Les contraintes de routage personnalisées sont rarement nécessaires. Avant d’implémenter une contrainte de routage personnalisée, envisagez des alternatives, telles que la liaison de modèle.

Le dossier ASP.NET Core Contraintes fournit de bons exemples de création de contraintes. Par exemple, GuidRouteConstraint.

Pour utiliser un IRouteConstraint personnalisé, le type de contrainte de routage doit être inscrit avec le ConstraintMap de l’application dans le conteneur de service de l’application. Un ConstraintMap est un dictionnaire qui mappe les clés de contrainte d’itinéraire aux implémentations IRouteConstraint qui valident ces contraintes. Le ConstraintMap d’une application peut être mis à jour dans Program.cs en tant qu’appel AddRouting ou en configurant RouteOptions directement avec builder.Services.Configure<RouteOptions>. Par exemple :

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

La contrainte précédente est appliquée dans le code suivant :

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

L’implémentation de NoZeroesRouteConstraint empêche l’utilisation de 0 dans un paramètre de routage :

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

Avertissement

Lorsque vous utilisez System.Text.RegularExpressions pour traiter une entrée non approuvée, passez un délai d’expiration. Un utilisateur malveillant peut fournir une entrée à RegularExpressions, provoquant une attaque par déni de service. Les API d’infrastructure ASP.NET Core qui utilisent RegularExpressions passent un délai d’expiration.

Le code précédent :

  • Empêche 0 dans le segment {id} de la route.
  • S’affiche pour fournir un exemple de base d’implémentation d’une contrainte personnalisée. Il ne doit pas être utilisé dans une application de production.

Le code suivant est une meilleure approche pour empêcher un id contenant un 0 d’être traité :

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

    return Content(id);
}

Le code précédent présente les avantages suivants sur l’approche NoZeroesRouteConstraint :

  • Il ne nécessite pas de contrainte personnalisée.
  • Il retourne une erreur plus descriptive lorsque le paramètre de routage inclut 0.

Les transformateurs de paramètres

Transformateurs de paramètre :

Par exemple, un transformateur de paramètre slugify personnalisé dans le modèle d’itinéraire blog\{article:slugify} avec Url.Action(new { article = "MyTestArticle" }) génère blog\my-test-article.

Examinez l’implémentation suivante 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();
    }
}

Pour utiliser un transformateur de paramètre dans un modèle d’itinéraire, configurez-le d’abord en utilisant ConstraintMap dans Program.cs :

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

L’infrastructure ASP.NET Core utilise des transformateurs de paramètres pour transformer l’URI où un point de terminaison est résolu. Par exemple, les transformateurs de paramètres transforment les valeurs de routage utilisées pour faire correspondre un area, controller, actionet page :

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

Avec le modèle de routage précédent, l’action SubscriptionManagementController.GetAll est mise en correspondance avec l’URI /subscription-management/get-all. Un transformateur de paramètre ne modifie pas les valeurs de routage utilisées pour générer un lien. Par exemple, Url.Action("GetAll", "SubscriptionManagement") produit /subscription-management/get-all.

ASP.NET Core fournit des conventions d’API pour l’utilisation des transformateurs de paramètre avec des routages générés :

Informations de référence sur la génération d’URL

Cette section contient une référence pour l’algorithme implémenté par génération d’URL. Dans la pratique, les exemples les plus complexes de génération d’URL utilisent des contrôleurs ou Razor Pages. Pour plus d’informations, consultez Routage dans les contrôleurs.

Le processus de génération d’URL commence par un appel à LinkGenerator.GetPathByAddress ou une méthode similaire. La méthode est fournie avec une adresse, un ensemble de valeurs de routage et éventuellement des informations sur la requête actuelle de HttpContext.

La première étape consiste à utiliser l’adresse pour résoudre un ensemble de points de terminaison candidats à l’aide d’un IEndpointAddressScheme<TAddress> correspondant au type de l’adresse.

Une fois que l’ensemble de candidats est trouvé par le schéma d’adresses, les points de terminaison sont classés et traités de manière itérative jusqu’à ce qu’une opération de génération d’URL réussisse. La génération d’URL ne vérifie pas les ambiguïtés, le premier résultat retourné est le résultat final.

Résolution des problèmes de génération d’URL avec la journalisation

La première étape de la résolution des problèmes de génération d’URL consiste à définir le niveau de journalisation de Microsoft.AspNetCore.Routing sur TRACE. LinkGenerator enregistre de nombreux détails sur son traitement, ce qui peut être utile pour résoudre les problèmes.

Consultez Référence de génération d’URL pour plus d’informations sur la génération d’URL.

Adresses

Les adresses sont le concept de génération d’URL utilisé pour lier un appel au générateur de liens à un ensemble de points de terminaison candidats.

Les adresses sont un concept extensible qui comprend deux implémentations par défaut :

  • Utilisation du nom du point de terminaison (string) comme adresse :
    • Fournit des fonctionnalités similaires au nom du routage de MVC.
    • Utilise le type de métadonnées IEndpointNameMetadata.
    • Résout la chaîne fournie par rapport aux métadonnées de tous les points de terminaison inscrits.
    • Lève une exception au démarrage si plusieurs points de terminaison utilisent le même nom.
    • Recommandé pour une utilisation à usage général en dehors des contrôleurs et de Razor Pages.
  • Utilisation des valeurs de route (RouteValuesAddress) comme adresse :
    • Fournit des fonctionnalités similaires à la génération d’URL hérité des contrôleurs et de Razor Pages.
    • Très complexe à étendre et à déboguer.
    • Fournit l’implémentation utilisée par IUrlHelper, l’assistance des balises, l’assistance HTML , Résultats d’action, etc.

Le rôle du schéma d’adresses consiste à faire l’association entre l’adresse et les points de terminaison correspondants selon des critères arbitraires :

  • Le schéma de noms de point de terminaison effectue une recherche de dictionnaire de base.
  • Le schéma de valeurs de route a un sous-ensemble complexe de l’algorithme défini.

Valeurs ambiantes et valeurs explicites

À partir de la requête actuelle, le routage accède aux valeurs de routage de la requête HttpContext.Request.RouteValuesactuelle. Les valeurs associées à la requête actuelle sont appelées valeurs ambiantes. À des fins de clarté, la documentation fait référence aux valeurs de routage transmises aux méthodes en tant que valeurs explicites.

L’exemple suivant montre les valeurs ambiantes et les valeurs explicites. Il fournit des valeurs ambiantes à partir de la requête actuelle et des valeurs explicites :

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

    // ...

Le code précédent :

Le code suivant fournit uniquement des valeurs explicites et aucune valeur ambiante :

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

La méthode précédente retourne /Home/Subscribe/17

Le code suivant dans le WidgetController retourne /Widget/Subscribe/17 :

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

Le code suivant fournit au contrôleur des valeurs ambiantes dans la requête actuelle et des valeurs explicites :

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

Dans le code précédent :

  • /Gadget/Edit/17 est retourné.
  • Url obtient IUrlHelper.
  • Action génère une URL avec un chemin absolu pour une méthode d’action. L’URL contient le nom de action spécifié et les valeurs route.

Le code suivant fournit des valeurs ambiantes à partir de la requête actuelle et des valeurs explicites :

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

        // ...
    }
}

Le code précédent définit url sur /Edit/17 lorsque la page Razor Modifier contient la directive de page suivante :

@page "{id:int}"

Si la page Modifier ne contient pas le modèle de route "{id:int}", url est /Edit?id=17.

Le comportement de l'IUrlHelper de MVC ajoute une couche de complexité en plus des règles décrites ici :

  • IUrlHelper fournit toujours les valeurs de routage de la requête actuelle en tant que valeurs ambiantes.
  • IUrlHelper.Action copie toujours les valeurs actuelles action et controller de routage en tant que valeurs explicites, sauf substitution par le développeur.
  • IUrlHelper.Page copie toujours la valeur de routage actuelle page en tant que valeur explicite, sauf si elle est remplacée.
  • IUrlHelper.Page remplace toujours la valeur de route handler actuelle par null comme valeurs explicites, sauf substitution.

Les utilisateurs sont souvent surpris par les détails comportementaux des valeurs ambiantes, car MVC ne semble pas suivre ses propres règles. Pour des raisons historiques et de compatibilité, certaines valeurs de routage telles que action, controller, page et handler ont leur propre comportement de cas spécial.

La fonctionnalité équivalente fournie par LinkGenerator.GetPathByAction et LinkGenerator.GetPathByPage duplique ces anomalies de IUrlHelper pour la compatibilité.

Processus de génération d’URL

Une fois l’ensemble de points de terminaison candidats trouvés, l’algorithme de génération d’URL :

  • Traite les points de terminaison de manière itérative.
  • Retourne le premier résultat réussi.

La première étape de ce processus est appelée invalidation des valeurs de routage. L’invalidation des valeurs de routage est le processus par lequel le routage détermine les valeurs de routage des valeurs ambiantes à utiliser et qui doivent être ignorées. Chaque valeur ambiante est considérée et combinée aux valeurs explicites ou ignorée.

La meilleure façon de penser au rôle des valeurs ambiantes est qu’elles tentent d’enregistrer la saisie par les développeurs d’applications, dans certains cas courants. Traditionnellement, les scénarios où les valeurs ambiantes sont utiles sont liés à MVC :

  • Lors de la liaison à une autre action dans le même contrôleur, le nom du contrôleur n’a pas besoin d’être spécifié.
  • Lors de la liaison à un autre contrôleur dans la même zone, le nom de la zone n’a pas besoin d’être spécifié.
  • Lors de la liaison à la même méthode d’action, les valeurs de routage n’ont pas besoin d’être spécifiées.
  • Lors de la liaison à une autre partie de l’application, vous ne souhaitez pas transporter les valeurs de routage qui n’ont aucune signification dans cette partie de l’application.

Les appels à ou LinkGenerator qui retournent IUrlHelper sont généralement dus à null une non-compréhension de l’invalidation de la valeur de route. Résolvez les problèmes d’invalidation des valeurs de routage en spécifiant explicitement davantage de valeurs de routage pour voir si cela résout le problème.

L’invalidation de la valeur de routage repose sur l’hypothèse que le schéma d’URL de l’application est hiérarchique, avec une hiérarchie formée de gauche à droite. Considérez le modèle de route de contrôleur de base {controller}/{action}/{id?} pour avoir un sens intuitif de la façon dont cela fonctionne dans la pratique. Une modification apportée à une valeur invalide toutes les valeurs de routage qui apparaissent à droite. Cela reflète l’hypothèse sur la hiérarchie. Si l’application a une valeur ambiante pour id, et que l’opération spécifie une valeur différente pour controller :

  • id ne sera pas réutilisée, car {controller} est à gauche de {id?}.

Voici quelques exemples illustrant ce principe :

  • Si les valeurs explicites contiennent une valeur pour id, la valeur ambiante de id est ignorée. Les valeurs ambiantes de controller et action peuvent être utilisées.
  • Si les valeurs explicites contiennent une valeur pour action, toute valeur ambiante de action est ignorée. Les valeurs ambiantes de controller peuvent être utilisées. Si la valeur explicite de action est différente de la valeur ambiante de action, la valeur id ne sera pas utilisée. Si la valeur explicite de action est identique à la valeur ambiante de action, la valeur id peut être utilisée.
  • Si les valeurs explicites contiennent une valeur de controller, toute valeur ambiante de controller est ignorée. Si la valeur explicite de controller est différente de la valeur ambiante de controller, les valeurs action et id ne seront pas utilisées. Si la valeur explicite de controller est identique à la valeur ambiante de controller, les valeurs action et id peuvent être utilisées.

Ce processus est encore plus compliqué à cause de l’existence de routes d’attributs et de routes conventionnelles dédiées. Les routes conventionnelles de contrôleur tels que {controller}/{action}/{id?} spécifient une hiérarchie à l’aide de paramètres de routage. Pour les routages conventionnels dédiés et les routes d’attribut aux contrôleurs et à Razor Pages :

  • Il existe une hiérarchie de valeurs de routage.
  • Elles n’apparaissent pas dans le modèle.

Dans ce cas, la génération d’URL définit le concept de valeurs requises. Les points de terminaison créés par les contrôleurs et Razor Pages ont des valeurs requises spécifiées qui autorisent l’invalidation de la valeur de routage à fonctionner.

Algorithme d’invalidation de valeur de routage en détail :

  • Les noms de valeurs requis sont combinés avec les paramètres de routage, puis traités de gauche à droite.
  • Pour chaque paramètre, la valeur ambiante et la valeur explicite sont comparées :
    • Si la valeur ambiante et la valeur explicite sont identiques, le processus continue.
    • Si la valeur ambiante est présente et que la valeur explicite ne l’est pas, la valeur ambiante est utilisée lors de la génération de l’URL.
    • Si la valeur ambiante n’est pas présente et que la valeur explicite l’est, rejetez la valeur ambiante et toutes les valeurs ambiantes suivantes.
    • Si la valeur ambiante et la valeur explicite sont présentes et que les deux valeurs sont différentes, rejetez la valeur ambiante et toutes les valeurs ambiantes suivantes.

À ce stade, l’opération de génération d’URL est prête à évaluer les contraintes de routage. L’ensemble de valeurs acceptées est combiné aux valeurs par défaut des paramètres, qui sont fournies aux contraintes. Si les contraintes passent toutes, l’opération se poursuit.

Ensuite, les valeurs acceptées peuvent être utilisées pour développer le modèle de routage. Le modèle de routage est traité :

  • De gauche à droite.
  • Chaque paramètre a sa valeur acceptée remplacée.
  • Avec les cas spéciaux suivants :
    • S’il manque une valeur aux valeurs acceptées et que le paramètre a une valeur par défaut, la valeur par défaut est utilisée.
    • S’il manque une valeur aux valeurs acceptées et que le paramètre est facultatif, le traitement se poursuit.
    • Si un paramètre de routage à droite d’un paramètre facultatif manquant a une valeur, l’opération échoue.
    • Les paramètres par défaut contigus et les paramètres facultatifs sont réduits si possible.

Les valeurs fournies explicitement mais qui n’ont pas de correspondance avec un segment de la route sont ajoutées à la chaîne de requête. Le tableau suivant présente le résultat en cas d’utilisation du modèle de routage {controller}/{action}/{id?}.

Valeurs ambiantes Valeurs explicites Résultat
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

Ordre des paramètres d’itinéraire facultatif

Les paramètres d’itinéraire facultatifs doivent être fournis après tous les paramètres d’itinéraire requis. Dans le code suivant, les paramètres id et name doivent venir après le paramètre 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 ?? ""}");
    }
}

Problèmes liés à l’invalidation des valeurs de routage

Le code suivant montre un exemple de schéma de génération d’URL qui n’est pas pris en charge par le routage :

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

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

Dans le code précédent, le paramètre de routage culture est utilisé pour la localisation. On veut que le paramètre culture soit toujours accepté comme valeur ambiante. Toutefois, le paramètre culture n’est pas accepté comme valeur ambiante en raison de la façon dont les valeurs requises fonctionnent :

  • Dans le modèle de routage "default", le paramètre de routage culture est à gauche de controller. Les modifications apportées à controller n’invalident donc pas culture.
  • Dans le modèle de routage "blog", le paramètre de routage culture est considéré comme à droite de controller, qui apparaît dans les valeurs requises.

Analyser les chemins d’URL avec LinkParser

La classe LinkParser ajoute la prise en charge de l’analyse d’un chemin d’URL dans un ensemble de valeurs de routage. La méthode ParsePathByEndpointName prend un nom de point de terminaison et un chemin d’URL et retourne un ensemble de valeurs de routage extraites du chemin d’URL.

Dans l’exemple de contrôleur suivant, l’action GetProduct utilise un modèle de routage de api/Products/{id} et a un Name de GetProduct :

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

Dans la même classe de contrôleur, l’action AddRelatedProduct attend un chemin d’URL, pathToRelatedProduct, qui peut être fourni en tant que paramètre de chaîne de requête :

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

    // ...

Dans l’exemple précédent, l’action AddRelatedProduct extrait la valeur de route id du chemin d’URL. Par exemple, avec un chemin d’URL de /api/Products/1, la valeur relatedProductId est définie sur 1. Cette approche permet aux clients de l’API d’utiliser des chemins d’URL lorsque vous faites référence à des ressources, sans avoir à connaître la façon dont cette URL est structurée.

Configurer les métadonnées de point de terminaison

Les liens suivants fournissent des informations sur la configuration des métadonnées de point de terminaison :

Correspondance de l’hôte dans les routages avec RequireHost

RequireHost applique une contrainte au routage qui nécessite l’hôte spécifié. Le paramètre RequireHost ou [Host] peut être un :

  • Hôte : www.domain.com, fait correspondre www.domain.com à n’importe quel port.
  • Hôte avec caractère générique : *.domain.com, fait correspondre www.domain.com, subdomain.domain.comou www.subdomain.domain.com sur n’importe quel port.
  • Port : *:5000, fait correspondre le port 5000 avec n’importe quel hôte.
  • Hôte et port : www.domain.com:5000 ou *.domain.com:5000, fait correspondre l’hôte et le port.

Plusieurs paramètres peuvent être spécifiés à l’aide RequireHost ou [Host]. La contrainte fait correspondre les hôtes valides pour l’un des paramètres. Par exemples, [Host("domain.com", "*.domain.com")] fait correspondre domain.com, www.domain.com et subdomain.domain.com.

Le code suivant utilise RequireHost pour exiger l’hôte spécifié sur le routage :

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

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

Le code suivant utilise l’attribut [Host] sur le contrôleur pour exiger l’un des hôtes spécifiés :

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

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

Lorsque l’attribut [Host] est appliqué à la fois au contrôleur et à la méthode d’action :

  • L’attribut de l’action est utilisé.
  • L’attribut du contrôleur est ignoré.

Groupes de routes

La méthode d’extension MapGroup permet d’organiser des groupes de points de terminaison avec un préfixe commun. Cela réduit le code répétitif et permet de personnaliser des groupes entiers de points de terminaison avec un seul appel à des méthodes comme RequireAuthorization et WithMetadata, qui ajoutent des métadonnées de point de terminaison.

Par exemple, le code suivant crée deux groupes de points de terminaison similaires :

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

Dans ce scénario, vous pouvez utiliser une adresse relative pour l’en-tête Location dans le résultat 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);
}

Le premier groupe de points de terminaison correspond uniquement aux requêtes précédées de /public/todos, accessibles sans authentification. Le second groupe de points de terminaison correspond uniquement aux requêtes préfixées par /private/todos, qui nécessitent une authentification.

La QueryPrivateTodosfabrique de filtre de point de terminaison est une fonction locale qui modifie les paramètres TodoDb du gestionnaire d’itinéraires pour permettre l’accès et le stockage de données todo privées.

Les groupes de routage prennent également en charge les groupes imbriqués et les modèles de préfixe complexes avec des contraintes et des paramètres de routage. Dans l’exemple suivant, un gestionnaire de routage mappé au groupe user peut capturer les paramètres de routage {org} et {group} définis dans les préfixes de groupe externe.

Le préfixe peut également être vide. Cela peut être utile pour ajouter des métadonnées ou des filtres de point de terminaison à un groupe de points de terminaison sans modifier le modèle de routage.

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

L’ajout de filtres ou de métadonnées à un groupe se comporte de la même façon que si vous les ajoutiez individuellement à chaque point de terminaison avant d’ajouter des filtres ou des métadonnées supplémentaires qui ont pu être ajoutés à un groupe interne ou à un point de terminaison spécifique.

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

Dans l’exemple ci-dessus, le filtre externe enregistre la requête entrante avant le filtre interne, même si elle a été ajoutée en deuxième. Étant donné que les filtres ont été appliqués à différents groupes, l’ordre dans lequel ils ont été ajoutés les uns par rapport aux autres n’a pas d’importance. Les filtres d’ordre ajoutés sont importants s’ils sont appliqués au même groupe ou au même point de terminaison spécifique.

Une requête sur /outer/inner/ journalisera les éléments suivants :

/outer group filter
/inner group filter
MapGet filter

Conseils sur les performances pour le routage

Lorsqu’une application rencontre des problèmes de performances, le routage est souvent soupçonné comme étant le problème. La raison pour laquelle le routage est soupçonné est que les infrastructures telles que les contrôleurs et Razor Pages signalent le temps passé à l’intérieur de l’infrastructure dans leurs messages de journalisation. En cas de différence significative entre le temps signalé par les contrôleurs et le temps total de la requête :

  • Les développeurs éliminent leur code d’application comme source du problème.
  • Il est courant de supposer que le routage est la cause.

Les performances du routage est testé à l’aide de milliers de points de terminaison. Il est peu probable qu’une application classique rencontre un problème de performances simplement en étant trop volumineuse. La cause racine la plus courante des performances de routage lentes est généralement un intergiciel personnalisé qui se comporte mal.

Cet exemple de code suivant illustre une technique de base pour affiner la source de délai :

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

Pour le routage temporel :

  • Entrelacez chaque intergiciel avec une copie de l’intergiciel de minutage indiqué dans le code précédent.
  • Ajoutez un identificateur unique pour mettre en corrélation les données de minutage avec le code.

Il s’agit d’un moyen de base de limiter le délai lorsqu’il est significatif, par exemple, plus que 10ms. Soustraire Time 2 de Time 1 signale le temps passé à l’intérieur de l’intergiciel UseRouting.

Le code suivant utilise une approche plus compacte du code de minutage précédent :

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

Fonctionnalités de routage potentiellement coûteuses

La liste suivante fournit un aperçu des fonctionnalités de routage relativement coûteuses par rapport aux modèles de routage de base :

  • Expressions régulières : il est possible d’écrire des expressions régulières qui sont complexes ou qui ont un temps d’exécution long avec une petite quantité d’entrée.
  • Segments complexes ({x}-{y}-{z}) :
    • Sont beaucoup plus coûteux que l’analyse d’un segment de chemin d’URL standard.
    • Entraînent l’allocation d’un grand nombre de sous-chaînes.
  • Accès aux données synchrones : de nombreuses applications complexes disposent d’un accès à la base de données dans le cadre de leur routage. Utilisez des points d’extensibilité tels que MatcherPolicy et EndpointSelectorContext, qui sont asynchrones.

Conseils pour les tables de routage volumineuses

Par défaut, ASP.NET Core utilise un algorithme de routage qui échange la mémoire pour le temps processeur. Cela a l’effet intéressant que le temps de correspondance de la route dépend uniquement de la longueur du chemin d’accès à mettre en correspondance et non du nombre de routes. Toutefois, cette approche peut être potentiellement problématique dans certains cas, lorsque l’application a un grand nombre de routes (dans les milliers) et qu’il existe un grand nombre de préfixes variables dans les routes. Par exemple, si les routes ont des paramètres dans les premiers segments de la route, comme {parameter}/some/literal.

Il est peu probable qu’une application rencontre un problème, sauf si :

  • Il existe un nombre élevé de routes dans l’application avec ce modèle.
  • Il existe un grand nombre de routes dans l’application.

Comment déterminer si une application s’exécute dans le problème de la table de routage volumineuse

  • Il existe deux symptômes à rechercher :
    • L’application est lente à démarrer sur la première requête.
      • Notez que cela est requis, mais pas suffisant. Il existe de nombreux autres problèmes non liés au routage qui peuvent entraîner un démarrage d’application lent. Vérifiez la condition ci-dessous pour déterminer avec précision que l’application se trouve dans cette situation.
    • L’application consomme beaucoup de mémoire au démarrage et un vidage de la mémoire affiche un grand nombre d’instances Microsoft.AspNetCore.Routing.Matching.DfaNode.

Comment résoudre ce problème

Plusieurs techniques et optimisations peuvent être appliquées aux routes qui améliorent en grande partie ce scénario :

  • Appliquez des contraintes de routage à vos paramètres, par exemple {parameter:int}, {parameter:guid}, {parameter:regex(\\d+)}, etc. si possible.
    • Cela permet à l’algorithme de routage d’optimiser en interne les structures utilisées pour la correspondance et de réduire considérablement la mémoire utilisée.
    • Dans la grande majorité des cas, cela suffit pour revenir à un comportement acceptable.
  • Modifiez les routes pour déplacer des paramètres vers des segments ultérieurs dans le modèle.
    • Cela réduit le nombre de « chemins » possibles pour correspondre à un point de terminaison donné un chemin d’accès.
  • Utilisez une route dynamique et effectuez le mappage sur un contrôleur/page dynamiquement.
    • Pour ce faire, vous pouvez utiliser MapDynamicControllerRoute et MapDynamicPageRoute.

Conseils pour les auteurs de bibliothèques

Cette section contient des conseils pour les auteurs de bibliothèques qui s’appuient sur le routage. Ces détails sont destinés à garantir que les développeurs d’applications ont une bonne expérience à l’aide de bibliothèques et d’infrastructures qui étendent le routage.

Définir des points de terminaison

Pour créer une infrastructure qui utilise le routage pour la correspondance d’URL, commencez par définir une expérience utilisateur qui s’appuie sur UseEndpoints.

GÉNÉREZ sur IEndpointRouteBuilder. Cela permet aux utilisateurs de composer votre infrastructure avec d’autres fonctionnalités ASP.NET Core sans confusion. Chaque modèle ASP.NET Core inclut le routage. Supposons que le routage est présent et familier pour les utilisateurs.

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

app.MapHealthChecks("/healthz");

RETOURNEZ un type concret scellé à partir d’un appel à MapMyFramework(...) qui implémente IEndpointConventionBuilder. La plupart des méthodes d’infrastructure Map... suivent ce modèle. L'interface IEndpointConventionBuilder :

  • Permet la composition des métadonnées.
  • Est ciblée par diverses méthodes d’extension.

La déclaration de votre propre type vous permet d’ajouter vos propres fonctionnalités spécifiques à l’infrastructure au générateur. Vous pouvez encapsuler un générateur déclaré par l’infrastructure et lui transférer les appels.

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

app.MapHealthChecks("/healthz");

ENVISAGEZ d’écrire votre propre EndpointDataSource. EndpointDataSource est la primitive de bas niveau permettant de déclarer et de mettre à jour une collection de points de terminaison. EndpointDataSource est une API puissante utilisée par les contrôleurs et Razor Pages.

Les tests de routage ont un exemple de base d’une source de données sans mise à jour.

ENVISAGEZ d’implémenter GetGroupedEndpoints. Cela donne un contrôle total sur les conventions de groupe en cours d’exécution et les métadonnées finales sur les points de terminaison groupés. Par exemple, cela permet aux implémentations personnalisées EndpointDataSource d’exécuter des filtres de point de terminaisons ajoutés aux groupes.

NE TENTEZ PAS d’inscrire un EndpointDataSource par défaut. Demandez aux utilisateurs d’inscrire votre infrastructure dans UseEndpoints. La philosophie du routage est que rien n’est inclus par défaut et que UseEndpoints est l’endroit où inscrire des points de terminaison.

Création d’un intergiciel intégré au routage

ENVISAGEZ de définir des types de métadonnées en tant qu’interface.

FAITES EN SORTE qu’il soit possible d’utiliser des types de métadonnées en tant qu’attribut sur des classes et des méthodes.

public interface ICoolMetadata
{
    bool IsCool { get; }
}

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

Les frameworks tels que les contrôleurs et Razor Pages prennent en charge l’application d’attributs de métadonnées aux types et méthodes. Si vous déclarez des types de métadonnées :

  • Rendez-les accessibles en tant qu’attributs.
  • La plupart des utilisateurs sont familiarisés avec l’application d’attributs.

La déclaration d’un type de métadonnées en tant qu’interface ajoute une autre couche de flexibilité :

  • Les interfaces sont composables.
  • Les développeurs peuvent déclarer leurs propres types qui combinent plusieurs stratégies.

FAITES EN SORTE qu’il soit possible de remplacer les métadonnées, comme illustré dans l’exemple suivant :

[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 meilleure façon de suivre ces instructions consiste à éviter de définir des métadonnées de marqueur :

  • Ne recherchez pas simplement la présence d’un type de métadonnées.
  • Définissez une propriété sur les métadonnées et vérifiez la propriété.

La collection de métadonnées est triée et prend en charge la substitution par priorité. Dans le cas des contrôleurs, les métadonnées sur la méthode d’action sont les plus spécifiques.

FAITES EN SORTE que l’intergiciel soit utile avec et sans routage :

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

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

À titre d’exemple de cette recommandation, considérez l’intergiciel UseAuthorization. L’intergiciel d’autorisation vous permet de passer une stratégie de secours. La stratégie de secours, si elle est spécifiée, s’applique aux :

  • Points de terminaison sans stratégie spécifiée.
  • Requêtes qui ne correspondent pas à un point de terminaison.

Cela rend l’intergiciel d’autorisation utile en dehors du contexte du routage. L’intergiciel d’autorisation peut être utilisé pour la programmation d’intergiciels traditionnels.

Déboguer les diagnostics

Pour obtenir une sortie de diagnostic de routage détaillée, définissez Logging:LogLevel:Microsoft sur Debug. Dans l’environnement de développement, définissez le niveau de journal dans appsettings.Development.json :

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

Ressources supplémentaires

Le routage est responsable de la correspondance des requêtes HTTP entrantes et de la distribution de ces requêtes aux points de terminaison exécutables de l’application. Les points de terminaison sont les unités de code de gestion des requêtes exécutables de l’application. Les points de terminaison sont définies dans l’application et configurées au démarrage de l’application. Le processus de correspondance de point de terminaison peut extraire des valeurs de l’URL de la requête et fournir ces valeurs pour le traitement des demandes. Avec les informations de point de terminaison fournies par l’application, le routage peut également générer des URL qui mappent vers des points de terminaison.

Les applications peuvent configurer le routage à l’aide des éléments suivants :

Cet article décrit les détails de bas niveau du routage ASP.NET Core. Pour plus d’informations sur la configuration du routage :

Concepts de base du routage

Le code suivant illustre un exemple de routage de base :

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

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

app.Run();

L’exemple précédent inclut un point de terminaison unique à l’aide de la méthode MapGet :

  • Lorsqu’une requête http GET est envoyée à l’URL racine / :
    • Le délégué de requête s’exécute.
    • Hello World! est écrit dans la réponse HTTP.
  • Si la méthode de requête n’est pas GET ou si l’URL racine n’est pas /, aucun routage ne correspond et un HTTP 404 est retourné.

Le routage utilise une paire d’intergiciels, inscrite par UseRouting et UseEndpoints :

  • UseRouting ajoute la correspondance de routage au pipeline d’intergiciels. Cet intergiciel examine l’ensemble des points de terminaison définis dans l’application et sélectionne la meilleure correspondance en fonction de la requête.
  • UseEndpoints ajoute l’exécution du point de terminaison au pipeline de l’intergiciel. Il exécute le délégué associé au point de terminaison sélectionné.

Les applications n’ont généralement pas besoin d’appeler UseRouting ou UseEndpoints. WebApplicationBuilder configure un pipeline d’intergiciels qui encapsule l’intergiciel ajouté dans Program.cs avec UseRouting et UseEndpoints. Toutefois, les applications peuvent modifier l’ordre dans lequel UseRouting et UseEndpoints s’exécutent en appelant ces méthodes explicitement. Par exemple, le code suivant effectue un appel explicite à UseRouting :

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

app.UseRouting();

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

Dans le code précédent :

  • L’appel à app.Use inscrit un intergiciel personnalisé qui s’exécute au début du pipeline.
  • L’appel à UseRouting configure l’intergiciel de correspondance de routage à exécuter après l’intergiciel personnalisé.
  • Le point de terminaison inscrit avec MapGet s’exécute à la fin du pipeline.

Si l’exemple précédent n’incluait pas d’appel à UseRouting, l’intergiciel personnalisé s’exécuterait après l’intergiciel de correspondance de routage.

Points de terminaison

La méthode MapGet est utilisée pour définir un point de terminaison. Un point de terminaison peut être :

  • Sélectionné, en correspondant à l’URL et à la méthode HTTP.
  • Exécuté, en exécutant le délégué.

Les points de terminaison qui peuvent être mis en correspondance et exécutés par l’application sont configurés dans UseEndpoints. Par exemple, MapGet, MapPost et des méthodes similaires connectent des délégués de requête au système de routage. Des méthodes supplémentaires peuvent être utilisées pour connecter les fonctionnalités d’infrastructure ASP.NET Core au système de routage :

L’exemple suivant montre le routage avec un modèle de routage plus sophistiqué :

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

La chaîne /hello/{name:alpha} est un modèle de routage. Un modèle de routage est utilisé pour configurer la mise en correspondance du point de terminaison. Dans ce cas, le modèle correspond à :

  • Un URL comme /hello/Docs
  • Tout chemin d’URL qui commence par /hello/ suivi d’une séquence de caractères alphabétiques. :alpha applique une contrainte de routage qui fait correspondre uniquement les caractères alphabétiques. Les contraintes de routage sont expliquées plus loin dans cet article.

Deuxième segment du chemin d’URL, {name:alpha} :

L’exemple suivant montre le routage avec les contrôles d’intégrité et l’autorisation :

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

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

L’exemple précédent montre comment :

  • L’intergiciel d’autorisation peut être utilisé avec le routage.
  • Les points de terminaison peuvent être utilisés pour configurer le comportement d’autorisation.

L’appel MapHealthChecks ajoute un point de terminaison de contrôle d’intégrité. Le chaînage RequireAuthorization sur cet appel attache une stratégie d’autorisation au point de terminaison.

Appeler UseAuthentication et UseAuthorization ajoute l’intergiciel d’authentification et d’autorisation. Ces intergiciels sont placés entre UseRouting et UseEndpoints afin qu’ils puissent :

  • Voir le point de terminaison sélectionné par UseRouting.
  • Appliquez une stratégie d’autorisation avant que UseEndpoints les distribue au point de terminaison.

Métadonnées de point de terminaison

Dans l’exemple précédent, il existe deux points de terminaison, mais seul le point de terminaison de contrôle d’intégrité a une stratégie d’autorisation attachée. Si la demande correspond au point de terminaison de contrôle d’intégrité, /healthz, une vérification d’autorisation est effectuée. Cela montre que les points de terminaison peuvent avoir des données supplémentaires attachées. Ces données supplémentaires sont appelées métadonnées de point de terminaison :

  • Les métadonnées peuvent être traitées par un intergiciel prenant en charge le routage.
  • Les métadonnées peuvent être de n’importe quel type .NET.

Concepts de routage

Le système de routage s’appuie sur le pipeline d’intergiciels en ajoutant le concept de point de terminaison puissant. Les points de terminaison représentent des unités des fonctionnalités de l’application qui sont distinctes les unes des autres en termes de routage, d’autorisation et de n’importe quel nombre de systèmes ASP.NET Core.

Définition de point de terminaison ASP.NET Core

Un point de terminaison ASP.NET Core est :

Le code suivant montre comment récupérer et inspecter le point de terminaison correspondant à la requête actuelle :

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

Le point de terminaison, s’il est sélectionné, peut être récupéré à partir de HttpContext. Ses propriétés peuvent être inspectées. Les objets de point de terminaison sont immuables et ne peuvent pas être modifiés après la création. Le type de point de terminaison le plus courant est RouteEndpoint. RouteEndpoint inclut des informations qui lui permettent d’être sélectionné par le système de routage.

Dans le code précédent, app.Use configure un intergiciel inclus.

Le code suivant montre que, selon l’endroit où app.Use est appelé dans le pipeline, il se peut qu’il n’y ait pas de point de terminaison :

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

L’exemple précédent ajoute des instructions Console.WriteLine qui indiquent si un point de terminaison a été sélectionné ou non. Pour plus de clarté, l’exemple affecte un nom complet au point de terminaison / fourni.

L’exemple précédent inclut également des appels vers UseRouting et UseEndpoints pour contrôler exactement quand ces intergiciels s’exécutent dans le pipeline.

L’exécution de ce code avec une URL / affiche :

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

L’exécution de ce code avec toute autre URL affiche :

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

Cette sortie montre que :

  • Le point de terminaison est toujours null avant que soit UseRouting appelé.
  • Si une correspondance est trouvée, le point de terminaison n’est pas null entre UseRouting et UseEndpoints.
  • L’intergiciel UseEndpoints est terminal lorsqu’une correspondance est trouvée. L’intergiciel terminal est défini plus loin dans cet article.
  • L’intergiciel après UseEndpoints s’exécute uniquement lorsqu’aucune correspondance n’est trouvée.

L’intergiciel UseRouting utilise la méthode SetEndpoint pour attacher le point de terminaison au contexte actuel. Il est possible de remplacer l’intergiciel UseRouting par une logique personnalisée et d’obtenir les avantages de l’utilisation de points de terminaison. Les points de terminaison sont une primitive de bas niveau comme l’intergiciel et ne sont pas couplés à l’implémentation du routage. La plupart des applications n’ont pas besoin de remplacer UseRouting par une logique personnalisée.

L’intergiciel UseEndpoints est conçu pour être utilisé en tandem avec l’intergiciel UseRouting. La logique principale pour exécuter un point de terminaison n’est pas compliquée. Utilisez GetEndpoint pour récupérer le point de terminaison, puis appelez sa propriété RequestDelegate.

Le code suivant montre comment l’intergiciel peut influencer ou réagir au routage :

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

L’exemple précédent illustre deux concepts importants :

  • L’intergiciel peut s’exécuter avant UseRouting pour modifier les données sur lesquelles le routage fonctionne.
  • L’intergiciel peut s’exécuter entre UseRouting et UseEndpoints pour traiter les résultats du routage avant l’exécution du point de terminaison.
    • Intergiciel qui s’exécute entre UseRouting et UseEndpoints :
      • Inspecte généralement les métadonnées pour comprendre les points de terminaison.
      • Prend souvent des décisions de sécurité, comme le font UseAuthorization et UseCors.
    • La combinaison d’intergiciels et de métadonnées permet de configurer des stratégies par point de terminaison.

Le code précédent montre un exemple d’intergiciel personnalisé qui prend en charge les stratégies par point de terminaison. L’intergiciel écrit un journal d’audit de l’accès aux données sensibles dans la console. L’intergiciel peut être configuré pour auditer un point de terminaison avec les métadonnées RequiresAuditAttribute. Cet exemple illustre un modèle d’activation dans lequel seuls les points de terminaison marqués comme sensibles sont audités. Il est possible de définir l’inverse de cette logique, en auditant tout ce qui n’est pas marqué comme sécurisé, par exemple. Le système de métadonnées de point de terminaison est flexible. Cette logique peut être conçue de quelque manière que ce soit en fonction du cas d’usage.

L’exemple de code précédent est destiné à illustrer les concepts de base des points de terminaison. L’exemple n’est pas destiné à une utilisation en production. Une version plus complète d’un intergiciel de journal d’audit :

  • Se connecterais à un fichier ou une base de données.
  • Inclurais des détails tels que l’utilisateur, l’adresse IP, le nom du point de terminaison sensible, etc.

Les métadonnées de stratégie d’audit RequiresAuditAttribute sont définies en tant que Attribute pour faciliter l’utilisation avec des infrastructures basées sur des classes telles que des contrôleurs et SignalR. Lors de l’utilisation de route vers le code :

  • Les métadonnées sont attachées à une API de générateur.
  • Les infrastructure basées sur des classes incluent tous les attributs sur la méthode et la classe correspondantes lors de la création de points de terminaison.

Les meilleures pratiques pour les types de métadonnées sont de les définir en tant qu’interfaces ou attributs. Les interfaces et les attributs autorisent la réutilisation du code. Le système de métadonnées est flexible et n’impose aucune limitation.

Comparer l’intergiciel terminal avec le routage

L’exemple suivant illustre à la fois l’intergiciel terminal et le routage :

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

Le style d’intergiciel indiqué avec Approach 1: est l’intergiciel terminal. Il est appelé intergiciel terminal, car il effectue une opération de correspondance :

  • L’opération de correspondance dans l’exemple précédent est Path == "/" pour l’intergiciel et Path == "/Routing" pour le routage.
  • Lorsqu’une correspondance réussit, elle exécute certaines fonctionnalités et retourne, plutôt que d’appeler l’intergiciel next.

Il est appelé intergiciel de terminal, car il met fin à la recherche, exécute certaines fonctionnalités, puis retourne.

La liste suivante compare les intergiciels de terminal avec le routage :

  • Les deux approches permettent de terminer le pipeline de traitement :
    • L’intergiciel met fin au pipeline en retournant plutôt qu’en appelant next.
    • Les points de terminaison sont toujours terminaux.
  • L’intergiciel terminal permet de positionner l’intergiciel à un emplacement arbitraire dans le pipeline :
    • Les points de terminaison s’exécutent à la position de UseEndpoints.
  • L’intergiciel de terminal permet au code arbitraire de déterminer quand l’intergiciel fait correspondre :
    • Le code de correspondance de routage personnalisé peut être détaillé et difficile à écrire correctement.
    • Le routage fournit des solutions simples pour les applications classiques. La plupart des applications ne nécessitent pas de code de correspondance de routage personnalisé.
  • L’interface des points de terminaison avec un intergiciel tel que UseAuthorization et UseCors.
    • L’utilisation d’un intergiciel terminal avec UseAuthorization ou UseCors nécessite une interaction manuelle avec le système d’autorisation.

Un point de terminaison définit les :

  • Le délégué pour traiter les demandes.
  • La collection de métadonnées arbitraires. Les métadonnées sont utilisées pour implémenter des problèmes transversaux basés sur des stratégies et une configuration attachées à chaque point de terminaison.

L’intergiciel terminal peut être un outil efficace, mais peut nécessiter :

  • Une quantité importante de codage et de test.
  • L’intégration manuelle avec d’autres systèmes pour atteindre le niveau de flexibilité souhaité.

Envisagez d’intégrer le routage avant d’écrire un intergiciel terminal.

Les intergiciels terminaux existants qui s’intègrent à Map ou MapWhen peuvent généralement être transformés en point de terminaison prenant en charge le routage. MapHealthChecks illustre le modèle de routeur-ware :

  • Écrire une méthode d’extension sur IEndpointRouteBuilder.
  • Créer un pipeline d’intergiciels imbriqués à l’aide de CreateApplicationBuilder.
  • Attacher l’intergiciel au nouveau pipeline. Dans ce cas, UseHealthChecks.
  • Build le pipeline d’intergiciel dans un RequestDelegate.
  • Appeler Map et fournir le nouveau pipeline d’intergiciels.
  • Retourner l’objet générateur fourni par Map à partir de la méthode d’extension.

Le code suivant montre l’utilisation de MapHealthChecks :

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

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

L’exemple précédent montre pourquoi le retour de l’objet générateur est important. Le renvoi de l’objet générateur permet au développeur d’applications de configurer des stratégies telles que l’autorisation pour le point de terminaison. Dans cet exemple, l’intergiciel de contrôle d’intégrité n’a pas d’intégration directe avec le système d’autorisation.

Le système de métadonnées a été créé en réponse aux problèmes rencontrés par les auteurs d’extensibilité à l’aide de l’intergiciel terminal. Il est problématique pour chaque intergiciel d’implémenter sa propre intégration avec le système d’autorisation.

Correspondance d’URL

  • La correspondance d’URL est le processus par lequel le routage distribue une requête entrante à un point de terminaison.
  • Est basé sur des données dans le chemin d’URL et les en-têtes.
  • Peut être étendu pour prendre en compte toutes les données de la demande.

Lorsqu’un intergiciel de routage s’exécute, il définit les valeurs de Endpoint et de routage et vers une fonctionnalité de requête sur HttpContext à partir de la requête actuelle :

  • L’appel de HttpContext.GetEndpoint obtient le point de terminaison.
  • HttpRequest.RouteValues récupère la collection de valeurs d’itinéraire.

L’intergiciel s’exécute après que l’intergiciel de routage puisse inspecter le point de terminaison et prendre des mesures. Par exemple, un intergiciel d’autorisation peut interroger la collection de métadonnées du point de terminaison pour une stratégie d’autorisation. Une fois que tous les intergiciels dans le pipeline de traitement de requêtes sont exécutés, le délégué du point de terminaison sélectionné est appelé.

Le système de routage dans le routage de point de terminaison est responsable de toutes les décisions de distribution. Étant donné que l’intergiciel applique des stratégies basées sur le point de terminaison sélectionné, il est important que :

  • Toute décision susceptible d’affecter la répartition ou l’application de stratégies de sécurité soit prise à l’intérieur du système de routage.

Avertissement

Pour une compatibilité descendante, lorsqu’un délégué de point de terminaison Contrôleur ou Razor Pages est exécuté, les propriétés de RouteContext.RouteData sont définies sur des valeurs appropriées en fonction du traitement des requêtes effectué jusqu’à présent.

Le type RouteContext sera marqué comme obsolète dans une version ultérieure :

  • Migrez RouteData.Values vers HttpRequest.RouteValues.
  • Migrez RouteData.DataTokens pour récupérer IDataTokensMetadata à partir des métadonnées du point de terminaison.

La correspondance d’URL fonctionne dans un ensemble configurable de phases. Dans chaque phase, la sortie est un ensemble de correspondances. L’ensemble de correspondances peut être réduit plus loin par la phase suivante. L’implémentation du routage ne garantit pas un ordre de traitement pour les points de terminaison correspondants. Toutes les correspondances possibles sont traitées simultanément. Les phases de correspondance d’URL se produisent dans l’ordre suivant. ASP.NET Core :

  1. Traite le chemin d’URL par rapport à l’ensemble de points de terminaison et à leurs modèles de routage, en collectant toutes les correspondances.
  2. Prend la liste précédente et supprime les correspondances qui échouent avec les contraintes de routage appliquées.
  3. Prend la liste précédente et supprime les correspondances qui échouent au jeu d’instances MatcherPolicy.
  4. Utilise EndpointSelector pour prendre une décision finale à partir de la liste précédente.

La liste des points de terminaison est hiérarchisée en fonction des éléments suivants :

Tous les points de terminaison correspondants sont traités dans chaque phase jusqu’à ce que EndpointSelector soit atteint. EndpointSelector est la phase finale. Il choisit le point de terminaison avec la priorité la plus élevée parmi les correspondances comme correspondance optimale. S’il existe d’autres correspondances avec la même priorité que la meilleure correspondance, une exception de correspondance ambiguë est levée.

La priorité du routage est calculée en fonction d’un modèle de routage plus spécifique qui reçoit une priorité plus élevée. Par exemple, considérez les modèles /hello et /{message} :

  • Les deux correspondent au chemin d’URL /hello.
  • /hello est plus spécifique et, par conséquent, a une priorité plus élevée.

En général, la priorité des routages permet de choisir la meilleure correspondance pour les types de schémas d’URL utilisés dans la pratique. Utilisez Order uniquement si nécessaire pour éviter une ambiguïté.

En raison des types d’extensibilité fournis par le routage, il n’est pas possible que le système de routage calcule à l’avance les routages ambigus. Prenons un exemple tel que les modèles de routage /{message:alpha} et /{message:int} :

  • La contrainte alpha ne fait correspondre que les caractères alphabétiques.
  • La contrainte int ne fait correspondre que les nombres.
  • Ces modèles ont la même priorité de routage, mais il n’existe aucune URL à laquelle ils correspondent.
  • Si le système de routage a signalé une erreur d’ambiguïté au démarrage, il bloque ce cas d’usage valide.

Avertissement

L’ordre des opérations à l’intérieur de UseEndpoints n’influence pas le comportement du routage, à une exception près. MapControllerRoute et MapAreaRoute attribuent automatiquement une valeur de commande à leurs points de terminaison en fonction de l’ordre qu’ils appellent. Cela simule le comportement long des contrôleurs sans le système de routage fournissant les mêmes garanties que les implémentations de routage plus anciennes.

Routage des points de terminaison dans ASP.NET Core :

  • N’a pas le concept de routages.
  • Ne fournit pas de garanties de commande. Tous les points de terminaison sont traités simultanément.

Priorité du modèle de routage et ordre de sélection du point de terminaison

La priorité du modèle de routage est un système qui attribue à chaque modèle de routage une valeur en fonction de sa spécificité. La priorité du modèle de routage :

  • Évite la nécessité d’ajuster l’ordre des points de terminaison dans les cas courants.
  • Tente de faire correspondre les attentes courantes du comportement de routage.

Par exemple, envisagez des modèles /Products/List et /Products/{id}. Il serait raisonnable de supposer que /Products/List est une meilleure correspondance que /Products/{id} pour le chemin d’URL /Products/List. Cela fonctionne parce que le segment littéral /List est considéré comme ayant une meilleure priorité que le segment de paramètre /{id}.

Les détails du fonctionnement de la priorité sont couplés à la façon dont les modèles de routage sont définis :

  • Les modèles avec plus de segments sont considérés comme plus spécifiques.
  • Un segment avec du texte littéral est considéré comme plus spécifique qu’un segment de paramètre.
  • Un segment de paramètre avec une contrainte est considéré comme plus spécifique qu’un segment sans.
  • Un segment complexe est considéré aussi spécifique qu’un segment de paramètre avec une contrainte.
  • Les paramètres catch-all sont les moins spécifiques. Consultez catch-all dans la section Modèles de routage pour obtenir des informations importantes sur les routages catch-all.

Concepts de génération d’URL

La génération des URL :

  • Est le processus par lequel le routage peut créer un chemin d’URL basé sur un ensemble de valeurs de route.
  • Permet une séparation logique entre les points de terminaison et les URL qui y accèdent.

Le routage des points de terminaison inclut l’API LinkGenerator. LinkGenerator est un service singleton disponible à partir de DI. L’API LinkGenerator peut être utilisée en dehors du contexte d’une requête en cours d’exécution. Mvc.IUrlHelper et les scénarios qui s’appuient sur IUrlHelper, comme l’Assistance des balises, l’assistance HTML et les résultats d’action, utilisent l’API LinkGenerator pour fournir les fonctionnalités de création de liens.

Le générateur de liens est basé sur le concept d’une adresse et de schémas d’adresse. Un schéma d’adresse est un moyen de déterminer les points de terminaison à prendre en compte pour la génération de liens. Par exemple, les scénarios de nom de route et de valeurs de route que de nombreux utilisateurs connaissent bien dans les contrôleurs et Razor Pages sont implémentés en tant que schémas d’adresse.

Le générateur de liens peut lier à des contrôleurs et Razor Pages via les méthodes d’extension suivantes :

Une surcharge de ces méthodes accepte des arguments qui incluent HttpContext. Ces méthodes sont fonctionnellement équivalentes à Url.Action et à Url.Page, mais elles offrent davantage de flexibilité et d’options.

Les méthodes GetPath* sont les plus similaires à Url.Action et Url.Page, car elles génèrent un URI contenant un chemin d’accès absolu. Les méthodes GetUri* génèrent toujours un URI absolu contenant un schéma et un hôte. Les méthodes qui acceptent un HttpContext génèrent un URI dans le contexte de la requête en cours d’exécution. Les valeurs de route ambiante, le chemin de base d’URL, le schéma et l’hôte de la requête en cours d’exécution sont utilisés, sauf s’ils sont remplacés.

LinkGenerator est appelé avec une adresse. La génération d’un URI se fait en deux étapes :

  1. Une adresse est liée à une liste de points de terminaison qui correspondent à l’adresse.
  2. Le RoutePattern de chaque point de terminaison est évalué jusqu’à ce qu’un modèle de route correspondant aux valeurs fournies soit trouvé. Le résultat obtenu est combiné avec d’autres parties de l’URI fournies par le générateur de liens, puis il est retourné.

Les méthodes fournies par LinkGenerator prennent en charge des fonctionnalités de génération de liens standard pour n’importe quel type d’adresse. La façon la plus pratique d’utiliser le générateur de liens est de le faire via des méthodes d’extension qui effectuent des opérations pour un type d’adresse spécifique :

Méthode d’extension Description
GetPathByAddress Génère un URI avec un chemin absolu basé sur les valeurs fournies.
GetUriByAddress Génère un URI absolu basé sur les valeurs fournies.

Avertissement

Faites attention aux implications suivantes de l’appel de méthodes LinkGenerator :

  • Utilisez les méthodes d’extension GetUri* avec précaution dans une configuration d’application qui ne valide pas l’en-tête Host des requêtes entrantes. Si l’en-tête Host des requêtes entrantes n’est pas validé, l’entrée de requête non approuvée peut être renvoyée au client dans les URI d’une page ou d’une vue. Nous recommandons que toutes les applications de production configurent leur serveur pour qu’il valide l’en-tête Host par rapport à des valeurs valides connues.

  • Utilisez LinkGenerator avec précaution dans le middleware en combinaison avec Map ou MapWhen. Map* modifie le chemin de base de la requête en cours d’exécution, ce qui affecte la sortie de la génération de liens. Toutes les API LinkGenerator permettent la spécification d’un chemin de base. Spécifiez un chemin de base vide pour annuler l’effet de Map* sur la génération de liens.

Exemple de middleware

Dans l’exemple suivant, un intergiciel utilise l’API LinkGenerator pour créer un lien vers une méthode d’action qui liste les produits d’un magasin. L’utilisation du générateur de liens en l’injectant dans une classe et en appelant GenerateLink est disponible pour n’importe quelle classe dans une application :

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

Modèles de route

Les jetons dans {} définissent les paramètres de routage liés si le routage est mis en correspondance. Plusieurs paramètres de routage peuvent être définis dans un segment de routage, mais les paramètres de routage doivent être séparés par une valeur littérale. Par exemple :

{controller=Home}{action=Index}

n’est pas un routage valide, car il n’y a pas de valeur littérale entre {controller} et {action}. Les paramètres de routage doivent avoir un nom, et ils autorisent la spécification d’attributs supplémentaires.

Un texte littéral autre que les paramètres de routage (par exemple, {id}) et le séparateur de chemin / doit correspondre au texte présent dans l’URL. La correspondance de texte ne respecte pas la casse et est basée sur la représentation décodée du chemin des URL. Pour mettre en correspondance un délimiteur de paramètre de route littéral { ou }, placez-le dans une séquence d’échappement en répétant le caractère. Par exemple {{ ou }}.

Astérisque * ou astérisque double ** :

  • Peut être utilisé comme préfixe pour un paramètre de routage pour établir une liaison au rest de l’URI.
  • Ils sont appelés des paramètres catch-all. Par exemple, blog/{**slug} :
    • Correspond à n’importe quel URI qui commence par blog/ et a n’importe quelle valeur qui suit.
    • La valeur suivant blog/ est affectée à la valeur de routage slug.

Avertissement

Un paramètre catch-all peut faire correspondre les mauvais routages en raison d’un bogue dans le routage. Les applications affectées par ce bogue présentent les caractéristiques suivantes :

  • Un routage catch-all, par exemple, {**slug}"
  • Le routage catch-all ne fait pas correspondre les demandes qu’il doit faire correspondre.
  • La suppression d’autres routes fait que la route catch-all commence à fonctionner.

Consultez les bogues GitHub 18677 et 16579, par exemple les cas qui ont rencontré ce bogue.

Un correctif d’opt-in pour ce bogue est contenu dans le Kit de développement logiciel (SDK) .NET Core 3.1.301 et versions ultérieures. Le code suivant définit un commutateur interne qui corrige ce bogue :

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

Les paramètres fourre-tout peuvent également établir une correspondance avec la chaîne vide.

Le paramètre catch-all place les caractères appropriés dans une séquence d’échappement lorsque la route est utilisée pour générer une URL, y compris les caractères de séparation de chemin /. Par exemple, la route foo/{*path} avec les valeurs de route { path = "my/path" } génère foo/my%2Fpath. Notez la barre oblique d’échappement. Pour les séparateurs de chemin aller-retour, utilisez le préfixe de paramètre de routage **. La route foo/{**path} avec { path = "my/path" } génère foo/my/path.

Les modèles d’URL qui tentent de capturer un nom de fichier avec une extension de fichier facultative doivent faire l’objet de considérations supplémentaires. Prenez par exemple le modèle files/{filename}.{ext?}. Quand des valeurs existent à la fois pour filename et pour ext, les deux valeurs sont renseignées. Si seule une valeur existe pour filename dans l’URL, une correspondance est trouvée pour la route, car le . de fin est facultatif. Les URL suivantes correspondent à cette route :

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

Les paramètres de route peuvent avoir des valeurs par défaut, désignées en spécifiant la valeur par défaut après le nom du paramètre, séparée par un signe égal (=). Par exemple, {controller=Home} définit Home comme valeur par défaut de controller. La valeur par défaut est utilisée si aucune valeur n’est présente dans l’URL pour le paramètre. Vous pouvez rendre facultatifs les paramètres de route en ajoutant un point d’interrogation (?) à la fin du nom du paramètre. Par exemple, id? La différence entre les valeurs facultatives et les paramètres de routage par défaut est la suivante :

  • Un paramètre de routage avec une valeur par défaut produit toujours une valeur.
  • Un paramètre facultatif a une valeur uniquement lorsqu’une valeur est fournie par l’URL de la requête.

Les paramètres de route peuvent avoir des contraintes, qui doivent correspondre à la valeur de route liée à partir de l’URL. L’ajout de : et d’un nom de contrainte après le nom du paramètre de routage spécifie une contrainte inline sur un paramètre de routage. Si la contrainte exige des arguments, ils sont fournis entre parenthèses (...) après le nom de la contrainte. Il est possible de spécifier plusieurs contraintes inline en ajoutant un autre : et le nom d’une autre contrainte.

Le nom de la contrainte et les arguments sont passés au service IInlineConstraintResolver pour créer une instance de IRouteConstraint à utiliser dans le traitement des URL. Par exemple, le modèle de routage blog/{article:minlength(10)} spécifie une contrainte minlength avec l’argument 10. Pour plus d’informations sur les contraintes de route et pour obtenir la liste des contraintes fournies par le framework, consultez la section Contraintes de route.

Les paramètres de route peuvent également avoir des transformateurs de paramètres. Les transformateurs de paramètres transforment la valeur d’un paramètre lors de la génération de liens et d’actions et de pages correspondantes en URL. À l’instar des contraintes, les transformateurs de paramètre peuvent être ajoutés inline à un paramètre de routage en ajoutant un : et le nom du transformateur après le nom du paramètre de routage. Par exemple, le modèle de routage blog/{article:slugify} spécifie un transformateur slugify. Pour plus d’informations sur les transformateurs de paramètre, consultez la section Transformateurs de paramètre.

Le tableau suivant montre des exemples de modèles de route et leur comportement.

Modèle de routage Exemple d’URI en correspondance URI de requête
hello /hello Correspond seulement au chemin unique /hello.
{Page=Home} / Correspond à Page et le définit sur Home.
{Page=Home} /Contact Correspond à Page et le définit sur Contact.
{controller}/{action}/{id?} /Products/List Mappe au contrôleur Products et à l’action List.
{controller}/{action}/{id?} /Products/Details/123 Mappe au contrôleur Products et à l’action Details avec id défini sur 123).
{controller=Home}/{action=Index}/{id?} / Mappe au contrôleur Home et à l’action Index. id est ignoré.
{controller=Home}/{action=Index}/{id?} /Products Mappe au contrôleur Products et à la méthode Index. id est ignoré.

L’utilisation d’un modèle est généralement l’approche la plus simple pour le routage. Il est également possible de spécifier des contraintes et des valeurs par défaut hors du modèle de routage.

Segments complexes

Les segments complexes sont traités en faisant correspondre les délimiteurs littéraux de droite à gauche de manière non gourmande. Par exemple, [Route("/a{b}c{d}")] est un segment complexe. Les segments complexes fonctionnent d’une manière particulière qui doit être comprise pour les utiliser correctement. L’exemple de cette section montre pourquoi les segments complexes ne fonctionnent vraiment bien que lorsque le texte du délimiteur n’apparaît pas dans les valeurs des paramètres. L’utilisation d’un regex, puis l’extraction manuelle des valeurs est nécessaire pour des cas plus complexes.

Avertissement

Lorsque vous utilisez System.Text.RegularExpressions pour traiter une entrée non approuvée, passez un délai d’expiration. Un utilisateur malveillant peut fournir une entrée à RegularExpressions, provoquant une attaque par déni de service. Les API d’infrastructure ASP.NET Core qui utilisent RegularExpressions passent un délai d’expiration.

Il s’agit d’un résumé des étapes effectuées par le routage avec le modèle /a{b}c{d} et le chemin d’URL /abcd. Un | est utilisé pour vous aider à visualiser le fonctionnement de l’algorithme :

  • Le premier littéral, de droite à gauche, est c. Donc /abcd est recherché à partir de la droite et trouve /ab|c|d.
  • Tout ce qui se trouve à droite (d) est désormais mis en correspondance avec le paramètre de routage {d}.
  • Le littéral suivant, de droite à gauche, est a. Donc /ab|c|d est recherché à partir de là où nous sommes partis, puis a est trouvé /|a|b|c|d.
  • La valeur à droite (b) est désormais associée au paramètre de routage {b}.
  • Il n’y a pas de texte restant et aucun modèle de routage restant. Il s’agit donc d’une correspondance.

Voici un exemple de cas négatif utilisant le même modèle /a{b}c{d} et le chemin d’URL /aabcd. Un | est utilisé pour vous aider à visualiser le fonctionnement de l’algorithme. Ce cas n’est pas une correspondance, qui est expliquée par le même algorithme :

  • Le premier littéral, de droite à gauche, est c. Donc /aabcd est recherché à partir de la droite et trouve /aab|c|d.
  • Tout ce qui se trouve à droite (d) est désormais mis en correspondance avec le paramètre de routage {d}.
  • Le littéral suivant, de droite à gauche, est a. Donc /aab|c|d est recherché à partir de là où nous sommes partis, puis a est trouvé /a|a|b|c|d.
  • La valeur à droite (b) est désormais associée au paramètre de routage {b}.
  • À ce stade, il reste du texte a, mais l’algorithme n’a plus de modèle de routage à analyser. Il ne s’agit donc pas d’une correspondance.

Étant donné que l’algorithme correspondant n’est pas gourmand :

  • Il correspond à la plus petite quantité de texte possible dans chaque étape.
  • Si la valeur de délimiteur apparaît à l’intérieur des valeurs de paramètre, elle ne correspond pas.

Les expressions régulières fournissent beaucoup plus de contrôle sur leur comportement de correspondance.

La correspondance gourmande, également appelée correspondance paresseuse, correspond à la plus grande chaîne possible. La chaîne non gourmande correspond à la plus petite chaîne possible.

Routage avec des caractères spéciaux

Le routage avec des caractères spéciaux peut entraîner des résultats inattendus. Par exemple, considérez un contrôleur avec la méthode d’action suivante :

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

Lorsque string id contient les valeurs encodées suivantes, des résultats inattendus peuvent se produire :

ASCII Encoded
/ %2F
+

Les paramètres de routage ne sont pas toujours décodés par URL. Ce problème peut être résolu à l’avenir. Pour plus d’informations, consultez ce problème GitHub ;

Contraintes d'itinéraire

Les contraintes de route s’exécutent quand une correspondance s’est produite pour l’URL entrante, et le chemin de l’URL est tokenisé en valeurs de route. En général, les contraintes de routage inspectent la valeur de route associée par le biais du modèle de routage, et créent une décision true ou false indiquant si la valeur est acceptable. Certaines contraintes de routage utilisent des données hors de la valeur de route pour déterminer si la requête peut être routée. Par exemple, HttpMethodRouteConstraint peut accepter ou rejeter une requête en fonction de son verbe HTTP. Les contraintes sont utilisées dans le routage des requêtes et la génération des liens.

Avertissement

N’utilisez pas de contraintes pour la validation des entrées. Si des contraintes sont utilisées pour la validation d’entrée, une entrée non valide génère une réponse introuvable 404. Une entrée non valide doit produire une demande incorrecte 400 avec un message d’erreur approprié. Les contraintes de route sont utilisées pour lever l’ambiguïté entre des routes similaires, et non pas pour valider les entrées d’une route particulière.

Le tableau suivant montre des exemples de contrainte de route et leur comportement attendu :

contrainte Exemple Exemples de correspondances Notes
int {id:int} 123456789, -123456789 Correspond à n’importe quel entier
bool {active:bool} true, FALSE Correspond à true ou false. Non-respect de la casse
datetime {dob:datetime} 2016-12-31, 2016-12-31 7:32pm Correspond à une valeur valide DateTime dans la culture invariante. Voir l’avertissement précédent.
decimal {price:decimal} 49.99, -1,000.01 Correspond à une valeur valide decimal dans la culture invariante. Voir l’avertissement précédent.
double {weight:double} 1.234, -1,001.01e8 Correspond à une valeur valide double dans la culture invariante. Voir l’avertissement précédent.
float {weight:float} 1.234, -1,001.01e8 Correspond à une valeur valide float dans la culture invariante. Voir l’avertissement précédent.
guid {id:guid} CD2C1638-1638-72D5-1638-DEADBEEF1638 Correspond à une valeur Guid valide
long {ticks:long} 123456789, -123456789 Correspond à une valeur long valide
minlength(value) {username:minlength(4)} Rick La chaîne doit comporter au moins 4 caractères
maxlength(value) {filename:maxlength(8)} MyFile La chaîne ne doit pas comporter plus de 8 caractères
length(length) {filename:length(12)} somefile.txt La chaîne doit comporter exactement 12 caractères
length(min,max) {filename:length(8,16)} somefile.txt La chaîne doit comporter au moins 8 caractères et pas plus de 16 caractères
min(value) {age:min(18)} 19 La valeur entière doit être au moins égale à 18
max(value) {age:max(120)} 91 La valeur entière ne doit pas être supérieure à 120
range(min,max) {age:range(18,120)} 91 La valeur entière doit être au moins égale à 18 mais ne doit pas être supérieure à 120
alpha {name:alpha} Rick La chaîne doit se composer d’un ou de plusieurs caractères alphabétiques (a-z, non-respect de la casse).
regex(expression) {ssn:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)} 123-45-6789 La chaîne doit correspondre à l’expression régulière. Consultez des conseils sur la définition d’une expression régulière.
required {name:required} Rick Utilisé pour garantir qu’une valeur autre qu’un paramètre est présente pendant la génération de l’URL

Avertissement

Lorsque vous utilisez System.Text.RegularExpressions pour traiter une entrée non approuvée, passez un délai d’expiration. Un utilisateur malveillant peut fournir une entrée à RegularExpressions, provoquant une attaque par déni de service. Les API d’infrastructure ASP.NET Core qui utilisent RegularExpressions passent un délai d’expiration.

Il est possible d’appliquer plusieurs contraintes séparées par un point-virgule à un même paramètre. Par exemple, la contrainte suivante limite un paramètre à une valeur entière supérieure ou égale à 1 :

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

Avertissement

Les contraintes de routage qui vérifient que l’URL peut être convertie en type CLR utilisent toujours la culture invariant. Par exemple, conversion en type CLR int ou DateTime. Ces contraintes partent du principe que l’URL ne peut pas être localisé. Les contraintes de routage fournies par le framework ne modifient pas les valeurs stockées dans les valeurs de route. Toutes les valeurs de route analysées à partir de l’URL sont stockées sous forme de chaînes. Par exemple, la contrainte float tente de convertir la valeur de route en valeur float, mais la valeur convertie est utilisée uniquement pour vérifier qu’elle peut être convertie en valeur float.

Expressions régulières dans les contraintes

Avertissement

Lorsque vous utilisez System.Text.RegularExpressions pour traiter une entrée non approuvée, passez un délai d’expiration. Un utilisateur malveillant peut fournir une entrée à RegularExpressions, provoquant une attaque par déni de service. Les API d’infrastructure ASP.NET Core qui utilisent RegularExpressions passent un délai d’expiration.

Les expressions régulières peuvent être spécifiées en tant que contraintes inline à l’aide de la contrainte de routage regex(...). Les méthodes de la famille MapControllerRoute acceptent également un littéral d’objet de contraintes. Si ce formulaire est utilisé, les valeurs de chaîne sont interprétées comme des expressions régulières.

Le code suivant utilise une contrainte d’expression régulière inline :

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

Le code suivant utilise un littéral d’objet pour spécifier une contrainte d’expression régulière :

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

Le framework ASP.NET Core ajoute RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant au constructeur d’expression régulière. Pour obtenir une description de ces membres, consultez RegexOptions.

Les expressions régulières utilisent les délimiteurs et des jetons semblables à ceux utilisés par le service de routage et le langage C#. Les jetons d’expression régulière doivent être placés dans une séquence d’échappement. Pour utiliser l’expression régulière ^\d{3}-\d{2}-\d{4}$ dans une contrainte inline, utilisez l’une des options suivantes :

  • Remplacez les caractères \ fournis dans la chaîne en tant que caractères \\ dans le fichier source C# afin d’échapper au caractère \ d’échappement de chaîne.
  • Littéraux de chaîne verbatim.

Pour placer en échappement les caractères de délimiteur de paramètre de route {, }, [, ], doublez les caractères dans l’expression, par exemple {{, }}, [[, ]]. Le tableau suivant montre une expression régulière et la version placée en échappement :

Expression régulière Expression régulière en échappement
^\d{3}-\d{2}-\d{4}$ ^\\d{{3}}-\\d{{2}}-\\d{{4}}$
^[a-z]{2}$ ^[[a-z]]{{2}}$

Les expressions régulières utilisées dans le routage commencent souvent par le caractère ^ et correspondent à la position de début de la chaîne. Les expressions se terminent souvent par le caractère $ et correspondent à la fin de la chaîne. Les caractères ^ et $ garantissent que l’expression régulière établit une correspondance avec la totalité de la valeur du paramètre de route. Sans les caractères ^ et $, l’expression régulière peut correspondre à n’importe quelle sous-chaîne dans la chaîne, ce qui est souvent indésirable. Le tableau suivant contient des exemples et explique pourquoi ils établissent ou non une correspondance :

Expression String Correspond Commentaire
[a-z]{2} hello Oui Correspondances de sous-chaînes
[a-z]{2} 123abc456 Oui Correspondances de sous-chaînes
[a-z]{2} mz Oui Correspondance avec l’expression
[a-z]{2} MZ Oui Non-respect de la casse
^[a-z]{2}$ hello Non Voir ^ et $ ci-dessus
^[a-z]{2}$ 123abc456 Non Voir ^ et $ ci-dessus

Pour plus d’informations sur la syntaxe des expressions régulières, consultez Expressions régulières du .NET Framework.

Pour contraindre un paramètre à un ensemble connu de valeurs possibles, utilisez une expression régulière. Par exemple, {action:regex(^(list|get|create)$)} établit une correspondance avec la valeur de route action uniquement pour list, get ou create. Si elle est passée dans le dictionnaire de contraintes, la chaîne ^(list|get|create)$ est équivalente. Les contraintes passées dans le dictionnaire de contraintes qui ne correspondent pas à l’une des contraintes connues sont également traitées comme des expressions régulières. Les contraintes passées dans un modèle qui ne correspondent pas à l’une des contraintes connues ne sont pas traitées comme des expressions régulières.

Contraintes de routage personnalisées

Les contraintes de routage personnalisées peuvent être créées en implémentant l’interface IRouteConstraint. L’interface IRouteConstraint contient une méthode unique, Match, qui retourne true si la contrainte est satisfaite et false dans le cas contraire.

Les contraintes de routage personnalisées sont rarement nécessaires. Avant d’implémenter une contrainte de routage personnalisée, envisagez des alternatives, telles que la liaison de modèle.

Le dossier ASP.NET Core Contraintes fournit de bons exemples de création de contraintes. Par exemple, GuidRouteConstraint.

Pour utiliser un IRouteConstraint personnalisé, le type de contrainte de routage doit être inscrit avec le ConstraintMap de l’application dans le conteneur de service de l’application. Un ConstraintMap est un dictionnaire qui mappe les clés de contrainte d’itinéraire aux implémentations IRouteConstraint qui valident ces contraintes. Le ConstraintMap d’une application peut être mis à jour dans Program.cs en tant qu’appel AddRouting ou en configurant RouteOptions directement avec builder.Services.Configure<RouteOptions>. Par exemple :

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

La contrainte précédente est appliquée dans le code suivant :

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

L’implémentation de NoZeroesRouteConstraint empêche l’utilisation de 0 dans un paramètre de routage :

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

Avertissement

Lorsque vous utilisez System.Text.RegularExpressions pour traiter une entrée non approuvée, passez un délai d’expiration. Un utilisateur malveillant peut fournir une entrée à RegularExpressions, provoquant une attaque par déni de service. Les API d’infrastructure ASP.NET Core qui utilisent RegularExpressions passent un délai d’expiration.

Le code précédent :

  • Empêche 0 dans le segment {id} de la route.
  • S’affiche pour fournir un exemple de base d’implémentation d’une contrainte personnalisée. Il ne doit pas être utilisé dans une application de production.

Le code suivant est une meilleure approche pour empêcher un id contenant un 0 d’être traité :

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

    return Content(id);
}

Le code précédent présente les avantages suivants sur l’approche NoZeroesRouteConstraint :

  • Il ne nécessite pas de contrainte personnalisée.
  • Il retourne une erreur plus descriptive lorsque le paramètre de routage inclut 0.

Les transformateurs de paramètres

Transformateurs de paramètre :

Par exemple, un transformateur de paramètre slugify personnalisé dans le modèle d’itinéraire blog\{article:slugify} avec Url.Action(new { article = "MyTestArticle" }) génère blog\my-test-article.

Examinez l’implémentation suivante 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();
    }
}

Pour utiliser un transformateur de paramètre dans un modèle d’itinéraire, configurez-le d’abord en utilisant ConstraintMap dans Program.cs :

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

L’infrastructure ASP.NET Core utilise des transformateurs de paramètres pour transformer l’URI où un point de terminaison est résolu. Par exemple, les transformateurs de paramètres transforment les valeurs de routage utilisées pour faire correspondre un area, controller, actionet page :

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

Avec le modèle de routage précédent, l’action SubscriptionManagementController.GetAll est mise en correspondance avec l’URI /subscription-management/get-all. Un transformateur de paramètre ne modifie pas les valeurs de routage utilisées pour générer un lien. Par exemple, Url.Action("GetAll", "SubscriptionManagement") produit /subscription-management/get-all.

ASP.NET Core fournit des conventions d’API pour l’utilisation des transformateurs de paramètre avec des routages générés :

Informations de référence sur la génération d’URL

Cette section contient une référence pour l’algorithme implémenté par génération d’URL. Dans la pratique, les exemples les plus complexes de génération d’URL utilisent des contrôleurs ou Razor Pages. Pour plus d’informations, consultez Routage dans les contrôleurs.

Le processus de génération d’URL commence par un appel à LinkGenerator.GetPathByAddress ou une méthode similaire. La méthode est fournie avec une adresse, un ensemble de valeurs de routage et éventuellement des informations sur la requête actuelle de HttpContext.

La première étape consiste à utiliser l’adresse pour résoudre un ensemble de points de terminaison candidats à l’aide d’un IEndpointAddressScheme<TAddress> correspondant au type de l’adresse.

Une fois que l’ensemble de candidats est trouvé par le schéma d’adresses, les points de terminaison sont classés et traités de manière itérative jusqu’à ce qu’une opération de génération d’URL réussisse. La génération d’URL ne vérifie pas les ambiguïtés, le premier résultat retourné est le résultat final.

Résolution des problèmes de génération d’URL avec la journalisation

La première étape de la résolution des problèmes de génération d’URL consiste à définir le niveau de journalisation de Microsoft.AspNetCore.Routing sur TRACE. LinkGenerator enregistre de nombreux détails sur son traitement, ce qui peut être utile pour résoudre les problèmes.

Consultez Référence de génération d’URL pour plus d’informations sur la génération d’URL.

Adresses

Les adresses sont le concept de génération d’URL utilisé pour lier un appel au générateur de liens à un ensemble de points de terminaison candidats.

Les adresses sont un concept extensible qui comprend deux implémentations par défaut :

  • Utilisation du nom du point de terminaison (string) comme adresse :
    • Fournit des fonctionnalités similaires au nom du routage de MVC.
    • Utilise le type de métadonnées IEndpointNameMetadata.
    • Résout la chaîne fournie par rapport aux métadonnées de tous les points de terminaison inscrits.
    • Lève une exception au démarrage si plusieurs points de terminaison utilisent le même nom.
    • Recommandé pour une utilisation à usage général en dehors des contrôleurs et de Razor Pages.
  • Utilisation des valeurs de route (RouteValuesAddress) comme adresse :
    • Fournit des fonctionnalités similaires à la génération d’URL hérité des contrôleurs et de Razor Pages.
    • Très complexe à étendre et à déboguer.
    • Fournit l’implémentation utilisée par IUrlHelper, l’assistance des balises, l’assistance HTML , Résultats d’action, etc.

Le rôle du schéma d’adresses consiste à faire l’association entre l’adresse et les points de terminaison correspondants selon des critères arbitraires :

  • Le schéma de noms de point de terminaison effectue une recherche de dictionnaire de base.
  • Le schéma de valeurs de route a un sous-ensemble complexe de l’algorithme défini.

Valeurs ambiantes et valeurs explicites

À partir de la requête actuelle, le routage accède aux valeurs de routage de la requête HttpContext.Request.RouteValuesactuelle. Les valeurs associées à la requête actuelle sont appelées valeurs ambiantes. À des fins de clarté, la documentation fait référence aux valeurs de routage transmises aux méthodes en tant que valeurs explicites.

L’exemple suivant montre les valeurs ambiantes et les valeurs explicites. Il fournit des valeurs ambiantes à partir de la requête actuelle et des valeurs explicites :

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

    // ...

Le code précédent :

Le code suivant fournit uniquement des valeurs explicites et aucune valeur ambiante :

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

La méthode précédente retourne /Home/Subscribe/17

Le code suivant dans le WidgetController retourne /Widget/Subscribe/17 :

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

Le code suivant fournit au contrôleur des valeurs ambiantes dans la requête actuelle et des valeurs explicites :

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

Dans le code précédent :

  • /Gadget/Edit/17 est retourné.
  • Url obtient IUrlHelper.
  • Action génère une URL avec un chemin absolu pour une méthode d’action. L’URL contient le nom de action spécifié et les valeurs route.

Le code suivant fournit des valeurs ambiantes à partir de la requête actuelle et des valeurs explicites :

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

        // ...
    }
}

Le code précédent définit url sur /Edit/17 lorsque la page Razor Modifier contient la directive de page suivante :

@page "{id:int}"

Si la page Modifier ne contient pas le modèle de route "{id:int}", url est /Edit?id=17.

Le comportement de l'IUrlHelper de MVC ajoute une couche de complexité en plus des règles décrites ici :

  • IUrlHelper fournit toujours les valeurs de routage de la requête actuelle en tant que valeurs ambiantes.
  • IUrlHelper.Action copie toujours les valeurs actuelles action et controller de routage en tant que valeurs explicites, sauf substitution par le développeur.
  • IUrlHelper.Page copie toujours la valeur de routage actuelle page en tant que valeur explicite, sauf si elle est remplacée.
  • IUrlHelper.Page remplace toujours la valeur de route handler actuelle par null comme valeurs explicites, sauf substitution.

Les utilisateurs sont souvent surpris par les détails comportementaux des valeurs ambiantes, car MVC ne semble pas suivre ses propres règles. Pour des raisons historiques et de compatibilité, certaines valeurs de routage telles que action, controller, page et handler ont leur propre comportement de cas spécial.

La fonctionnalité équivalente fournie par LinkGenerator.GetPathByAction et LinkGenerator.GetPathByPage duplique ces anomalies de IUrlHelper pour la compatibilité.

Processus de génération d’URL

Une fois l’ensemble de points de terminaison candidats trouvés, l’algorithme de génération d’URL :

  • Traite les points de terminaison de manière itérative.
  • Retourne le premier résultat réussi.

La première étape de ce processus est appelée invalidation des valeurs de routage. L’invalidation des valeurs de routage est le processus par lequel le routage détermine les valeurs de routage des valeurs ambiantes à utiliser et qui doivent être ignorées. Chaque valeur ambiante est considérée et combinée aux valeurs explicites ou ignorée.

La meilleure façon de penser au rôle des valeurs ambiantes est qu’elles tentent d’enregistrer la saisie par les développeurs d’applications, dans certains cas courants. Traditionnellement, les scénarios où les valeurs ambiantes sont utiles sont liés à MVC :

  • Lors de la liaison à une autre action dans le même contrôleur, le nom du contrôleur n’a pas besoin d’être spécifié.
  • Lors de la liaison à un autre contrôleur dans la même zone, le nom de la zone n’a pas besoin d’être spécifié.
  • Lors de la liaison à la même méthode d’action, les valeurs de routage n’ont pas besoin d’être spécifiées.
  • Lors de la liaison à une autre partie de l’application, vous ne souhaitez pas transporter les valeurs de routage qui n’ont aucune signification dans cette partie de l’application.

Les appels à ou LinkGenerator qui retournent IUrlHelper sont généralement dus à null une non-compréhension de l’invalidation de la valeur de route. Résolvez les problèmes d’invalidation des valeurs de routage en spécifiant explicitement davantage de valeurs de routage pour voir si cela résout le problème.

L’invalidation de la valeur de routage repose sur l’hypothèse que le schéma d’URL de l’application est hiérarchique, avec une hiérarchie formée de gauche à droite. Considérez le modèle de route de contrôleur de base {controller}/{action}/{id?} pour avoir un sens intuitif de la façon dont cela fonctionne dans la pratique. Une modification apportée à une valeur invalide toutes les valeurs de routage qui apparaissent à droite. Cela reflète l’hypothèse sur la hiérarchie. Si l’application a une valeur ambiante pour id, et que l’opération spécifie une valeur différente pour controller :

  • id ne sera pas réutilisée, car {controller} est à gauche de {id?}.

Voici quelques exemples illustrant ce principe :

  • Si les valeurs explicites contiennent une valeur pour id, la valeur ambiante de id est ignorée. Les valeurs ambiantes de controller et action peuvent être utilisées.
  • Si les valeurs explicites contiennent une valeur pour action, toute valeur ambiante de action est ignorée. Les valeurs ambiantes de controller peuvent être utilisées. Si la valeur explicite de action est différente de la valeur ambiante de action, la valeur id ne sera pas utilisée. Si la valeur explicite de action est identique à la valeur ambiante de action, la valeur id peut être utilisée.
  • Si les valeurs explicites contiennent une valeur de controller, toute valeur ambiante de controller est ignorée. Si la valeur explicite de controller est différente de la valeur ambiante de controller, les valeurs action et id ne seront pas utilisées. Si la valeur explicite de controller est identique à la valeur ambiante de controller, les valeurs action et id peuvent être utilisées.

Ce processus est encore plus compliqué à cause de l’existence de routes d’attributs et de routes conventionnelles dédiées. Les routes conventionnelles de contrôleur tels que {controller}/{action}/{id?} spécifient une hiérarchie à l’aide de paramètres de routage. Pour les routages conventionnels dédiés et les routes d’attribut aux contrôleurs et à Razor Pages :

  • Il existe une hiérarchie de valeurs de routage.
  • Elles n’apparaissent pas dans le modèle.

Dans ce cas, la génération d’URL définit le concept de valeurs requises. Les points de terminaison créés par les contrôleurs et Razor Pages ont des valeurs requises spécifiées qui autorisent l’invalidation de la valeur de routage à fonctionner.

Algorithme d’invalidation de valeur de routage en détail :

  • Les noms de valeurs requis sont combinés avec les paramètres de routage, puis traités de gauche à droite.
  • Pour chaque paramètre, la valeur ambiante et la valeur explicite sont comparées :
    • Si la valeur ambiante et la valeur explicite sont identiques, le processus continue.
    • Si la valeur ambiante est présente et que la valeur explicite ne l’est pas, la valeur ambiante est utilisée lors de la génération de l’URL.
    • Si la valeur ambiante n’est pas présente et que la valeur explicite l’est, rejetez la valeur ambiante et toutes les valeurs ambiantes suivantes.
    • Si la valeur ambiante et la valeur explicite sont présentes et que les deux valeurs sont différentes, rejetez la valeur ambiante et toutes les valeurs ambiantes suivantes.

À ce stade, l’opération de génération d’URL est prête à évaluer les contraintes de routage. L’ensemble de valeurs acceptées est combiné aux valeurs par défaut des paramètres, qui sont fournies aux contraintes. Si les contraintes passent toutes, l’opération se poursuit.

Ensuite, les valeurs acceptées peuvent être utilisées pour développer le modèle de routage. Le modèle de routage est traité :

  • De gauche à droite.
  • Chaque paramètre a sa valeur acceptée remplacée.
  • Avec les cas spéciaux suivants :
    • S’il manque une valeur aux valeurs acceptées et que le paramètre a une valeur par défaut, la valeur par défaut est utilisée.
    • S’il manque une valeur aux valeurs acceptées et que le paramètre est facultatif, le traitement se poursuit.
    • Si un paramètre de routage à droite d’un paramètre facultatif manquant a une valeur, l’opération échoue.
    • Les paramètres par défaut contigus et les paramètres facultatifs sont réduits si possible.

Les valeurs fournies explicitement mais qui n’ont pas de correspondance avec un segment de la route sont ajoutées à la chaîne de requête. Le tableau suivant présente le résultat en cas d’utilisation du modèle de routage {controller}/{action}/{id?}.

Valeurs ambiantes Valeurs explicites Résultat
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

Problèmes liés à l’invalidation des valeurs de routage

Le code suivant montre un exemple de schéma de génération d’URL qui n’est pas pris en charge par le routage :

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

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

Dans le code précédent, le paramètre de routage culture est utilisé pour la localisation. On veut que le paramètre culture soit toujours accepté comme valeur ambiante. Toutefois, le paramètre culture n’est pas accepté comme valeur ambiante en raison de la façon dont les valeurs requises fonctionnent :

  • Dans le modèle de routage "default", le paramètre de routage culture est à gauche de controller. Les modifications apportées à controller n’invalident donc pas culture.
  • Dans le modèle de routage "blog", le paramètre de routage culture est considéré comme à droite de controller, qui apparaît dans les valeurs requises.

Analyser les chemins d’URL avec LinkParser

La classe LinkParser ajoute la prise en charge de l’analyse d’un chemin d’URL dans un ensemble de valeurs de routage. La méthode ParsePathByEndpointName prend un nom de point de terminaison et un chemin d’URL et retourne un ensemble de valeurs de routage extraites du chemin d’URL.

Dans l’exemple de contrôleur suivant, l’action GetProduct utilise un modèle de routage de api/Products/{id} et a un Name de GetProduct :

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

Dans la même classe de contrôleur, l’action AddRelatedProduct attend un chemin d’URL, pathToRelatedProduct, qui peut être fourni en tant que paramètre de chaîne de requête :

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

    // ...

Dans l’exemple précédent, l’action AddRelatedProduct extrait la valeur de route id du chemin d’URL. Par exemple, avec un chemin d’URL de /api/Products/1, la valeur relatedProductId est définie sur 1. Cette approche permet aux clients de l’API d’utiliser des chemins d’URL lorsque vous faites référence à des ressources, sans avoir à connaître la façon dont cette URL est structurée.

Configurer les métadonnées de point de terminaison

Les liens suivants fournissent des informations sur la configuration des métadonnées de point de terminaison :

Correspondance de l’hôte dans les routages avec RequireHost

RequireHost applique une contrainte au routage qui nécessite l’hôte spécifié. Le paramètre RequireHost ou [Host] peut être un :

  • Hôte : www.domain.com, fait correspondre www.domain.com à n’importe quel port.
  • Hôte avec caractère générique : *.domain.com, fait correspondre www.domain.com, subdomain.domain.comou www.subdomain.domain.com sur n’importe quel port.
  • Port : *:5000, fait correspondre le port 5000 avec n’importe quel hôte.
  • Hôte et port : www.domain.com:5000 ou *.domain.com:5000, fait correspondre l’hôte et le port.

Plusieurs paramètres peuvent être spécifiés à l’aide RequireHost ou [Host]. La contrainte fait correspondre les hôtes valides pour l’un des paramètres. Par exemples, [Host("domain.com", "*.domain.com")] fait correspondre domain.com, www.domain.com et subdomain.domain.com.

Le code suivant utilise RequireHost pour exiger l’hôte spécifié sur le routage :

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

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

Le code suivant utilise l’attribut [Host] sur le contrôleur pour exiger l’un des hôtes spécifiés :

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

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

Lorsque l’attribut [Host] est appliqué à la fois au contrôleur et à la méthode d’action :

  • L’attribut de l’action est utilisé.
  • L’attribut du contrôleur est ignoré.

Conseils sur les performances pour le routage

Lorsqu’une application rencontre des problèmes de performances, le routage est souvent soupçonné comme étant le problème. La raison pour laquelle le routage est soupçonné est que les infrastructures telles que les contrôleurs et Razor Pages signalent le temps passé à l’intérieur de l’infrastructure dans leurs messages de journalisation. En cas de différence significative entre le temps signalé par les contrôleurs et le temps total de la requête :

  • Les développeurs éliminent leur code d’application comme source du problème.
  • Il est courant de supposer que le routage est la cause.

Les performances du routage est testé à l’aide de milliers de points de terminaison. Il est peu probable qu’une application classique rencontre un problème de performances simplement en étant trop volumineuse. La cause racine la plus courante des performances de routage lentes est généralement un intergiciel personnalisé qui se comporte mal.

Cet exemple de code suivant illustre une technique de base pour affiner la source de délai :

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

Pour le routage temporel :

  • Entrelacez chaque intergiciel avec une copie de l’intergiciel de minutage indiqué dans le code précédent.
  • Ajoutez un identificateur unique pour mettre en corrélation les données de minutage avec le code.

Il s’agit d’un moyen de base de limiter le délai lorsqu’il est significatif, par exemple, plus que 10ms. Soustraire Time 2 de Time 1 signale le temps passé à l’intérieur de l’intergiciel UseRouting.

Le code suivant utilise une approche plus compacte du code de minutage précédent :

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

Fonctionnalités de routage potentiellement coûteuses

La liste suivante fournit un aperçu des fonctionnalités de routage relativement coûteuses par rapport aux modèles de routage de base :

  • Expressions régulières : il est possible d’écrire des expressions régulières qui sont complexes ou qui ont un temps d’exécution long avec une petite quantité d’entrée.
  • Segments complexes ({x}-{y}-{z}) :
    • Sont beaucoup plus coûteux que l’analyse d’un segment de chemin d’URL standard.
    • Entraînent l’allocation d’un grand nombre de sous-chaînes.
  • Accès aux données synchrones : de nombreuses applications complexes disposent d’un accès à la base de données dans le cadre de leur routage. Utilisez des points d’extensibilité tels que MatcherPolicy et EndpointSelectorContext, qui sont asynchrones.

Conseils pour les tables de routage volumineuses

Par défaut, ASP.NET Core utilise un algorithme de routage qui échange la mémoire pour le temps processeur. Cela a l’effet intéressant que le temps de correspondance de la route dépend uniquement de la longueur du chemin d’accès à mettre en correspondance et non du nombre de routes. Toutefois, cette approche peut être potentiellement problématique dans certains cas, lorsque l’application a un grand nombre de routes (dans les milliers) et qu’il existe un grand nombre de préfixes variables dans les routes. Par exemple, si les routes ont des paramètres dans les premiers segments de la route, comme {parameter}/some/literal.

Il est peu probable qu’une application rencontre un problème, sauf si :

  • Il existe un nombre élevé de routes dans l’application avec ce modèle.
  • Il existe un grand nombre de routes dans l’application.

Comment déterminer si une application s’exécute dans le problème de la table de routage volumineuse

  • Il existe deux symptômes à rechercher :
    • L’application est lente à démarrer sur la première requête.
      • Notez que cela est requis, mais pas suffisant. Il existe de nombreux autres problèmes non liés au routage qui peuvent entraîner un démarrage d’application lent. Vérifiez la condition ci-dessous pour déterminer avec précision que l’application se trouve dans cette situation.
    • L’application consomme beaucoup de mémoire au démarrage et un vidage de la mémoire affiche un grand nombre d’instances Microsoft.AspNetCore.Routing.Matching.DfaNode.

Comment résoudre ce problème

Plusieurs techniques et optimisations peuvent être appliquées aux routes qui améliorent en grande partie ce scénario :

  • Appliquez des contraintes de routage à vos paramètres, par exemple {parameter:int}, {parameter:guid}, {parameter:regex(\\d+)}, etc. si possible.
    • Cela permet à l’algorithme de routage d’optimiser en interne les structures utilisées pour la correspondance et de réduire considérablement la mémoire utilisée.
    • Dans la grande majorité des cas, cela suffit pour revenir à un comportement acceptable.
  • Modifiez les routes pour déplacer des paramètres vers des segments ultérieurs dans le modèle.
    • Cela réduit le nombre de « chemins » possibles pour correspondre à un point de terminaison donné un chemin d’accès.
  • Utilisez une route dynamique et effectuez le mappage sur un contrôleur/page dynamiquement.
    • Pour ce faire, vous pouvez utiliser MapDynamicControllerRoute et MapDynamicPageRoute.

Conseils pour les auteurs de bibliothèques

Cette section contient des conseils pour les auteurs de bibliothèques qui s’appuient sur le routage. Ces détails sont destinés à garantir que les développeurs d’applications ont une bonne expérience à l’aide de bibliothèques et d’infrastructures qui étendent le routage.

Définir des points de terminaison

Pour créer une infrastructure qui utilise le routage pour la correspondance d’URL, commencez par définir une expérience utilisateur qui s’appuie sur UseEndpoints.

GÉNÉREZ sur IEndpointRouteBuilder. Cela permet aux utilisateurs de composer votre infrastructure avec d’autres fonctionnalités ASP.NET Core sans confusion. Chaque modèle ASP.NET Core inclut le routage. Supposons que le routage est présent et familier pour les utilisateurs.

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

app.MapHealthChecks("/healthz");

RETOURNEZ un type concret scellé à partir d’un appel à MapMyFramework(...) qui implémente IEndpointConventionBuilder. La plupart des méthodes d’infrastructure Map... suivent ce modèle. L'interface IEndpointConventionBuilder :

  • Permet la composition des métadonnées.
  • Est ciblée par diverses méthodes d’extension.

La déclaration de votre propre type vous permet d’ajouter vos propres fonctionnalités spécifiques à l’infrastructure au générateur. Vous pouvez encapsuler un générateur déclaré par l’infrastructure et lui transférer les appels.

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

app.MapHealthChecks("/healthz");

ENVISAGEZ d’écrire votre propre EndpointDataSource. EndpointDataSource est la primitive de bas niveau permettant de déclarer et de mettre à jour une collection de points de terminaison. EndpointDataSource est une API puissante utilisée par les contrôleurs et Razor Pages.

Les tests de routage ont un exemple de base d’une source de données sans mise à jour.

NE TENTEZ PAS d’inscrire un EndpointDataSource par défaut. Demandez aux utilisateurs d’inscrire votre infrastructure dans UseEndpoints. La philosophie du routage est que rien n’est inclus par défaut et que UseEndpoints est l’endroit où inscrire des points de terminaison.

Création d’un intergiciel intégré au routage

ENVISAGEZ de définir des types de métadonnées en tant qu’interface.

FAITES EN SORTE qu’il soit possible d’utiliser des types de métadonnées en tant qu’attribut sur des classes et des méthodes.

public interface ICoolMetadata
{
    bool IsCool { get; }
}

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

Les frameworks tels que les contrôleurs et Razor Pages prennent en charge l’application d’attributs de métadonnées aux types et méthodes. Si vous déclarez des types de métadonnées :

  • Rendez-les accessibles en tant qu’attributs.
  • La plupart des utilisateurs sont familiarisés avec l’application d’attributs.

La déclaration d’un type de métadonnées en tant qu’interface ajoute une autre couche de flexibilité :

  • Les interfaces sont composables.
  • Les développeurs peuvent déclarer leurs propres types qui combinent plusieurs stratégies.

FAITES EN SORTE qu’il soit possible de remplacer les métadonnées, comme illustré dans l’exemple suivant :

[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 meilleure façon de suivre ces instructions consiste à éviter de définir des métadonnées de marqueur :

  • Ne recherchez pas simplement la présence d’un type de métadonnées.
  • Définissez une propriété sur les métadonnées et vérifiez la propriété.

La collection de métadonnées est triée et prend en charge la substitution par priorité. Dans le cas des contrôleurs, les métadonnées sur la méthode d’action sont les plus spécifiques.

FAITES EN SORTE que l’intergiciel soit utile avec et sans routage :

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

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

À titre d’exemple de cette recommandation, considérez l’intergiciel UseAuthorization. L’intergiciel d’autorisation vous permet de passer une stratégie de secours. La stratégie de secours, si elle est spécifiée, s’applique aux :

  • Points de terminaison sans stratégie spécifiée.
  • Requêtes qui ne correspondent pas à un point de terminaison.

Cela rend l’intergiciel d’autorisation utile en dehors du contexte du routage. L’intergiciel d’autorisation peut être utilisé pour la programmation d’intergiciels traditionnels.

Déboguer les diagnostics

Pour obtenir une sortie de diagnostic de routage détaillée, définissez Logging:LogLevel:Microsoft sur Debug. Dans l’environnement de développement, définissez le niveau de journal dans appsettings.Development.json :

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

Ressources supplémentaires

Le routage est responsable de la correspondance des requêtes HTTP entrantes et de la distribution de ces requêtes aux points de terminaison exécutables de l’application. Les points de terminaison sont les unités de code de gestion des requêtes exécutables de l’application. Les points de terminaison sont définies dans l’application et configurées au démarrage de l’application. Le processus de correspondance de point de terminaison peut extraire des valeurs de l’URL de la requête et fournir ces valeurs pour le traitement des demandes. Avec les informations de point de terminaison fournies par l’application, le routage peut également générer des URL qui mappent vers des points de terminaison.

Les applications peuvent configurer le routage à l’aide des éléments suivants :

Ce document traite du routage ASP.NET Core de bas niveau. Pour plus d’informations sur la configuration du routage :

Le système de routage des points de terminaison décrit dans ce document s’applique à ASP.NET Core 3.0 et versions ultérieures. Pour plus d’informations sur le système de routage précédent basé sur IRouter, sélectionnez la version ASP.NET Core 2.1 à l’aide de l’une des approches suivantes :

Affichez ou téléchargez l’exemple de code (procédure de téléchargement)

Les exemples de téléchargement de ce document sont activés par une classe Startup spécifique. Pour exécuter un exemple spécifique, modifiez Program.cs pour appeler la classe Startup souhaitée.

Concepts de base du routage

Tous les modèles ASP.NET Core incluent le routage dans le code généré. Le routage est inscrit dans le pipeline d’intergiciel dans Startup.Configure.

Le code suivant illustre un exemple de routage de base :

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

Le routage utilise une paire d’intergiciels, inscrite par UseRouting et UseEndpoints :

  • UseRouting ajoute la correspondance de routage au pipeline d’intergiciels. Cet intergiciel examine l’ensemble des points de terminaison définis dans l’application et sélectionne la meilleure correspondance en fonction de la requête.
  • UseEndpoints ajoute l’exécution du point de terminaison au pipeline de l’intergiciel. Il exécute le délégué associé au point de terminaison sélectionné.

L’exemple précédent inclut une route unique vers le point de terminaison de code à l’aide de la méthode MapGet :

  • Lorsqu’une requête http GET est envoyée à l’URL racine / :
    • Le délégué de requête affiché s’exécute.
    • Hello World! est écrit dans la réponse HTTP. Par défaut, l’URL racine / est https://localhost:5001/.
  • Si la méthode de requête n’est pas GET ou si l’URL racine n’est pas /, aucun routage ne correspond et un HTTP 404 est retourné.

Point de terminaison

La méthode MapGet est utilisée pour définir un point de terminaison. Un point de terminaison peut être :

  • Sélectionné, en correspondant à l’URL et à la méthode HTTP.
  • Exécuté, en exécutant le délégué.

Les points de terminaison qui peuvent être mis en correspondance et exécutés par l’application sont configurés dans UseEndpoints. Par exemple, MapGet, MapPost et des méthodes similaires connectent des délégués de requête au système de routage. Des méthodes supplémentaires peuvent être utilisées pour connecter les fonctionnalités d’infrastructure ASP.NET Core au système de routage :

L’exemple suivant montre le routage avec un modèle de routage plus sophistiqué :

app.UseEndpoints(endpoints =>
{
    endpoints.MapGet("/hello/{name:alpha}", async context =>
    {
        var name = context.Request.RouteValues["name"];
        await context.Response.WriteAsync($"Hello {name}!");
    });
});

La chaîne /hello/{name:alpha} est un modèle de routage. Il est utilisé pour configurer la mise en correspondance du point de terminaison. Dans ce cas, le modèle correspond à :

  • Un URL comme /hello/Ryan
  • Tout chemin d’URL qui commence par /hello/ suivi d’une séquence de caractères alphabétiques. :alpha applique une contrainte de routage qui fait correspondre uniquement les caractères alphabétiques. Les contraintes de routage sont expliquées plus loin dans cet article.

Deuxième segment du chemin d’URL, {name:alpha} :

Le système de routage des points de terminaison décrit dans ce document est nouveau à partir de ASP.NET Core 3.0. Toutefois, toutes les versions de ASP.NET Core prennent en charge le même ensemble de fonctionnalités de modèle de routage et de contraintes de routage.

L’exemple suivant montre le routage avec les contrôles d’intégrité et l’autorisation :

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 vous souhaitez voir les commentaires de code traduits dans une langue autre que l’anglais, dites-le nous dans cette discussion GitHub.

L’exemple précédent montre comment :

  • L’intergiciel d’autorisation peut être utilisé avec le routage.
  • Les points de terminaison peuvent être utilisés pour configurer le comportement d’autorisation.

L’appel MapHealthChecks ajoute un point de terminaison de contrôle d’intégrité. Le chaînage RequireAuthorization sur cet appel attache une stratégie d’autorisation au point de terminaison.

Appeler UseAuthentication et UseAuthorization ajoute l’intergiciel d’authentification et d’autorisation. Ces intergiciels sont placés entre UseRouting et UseEndpoints afin qu’ils puissent :

  • Voir le point de terminaison sélectionné par UseRouting.
  • Appliquez une stratégie d’autorisation avant que UseEndpoints les distribue au point de terminaison.

Métadonnées de point de terminaison

Dans l’exemple précédent, il existe deux points de terminaison, mais seul le point de terminaison de contrôle d’intégrité a une stratégie d’autorisation attachée. Si la demande correspond au point de terminaison de contrôle d’intégrité, /healthz, une vérification d’autorisation est effectuée. Cela montre que les points de terminaison peuvent avoir des données supplémentaires attachées. Ces données supplémentaires sont appelées métadonnées de point de terminaison :

  • Les métadonnées peuvent être traitées par un intergiciel prenant en charge le routage.
  • Les métadonnées peuvent être de n’importe quel type .NET.

Concepts de routage

Le système de routage s’appuie sur le pipeline d’intergiciels en ajoutant le concept de point de terminaison puissant. Les points de terminaison représentent des unités des fonctionnalités de l’application qui sont distinctes les unes des autres en termes de routage, d’autorisation et de n’importe quel nombre de systèmes ASP.NET Core.

Définition de point de terminaison ASP.NET Core

Un point de terminaison ASP.NET Core est :

Le code suivant montre comment récupérer et inspecter le point de terminaison correspondant à la requête actuelle :

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

Le point de terminaison, s’il est sélectionné, peut être récupéré à partir de HttpContext. Ses propriétés peuvent être inspectées. Les objets de point de terminaison sont immuables et ne peuvent pas être modifiés après la création. Le type de point de terminaison le plus courant est RouteEndpoint. RouteEndpoint inclut des informations qui lui permettent d’être sélectionné par le système de routage.

Dans le code précédent, app.Use configure un intergiciel in-line.

Le code suivant montre que, selon l’endroit où app.Use est appelé dans le pipeline, il se peut qu’il n’y ait pas de point de terminaison :

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

L’exemple précédent ajoute des instructions Console.WriteLine qui indiquent si un point de terminaison a été sélectionné ou non. Pour plus de clarté, l’exemple affecte un nom complet au point de terminaison / fourni.

L’exécution de ce code avec une URL / affiche :

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

L’exécution de ce code avec toute autre URL affiche :

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

Cette sortie montre que :

  • Le point de terminaison est toujours null avant que soit UseRouting appelé.
  • Si une correspondance est trouvée, le point de terminaison n’est pas null entre UseRouting et UseEndpoints.
  • L’intergiciel UseEndpoints est terminal lorsqu’une correspondance est trouvée. L’intergiciel terminal est défini plus loin dans cet article.
  • L’intergiciel après UseEndpoints s’exécute uniquement lorsqu’aucune correspondance n’est trouvée.

L’intergiciel UseRouting utilise la méthode SetEndpoint pour attacher le point de terminaison au contexte actuel. Il est possible de remplacer l’intergiciel UseRouting par une logique personnalisée et d’obtenir les avantages de l’utilisation de points de terminaison. Les points de terminaison sont une primitive de bas niveau comme l’intergiciel et ne sont pas couplés à l’implémentation du routage. La plupart des applications n’ont pas besoin de remplacer UseRouting par une logique personnalisée.

L’intergiciel UseEndpoints est conçu pour être utilisé en tandem avec l’intergiciel UseRouting. La logique principale pour exécuter un point de terminaison n’est pas compliquée. Utilisez GetEndpoint pour récupérer le point de terminaison, puis appelez sa propriété RequestDelegate.

Le code suivant montre comment l’intergiciel peut influencer ou réagir au routage :

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

L’exemple précédent illustre deux concepts importants :

  • L’intergiciel peut s’exécuter avant UseRouting pour modifier les données sur lesquelles le routage fonctionne.
  • L’intergiciel peut s’exécuter entre UseRouting et UseEndpoints pour traiter les résultats du routage avant l’exécution du point de terminaison.
    • Intergiciel qui s’exécute entre UseRouting et UseEndpoints :
      • Inspecte généralement les métadonnées pour comprendre les points de terminaison.
      • Prend souvent des décisions de sécurité, comme le font UseAuthorization et UseCors.
    • La combinaison d’intergiciels et de métadonnées permet de configurer des stratégies par point de terminaison.

Le code précédent montre un exemple d’intergiciel personnalisé qui prend en charge les stratégies par point de terminaison. L’intergiciel écrit un journal d’audit de l’accès aux données sensibles dans la console. L’intergiciel peut être configuré pour auditer un point de terminaison avec les métadonnées AuditPolicyAttribute. Cet exemple illustre un modèle d’activation dans lequel seuls les points de terminaison marqués comme sensibles sont audités. Il est possible de définir l’inverse de cette logique, en auditant tout ce qui n’est pas marqué comme sécurisé, par exemple. Le système de métadonnées de point de terminaison est flexible. Cette logique peut être conçue de quelque manière que ce soit en fonction du cas d’usage.

L’exemple de code précédent est destiné à illustrer les concepts de base des points de terminaison. L’exemple n’est pas destiné à une utilisation en production. Une version plus complète d’un intergiciel de journal d’audit :

  • Se connecterais à un fichier ou une base de données.
  • Inclurais des détails tels que l’utilisateur, l’adresse IP, le nom du point de terminaison sensible, etc.

Les métadonnées de stratégie d’audit AuditPolicyAttribute sont définies en tant que Attribute pour faciliter l’utilisation avec des infrastructures basées sur des classes telles que des contrôleurs et SignalR. Lors de l’utilisation de route vers le code :

  • Les métadonnées sont attachées à une API de générateur.
  • Les infrastructure basées sur des classes incluent tous les attributs sur la méthode et la classe correspondantes lors de la création de points de terminaison.

Les meilleures pratiques pour les types de métadonnées sont de les définir en tant qu’interfaces ou attributs. Les interfaces et les attributs autorisent la réutilisation du code. Le système de métadonnées est flexible et n’impose aucune limitation.

Comparaison d’un intergiciel terminal et d’un routage

L’exemple de code suivant contraste l’utilisation d’un intergiciel l’utilisation du routage :

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

Le style d’intergiciel indiqué avec Approach 1: est l’intergiciel terminal. Il est appelé intergiciel terminal, car il effectue une opération de correspondance :

  • L’opération de correspondance dans l’exemple précédent est Path == "/" pour l’intergiciel et Path == "/Movie" pour le routage.
  • Lorsqu’une correspondance réussit, elle exécute certaines fonctionnalités et retourne, plutôt que d’appeler l’intergiciel next.

Il est appelé intergiciel de terminal, car il met fin à la recherche, exécute certaines fonctionnalités, puis retourne.

Comparaison d’un intergiciel de terminal et d’un routage :

  • Les deux approches permettent de terminer le pipeline de traitement :
    • L’intergiciel met fin au pipeline en retournant plutôt qu’en appelant next.
    • Les points de terminaison sont toujours terminaux.
  • L’intergiciel terminal permet de positionner l’intergiciel à un emplacement arbitraire dans le pipeline :
    • Les points de terminaison s’exécutent à la position de UseEndpoints.
  • L’intergiciel de terminal permet au code arbitraire de déterminer quand l’intergiciel fait correspondre :
    • Le code de correspondance de routage personnalisé peut être détaillé et difficile à écrire correctement.
    • Le routage fournit des solutions simples pour les applications classiques. La plupart des applications ne nécessitent pas de code de correspondance de routage personnalisé.
  • L’interface des points de terminaison avec un intergiciel tel que UseAuthorization et UseCors.
    • L’utilisation d’un intergiciel terminal avec UseAuthorization ou UseCors nécessite une interaction manuelle avec le système d’autorisation.

Un point de terminaison définit les :

  • Le délégué pour traiter les demandes.
  • La collection de métadonnées arbitraires. Les métadonnées sont utilisées pour implémenter des problèmes transversaux basés sur des stratégies et une configuration attachées à chaque point de terminaison.

L’intergiciel terminal peut être un outil efficace, mais peut nécessiter :

  • Une quantité importante de codage et de test.
  • L’intégration manuelle avec d’autres systèmes pour atteindre le niveau de flexibilité souhaité.

Envisagez d’intégrer le routage avant d’écrire un intergiciel terminal.

Les intergiciels terminaux existants qui s’intègrent à Map ou MapWhen peuvent généralement être transformés en point de terminaison prenant en charge le routage. MapHealthChecks illustre le modèle de routeur-ware :

  • Écrire une méthode d’extension sur IEndpointRouteBuilder.
  • Créer un pipeline d’intergiciels imbriqués à l’aide de CreateApplicationBuilder.
  • Attacher l’intergiciel au nouveau pipeline. Dans ce cas, UseHealthChecks.
  • Build le pipeline d’intergiciel dans un RequestDelegate.
  • Appeler Map et fournir le nouveau pipeline d’intergiciels.
  • Retourner l’objet générateur fourni par Map à partir de la méthode d’extension.

Le code suivant montre l’utilisation 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!");
        });
    });
}

L’exemple précédent montre pourquoi le retour de l’objet générateur est important. Le renvoi de l’objet générateur permet au développeur d’applications de configurer des stratégies telles que l’autorisation pour le point de terminaison. Dans cet exemple, l’intergiciel de contrôle d’intégrité n’a pas d’intégration directe avec le système d’autorisation.

Le système de métadonnées a été créé en réponse aux problèmes rencontrés par les auteurs d’extensibilité à l’aide de l’intergiciel terminal. Il est problématique pour chaque intergiciel d’implémenter sa propre intégration avec le système d’autorisation.

Correspondance d’URL

  • La correspondance d’URL est le processus par lequel le routage distribue une requête entrante à un point de terminaison.
  • Est basé sur des données dans le chemin d’URL et les en-têtes.
  • Peut être étendu pour prendre en compte toutes les données de la demande.

Lorsqu’un intergiciel de routage s’exécute, il définit les valeurs de Endpoint et de routage et vers une fonctionnalité de requête sur HttpContext à partir de la requête actuelle :

  • L’appel de HttpContext.GetEndpoint obtient le point de terminaison.
  • HttpRequest.RouteValues récupère la collection de valeurs d’itinéraire.

L’intergiciel s’exécute après que l’intergiciel de routage puisse inspecter le point de terminaison et prendre des mesures. Par exemple, un intergiciel d’autorisation peut interroger la collection de métadonnées du point de terminaison pour une stratégie d’autorisation. Une fois que tous les intergiciels dans le pipeline de traitement de requêtes sont exécutés, le délégué du point de terminaison sélectionné est appelé.

Le système de routage dans le routage de point de terminaison est responsable de toutes les décisions de distribution. Étant donné que l’intergiciel applique des stratégies basées sur le point de terminaison sélectionné, il est important que :

  • Toute décision susceptible d’affecter la répartition ou l’application de stratégies de sécurité soit prise à l’intérieur du système de routage.

Avertissement

Pour une compatibilité descendante, lorsqu’un délégué de point de terminaison Contrôleur ou Razor Pages est exécuté, les propriétés de RouteContext.RouteData sont définies sur des valeurs appropriées en fonction du traitement des requêtes effectué jusqu’à présent.

Le type RouteContext sera marqué comme obsolète dans une version ultérieure :

  • Migrez RouteData.Values vers HttpRequest.RouteValues.
  • Migrez RouteData.DataTokens pour récupérer IDataTokensMetadata à partir des métadonnées de point de terminaison.

La correspondance d’URL fonctionne dans un ensemble configurable de phases. Dans chaque phase, la sortie est un ensemble de correspondances. L’ensemble de correspondances peut être réduit plus loin par la phase suivante. L’implémentation du routage ne garantit pas un ordre de traitement pour les points de terminaison correspondants. Toutes les correspondances possibles sont traitées simultanément. Les phases de correspondance d’URL se produisent dans l’ordre suivant. ASP.NET Core :

  1. Traite le chemin d’URL par rapport à l’ensemble de points de terminaison et à leurs modèles de routage, en collectant toutes les correspondances.
  2. Prend la liste précédente et supprime les correspondances qui échouent avec les contraintes de routage appliquées.
  3. Prend la liste précédente et supprime les correspondances qui échouent à l’ensemble d’instances MatcherPolicy .
  4. Utilise EndpointSelector pour prendre une décision finale dans la liste précédente.

La liste des points de terminaison est hiérarchisée en fonction des éléments suivants :

Tous les points de terminaison correspondants sont traités dans chaque phase jusqu’à ce que EndpointSelector soit atteint. EndpointSelector est la phase finale. Il choisit le point de terminaison avec la priorité la plus élevée parmi les correspondances comme correspondance optimale. S’il existe d’autres correspondances avec la même priorité que la meilleure correspondance, une exception de correspondance ambiguë est levée.

La priorité du routage est calculée en fonction d’un modèle de routage plus spécifique qui reçoit une priorité plus élevée. Par exemple, considérez les modèles /hello et /{message} :

  • Les deux correspondent au chemin d’URL /hello.
  • /hello est plus spécifique et, par conséquent, a une priorité plus élevée.

En général, la priorité des routages permet de choisir la meilleure correspondance pour les types de schémas d’URL utilisés dans la pratique. Utilisez Order uniquement si nécessaire pour éviter une ambiguïté.

En raison des types d’extensibilité fournis par le routage, il n’est pas possible que le système de routage calcule à l’avance les routages ambigus. Prenons un exemple tel que les modèles de routage /{message:alpha} et /{message:int} :

  • La contrainte alpha ne fait correspondre que les caractères alphabétiques.
  • La contrainte int ne fait correspondre que les nombres.
  • Ces modèles ont la même priorité de routage, mais il n’existe aucune URL à laquelle ils correspondent.
  • Si le système de routage a signalé une erreur d’ambiguïté au démarrage, il bloque ce cas d’usage valide.

Avertissement

L’ordre des opérations à l’intérieur de UseEndpoints n’influence pas le comportement du routage, à une exception près. MapControllerRoute et MapAreaRoute attribuent automatiquement une valeur de commande à leurs points de terminaison en fonction de l’ordre qu’ils appellent. Cela simule le comportement long des contrôleurs sans le système de routage fournissant les mêmes garanties que les implémentations de routage plus anciennes.

Dans l’implémentation héritée du routage, il est possible d’implémenter l’extensibilité du routage qui a une dépendance sur l’ordre dans lequel les itinéraires sont traités. Routage des points de terminaison dans ASP.NET Core 3.0 et versions ultérieures :

  • N’a pas de concept de routes.
  • Ne fournit pas de garanties de commande. Tous les points de terminaison sont traités simultanément.

Priorité du modèle de routage et ordre de sélection du point de terminaison

La priorité du modèle de routage est un système qui attribue à chaque modèle de routage une valeur en fonction de sa spécificité. La priorité du modèle de routage :

  • Évite la nécessité d’ajuster l’ordre des points de terminaison dans les cas courants.
  • Tente de faire correspondre les attentes courantes du comportement de routage.

Par exemple, envisagez des modèles /Products/List et /Products/{id}. Il serait raisonnable de supposer que /Products/List est une meilleure correspondance que /Products/{id} pour le chemin d’URL /Products/List. Cela fonctionne parce que le segment littéral /List est considéré comme ayant une meilleure priorité que le segment de paramètre /{id}.

Les détails du fonctionnement de la priorité sont couplés à la façon dont les modèles de routage sont définis :

  • Les modèles avec plus de segments sont considérés comme plus spécifiques.
  • Un segment avec du texte littéral est considéré comme plus spécifique qu’un segment de paramètre.
  • Un segment de paramètre avec une contrainte est considéré comme plus spécifique qu’un segment sans.
  • Un segment complexe est considéré aussi spécifique qu’un segment de paramètre avec une contrainte.
  • Les paramètres catch-all sont les moins spécifiques. Consultez catch-all dans la référence Modèles de routage pour obtenir des informations importantes sur les routages catch-all.

Consultez le code source sur GitHub pour obtenir une référence de valeurs exactes.

Concepts de génération d’URL

La génération des URL :

  • Est le processus par lequel le routage peut créer un chemin d’URL basé sur un ensemble de valeurs de route.
  • Permet une séparation logique entre les points de terminaison et les URL qui y accèdent.

Le routage des points de terminaison inclut l’API LinkGenerator. LinkGenerator est un service singleton disponible à partir de DI. L’API LinkGenerator peut être utilisée en dehors du contexte d’une requête en cours d’exécution. Mvc.IUrlHelper et les scénarios qui s’appuient sur IUrlHelper, comme l’Assistance des balises, l’assistance HTML et les résultats d’action, utilisent l’API LinkGenerator pour fournir les fonctionnalités de création de liens.

Le générateur de liens est basé sur le concept d’une adresse et de schémas d’adresse. Un schéma d’adresse est un moyen de déterminer les points de terminaison à prendre en compte pour la génération de liens. Par exemple, les scénarios de nom de route et de valeurs de route que de nombreux utilisateurs connaissent bien dans les contrôleurs et Razor Pages sont implémentés en tant que schémas d’adresse.

Le générateur de liens peut lier à des contrôleurs et Razor Pages via les méthodes d’extension suivantes :

Une surcharge de ces méthodes accepte des arguments qui incluent HttpContext. Ces méthodes sont fonctionnellement équivalentes à Url.Action et à Url.Page, mais elles offrent davantage de flexibilité et d’options.

Les méthodes GetPath* sont les plus similaires à Url.Action et Url.Page, car elles génèrent un URI contenant un chemin d’accès absolu. Les méthodes GetUri* génèrent toujours un URI absolu contenant un schéma et un hôte. Les méthodes qui acceptent un HttpContext génèrent un URI dans le contexte de la requête en cours d’exécution. Les valeurs de route ambiante, le chemin de base d’URL, le schéma et l’hôte de la requête en cours d’exécution sont utilisés, sauf s’ils sont remplacés.

LinkGenerator est appelé avec une adresse. La génération d’un URI se fait en deux étapes :

  1. Une adresse est liée à une liste de points de terminaison qui correspondent à l’adresse.
  2. Le RoutePattern de chaque point de terminaison est évalué jusqu’à ce qu’un modèle de route correspondant aux valeurs fournies soit trouvé. Le résultat obtenu est combiné avec d’autres parties de l’URI fournies par le générateur de liens, puis il est retourné.

Les méthodes fournies par LinkGenerator prennent en charge des fonctionnalités de génération de liens standard pour n’importe quel type d’adresse. La façon la plus pratique d’utiliser le générateur de liens est de le faire via des méthodes d’extension qui effectuent des opérations pour un type d’adresse spécifique :

Méthode d’extension Description
GetPathByAddress Génère un URI avec un chemin absolu basé sur les valeurs fournies.
GetUriByAddress Génère un URI absolu basé sur les valeurs fournies.

Avertissement

Faites attention aux implications suivantes de l’appel de méthodes LinkGenerator :

  • Utilisez les méthodes d’extension GetUri* avec précaution dans une configuration d’application qui ne valide pas l’en-tête Host des requêtes entrantes. Si l’en-tête Host des requêtes entrantes n’est pas validé, l’entrée de requête non approuvée peut être renvoyée au client dans les URI d’une page ou d’une vue. Nous recommandons que toutes les applications de production configurent leur serveur pour qu’il valide l’en-tête Host par rapport à des valeurs valides connues.

  • Utilisez LinkGenerator avec précaution dans le middleware en combinaison avec Map ou MapWhen. Map* modifie le chemin de base de la requête en cours d’exécution, ce qui affecte la sortie de la génération de liens. Toutes les API LinkGenerator permettent la spécification d’un chemin de base. Spécifiez un chemin de base vide pour annuler l’effet de Map* sur la génération de liens.

Exemple de middleware

Dans l’exemple suivant, un intergiciel utilise l’API LinkGenerator pour créer un lien vers une méthode d’action qui liste les produits d’un magasin. L’utilisation du générateur de liens en l’injectant dans une classe et en appelant GenerateLink est disponible pour n’importe quelle classe dans une application :

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

Informations de référence sur les modèles de routage

Les jetons dans {} définissent les paramètres de routage liés si le routage est mis en correspondance. Plusieurs paramètres de routage peuvent être définis dans un segment de routage, mais les paramètres de routage doivent être séparés par une valeur littérale. Par exemple {controller=Home}{action=Index} n’est pas une route valide, car il n’y a aucune valeur littérale entre {controller} et {action}. Les paramètres de routage doivent avoir un nom, et ils autorisent la spécification d’attributs supplémentaires.

Un texte littéral autre que les paramètres de routage (par exemple, {id}) et le séparateur de chemin / doit correspondre au texte présent dans l’URL. La correspondance de texte ne respecte pas la casse et est basée sur la représentation décodée du chemin des URL. Pour mettre en correspondance un délimiteur de paramètre de route littéral { ou }, placez-le dans une séquence d’échappement en répétant le caractère. Par exemple {{ ou }}.

Astérisque * ou astérisque double ** :

  • Peut être utilisé comme préfixe pour un paramètre de routage pour établir une liaison au rest de l’URI.
  • Ils sont appelés des paramètres catch-all. Par exemple, blog/{**slug} :
    • Correspond à n’importe quel URI qui commence par /blog et a n’importe quelle valeur qui suit.
    • La valeur suivant /blog est affectée à la valeur de routage slug.

Avertissement

Un paramètre catch-all peut faire correspondre les mauvais routages en raison d’un bogue dans le routage. Les applications affectées par ce bogue présentent les caractéristiques suivantes :

  • Un routage catch-all, par exemple, {**slug}"
  • Le routage catch-all ne fait pas correspondre les demandes qu’il doit faire correspondre.
  • La suppression d’autres routes fait que la route catch-all commence à fonctionner.

Consultez les bogues GitHub 18677 et 16579, par exemple les cas qui ont rencontré ce bogue.

Un correctif d’opt-in pour ce bogue est contenu dans le Kit de développement logiciel (SDK) .NET Core 3.1.301 et versions ultérieures. Le code suivant définit un commutateur interne qui corrige ce bogue :

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

Les paramètres fourre-tout peuvent également établir une correspondance avec la chaîne vide.

Le paramètre catch-all place les caractères appropriés dans une séquence d’échappement lorsque la route est utilisée pour générer une URL, y compris les caractères de séparation de chemin /. Par exemple, la route foo/{*path} avec les valeurs de route { path = "my/path" } génère foo/my%2Fpath. Notez la barre oblique d’échappement. Pour les séparateurs de chemin aller-retour, utilisez le préfixe de paramètre de routage **. La route foo/{**path} avec { path = "my/path" } génère foo/my/path.

Les modèles d’URL qui tentent de capturer un nom de fichier avec une extension de fichier facultative doivent faire l’objet de considérations supplémentaires. Prenez par exemple le modèle files/{filename}.{ext?}. Quand des valeurs existent à la fois pour filename et pour ext, les deux valeurs sont renseignées. Si seule une valeur existe pour filename dans l’URL, une correspondance est trouvée pour la route, car le . de fin est facultatif. Les URL suivantes correspondent à cette route :

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

Les paramètres de route peuvent avoir des valeurs par défaut, désignées en spécifiant la valeur par défaut après le nom du paramètre, séparée par un signe égal (=). Par exemple, {controller=Home} définit Home comme valeur par défaut de controller. La valeur par défaut est utilisée si aucune valeur n’est présente dans l’URL pour le paramètre. Vous pouvez rendre facultatifs les paramètres de route en ajoutant un point d’interrogation (?) à la fin du nom du paramètre. Par exemple, id? La différence entre les valeurs facultatives et les paramètres de routage par défaut est la suivante :

  • Un paramètre de routage avec une valeur par défaut produit toujours une valeur.
  • Un paramètre facultatif a une valeur uniquement lorsqu’une valeur est fournie par l’URL de la requête.

Les paramètres de route peuvent avoir des contraintes, qui doivent correspondre à la valeur de route liée à partir de l’URL. L’ajout de : et d’un nom de contrainte après le nom du paramètre de routage spécifie une contrainte inline sur un paramètre de routage. Si la contrainte exige des arguments, ils sont fournis entre parenthèses (...) après le nom de la contrainte. Il est possible de spécifier plusieurs contraintes inline en ajoutant un autre : et le nom d’une autre contrainte.

Le nom de la contrainte et les arguments sont passés au service IInlineConstraintResolver pour créer une instance de IRouteConstraint à utiliser dans le traitement des URL. Par exemple, le modèle de routage blog/{article:minlength(10)} spécifie une contrainte minlength avec l’argument 10. Pour plus d’informations sur les contraintes de route et pour obtenir la liste des contraintes fournies par le framework, consultez la section Informations de référence sur les contraintes de route.

Les paramètres de route peuvent également avoir des transformateurs de paramètres. Les transformateurs de paramètres transforment la valeur d’un paramètre lors de la génération de liens et d’actions et de pages correspondantes en URL. À l’instar des contraintes, les transformateurs de paramètre peuvent être ajoutés inline à un paramètre de routage en ajoutant un : et le nom du transformateur après le nom du paramètre de routage. Par exemple, le modèle de routage blog/{article:slugify} spécifie un transformateur slugify. Pour plus d’informations sur les transformateurs de paramètre, consultez la section Informations de référence sur les transformateurs de paramètre.

Le tableau suivant montre des exemples de modèles de route et leur comportement.

Modèle de routage Exemple d’URI en correspondance URI de requête
hello /hello Correspond seulement au chemin unique /hello.
{Page=Home} / Correspond à Page et le définit sur Home.
{Page=Home} /Contact Correspond à Page et le définit sur Contact.
{controller}/{action}/{id?} /Products/List Mappe au contrôleur Products et à l’action List.
{controller}/{action}/{id?} /Products/Details/123 Mappe au contrôleur Products et à l’action Details avec id défini sur 123).
{controller=Home}/{action=Index}/{id?} / Mappe au contrôleur Home et à l’action Index. id est ignoré.
{controller=Home}/{action=Index}/{id?} /Products Mappe au contrôleur Products et à la méthode Index. id est ignoré.

L’utilisation d’un modèle est généralement l’approche la plus simple pour le routage. Il est également possible de spécifier des contraintes et des valeurs par défaut hors du modèle de routage.

Segments complexes

Les segments complexes sont traités en faisant correspondre les délimiteurs littéraux de droite à gauche de manière non gourmande. Par exemple, [Route("/a{b}c{d}")] est un segment complexe. Les segments complexes fonctionnent d’une manière particulière qui doit être comprise pour les utiliser correctement. L’exemple de cette section montre pourquoi les segments complexes ne fonctionnent vraiment bien que lorsque le texte du délimiteur n’apparaît pas dans les valeurs des paramètres. L’utilisation d’un regex, puis l’extraction manuelle des valeurs est nécessaire pour des cas plus complexes.

Avertissement

Lorsque vous utilisez System.Text.RegularExpressions pour traiter une entrée non approuvée, passez un délai d’expiration. Un utilisateur malveillant peut fournir une entrée à RegularExpressions, provoquant une attaque par déni de service. Les API d’infrastructure ASP.NET Core qui utilisent RegularExpressions passent un délai d’expiration.

Il s’agit d’un résumé des étapes effectuées par le routage avec le modèle /a{b}c{d} et le chemin d’URL /abcd. Un | est utilisé pour vous aider à visualiser le fonctionnement de l’algorithme :

  • Le premier littéral, de droite à gauche, est c. Donc /abcd est recherché à partir de la droite et trouve /ab|c|d.
  • Tout ce qui se trouve à droite (d) est désormais mis en correspondance avec le paramètre de routage {d}.
  • Le littéral suivant, de droite à gauche, est a. Donc /ab|c|d est recherché à partir de là où nous sommes partis, puis a est trouvé /|a|b|c|d.
  • La valeur à droite (b) est désormais associée au paramètre de routage {b}.
  • Il n’y a pas de texte restant et aucun modèle de routage restant. Il s’agit donc d’une correspondance.

Voici un exemple de cas négatif utilisant le même modèle /a{b}c{d} et le chemin d’URL /aabcd. Un | est utilisé pour vous aider à visualiser le fonctionnement de l’algorithme. Ce cas n’est pas une correspondance, qui est expliquée par le même algorithme :

  • Le premier littéral, de droite à gauche, est c. Donc /aabcd est recherché à partir de la droite et trouve /aab|c|d.
  • Tout ce qui se trouve à droite (d) est désormais mis en correspondance avec le paramètre de routage {d}.
  • Le littéral suivant, de droite à gauche, est a. Donc /aab|c|d est recherché à partir de là où nous sommes partis, puis a est trouvé /a|a|b|c|d.
  • La valeur à droite (b) est désormais associée au paramètre de routage {b}.
  • À ce stade, il reste du texte a, mais l’algorithme n’a plus de modèle de routage à analyser. Il ne s’agit donc pas d’une correspondance.

Étant donné que l’algorithme correspondant n’est pas gourmand :

  • Il correspond à la plus petite quantité de texte possible dans chaque étape.
  • Si la valeur de délimiteur apparaît à l’intérieur des valeurs de paramètre, elle ne correspond pas.

Les expressions régulières fournissent beaucoup plus de contrôle sur leur comportement de correspondance.

La correspondance gourmande, également appelée correspondance paresseuse, correspond à la plus grande chaîne possible. La chaîne non gourmande correspond à la plus petite chaîne possible.

Informations de référence sur les contraintes de routage

Les contraintes de route s’exécutent quand une correspondance s’est produite pour l’URL entrante, et le chemin de l’URL est tokenisé en valeurs de route. En général, les contraintes de routage inspectent la valeur de route associée par le biais du modèle de routage, et créent une décision true ou false indiquant si la valeur est acceptable. Certaines contraintes de routage utilisent des données hors de la valeur de route pour déterminer si la requête peut être routée. Par exemple, HttpMethodRouteConstraint peut accepter ou rejeter une requête en fonction de son verbe HTTP. Les contraintes sont utilisées dans le routage des requêtes et la génération des liens.

Avertissement

N’utilisez pas de contraintes pour la validation des entrées. Si des contraintes sont utilisées pour la validation d’entrée, une entrée non valide génère une réponse introuvable 404. Une entrée non valide doit produire une demande incorrecte 400 avec un message d’erreur approprié. Les contraintes de route sont utilisées pour lever l’ambiguïté entre des routes similaires, et non pas pour valider les entrées d’une route particulière.

Le tableau suivant montre des exemples de contrainte de route et leur comportement attendu :

contrainte Exemple Exemples de correspondances Notes
int {id:int} 123456789, -123456789 Correspond à n’importe quel entier
bool {active:bool} true, FALSE Correspond à true ou false. Non-respect de la casse
datetime {dob:datetime} 2016-12-31, 2016-12-31 7:32pm Correspond à une valeur valide DateTime dans la culture invariante. Voir l’avertissement précédent.
decimal {price:decimal} 49.99, -1,000.01 Correspond à une valeur valide decimal dans la culture invariante. Voir l’avertissement précédent.
double {weight:double} 1.234, -1,001.01e8 Correspond à une valeur valide double dans la culture invariante. Voir l’avertissement précédent.
float {weight:float} 1.234, -1,001.01e8 Correspond à une valeur valide float dans la culture invariante. Voir l’avertissement précédent.
guid {id:guid} CD2C1638-1638-72D5-1638-DEADBEEF1638 Correspond à une valeur Guid valide
long {ticks:long} 123456789, -123456789 Correspond à une valeur long valide
minlength(value) {username:minlength(4)} Rick La chaîne doit comporter au moins 4 caractères
maxlength(value) {filename:maxlength(8)} MyFile La chaîne ne doit pas comporter plus de 8 caractères
length(length) {filename:length(12)} somefile.txt La chaîne doit comporter exactement 12 caractères
length(min,max) {filename:length(8,16)} somefile.txt La chaîne doit comporter au moins 8 caractères et pas plus de 16 caractères
min(value) {age:min(18)} 19 La valeur entière doit être au moins égale à 18
max(value) {age:max(120)} 91 La valeur entière ne doit pas être supérieure à 120
range(min,max) {age:range(18,120)} 91 La valeur entière doit être au moins égale à 18 mais ne doit pas être supérieure à 120
alpha {name:alpha} Rick La chaîne doit se composer d’un ou de plusieurs caractères alphabétiques (a-z, non-respect de la casse).
regex(expression) {ssn:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)} 123-45-6789 La chaîne doit correspondre à l’expression régulière. Consultez des conseils sur la définition d’une expression régulière.
required {name:required} Rick Utilisé pour garantir qu’une valeur autre qu’un paramètre est présente pendant la génération de l’URL

Avertissement

Lorsque vous utilisez System.Text.RegularExpressions pour traiter une entrée non approuvée, passez un délai d’expiration. Un utilisateur malveillant peut fournir une entrée à RegularExpressions, provoquant une attaque par déni de service. Les API d’infrastructure ASP.NET Core qui utilisent RegularExpressions passent un délai d’expiration.

Il est possible d’appliquer plusieurs contraintes séparées par un point-virgule à un même paramètre. Par exemple, la contrainte suivante limite un paramètre à une valeur entière supérieure ou égale à 1 :

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

Avertissement

Les contraintes de routage qui vérifient que l’URL peut être convertie en type CLR utilisent toujours la culture invariant. Par exemple, conversion en type CLR int ou DateTime. Ces contraintes partent du principe que l’URL ne peut pas être localisé. Les contraintes de routage fournies par le framework ne modifient pas les valeurs stockées dans les valeurs de route. Toutes les valeurs de route analysées à partir de l’URL sont stockées sous forme de chaînes. Par exemple, la contrainte float tente de convertir la valeur de route en valeur float, mais la valeur convertie est utilisée uniquement pour vérifier qu’elle peut être convertie en valeur float.

Expressions régulières dans les contraintes

Avertissement

Lorsque vous utilisez System.Text.RegularExpressions pour traiter une entrée non approuvée, passez un délai d’expiration. Un utilisateur malveillant peut fournir une entrée à RegularExpressions, provoquant une attaque par déni de service. Les API d’infrastructure ASP.NET Core qui utilisent RegularExpressions passent un délai d’expiration.

Les expressions régulières peuvent être spécifiées en tant que contraintes inline à l’aide de la contrainte de routage regex(...). Les méthodes de la famille MapControllerRoute acceptent également un littéral d’objet de contraintes. Si ce formulaire est utilisé, les valeurs de chaîne sont interprétées comme des expressions régulières.

Le code suivant utilise une contrainte d’expression régulière inline :

app.UseEndpoints(endpoints =>
{
    endpoints.MapGet("{message:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)}",
        context => 
        {
            return context.Response.WriteAsync("inline-constraint match");
        });
 });

Le code suivant utilise un littéral d’objet pour spécifier une contrainte d’expression régulière :

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

Le framework ASP.NET Core ajoute RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant au constructeur d’expression régulière. Pour obtenir une description de ces membres, consultez RegexOptions.

Les expressions régulières utilisent les délimiteurs et des jetons semblables à ceux utilisés par le service de routage et le langage C#. Les jetons d’expression régulière doivent être placés dans une séquence d’échappement. Pour utiliser l’expression régulière ^\d{3}-\d{2}-\d{4}$ dans une contrainte inline, utilisez l’une des options suivantes :

  • Remplacez les caractères \ fournis dans la chaîne en tant que caractères \\ dans le fichier source C# afin d’échapper au caractère \ d’échappement de chaîne.
  • Littéraux de chaîne verbatim.

Pour placer en échappement les caractères de délimiteur de paramètre de route {, }, [, ], doublez les caractères dans l’expression, par exemple {{, }}, [[, ]]. Le tableau suivant montre une expression régulière et la version placée en échappement :

Expression régulière Expression régulière en échappement
^\d{3}-\d{2}-\d{4}$ ^\\d{{3}}-\\d{{2}}-\\d{{4}}$
^[a-z]{2}$ ^[[a-z]]{{2}}$

Les expressions régulières utilisées dans le routage commencent souvent par le caractère ^ et correspondent à la position de début de la chaîne. Les expressions se terminent souvent par le caractère $ et correspondent à la fin de la chaîne. Les caractères ^ et $ garantissent que l’expression régulière établit une correspondance avec la totalité de la valeur du paramètre de route. Sans les caractères ^ et $, l’expression régulière peut correspondre à n’importe quelle sous-chaîne dans la chaîne, ce qui est souvent indésirable. Le tableau suivant contient des exemples et explique pourquoi ils établissent ou non une correspondance :

Expression String Correspond Commentaire
[a-z]{2} hello Oui Correspondances de sous-chaînes
[a-z]{2} 123abc456 Oui Correspondances de sous-chaînes
[a-z]{2} mz Oui Correspondance avec l’expression
[a-z]{2} MZ Oui Non-respect de la casse
^[a-z]{2}$ hello Non Voir ^ et $ ci-dessus
^[a-z]{2}$ 123abc456 Non Voir ^ et $ ci-dessus

Pour plus d’informations sur la syntaxe des expressions régulières, consultez Expressions régulières du .NET Framework.

Pour contraindre un paramètre à un ensemble connu de valeurs possibles, utilisez une expression régulière. Par exemple, {action:regex(^(list|get|create)$)} établit une correspondance avec la valeur de route action uniquement pour list, get ou create. Si elle est passée dans le dictionnaire de contraintes, la chaîne ^(list|get|create)$ est équivalente. Les contraintes passées dans le dictionnaire de contraintes qui ne correspondent pas à l’une des contraintes connues sont également traitées comme des expressions régulières. Les contraintes passées dans un modèle qui ne correspondent pas à l’une des contraintes connues ne sont pas traitées comme des expressions régulières.

Contraintes de routage personnalisées

Les contraintes de routage personnalisées peuvent être créées en implémentant l’interface IRouteConstraint. L’interface IRouteConstraint contient une méthode unique, Match, qui retourne true si la contrainte est satisfaite et false dans le cas contraire.

Les contraintes de routage personnalisées sont rarement nécessaires. Avant d’implémenter une contrainte de routage personnalisée, envisagez des alternatives, telles que la liaison de modèle.

Le dossier ASP.NET Core Contraintes fournit de bons exemples de création de contraintes. Par exemple, GuidRouteConstraint.

Pour utiliser un IRouteConstraint personnalisé, le type de contrainte de routage doit être inscrit avec le ConstraintMap de l’application dans le conteneur de service de l’application. Un ConstraintMap est un dictionnaire qui mappe les clés de contrainte d’itinéraire aux implémentations IRouteConstraint qui valident ces contraintes. Le ConstraintMap d’une application peut être mis à jour dans Startup.ConfigureServices dans le cadre d’un appel services.AddRouting ou en configurant RouteOptions directement avec services.Configure<RouteOptions>. Par exemple :

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();

    services.AddRouting(options =>
    {
        options.ConstraintMap.Add("customName", typeof(MyCustomConstraint));
    });
}

La contrainte précédente est appliquée dans le code suivant :

[Route("api/[controller]")]
[ApiController]
public class TestController : ControllerBase
{
    // GET /api/test/3
    [HttpGet("{id:customName}")]
    public IActionResult Get(string id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }

    // GET /api/test/my/3
    [HttpGet("my/{id:customName}")]
    public IActionResult Get(int id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
}

MyDisplayRouteInfo est fourni par le package NuGet Rick.Docs.Samples.RouteInfo et affiche les informations de routage.

L’implémentation de MyCustomConstraint empêche l’utilisation de 0 dans un paramètre de routage :

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

Avertissement

Lorsque vous utilisez System.Text.RegularExpressions pour traiter une entrée non approuvée, passez un délai d’expiration. Un utilisateur malveillant peut fournir une entrée à RegularExpressions, provoquant une attaque par déni de service. Les API d’infrastructure ASP.NET Core qui utilisent RegularExpressions passent un délai d’expiration.

Le code précédent :

  • Empêche 0 dans le segment {id} de la route.
  • S’affiche pour fournir un exemple de base d’implémentation d’une contrainte personnalisée. Il ne doit pas être utilisé dans une application de production.

Le code suivant est une meilleure approche pour empêcher un id contenant un 0 d’être traité :

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

    return ControllerContext.MyDisplayRouteInfo(id);
}

Le code précédent présente les avantages suivants sur l’approche MyCustomConstraint :

  • Il ne nécessite pas de contrainte personnalisée.
  • Il retourne une erreur plus descriptive lorsque le paramètre de routage inclut 0.

Informations de référence sur le transformateur de paramètre

Transformateurs de paramètre :

Par exemple, un transformateur de paramètre slugify personnalisé dans le modèle d’itinéraire blog\{article:slugify} avec Url.Action(new { article = "MyTestArticle" }) génère blog\my-test-article.

Examinez l’implémentation suivante 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();
    }
}

Pour utiliser un transformateur de paramètre dans un modèle d’itinéraire, configurez-le d’abord en utilisant ConstraintMap dans Startup.ConfigureServices :

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();

    services.AddRouting(options =>
    {
        options.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer);
    });
}

L’infrastructure ASP.NET Core utilise des transformateurs de paramètres pour transformer l’URI où un point de terminaison est résolu. Par exemple, les transformateurs de paramètres transforment les valeurs de routage utilisées pour faire correspondre un area, controller, actionet page.

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

Avec le modèle de routage précédent, l’action SubscriptionManagementController.GetAll est mise en correspondance avec l’URI /subscription-management/get-all. Un transformateur de paramètre ne modifie pas les valeurs de routage utilisées pour générer un lien. Par exemple, Url.Action("GetAll", "SubscriptionManagement") produit /subscription-management/get-all.

ASP.NET Core fournit des conventions d’API pour l’utilisation des transformateurs de paramètre avec des routages générés :

Informations de référence sur la génération d’URL

Cette section contient une référence pour l’algorithme implémenté par génération d’URL. Dans la pratique, les exemples les plus complexes de génération d’URL utilisent des contrôleurs ou Razor Pages. Pour plus d’informations, consultez Routage dans les contrôleurs.

Le processus de génération d’URL commence par un appel à LinkGenerator.GetPathByAddress ou une méthode similaire. La méthode est fournie avec une adresse, un ensemble de valeurs de routage et éventuellement des informations sur la requête actuelle de HttpContext.

La première étape consiste à utiliser l’adresse pour résoudre un ensemble de points de terminaison candidats à l’aide d’un IEndpointAddressScheme<TAddress> correspondant au type de l’adresse.

Une fois que l’ensemble de candidats est trouvé par le schéma d’adresses, les points de terminaison sont classés et traités de manière itérative jusqu’à ce qu’une opération de génération d’URL réussisse. La génération d’URL ne vérifie pas les ambiguïtés, le premier résultat retourné est le résultat final.

Résolution des problèmes de génération d’URL avec la journalisation

La première étape de la résolution des problèmes de génération d’URL consiste à définir le niveau de journalisation de Microsoft.AspNetCore.Routing sur TRACE. LinkGenerator enregistre de nombreux détails sur son traitement, ce qui peut être utile pour résoudre les problèmes.

Consultez Référence de génération d’URL pour plus d’informations sur la génération d’URL.

Adresses

Les adresses sont le concept de génération d’URL utilisé pour lier un appel au générateur de liens à un ensemble de points de terminaison candidats.

Les adresses sont un concept extensible qui comprend deux implémentations par défaut :

  • Utilisation du nom du point de terminaison (string) comme adresse :
    • Fournit des fonctionnalités similaires au nom du routage de MVC.
    • Utilise le type de métadonnées IEndpointNameMetadata.
    • Résout la chaîne fournie par rapport aux métadonnées de tous les points de terminaison inscrits.
    • Lève une exception au démarrage si plusieurs points de terminaison utilisent le même nom.
    • Recommandé pour une utilisation à usage général en dehors des contrôleurs et de Razor Pages.
  • Utilisation des valeurs de route (RouteValuesAddress) comme adresse :
    • Fournit des fonctionnalités similaires à la génération d’URL hérité des contrôleurs et de Razor Pages.
    • Très complexe à étendre et à déboguer.
    • Fournit l’implémentation utilisée par IUrlHelper, l’assistance des balises, l’assistance HTML , Résultats d’action, etc.

Le rôle du schéma d’adresses consiste à faire l’association entre l’adresse et les points de terminaison correspondants selon des critères arbitraires :

  • Le schéma de noms de point de terminaison effectue une recherche de dictionnaire de base.
  • Le schéma de valeurs de route a un sous-ensemble complexe de l’algorithme défini.

Valeurs ambiantes et valeurs explicites

À partir de la requête actuelle, le routage accède aux valeurs de routage de la requête HttpContext.Request.RouteValuesactuelle. Les valeurs associées à la requête actuelle sont appelées valeurs ambiantes. À des fins de clarté, la documentation fait référence aux valeurs de routage transmises aux méthodes en tant que valeurs explicites.

L’exemple suivant montre les valeurs ambiantes et les valeurs explicites. Il fournit des valeurs ambiantes à partir de la requête actuelle et des valeurs explicites : { 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);
    }

Le code précédent :

Le code suivant ne fournit aucune valeur ambiante et valeurs explicites : { controller = "Home", action = "Subscribe", id = 17, } :

public IActionResult Index2()
{
    var url = _linkGenerator.GetPathByAction("Subscribe", "Home",
                                             new { id = 17, });
    return Content(url);
}

La méthode précédente retourne /Home/Subscribe/17

Le code suivant dans le WidgetController retourne /Widget/Subscribe/17 :

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

Le code suivant fournit le contrôleur à partir de valeurs ambiantes dans la requête actuelle et les valeurs explicites : { action = "Edit", id = 17, } :

public class GadgetController : Controller
{
    public IActionResult Index()
    {
        var url = Url.Action("Edit", new { id = 17, });
        return Content(url);
    }

Dans le code précédent :

  • /Gadget/Edit/17 est retourné.
  • Url obtient IUrlHelper.
  • Action génère une URL avec un chemin absolu pour une méthode d’action. L’URL contient le nom de action spécifié et les valeurs route.

Le code suivant fournit des valeurs ambiantes à partir de la requête actuelle et des valeurs explicites : { page = "./Edit, id = 17, } :

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

Le code précédent définit url sur /Edit/17 lorsque la page Razor Modifier contient la directive de page suivante :

@page "{id:int}"

Si la page Modifier ne contient pas le modèle de route "{id:int}", url est /Edit?id=17.

Le comportement de l'IUrlHelper de MVC ajoute une couche de complexité en plus des règles décrites ici :

  • IUrlHelper fournit toujours les valeurs de routage de la requête actuelle en tant que valeurs ambiantes.
  • IUrlHelper.Action copie toujours les valeurs actuelles action et controller de routage en tant que valeurs explicites, sauf substitution par le développeur.
  • IUrlHelper.Page copie toujours la valeur de routage actuelle page en tant que valeur explicite, sauf si elle est remplacée.
  • IUrlHelper.Page remplace toujours la valeur de route handler actuelle par null comme valeurs explicites, sauf substitution.

Les utilisateurs sont souvent surpris par les détails comportementaux des valeurs ambiantes, car MVC ne semble pas suivre ses propres règles. Pour des raisons historiques et de compatibilité, certaines valeurs de routage telles que action, controller, page et handler ont leur propre comportement de cas spécial.

La fonctionnalité équivalente fournie par LinkGenerator.GetPathByAction et LinkGenerator.GetPathByPage duplique ces anomalies de IUrlHelper pour la compatibilité.

Processus de génération d’URL

Une fois l’ensemble de points de terminaison candidats trouvés, l’algorithme de génération d’URL :

  • Traite les points de terminaison de manière itérative.
  • Retourne le premier résultat réussi.

La première étape de ce processus est appelée invalidation des valeurs de routage. L’invalidation des valeurs de routage est le processus par lequel le routage détermine les valeurs de routage des valeurs ambiantes à utiliser et qui doivent être ignorées. Chaque valeur ambiante est considérée et combinée aux valeurs explicites ou ignorée.

La meilleure façon de penser au rôle des valeurs ambiantes est qu’elles tentent d’enregistrer la saisie par les développeurs d’applications, dans certains cas courants. Traditionnellement, les scénarios où les valeurs ambiantes sont utiles sont liés à MVC :

  • Lors de la liaison à une autre action dans le même contrôleur, le nom du contrôleur n’a pas besoin d’être spécifié.
  • Lors de la liaison à un autre contrôleur dans la même zone, le nom de la zone n’a pas besoin d’être spécifié.
  • Lors de la liaison à la même méthode d’action, les valeurs de routage n’ont pas besoin d’être spécifiées.
  • Lors de la liaison à une autre partie de l’application, vous ne souhaitez pas transporter les valeurs de routage qui n’ont aucune signification dans cette partie de l’application.

Les appels à ou LinkGenerator qui retournent IUrlHelper sont généralement dus à null une non-compréhension de l’invalidation de la valeur de route. Résolvez les problèmes d’invalidation des valeurs de routage en spécifiant explicitement davantage de valeurs de routage pour voir si cela résout le problème.

L’invalidation de la valeur de routage repose sur l’hypothèse que le schéma d’URL de l’application est hiérarchique, avec une hiérarchie formée de gauche à droite. Considérez le modèle de route de contrôleur de base {controller}/{action}/{id?} pour avoir un sens intuitif de la façon dont cela fonctionne dans la pratique. Une modification apportée à une valeur invalide toutes les valeurs de routage qui apparaissent à droite. Cela reflète l’hypothèse sur la hiérarchie. Si l’application a une valeur ambiante pour id, et que l’opération spécifie une valeur différente pour controller :

  • id ne sera pas réutilisée, car {controller} est à gauche de {id?}.

Voici quelques exemples illustrant ce principe :

  • Si les valeurs explicites contiennent une valeur pour id, la valeur ambiante de id est ignorée. Les valeurs ambiantes de controller et action peuvent être utilisées.
  • Si les valeurs explicites contiennent une valeur pour action, toute valeur ambiante de action est ignorée. Les valeurs ambiantes de controller peuvent être utilisées. Si la valeur explicite de action est différente de la valeur ambiante de action, la valeur id ne sera pas utilisée. Si la valeur explicite de action est identique à la valeur ambiante de action, la valeur id peut être utilisée.
  • Si les valeurs explicites contiennent une valeur de controller, toute valeur ambiante de controller est ignorée. Si la valeur explicite de controller est différente de la valeur ambiante de controller, les valeurs action et id ne seront pas utilisées. Si la valeur explicite de controller est identique à la valeur ambiante de controller, les valeurs action et id peuvent être utilisées.

Ce processus est encore plus compliqué à cause de l’existence de routes d’attributs et de routes conventionnelles dédiées. Les routes conventionnelles de contrôleur tels que {controller}/{action}/{id?} spécifient une hiérarchie à l’aide de paramètres de routage. Pour les routages conventionnels dédiés et les routes d’attribut aux contrôleurs et à Razor Pages :

  • Il existe une hiérarchie de valeurs de routage.
  • Elles n’apparaissent pas dans le modèle.

Dans ce cas, la génération d’URL définit le concept de valeurs requises. Les points de terminaison créés par les contrôleurs et Razor Pages ont des valeurs requises spécifiées qui autorisent l’invalidation de la valeur de routage à fonctionner.

Algorithme d’invalidation de valeur de routage en détail :

  • Les noms de valeurs requis sont combinés avec les paramètres de routage, puis traités de gauche à droite.
  • Pour chaque paramètre, la valeur ambiante et la valeur explicite sont comparées :
    • Si la valeur ambiante et la valeur explicite sont identiques, le processus continue.
    • Si la valeur ambiante est présente et que la valeur explicite ne l’est pas, la valeur ambiante est utilisée lors de la génération de l’URL.
    • Si la valeur ambiante n’est pas présente et que la valeur explicite l’est, rejetez la valeur ambiante et toutes les valeurs ambiantes suivantes.
    • Si la valeur ambiante et la valeur explicite sont présentes et que les deux valeurs sont différentes, rejetez la valeur ambiante et toutes les valeurs ambiantes suivantes.

À ce stade, l’opération de génération d’URL est prête à évaluer les contraintes de routage. L’ensemble de valeurs acceptées est combiné aux valeurs par défaut des paramètres, qui sont fournies aux contraintes. Si les contraintes passent toutes, l’opération se poursuit.

Ensuite, les valeurs acceptées peuvent être utilisées pour développer le modèle de routage. Le modèle de routage est traité :

  • De gauche à droite.
  • Chaque paramètre a sa valeur acceptée remplacée.
  • Avec les cas spéciaux suivants :
    • S’il manque une valeur aux valeurs acceptées et que le paramètre a une valeur par défaut, la valeur par défaut est utilisée.
    • S’il manque une valeur aux valeurs acceptées et que le paramètre est facultatif, le traitement se poursuit.
    • Si un paramètre de routage à droite d’un paramètre facultatif manquant a une valeur, l’opération échoue.
    • Les paramètres par défaut contigus et les paramètres facultatifs sont réduits si possible.

Les valeurs fournies explicitement mais qui n’ont pas de correspondance avec un segment de la route sont ajoutées à la chaîne de requête. Le tableau suivant présente le résultat en cas d’utilisation du modèle de routage {controller}/{action}/{id?}.

Valeurs ambiantes Valeurs explicites Résultat
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

Problèmes liés à l’invalidation des valeurs de routage

Depuis ASP.NET Core 3.0, certains schémas de génération d’URL utilisés dans les versions antérieures ASP.NET Core ne fonctionnent pas correctement avec la génération d’URL. L’équipe ASP.NET Core prévoit d’ajouter des fonctionnalités pour répondre à ces besoins dans une prochaine version. Pour l’instant, la meilleure solution consiste à utiliser le routage hérité.

Le code suivant montre un exemple de schéma de génération d’URL qui n’est pas pris en charge par le routage.

app.UseEndpoints(endpoints =>
{
    endpoints.MapControllerRoute("default", 
                                     "{culture}/{controller=Home}/{action=Index}/{id?}");
    endpoints.MapControllerRoute("blog", "{culture}/{**slug}", 
                                      new { controller = "Blog", action = "ReadPost", });
});

Dans le code précédent, le paramètre de routage culture est utilisé pour la localisation. On veut que le paramètre culture soit toujours accepté comme valeur ambiante. Toutefois, le paramètre culture n’est pas accepté comme valeur ambiante en raison de la façon dont les valeurs requises fonctionnent :

  • Dans le modèle de routage "default", le paramètre de routage culture est à gauche de controller. Les modifications apportées à controller n’invalident donc pas culture.
  • Dans le modèle de routage "blog", le paramètre de routage culture est considéré comme à droite de controller, qui apparaît dans les valeurs requises.

Configuration des métadonnées de point de terminaison

Les liens suivants fournissent des informations sur la configuration des métadonnées de point de terminaison :

Correspondance de l’hôte dans les routages avec RequireHost

RequireHost applique une contrainte au routage qui nécessite l’hôte spécifié. Le paramètre RequireHost ou [Host] peut être un :

  • Hôte : www.domain.com, fait correspondre www.domain.com à n’importe quel port.
  • Hôte avec caractère générique : *.domain.com, fait correspondre www.domain.com, subdomain.domain.comou www.subdomain.domain.com sur n’importe quel port.
  • Port : *:5000, fait correspondre le port 5000 avec n’importe quel hôte.
  • Hôte et port : www.domain.com:5000 ou *.domain.com:5000, fait correspondre l’hôte et le port.

Plusieurs paramètres peuvent être spécifiés à l’aide RequireHost ou [Host]. La contrainte fait correspondre les hôtes valides pour l’un des paramètres. Par exemples, [Host("domain.com", "*.domain.com")] fait correspondre domain.com, www.domain.com et subdomain.domain.com.

Le code suivant utilise RequireHost pour exiger l’hôte spécifié sur le routage :

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

Le code suivant utilise l’attribut [Host] sur le contrôleur pour exiger l’un des hôtes spécifiés :

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

Lorsque l’attribut [Host] est appliqué à la fois au contrôleur et à la méthode d’action :

  • L’attribut de l’action est utilisé.
  • L’attribut du contrôleur est ignoré.

Conseils sur les performances pour le routage

La plupart du routage a été mis à jour dans ASP.NET Core 3.0 pour augmenter les performances.

Lorsqu’une application rencontre des problèmes de performances, le routage est souvent soupçonné comme étant le problème. La raison pour laquelle le routage est soupçonné est que les infrastructures telles que les contrôleurs et Razor Pages signalent le temps passé à l’intérieur de l’infrastructure dans leurs messages de journalisation. En cas de différence significative entre le temps signalé par les contrôleurs et le temps total de la requête :

  • Les développeurs éliminent leur code d’application comme source du problème.
  • Il est courant de supposer que le routage est la cause.

Les performances du routage est testé à l’aide de milliers de points de terminaison. Il est peu probable qu’une application classique rencontre un problème de performances simplement en étant trop volumineuse. La cause racine la plus courante des performances de routage lentes est généralement un intergiciel personnalisé qui se comporte mal.

Cet exemple de code suivant illustre une technique de base pour affiner la source de délai :

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

Pour le routage temporel :

  • Entrelacez chaque intergiciel avec une copie de l’intergiciel de minutage indiqué dans le code précédent.
  • Ajoutez un identificateur unique pour mettre en corrélation les données de minutage avec le code.

Il s’agit d’un moyen de base de limiter le délai lorsqu’il est significatif, par exemple, plus que 10ms. Soustraire Time 2 de Time 1 signale le temps passé à l’intérieur de l’intergiciel UseRouting.

Le code suivant utilise une approche plus compacte du code de minutage précédent :

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

Fonctionnalités de routage potentiellement coûteuses

La liste suivante fournit un aperçu des fonctionnalités de routage relativement coûteuses par rapport aux modèles de routage de base :

  • Expressions régulières : il est possible d’écrire des expressions régulières qui sont complexes ou qui ont un temps d’exécution long avec une petite quantité d’entrée.
  • Segments complexes ({x}-{y}-{z}) :
    • Sont beaucoup plus coûteux que l’analyse d’un segment de chemin d’URL standard.
    • Entraînent l’allocation d’un grand nombre de sous-chaînes.
    • La logique de segment complexe n’a pas été mise à jour dans la mise à jour des performances de routage ASP.NET Core 3.0.
  • Accès aux données synchrones : de nombreuses applications complexes disposent d’un accès à la base de données dans le cadre de leur routage. Le routage ASP.NET Core 2.2 et versions antérieures peuvent ne pas fournir les points d’extensibilité appropriés pour prendre en charge le routage de l’accès à la base de données. Par exemple, IRouteConstraint et IActionConstraint sont synchrones. Les points d’extensibilité tels que MatcherPolicy et EndpointSelectorContext sont asynchrones.

Conseils pour les auteurs de bibliothèques

Cette section contient des conseils pour les auteurs de bibliothèques qui s’appuient sur le routage. Ces détails sont destinés à garantir que les développeurs d’applications ont une bonne expérience à l’aide de bibliothèques et d’infrastructures qui étendent le routage.

Définir des points de terminaison

Pour créer une infrastructure qui utilise le routage pour la correspondance d’URL, commencez par définir une expérience utilisateur qui s’appuie sur UseEndpoints.

GÉNÉREZ sur IEndpointRouteBuilder. Cela permet aux utilisateurs de composer votre infrastructure avec d’autres fonctionnalités ASP.NET Core sans confusion. Chaque modèle ASP.NET Core inclut le routage. Supposons que le routage est présent et familier pour les utilisateurs.

app.UseEndpoints(endpoints =>
{
    // Your framework
    endpoints.MapMyFramework(...);

    endpoints.MapHealthChecks("/healthz");
});

RETOURNEZ un type concret scellé à partir d’un appel à MapMyFramework(...) qui implémente IEndpointConventionBuilder. La plupart des méthodes d’infrastructure Map... suivent ce modèle. L'interface IEndpointConventionBuilder :

  • Permet la composition des métadonnées.
  • Est ciblée par diverses méthodes d’extension.

La déclaration de votre propre type vous permet d’ajouter vos propres fonctionnalités spécifiques à l’infrastructure au générateur. Vous pouvez encapsuler un générateur déclaré par l’infrastructure et lui transférer les appels.

app.UseEndpoints(endpoints =>
{
    // Your framework
    endpoints.MapMyFramework(...).RequireAuthorization()
                                 .WithMyFrameworkFeature(awesome: true);

    endpoints.MapHealthChecks("/healthz");
});

ENVISAGEZ d’écrire votre propre EndpointDataSource. EndpointDataSource est la primitive de bas niveau permettant de déclarer et de mettre à jour une collection de points de terminaison. EndpointDataSource est une API puissante utilisée par les contrôleurs et Razor Pages.

Les tests de routage ont un exemple de base d’une source de données sans mise à jour.

NE TENTEZ PAS d’inscrire un EndpointDataSource par défaut. Demandez aux utilisateurs d’inscrire votre infrastructure dans UseEndpoints. La philosophie du routage est que rien n’est inclus par défaut et que UseEndpoints est l’endroit où inscrire des points de terminaison.

Création d’un intergiciel intégré au routage

ENVISAGEZ de définir des types de métadonnées en tant qu’interface.

FAITES EN SORTE qu’il soit possible d’utiliser des types de métadonnées en tant qu’attribut sur des classes et des méthodes.

public interface ICoolMetadata
{
    bool IsCool { get; }
}

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

Les frameworks tels que les contrôleurs et Razor Pages prennent en charge l’application d’attributs de métadonnées aux types et méthodes. Si vous déclarez des types de métadonnées :

  • Rendez-les accessibles en tant qu’attributs.
  • La plupart des utilisateurs sont familiarisés avec l’application d’attributs.

La déclaration d’un type de métadonnées en tant qu’interface ajoute une autre couche de flexibilité :

  • Les interfaces sont composables.
  • Les développeurs peuvent déclarer leurs propres types qui combinent plusieurs stratégies.

FAITES EN SORTE qu’il soit possible de remplacer les métadonnées, comme illustré dans l’exemple suivant :

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 meilleure façon de suivre ces instructions consiste à éviter de définir des métadonnées de marqueur :

  • Ne recherchez pas simplement la présence d’un type de métadonnées.
  • Définissez une propriété sur les métadonnées et vérifiez la propriété.

La collection de métadonnées est triée et prend en charge la substitution par priorité. Dans le cas des contrôleurs, les métadonnées sur la méthode d’action sont les plus spécifiques.

FAITES EN SORTE que l’intergiciel soit utile avec et sans routage.

app.UseRouting();

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

app.UseEndpoints(endpoints =>
{
    // Your framework
    endpoints.MapMyFramework(...).RequireAuthorization();
});

À titre d’exemple de cette recommandation, considérez l’intergiciel UseAuthorization. L’intergiciel d’autorisation vous permet de passer une stratégie de secours. La stratégie de secours, si elle est spécifiée, s’applique aux :

  • Points de terminaison sans stratégie spécifiée.
  • Requêtes qui ne correspondent pas à un point de terminaison.

Cela rend l’intergiciel d’autorisation utile en dehors du contexte du routage. L’intergiciel d’autorisation peut être utilisé pour la programmation d’intergiciels traditionnels.

Déboguer les diagnostics

Pour obtenir une sortie de diagnostic de routage détaillée, définissez Logging:LogLevel:Microsoft sur Debug. Dans l’environnement de développement, définissez le niveau de journal dans appsettings.Development.json :

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