Routage et sélection d’actions dans l’API web ASP.NET

Cet article décrit comment API Web ASP.NET achemine une requête HTTP vers une action particulière sur un contrôleur.

Notes

Pour obtenir une vue d’ensemble du routage, consultez Routage dans API Web ASP.NET.

Cet article examine les détails du processus de routage. Si vous créez un projet d’API web et constatez que certaines demandes ne sont pas acheminées comme prévu, nous espérons que cet article vous aidera.

Le routage comporte trois phases main :

  1. Mise en correspondance de l’URI à un modèle d’itinéraire.
  2. Sélection d’un contrôleur.
  3. Sélection d’une action.

Vous pouvez remplacer certaines parties du processus par vos propres comportements personnalisés. Dans cet article, je décrit le comportement par défaut. À la fin, je note les endroits où vous pouvez personnaliser le comportement.

Modèles de routage

Un modèle d’itinéraire ressemble à un chemin d’URI, mais il peut avoir des valeurs d’espace réservé, indiquées avec des accolades :

"api/{controller}/public/{category}/{id}"

Lorsque vous créez un itinéraire, vous pouvez fournir des valeurs par défaut pour tout ou partie des espaces réservés :

defaults: new { category = "all" }

Vous pouvez également fournir des contraintes, qui limitent la façon dont un segment d’URI peut correspondre à un espace réservé :

constraints: new { id = @"\d+" }   // Only matches if "id" is one or more digits.

L’infrastructure tente de faire correspondre les segments dans le chemin d’uri du modèle. Les littéraux du modèle doivent correspondre exactement. Un espace réservé correspond à n’importe quelle valeur, sauf si vous spécifiez des contraintes. L’infrastructure ne correspond pas à d’autres parties de l’URI, telles que le nom d’hôte ou les paramètres de requête. L’infrastructure sélectionne la première route de la table de routage qui correspond à l’URI.

Il existe deux espaces réservés spéciaux : « {controller} » et « {action} ».

  • « {controller} » indique le nom du contrôleur.
  • « {action} » indique le nom de l’action. Dans l’API web, la convention habituelle consiste à omettre « {action} ».

Valeurs par défaut

Si vous fournissez des valeurs par défaut, l’itinéraire correspond à un URI qui manque ces segments. Par exemple :

routes.MapHttpRoute(
    name: "DefaultApi",
    routeTemplate: "api/{controller}/{category}",
    defaults: new { category = "all" }
);

http://localhost/api/products/all URI et http://localhost/api/products correspondent à l’itinéraire précédent. Dans ce dernier URI, la valeur allpar défaut est attribuée au segment manquant{category}.

Dictionnaire de routes

Si l’infrastructure trouve une correspondance pour un URI, elle crée un dictionnaire qui contient la valeur de chaque espace réservé. Les clés sont les noms d’espace réservé, sans inclure les accolades. Les valeurs sont extraites du chemin d’accès de l’URI ou des valeurs par défaut. Le dictionnaire est stocké dans l’objet IHttpRouteData .

Pendant cette phase de correspondance d’itinéraire, les espaces réservés spéciaux « {controller} » et « {action} » sont traités comme les autres espaces réservés. Ils sont simplement stockés dans le dictionnaire avec les autres valeurs.

Une valeur par défaut peut avoir la valeur spéciale RouteParameter.Optional. Si cette valeur est attribuée à un espace réservé, la valeur n’est pas ajoutée au dictionnaire de routage. Par exemple :

routes.MapHttpRoute(
    name: "DefaultApi",
    routeTemplate: "api/{controller}/{category}/{id}",
    defaults: new { category = "all", id = RouteParameter.Optional }
);

Pour le chemin d’URI « api/products », le dictionnaire d’itinéraires contient :

  • contrôleur : « products »
  • category: « all »

Pour « api/products/toys/123 », toutefois, le dictionnaire d’itinéraires contient :

  • contrôleur : « products »
  • catégorie: « jouets »
  • id : « 123 »

Les valeurs par défaut peuvent également inclure une valeur qui n’apparaît nulle part dans le modèle de routage. Si l’itinéraire correspond, cette valeur est stockée dans le dictionnaire. Par exemple :

routes.MapHttpRoute(
    name: "Root",
    routeTemplate: "api/root/{id}",
    defaults: new { controller = "customers", id = RouteParameter.Optional }
);

Si le chemin d’accès de l’URI est « api/root/8 », le dictionnaire contient deux valeurs :

  • contrôleur : « clients »
  • id : « 8 »

Sélection d’un contrôleur

La sélection du contrôleur est gérée par la méthode IHttpControllerSelector.SelectController . Cette méthode prend une instance HttpRequestMessage et retourne un HttpControllerDescriptor. L’implémentation par défaut est fournie par la classe DefaultHttpControllerSelector . Cette classe utilise un algorithme simple :

  1. Dans le dictionnaire de routes, recherchez la clé « contrôleur ».
  2. Prenez la valeur de cette clé et ajoutez la chaîne « Controller » pour obtenir le nom du type de contrôleur.
  3. Recherchez un contrôleur d’API web portant ce nom de type.

