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 :
- Contrôleurs
- Razor Pages
- SignalR
- Services gRPC
- Intergiciels avec point de terminaison, tels que les vérifications d’intégrité.
- Délégués et lambdas inscrits avec le routage.
Cet article décrit les détails de bas niveau du routage ASP.NET Core. Pour plus d’informations sur la configuration du routage :
- Pour les contrôleurs, consultez Routage vers les actions du contrôleur dans ASP.NET Core.
- Pour les conventions Razor Pages, consultez les conventions de routage et d’application Razor Pages dans ASP.NET Core.
- Pour obtenir des instructions sur le routage Blazor, en complément ou en remplacement des instructions de cet article, consultez Routage et navigation Blazor ASP.NET Core.
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 :
- MapRazorPages pour Razor Pages
- MapControllers pour les contrôleurs
- MapHub<THub> pour SignalR
- MapGrpcService<TService> pour gRPC
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}
:
- Est lié au paramètre
name
. - Est capturé et stocké dans HttpRequest.RouteValues.
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 :
- Exécutable : a un RequestDelegate.
- Extensible : possède une collection de métadonnées.
- Sélectionnable : peut contenir des informations de routage.
- Énumérable : la collection de points de terminaison peut être répertoriée en récupérant EndpointDataSource à partir de DI.
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.- Généralement, l’intergiciel qui apparaît avant le routage modifie une propriété de la demande, telle que UseRewriter, UseHttpMethodOverrideou UsePathBase.
- 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
etUseEndpoints
:- 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
etUseCors
.
- La combinaison d’intergiciels et de métadonnées permet de configurer des stratégies par point de terminaison.
- Intergiciel qui s’exécute entre
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 etPath == "/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 met fin au pipeline en retournant plutôt qu’en appelant
- 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
etUseCors
.- L’utilisation d’un intergiciel terminal avec
UseAuthorization
ouUseCors
nécessite une interaction manuelle avec le système d’autorisation.
- L’utilisation d’un intergiciel terminal avec
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
versHttpRequest.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 :
- Traite le chemin d’URL par rapport à l’ensemble de points de terminaison et à leurs modèles de routage, en collectant toutes les correspondances.
- Prend la liste précédente et supprime les correspondances qui échouent avec les contraintes de routage appliquées.
- Prend la liste précédente et supprime les correspondances qui échouent au jeu d’instances MatcherPolicy.
- 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 :
- Une adresse est liée à une liste de points de terminaison qui correspondent à l’adresse.
- 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êteHost
des requêtes entrantes. Si l’en-têteHost
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êteHost
par rapport à des valeurs valides connues.Utilisez LinkGenerator avec précaution dans le middleware en combinaison avec
Map
ouMapWhen
.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 deMap*
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.
- Correspond à n’importe quel URI qui commence par
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, puisa
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, puisa
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 :
- Sont exécutés lors de la génération d’un lien à l’aide de LinkGenerator.
- Implémentez Microsoft.AspNetCore.Routing.IOutboundParameterTransformer.
- Sont configurés à l’aide de ConstraintMap.
- Prennent la valeur de routage du paramètre et la convertissent en une nouvelle valeur de chaîne.
- Aboutissent à l’utilisation de la valeur transformée dans le lien généré.
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
, action
et 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 :
- La convention Microsoft.AspNetCore.Mvc.ApplicationModels.RouteTokenTransformerConvention MVC applique un transformateur de paramètres spécifié à tous les routages d’attributs de l’application. Le transformateur de paramètre transforme les jetons de routage d’attribut quand ils sont remplacés. Pour plus d’informations, consultez Utiliser un transformateur de paramètre pour personnaliser le remplacement des jetons.
- Razor Pages utilise la convention d’API PageRouteTransformerConvention. Cette convention applique un transformateur de paramètre spécifié à toutes les pages Razor découvertes automatiquement. Le transformateur de paramètre transforme les segments du nom de dossier et du nom de fichier des routes Razor Pages. Pour plus d’informations, consultez Utiliser un transformateur de paramètre pour personnaliser les routages de pages.
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.RouteValues
actuelle. 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 :
- Retourne
/Widget/Index/17
. - Obtient LinkGenerator via DI.
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 valeursroute
.
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
etcontroller
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 routehandler
actuelle parnull
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 deid
est ignorée. Les valeurs ambiantes decontroller
etaction
peuvent être utilisées. - Si les valeurs explicites contiennent une valeur pour
action
, toute valeur ambiante deaction
est ignorée. Les valeurs ambiantes decontroller
peuvent être utilisées. Si la valeur explicite deaction
est différente de la valeur ambiante deaction
, la valeurid
ne sera pas utilisée. Si la valeur explicite deaction
est identique à la valeur ambiante deaction
, la valeurid
peut être utilisée. - Si les valeurs explicites contiennent une valeur de
controller
, toute valeur ambiante decontroller
est ignorée. Si la valeur explicite decontroller
est différente de la valeur ambiante decontroller
, les valeursaction
etid
ne seront pas utilisées. Si la valeur explicite decontroller
est identique à la valeur ambiante decontroller
, les valeursaction
etid
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 routageculture
est à gauche decontroller
. Les modifications apportées àcontroller
n’invalident donc pasculture
. - Dans le modèle de routage
"blog"
, le paramètre de routageculture
est considéré comme à droite decontroller
, 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 :
- Activer Cors avec le routage des points de terminaison
- Exemple IAuthorizationPolicyProvider à l’aide d’un attribut personnalisé
[MinimumAgeAuthorize]
- Tester l’authentification avec l’attribut [Authorize]
- RequireAuthorization
- Sélection du schéma avec l’attribut [Authorize]
- Appliquer des stratégies à l’aide de l’attribut [Authorize]
- Autorisation basée sur les rôles dans ASP.NET Core
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 correspondrewww.domain.com
à n’importe quel port. - Hôte avec caractère générique :
*.domain.com
, fait correspondrewww.domain.com
,subdomain.domain.com
ouwww.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 :
- Utilisez HttpContext.Connection (ConnectionInfo.LocalPort) où les ports sont vérifiés.
- Utilisez le filtrage d’hôtes.
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 QueryPrivateTodos
fabrique 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
.
- L’application est lente à démarrer sur la première requête.
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
etMapDynamicPageRoute
.
- Pour ce faire, vous pouvez utiliser
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 :
- Contrôleurs
- Razor Pages
- SignalR
- Services gRPC
- Intergiciels avec point de terminaison, tels que les vérifications d’intégrité.
- Délégués et lambdas inscrits avec le routage.
Cet article décrit les détails de bas niveau du routage ASP.NET Core. Pour plus d’informations sur la configuration du routage :
- Pour les contrôleurs, consultez Routage vers les actions du contrôleur dans ASP.NET Core.
- Pour les conventions Razor Pages, consultez les conventions de routage et d’application Razor Pages dans ASP.NET Core.
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 :
- MapRazorPages pour Razor Pages
- MapControllers pour les contrôleurs
- MapHub<THub> pour SignalR
- MapGrpcService<TService> pour gRPC
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}
:
- Est lié au paramètre
name
. - Est capturé et stocké dans HttpRequest.RouteValues.
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 :
- Exécutable : a un RequestDelegate.
- Extensible : possède une collection de métadonnées.
- Sélectionnable : peut contenir des informations de routage.
- Énumérable : la collection de points de terminaison peut être répertoriée en récupérant EndpointDataSource à partir de DI.
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.- Généralement, l’intergiciel qui apparaît avant le routage modifie une propriété de la demande, telle que UseRewriter, UseHttpMethodOverrideou UsePathBase.
- 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
etUseEndpoints
:- 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
etUseCors
.
- La combinaison d’intergiciels et de métadonnées permet de configurer des stratégies par point de terminaison.
- Intergiciel qui s’exécute entre
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 etPath == "/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 met fin au pipeline en retournant plutôt qu’en appelant
- 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
etUseCors
.- L’utilisation d’un intergiciel terminal avec
UseAuthorization
ouUseCors
nécessite une interaction manuelle avec le système d’autorisation.
- L’utilisation d’un intergiciel terminal avec
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
versHttpRequest.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 :
- Traite le chemin d’URL par rapport à l’ensemble de points de terminaison et à leurs modèles de routage, en collectant toutes les correspondances.
- Prend la liste précédente et supprime les correspondances qui échouent avec les contraintes de routage appliquées.
- Prend la liste précédente et supprime les correspondances qui échouent au jeu d’instances MatcherPolicy.
- 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 :
- Une adresse est liée à une liste de points de terminaison qui correspondent à l’adresse.
- 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êteHost
des requêtes entrantes. Si l’en-têteHost
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êteHost
par rapport à des valeurs valides connues.Utilisez LinkGenerator avec précaution dans le middleware en combinaison avec
Map
ouMapWhen
.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 deMap*
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.
- Correspond à n’importe quel URI qui commence par
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, puisa
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, puisa
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 :
- Sont exécutés lors de la génération d’un lien à l’aide de LinkGenerator.
- Implémentez Microsoft.AspNetCore.Routing.IOutboundParameterTransformer.
- Sont configurés à l’aide de ConstraintMap.
- Prennent la valeur de routage du paramètre et la convertissent en une nouvelle valeur de chaîne.
- Aboutissent à l’utilisation de la valeur transformée dans le lien généré.
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
, action
et 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 :
- La convention Microsoft.AspNetCore.Mvc.ApplicationModels.RouteTokenTransformerConvention MVC applique un transformateur de paramètres spécifié à tous les routages d’attributs de l’application. Le transformateur de paramètre transforme les jetons de routage d’attribut quand ils sont remplacés. Pour plus d’informations, consultez Utiliser un transformateur de paramètre pour personnaliser le remplacement des jetons.
- Razor Pages utilise la convention d’API PageRouteTransformerConvention. Cette convention applique un transformateur de paramètre spécifié à toutes les pages Razor découvertes automatiquement. Le transformateur de paramètre transforme les segments du nom de dossier et du nom de fichier des routes Razor Pages. Pour plus d’informations, consultez Utiliser un transformateur de paramètre pour personnaliser les routages de pages.
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.RouteValues
actuelle. 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 :
- Retourne
/Widget/Index/17
. - Obtient LinkGenerator via DI.
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 valeursroute
.
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
etcontroller
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 routehandler
actuelle parnull
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 deid
est ignorée. Les valeurs ambiantes decontroller
etaction
peuvent être utilisées. - Si les valeurs explicites contiennent une valeur pour
action
, toute valeur ambiante deaction
est ignorée. Les valeurs ambiantes decontroller
peuvent être utilisées. Si la valeur explicite deaction
est différente de la valeur ambiante deaction
, la valeurid
ne sera pas utilisée. Si la valeur explicite deaction
est identique à la valeur ambiante deaction
, la valeurid
peut être utilisée. - Si les valeurs explicites contiennent une valeur de
controller
, toute valeur ambiante decontroller
est ignorée. Si la valeur explicite decontroller
est différente de la valeur ambiante decontroller
, les valeursaction
etid
ne seront pas utilisées. Si la valeur explicite decontroller
est identique à la valeur ambiante decontroller
, les valeursaction
etid
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 routageculture
est à gauche decontroller
. Les modifications apportées àcontroller
n’invalident donc pasculture
. - Dans le modèle de routage
"blog"
, le paramètre de routageculture
est considéré comme à droite decontroller
, 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 :
- Activer Cors avec le routage des points de terminaison
- Exemple IAuthorizationPolicyProvider à l’aide d’un attribut personnalisé
[MinimumAgeAuthorize]
- Tester l’authentification avec l’attribut [Authorize]
- RequireAuthorization
- Sélection du schéma avec l’attribut [Authorize]
- Appliquer des stratégies à l’aide de l’attribut [Authorize]
- Autorisation basée sur les rôles dans ASP.NET Core
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 correspondrewww.domain.com
à n’importe quel port. - Hôte avec caractère générique :
*.domain.com
, fait correspondrewww.domain.com
,subdomain.domain.com
ouwww.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 QueryPrivateTodos
fabrique 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
.
- L’application est lente à démarrer sur la première requête.
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
etMapDynamicPageRoute
.
- Pour ce faire, vous pouvez utiliser
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 :
- Contrôleurs
- Razor Pages
- SignalR
- Services gRPC
- Intergiciels avec point de terminaison, tels que les vérifications d’intégrité.
- Délégués et lambdas inscrits avec le routage.
Cet article décrit les détails de bas niveau du routage ASP.NET Core. Pour plus d’informations sur la configuration du routage :
- Pour les contrôleurs, consultez Routage vers les actions du contrôleur dans ASP.NET Core.
- Pour les conventions Razor Pages, consultez les conventions de routage et d’application Razor Pages dans ASP.NET Core.
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 :
- MapRazorPages pour Razor Pages
- MapControllers pour les contrôleurs
- MapHub<THub> pour SignalR
- MapGrpcService<TService> pour gRPC
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}
:
- Est lié au paramètre
name
. - Est capturé et stocké dans HttpRequest.RouteValues.
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 :
- Exécutable : a un RequestDelegate.
- Extensible : possède une collection de métadonnées.
- Sélectionnable : peut contenir des informations de routage.
- Énumérable : la collection de points de terminaison peut être répertoriée en récupérant EndpointDataSource à partir de DI.
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.- Généralement, l’intergiciel qui apparaît avant le routage modifie une propriété de la demande, telle que UseRewriter, UseHttpMethodOverrideou UsePathBase.
- 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
etUseEndpoints
:- 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
etUseCors
.
- La combinaison d’intergiciels et de métadonnées permet de configurer des stratégies par point de terminaison.
- Intergiciel qui s’exécute entre
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 etPath == "/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 met fin au pipeline en retournant plutôt qu’en appelant
- 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
etUseCors
.- L’utilisation d’un intergiciel terminal avec
UseAuthorization
ouUseCors
nécessite une interaction manuelle avec le système d’autorisation.
- L’utilisation d’un intergiciel terminal avec
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
versHttpRequest.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 :
- Traite le chemin d’URL par rapport à l’ensemble de points de terminaison et à leurs modèles de routage, en collectant toutes les correspondances.
- Prend la liste précédente et supprime les correspondances qui échouent avec les contraintes de routage appliquées.
- Prend la liste précédente et supprime les correspondances qui échouent au jeu d’instances MatcherPolicy.
- 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 :
- Une adresse est liée à une liste de points de terminaison qui correspondent à l’adresse.
- 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êteHost
des requêtes entrantes. Si l’en-têteHost
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êteHost
par rapport à des valeurs valides connues.Utilisez LinkGenerator avec précaution dans le middleware en combinaison avec
Map
ouMapWhen
.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 deMap*
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.
- Correspond à n’importe quel URI qui commence par
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, puisa
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, puisa
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 :
- Sont exécutés lors de la génération d’un lien à l’aide de LinkGenerator.
- Implémentez Microsoft.AspNetCore.Routing.IOutboundParameterTransformer.
- Sont configurés à l’aide de ConstraintMap.
- Prennent la valeur de routage du paramètre et la convertissent en une nouvelle valeur de chaîne.
- Aboutissent à l’utilisation de la valeur transformée dans le lien généré.
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
, action
et 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 :
- La convention Microsoft.AspNetCore.Mvc.ApplicationModels.RouteTokenTransformerConvention MVC applique un transformateur de paramètres spécifié à tous les routages d’attributs de l’application. Le transformateur de paramètre transforme les jetons de routage d’attribut quand ils sont remplacés. Pour plus d’informations, consultez Utiliser un transformateur de paramètre pour personnaliser le remplacement des jetons.
- Razor Pages utilise la convention d’API PageRouteTransformerConvention. Cette convention applique un transformateur de paramètre spécifié à toutes les pages Razor découvertes automatiquement. Le transformateur de paramètre transforme les segments du nom de dossier et du nom de fichier des routes Razor Pages. Pour plus d’informations, consultez Utiliser un transformateur de paramètre pour personnaliser les routages de pages.
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.RouteValues
actuelle. 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 :
- Retourne
/Widget/Index/17
. - Obtient LinkGenerator via DI.
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 valeursroute
.
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
etcontroller
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 routehandler
actuelle parnull
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 deid
est ignorée. Les valeurs ambiantes decontroller
etaction
peuvent être utilisées. - Si les valeurs explicites contiennent une valeur pour
action
, toute valeur ambiante deaction
est ignorée. Les valeurs ambiantes decontroller
peuvent être utilisées. Si la valeur explicite deaction
est différente de la valeur ambiante deaction
, la valeurid
ne sera pas utilisée. Si la valeur explicite deaction
est identique à la valeur ambiante deaction
, la valeurid
peut être utilisée. - Si les valeurs explicites contiennent une valeur de
controller
, toute valeur ambiante decontroller
est ignorée. Si la valeur explicite decontroller
est différente de la valeur ambiante decontroller
, les valeursaction
etid
ne seront pas utilisées. Si la valeur explicite decontroller
est identique à la valeur ambiante decontroller
, les valeursaction
etid
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 routageculture
est à gauche decontroller
. Les modifications apportées àcontroller
n’invalident donc pasculture
. - Dans le modèle de routage
"blog"
, le paramètre de routageculture
est considéré comme à droite decontroller
, 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 :
- Activer Cors avec le routage des points de terminaison
- Exemple IAuthorizationPolicyProvider à l’aide d’un attribut personnalisé
[MinimumAgeAuthorize]
- Tester l’authentification avec l’attribut [Authorize]
- RequireAuthorization
- Sélection du schéma avec l’attribut [Authorize]
- Appliquer des stratégies à l’aide de l’attribut [Authorize]
- Autorisation basée sur les rôles dans ASP.NET Core
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 correspondrewww.domain.com
à n’importe quel port. - Hôte avec caractère générique :
*.domain.com
, fait correspondrewww.domain.com
,subdomain.domain.com
ouwww.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
.
- L’application est lente à démarrer sur la première requête.
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
etMapDynamicPageRoute
.
- Pour ce faire, vous pouvez utiliser
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 :
- Contrôleurs
- Razor Pages
- SignalR
- Services gRPC
- Intergiciels avec point de terminaison, tels que les vérifications d’intégrité.
- Délégués et lambdas inscrits avec le routage.
Ce document traite du routage ASP.NET Core de bas niveau. Pour plus d’informations sur la configuration du routage :
- Pour les contrôleurs, consultez Routage vers les actions du contrôleur dans ASP.NET Core.
- Pour les conventions Razor Pages, consultez les conventions de routage et d’application Razor Pages dans ASP.NET Core.
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 :
- Sélecteur de version pour une version précédente.
- Sélectionnez le routage ASP.NET Core 2.1.
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/
esthttps://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 :
- MapRazorPages pour Razor Pages
- MapControllers pour les contrôleurs
- MapHub<THub> pour SignalR
- MapGrpcService<TService> pour gRPC
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}
:
- Est lié au paramètre
name
. - Est capturé et stocké dans HttpRequest.RouteValues.
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 :
- Exécutable : a un RequestDelegate.
- Extensible : possède une collection de métadonnées.
- Sélectionnable : peut contenir des informations de routage.
- Énumérable : la collection de points de terminaison peut être répertoriée en récupérant EndpointDataSource à partir de DI.
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.- Généralement, l’intergiciel qui apparaît avant le routage modifie une propriété de la demande, telle que UseRewriter, UseHttpMethodOverrideou UsePathBase.
- 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
etUseEndpoints
:- 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
etUseCors
.
- La combinaison d’intergiciels et de métadonnées permet de configurer des stratégies par point de terminaison.
- Intergiciel qui s’exécute entre
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 etPath == "/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 met fin au pipeline en retournant plutôt qu’en appelant
- 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
etUseCors
.- L’utilisation d’un intergiciel terminal avec
UseAuthorization
ouUseCors
nécessite une interaction manuelle avec le système d’autorisation.
- L’utilisation d’un intergiciel terminal avec
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
versHttpRequest.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 :
- Traite le chemin d’URL par rapport à l’ensemble de points de terminaison et à leurs modèles de routage, en collectant toutes les correspondances.
- Prend la liste précédente et supprime les correspondances qui échouent avec les contraintes de routage appliquées.
- Prend la liste précédente et supprime les correspondances qui échouent à l’ensemble d’instances MatcherPolicy .
- 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 :
- Une adresse est liée à une liste de points de terminaison qui correspondent à l’adresse.
- 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êteHost
des requêtes entrantes. Si l’en-têteHost
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êteHost
par rapport à des valeurs valides connues.Utilisez LinkGenerator avec précaution dans le middleware en combinaison avec
Map
ouMapWhen
.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 deMap*
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.
- Correspond à n’importe quel URI qui commence par
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, puisa
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, puisa
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 :
- Sont exécutés lors de la génération d’un lien à l’aide de LinkGenerator.
- Implémentez Microsoft.AspNetCore.Routing.IOutboundParameterTransformer.
- Sont configurés à l’aide de ConstraintMap.
- Prennent la valeur de routage du paramètre et la convertissent en une nouvelle valeur de chaîne.
- Aboutissent à l’utilisation de la valeur transformée dans le lien généré.
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
, action
et 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 :
- La convention Microsoft.AspNetCore.Mvc.ApplicationModels.RouteTokenTransformerConvention MVC applique un transformateur de paramètres spécifié à tous les routages d’attributs de l’application. Le transformateur de paramètre transforme les jetons de routage d’attribut quand ils sont remplacés. Pour plus d’informations, consultez Utiliser un transformateur de paramètre pour personnaliser le remplacement des jetons.
- Razor Pages utilise la convention d’API PageRouteTransformerConvention. Cette convention applique un transformateur de paramètre spécifié à toutes les pages Razor découvertes automatiquement. Le transformateur de paramètre transforme les segments du nom de dossier et du nom de fichier des routes Razor Pages. Pour plus d’informations, consultez Utiliser un transformateur de paramètre pour personnaliser les routages de pages.
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.RouteValues
actuelle. 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 :
- Retourne
/Widget/Index/17
. - Obtient LinkGenerator via DI.
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 valeursroute
.
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
etcontroller
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 routehandler
actuelle parnull
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 deid
est ignorée. Les valeurs ambiantes decontroller
etaction
peuvent être utilisées. - Si les valeurs explicites contiennent une valeur pour
action
, toute valeur ambiante deaction
est ignorée. Les valeurs ambiantes decontroller
peuvent être utilisées. Si la valeur explicite deaction
est différente de la valeur ambiante deaction
, la valeurid
ne sera pas utilisée. Si la valeur explicite deaction
est identique à la valeur ambiante deaction
, la valeurid
peut être utilisée. - Si les valeurs explicites contiennent une valeur de
controller
, toute valeur ambiante decontroller
est ignorée. Si la valeur explicite decontroller
est différente de la valeur ambiante decontroller
, les valeursaction
etid
ne seront pas utilisées. Si la valeur explicite decontroller
est identique à la valeur ambiante decontroller
, les valeursaction
etid
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 routageculture
est à gauche decontroller
. Les modifications apportées àcontroller
n’invalident donc pasculture
. - Dans le modèle de routage
"blog"
, le paramètre de routageculture
est considéré comme à droite decontroller
, 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 :
- Activer Cors avec le routage des points de terminaison
- Exemple IAuthorizationPolicyProvider à l’aide d’un attribut personnalisé
[MinimumAgeAuthorize]
- Tester l’authentification avec l’attribut [Authorize]
- RequireAuthorization
- Sélection du schéma avec l’attribut [Authorize]
- Appliquer des stratégies à l’aide de l’attribut [Authorize]
- Autorisation basée sur les rôles dans ASP.NET Core
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 correspondrewww.domain.com
à n’importe quel port. - Hôte avec caractère générique :
*.domain.com
, fait correspondrewww.domain.com
,subdomain.domain.com
ouwww.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"
}
}
}