Par exemple, si le dictionnaire de routes contient la paire clé-valeur « controller » = « products », le type de contrôleur est « ProductsController ». S’il n’existe aucun type correspondant ou plusieurs correspondances, l’infrastructure retourne une erreur au client.

Pour l’étape 3, DefaultHttpControllerSelector utilise l’interface IHttpControllerTypeResolver pour obtenir la liste des types de contrôleurs d’API web. L’implémentation par défaut de IHttpControllerTypeResolver retourne toutes les classes publiques qui (a) implémentent IHttpController, (b) ne sont pas abstraites et (c) ont un nom qui se termine par « Contrôleur ».

Sélection de l’action

Après avoir sélectionné le contrôleur, l’infrastructure sélectionne l’action en appelant la méthode IHttpActionSelector.SelectAction . Cette méthode prend un HttpControllerContext et retourne un HttpActionDescriptor.

L’implémentation par défaut est fournie par la classe ApiControllerActionSelector . Pour sélectionner une action, il examine les éléments suivants :

  • Méthode HTTP de la demande.
  • Espace réservé « {action} » dans le modèle d’itinéraire, le cas échéant.
  • Paramètres des actions sur le contrôleur.

Avant d’examiner l’algorithme de sélection, nous devons comprendre certaines choses sur les actions du contrôleur.

Quelles méthodes sur le contrôleur sont considérées comme des « actions » ? Lors de la sélection d’une action, l’infrastructure examine uniquement les méthodes de instance publiques sur le contrôleur. En outre, il exclut les méthodes « nom spécial » (constructeurs, événements, surcharges d’opérateur, etc.) et les méthodes héritées de la classe ApiController .

Méthodes HTTP. L’infrastructure choisit uniquement les actions qui correspondent à la méthode HTTP de la requête, déterminée comme suit :

  1. Vous pouvez spécifier la méthode HTTP avec un attribut : AcceptVerbs, HttpDelete, HttpGet, HttpHead, HttpOptions, HttpPatch, HttpPost ou HttpPut.
  2. Sinon, si le nom de la méthode de contrôleur commence par « Get », « Post », « Put », « Delete », « Head », « Options » ou « Patch », alors, par convention, l’action prend en charge cette méthode HTTP.
  3. Si aucun des éléments ci-dessus n’est indiqué, la méthode prend en charge POST.

Liaisons de paramètres. Une liaison de paramètre est la façon dont l’API web crée une valeur pour un paramètre. Voici la règle par défaut pour la liaison de paramètres :

  • Les types simples sont extraits de l’URI.
  • Les types complexes sont extraits du corps de la requête.

Les types simples incluent tous les types primitifs .NET Framework, plus DateTime, Decimal, Guid, String et TimeSpan. Pour chaque action, au maximum un paramètre peut lire le corps de la demande.

Notes

Il est possible de remplacer les règles de liaison par défaut. Consultez Liaison de paramètre WebAPI sous le capot.

Avec cet arrière-plan, voici l’algorithme de sélection d’action.

  1. Créez la liste de toutes les actions sur le contrôleur qui correspondent à la méthode de requête HTTP.

  2. Si le dictionnaire de routes a une entrée « action », supprimez les actions dont le nom ne correspond pas à cette valeur.

  3. Essayez de faire correspondre les paramètres d’action à l’URI, comme suit :

    1. Pour chaque action, obtenez la liste des paramètres qui sont un type simple, où la liaison obtient le paramètre à partir de l’URI. Excluez les paramètres facultatifs.
    2. Dans cette liste, essayez de trouver une correspondance pour chaque nom de paramètre, soit dans le dictionnaire de routage, soit dans la chaîne de requête URI. Les correspondances ne respectent pas la casse et ne dépendent pas de l’ordre des paramètres.
    3. Sélectionnez une action où chaque paramètre de la liste a une correspondance dans l’URI.
    4. Si plusieurs actions répondent à ces critères, choisissez celle qui a le plus de correspondances de paramètre.
  4. Ignorez les actions avec l’attribut [NonAction].

L’étape 3 est probablement la plus déroutante. L’idée de base est qu’un paramètre peut obtenir sa valeur à partir de l’URI, du corps de la requête ou d’une liaison personnalisée. Pour les paramètres qui proviennent de l’URI, nous voulons nous assurer que l’URI contient réellement une valeur pour ce paramètre, soit dans le chemin d’accès (via le dictionnaire de routage) soit dans la chaîne de requête.

Par exemple, considérez l’action suivante :

public void Get(int id)

Le paramètre id est lié à l’URI. Par conséquent, cette action ne peut correspondre qu’à un URI qui contient une valeur pour « id », soit dans le dictionnaire de routage, soit dans la chaîne de requête.

Les paramètres facultatifs constituent une exception, car ils sont facultatifs. Pour un paramètre facultatif, il est ok si la liaison ne peut pas obtenir la valeur à partir de l’URI.

Les types complexes sont une exception pour une raison différente. Un type complexe peut uniquement se lier à l’URI par le biais d’une liaison personnalisée. Mais dans ce cas, l’infrastructure ne peut pas savoir à l’avance si le paramètre est lié à un URI particulier. Pour le savoir, il doit appeler la liaison. L’objectif de l’algorithme de sélection est de sélectionner une action dans la description statique, avant d’appeler des liaisons. Par conséquent, les types complexes sont exclus de l’algorithme de correspondance.

Une fois l’action sélectionnée, toutes les liaisons de paramètres sont appelées.

Résumé :

  • L’action doit correspondre à la méthode HTTP de la requête.
  • Le nom de l’action doit correspondre à l’entrée « action » dans le dictionnaire de routage, le cas échéant.
  • Pour chaque paramètre de l’action, si le paramètre est extrait de l’URI, le nom du paramètre doit être trouvé dans le dictionnaire de routage ou dans la chaîne de requête URI. (Les paramètres facultatifs et les paramètres avec des types complexes sont exclus.)
  • Essayez de faire correspondre le plus grand nombre de paramètres. La meilleure correspondance peut être une méthode sans paramètre.

Exemple étendu

Itinéraires :

routes.MapHttpRoute(
    name: "ApiRoot",
    routeTemplate: "api/root/{id}",
    defaults: new { controller = "products", id = RouteParameter.Optional }
);
routes.MapHttpRoute(
    name: "DefaultApi",
    routeTemplate: "api/{controller}/{id}",
    defaults: new { id = RouteParameter.Optional }
);

Contrôleur :

public class ProductsController : ApiController
{
    public IEnumerable<Product> GetAll() {}
    public Product GetById(int id, double version = 1.0) {}
    [HttpGet]
    public void FindProductsByName(string name) {}
    public void Post(Product value) {}
    public void Put(int id, Product value) {}
}

Requête HTTP :

GET http://localhost:34701/api/products/1?version=1.5&details=1

Correspondance des itinéraires

L’URI correspond à l’itinéraire nommé « DefaultApi ». Le dictionnaire d’itinéraires contient les entrées suivantes :

  • contrôleur : « products »
  • id : « 1 »

Le dictionnaire de routes ne contient pas les paramètres de chaîne de requête , « version » et « details », mais ceux-ci seront toujours pris en compte lors de la sélection de l’action.

Sélection du contrôleur

À partir de l’entrée « controller » dans le dictionnaire de routage, le type de contrôleur est ProductsController.

Sélection de l’action

La requête HTTP est une requête GET. Les actions du contrôleur qui prennent en charge GET sont GetAll, GetByIdet FindProductsByName. Le dictionnaire de routes ne contient pas d’entrée pour « action », nous n’avons donc pas besoin de faire correspondre le nom de l’action.

Ensuite, nous essayons de faire correspondre les noms de paramètres pour les actions, en examinant uniquement les actions GET.

Action Paramètres à mettre en correspondance
GetAll aucun
GetById « ID »
FindProductsByName « name »

Notez que le paramètre de version de n’est GetById pas pris en compte, car il s’agit d’un paramètre facultatif.

La GetAll méthode correspond de façon triviale. La GetById méthode correspond également, car le dictionnaire d’itinéraires contient « id ». La FindProductsByName méthode ne correspond pas.

La GetById méthode gagne, car elle correspond à un paramètre, par rapport à aucun paramètre pour GetAll. La méthode est appelée avec les valeurs de paramètre suivantes :

  • id = 1
  • version = 1.5

Notez que même si la version n’a pas été utilisée dans l’algorithme de sélection, la valeur du paramètre provient de la chaîne de requête URI.

Points d’extension

L’API web fournit des points d’extension pour certaines parties du processus de routage.

Interface Description
IHttpControllerSelector Sélectionne le contrôleur.
IHttpControllerTypeResolver Obtient la liste des types de contrôleurs. Le DefaultHttpControllerSelector choisit le type de contrôleur dans cette liste.
IAssembliesResolver Obtient la liste des assemblys de projet. L’interface IHttpControllerTypeResolver utilise cette liste pour rechercher les types de contrôleurs.
IHttpControllerActivator Crée de nouvelles instances de contrôleur.
IHttpActionSelector Sélectionne l'action.
IHttpActionInvoker Appelle l’action.

Pour fournir votre propre implémentation pour l’une de ces interfaces, utilisez la collection Services sur l’objet HttpConfiguration :

var config = GlobalConfiguration.Configuration;
config.Services.Replace(typeof(IHttpControllerSelector), new MyControllerSelector(config));