Quand on utilise la technologie ASP.NET Core pour implémenter une application web, il est possible d’utiliser le middleware et le pattern MVC (i.e. Model-View-Controler) pour organiser les classes qui répondront aux différentes requêtes HTTP. ASP.NET Core permet de router les requêtes vers les bonnes instances d’objets à condition que le “routage” soir configuré. Dans le cadre de MVC, cette fonctionnalité de “routage” (i.e. routing) va ainsi permettre de trouver le controller et l’action dans le controller qui sera évoquée en fonction de l’adresse web de la requête HTTP.
Le but de cet article n’est pas de paraphraser la documentation mais d’être un aide-mémoire sur les principales caractéristiques du routage. La documentation de Microsoft permet d’avoir des détails plus exhaustifs sur tous les aspects du routage.
Configurer les routes
Routage basique
Template d’une route
Elément à valeur fixe
Elément facultatif
Element par défaut
Template par défaut
Ajouter une route
Contrainte sur un élément d’une route
Contraintes de type
Contraintes de valeurs
Contrainte avec une regex
Définition des routes avec des attributs
RouteAttribute
Au niveau d’un controller
Au niveau d’une action
Utilisation de “tokens” de remplacement
La définition des routes se combinent
HttpGetAttribute, HttpPostAttribute, HttpPutAttribute et HttpDeleteAttribute
Exemples
Routage avec des attributs
Routage avec template
Définir un route handler spécifique
Fonctionnement général
Lorsqu’une requête HTTP est adressée à une application web ASP.NET Core, elle traverse différentes couches de l’application en utilisant le pipeline de middlewares. Ces middlewares sont évoqués successivement et permettent d’adresser différents points techniques liés à la requête comme par exemple la gestion d’erreurs, la gestion des cookies, l’authentification, la gestion de sessions etc… L’exécution successive des différents middlewares aboutira le cas échéant à créer une réponse à la requête. Le routage est l’un de ces middlewares.
Après avoir exécuté les middlewares précédents, la requête parvient au middleware routing (correspond aux classes dans le namespace Microsoft.AspNetCore.Routing) qui va effectuer les étapes suivantes:
- Parser l’URL pour déterminer les différents paramètres de la requête.
- En fonction des paramètres, trouver la route parmi les différentes routes configurées qui permettra de répondre à la requête.
- Si une route est trouvée alors la requête est passée à une classe satisfaisant
IRouteHandler
(dansMicrosoft.AspNetCore.Routing
). Par défaut, la classeRouteHandler
. - Si aucune route n’est trouvée, la requête est passée au middleware suivant.
Ainsi, le middleware routing est composé de différents objets:
- Les routes (la classe correspondante est
Route
): elles définissent les différentes chemin de routage que pourrait emprunter la requête.
Par exemple, une route pourrait être définie en utilisant l’expression:"{controller=Home}/{action=Index}/{id?}"
- Une liste de routes (i.e. route collection): les routes sont testées successivement parmi cette liste pour déterminer qu’elle est la première qui convient. Si une route convient aux paramètres de la requête, les routes suivantes ne sont pas testées.
IRouter
: la classe satisfaisant cette interface va être appelée pour déterminer quelle classe va traiter la requête pour en générer une partie de la réponse. Dans le cas d’une application ASP.NET Core MVC, le route handler, par défaut, est la classeMvcRouteHandler
.Cette classe va chercher le controller et l’action dans le controller à évoquer pour générer une réponse.
Ces différents objets doivent être configurés pour que le routage s’effectue correctement.
Configurer les routes
Routage basique
Pour configurer un routage par défaut, il faut d’abord ajouter les services MVC pour indiquer que l’application ASP.NET Core utilisera ce pattern dans la classe Startup
servant à la configuration initiale:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
// ...
}
}
SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
permet d’indiquer qu’on utilise une logique de routage compatible avec celle d’ASP.NET Core 2.2.
On indique ensuite, qu’on souhaite utiliser le middleware de routage et on l’ajoute au pipeline de middleware avec la ligne:
public class Startup
{
public void Configure(IApplicationBuilder app)
{
app.UseMvc();
}
}
UseMvc()
ajoute le middleware de routage mais ne configure aucune route. Pour configurer une route par défaut, il faut utiliser la méthode (on explicitera par la suite quelle est la route par défaut):
app.UseMvcWithDefaultRoute();
Les 2 ajouts sont nécessaires services.AddMvc()
et app.UseMvc()
.
Template d’une route
Par défaut, dans le cadre de MVC, une route se définit en indiquant un template. Ce template contient des indications concernant:
- Le controller qui implémente le code permettant de construire la réponse à la requête,
- L’action c’est-à-dire la fonction dans le controller qui sera exécutée pour générer la réponse,
- Eventuellement des arguments nécessaires à l’exécution de l’action.
De façon générique, une route se définit en indiquant les différents éléments successivement:
"{controller}/{action}/{id}"
L’utilisation de { }
permet d’indiquer qu’un élément n’a pas une valeur fixe.
Par exemple, le template de la route définie précédemment sera utilisée si l’adresse appelée est du type:
pizzaOrder/get/23
Dans ce cas:
{controller}
est égal àpizzaOrder
,{action}
est égal àget
et{id}
est égal à23
Elément à valeur fixe
On peut indiquer qu’un élément de l’adresse a une valeur fixe. Par exemple, si le template de la route est:
admin/{controller}/{action}/{id}
admin
est fixe et toutes les adresses devront commencer par admin
pour être prise en compte par la route. Par exemple:
admin/pizzaOrder/get/23
Elément facultatif
On peut indiquer qu’un élément est facultatif en utilisant ?
, par exemple:
"{controller}/{action}/{id?}"
Cette route correspond à des adresses du type:
pizzaOrder/list
pizzaOrder/list/12
Element par défaut
Si un élément n’est pas précisé, alors la valeur par défaut sera utilisée. Pour préciser la valeur par défaut, il faut utiliser le caractère =<valeur par défaut>
, par exemple:
"{controller}/{action=index}/{id?}"
Dans ce cas, l’action par défaut sera index
.
Si on utilise l’adresse:
pizzaOrder
L’adresse équivalente sera: pizzaOrder/index
(l’élément id
étant facultatif).
Mise à part controller, action
d’autres mots clés peuvent désigner des éléments précis dans l’application ASP.NET Core comme area, handler
et page
.
Template par défaut
Si on utilise app.UseMvcWithDefaultRoute()
dans StartUp.Configure()
, le template par défaut utilisé est:
"{controller=Home}/{action=Index}/{id?}"
Ajouter une route
L’ajout explicite d’une route se fait en utilisant l’une des surcharges suivantes dans StartUp.Configure()
:
app.UseMvc(routes => {
routes.MapRoute("<nom unique de la route>", "<template de la route>");
});
Par exemple:
public class Startup
{
public void Configure(IApplicationBuilder app)
{
app.UseMvc(routes => {
routes.MapRoute("default", "{controller}/{action=index}");
});
}
}
D’autres surcharges sont possibles:
- Pour indiquer que des éléments sont fixes:
routes.MapRoute("Home", "{home}", new { Controller = "Home", Action = "Index" });
Dans ce cas, la route nommée
"Home"
contient le template"{home}"
désignant le controllerHomeController
et l’actionIndex
. - Pour préciser des éléments par défaut:
routes.MapRoute("Home", "{controller}/{action}", defaults: new { Controller = "Home", Action = "Index" });
Le template de la route est
"{controller}/{action}"
et les valeurs par défaut sont"Home"
pour le controller et"Index"
pour l’action. - Pour préciser des contraintes sur les éléments:
routes.MapRoute("Home", "{controller}/{action}", defaults: new { Controller = "Home", Action = "Index" }, constraints: new { id = new IntRouteConstraint() });
La contrainte impose que l’élément
id
doit être un entier. - D’autres contraintes existent comme:
- Pour imposer une contrainte sur le type:
BoolRouteConstraint, DateTimeRouteConstraint, DecimalRouteConstraint, DoubleRouteConstraint, GuidRouteContraint, FloatRouteConstraint
ouLongRouteConstraint
. - Pour imposer une contrainte sur la longueur d’une chaîne de caractères:
MinLengthRouteConstraint
ouMaxLengthConstraint
- Pour imposer une contrainte sur une valeur:
MinRouteConstraint, MaxRouteConstraint
ouRangeRouteConstraint
. - Pour imposer une contrainte avec une regex:
RegexInlineRouteConstraint
.
- Pour imposer une contrainte sur le type:
L’ordre d’ajout des routes est important puisque le parcours des routes dans la liste de routes se fera dans l’ordre d’ajout de celles-ci.
Contrainte sur un élément d’une route
Il est possible d’indiquer des contraintes concernant un élément dans le template d’une route. Par exemple, si on considère le template de route suivant:
"{controller}/{action}/{id}"
Si on souhaite ajouter des contraintes sur le paramètre id
, il faut utiliser la syntaxe suivante:
"{controller}/{action}/{id:<contrainte 1>:<contrainte 2>:<etc...>}"
On peut utiliser le caractère ?
pour indiquer que le paramètre id
est facultatif:
"{controller}/{action}/{id:<contrainte 1>:<contrainte 2>:<etc...>?}"
Par exemple pour indiquer que le paramètre id
doit être de type entier, on utilise la syntaxe:
"{controller}/{action}/{id:int}"
Contraintes de type
Pour contraindre le type d’un paramètre, on peut utiliser les syntaxes suivantes:
Entier | {id:int} |
Alphabétique (caractères de A à Z et a à z seulement) | {id:alpha} |
bool |
{id:bool} |
DateTime |
{id:datetime} |
decimal |
{id:decimal} |
double |
{id:double} |
GUID |
{id:guid} |
float |
{id:float} |
Contraintes de valeurs
Pour contraindre un paramètre à avoir des valeurs dans un intervalle spécifique:
Chaine de caractères d’une longueur minimum (par exemple 5 caractères) | {id:minlength(5)} |
Chaine de caractères d’un longueur maximum (par exemple 10 caractères) | {id:maxlength(10)} |
Chaine de caractères de longueur spécifique (par exemple 7 caractères) | {id:length(7)} |
Chaine de caractères de longueur bornée (par exemple comprise entre 4 et 9 caractères) | {id:length(4, 9)} |
Entier supérieur ou égal (par exemple 5) | {id:min(5)} |
Entier inférieur ou égal (par exemple 14) | {id:max(14)} |
Entier compris dans un interval borné (par exemple entre 3 et 9) | {id:range(3, 9)} |
Contrainte avec une regex
Pour contraindre un paramètre à respecter une regex:
"{id:regex(<regex>)}"
Par exemple:
"{id:regex(^2019$)}"
Il faut utiliser des caractères d’échappement quand on utilise \,{, }, [
ou ]
. Par exemple la regex ^\d{5}$
doit s’écrire:
"{id:regex(^\\d{{5}}$)}"
Définition des routes avec des attributs
Il n’est pas obligatoire de préciser des routes en utilisant un template comme dans le paragraphe précédent. On peut utiliser des attributs placés sur:
- un controller et/ou
- une action d’un controller
Pour définir les routes par attributs, il faut utiliser la fonction suivante dans StartUp.Configure()
:
public class Startup
{
public void Configure(IApplicationBuilder app)
{
app.UseMvc();
}
}
RouteAttribute
Au niveau d’un controller
L’attribut RouteAttribute
peut être utilisé au niveau d’un controller pour définir une route:
[Route("PizzaOrder")]
public class PizzaOrderController : Controller
{
// ...
}
Ce controller sera appelé dès que l’adresse commence par PizzaOrder
.
Au niveau d’une action
RouteAttribute
peut aussi être utilisé au niveau d’une action:
[Route("PizzaOrder")]
public class PizzaOrderController : Controller
{
[Route("Index")]
public IActionResult Index()
{
// ...
}
}
L’action sera appelée si l’adresse est: PizzaOrder/Index
.
Utilisation de “tokens” de remplacement
On peut utiliser des tokens pour éviter d’avoir à préciser le nom du controller ou de l’action. Au lieu d’indiquer le nom du controller ou de l’action, on utilise:
[controller]
pour désigner le nom du controller[action]
pour désigner le nom de l’action.[area]
pour indiquer le nom de la zone.
Par exemple:
[Route("[controller]")]
public class PizzaOrderController : Controller
{
[Route("[action]")]
public IActionResult Index()
{
// ...
}
}
On peut aussi tout préciser directement au niveau du controller:
[Route("[controller]/[action]")]
public class PizzaOrderController : Controller
{
public IActionResult Index()
{
// ...
}
}
La définition des routes se combinent
Les attributs utilisés au niveau d’un controller et d’une action se combinent toutefois il est aussi possible d’utiliser plusieurs attributs au même niveau. Par exemple si on indique:
[Route("PizzaOrder")]
public class PizzaOrderController : Controller
{
[Route("")]
[Route("Index")]
[Route("/")]
public IActionResult Index()
{
// ...
}
}
L’action Index()
est appelée si pour les adresses:
PizzaOrder
à cause de[Route("PizzaOrder")]
et[Route("")]
PizzaOrder/Index
à cause de[Route("PizzaOrder")]
et[Route("Index")]
""
(ou rien) à cause de[Route("PizzaOrder")]
et[Route("/")]
HttpGetAttribute, HttpPostAttribute, HttpPutAttribute et HttpDeleteAttribute
Dans le cadre d’une API REST (i.e. REpresentational State Transfer) on utilise les verbes HTTP pour effectuer les requêtes comme:
GET
pour effectuer des opérations de lecture sur une ressource,POST
pour créer une ressource,PUT
pour mettre à jour une ressource etDELETE
pour supprimer une ressource.
Il est possible d’utiliser les attributs suivants pour définir des routes correspondant aux requêtes contenant les verbes HTTP:
- HttpGetAttribute pour répondre aux requêtes GET.
- HttpPostAttribute pour répondre aux requêtes POST.
- HttpPutAttribute pour répondre aux requêtes PUT.
- HttpDeleteAttribute pour répondre aux requêtes DELETE.
Ces attributs sont à utiliser au niveau des actions:
[Route("PizzaOrder")]
public class PizzaOrderController : Controller
{
[HttpGet("")]
public IActionResult ListOrders()
{
// ...
}
[HttpGet("{id}")]
public IActionResult GetOrder(int id)
{
// ...
}
[HttpPost("")]
public IActionResult CreateOrder(Order newOrder)
{
// ...
}
}
Dans ce cas, l’utilisation de ces attributs permet de différencier les actions à appeler même dans le cas où les adresses sont les mêmes, par exemple:
- Requête
GET
à l’adressePizzaOrder
appelera l’actionListOrders()
à cause de[Route("PizzaOrder")]
et[HttpGet("")]
. - Requête
GET
à l’adressePizzaOrder/23
appelera l’actionGetOrder()
à cause de[Route("PizzaOrder")]
et[HttpGet("{id}")]
. - Requête
POST
à l’adressePizzaOrder
appelera l’actionCreateOrder()
à cause de[Route("PizzaOrder")]
et[HttpPost("")]
.
Il est possible d’utiliser des contraintes sur les paramètres utilisés avec les attributs comme pour les templates de route.
Par exemple, pour contraindre le paramètre id
à être entier et à être compris entre 4 et 9:
[HttpGet("{id:int:range(4,9)}")]
public IActionResult GetOrder(int id)
{
// ...
}
Exemples
Pour mettre en application le routage, on se propose de créer une Web Api.
Pour installer .NET Core sur Debian, il faut suivre les étapes suivantes:
Il faut ensuite générer un squelette de Web Api en exécutant la commande:
user@debian:~/% dotnet new webapi --name webapi_example
Pour avoir Swagger et requêter facilement la Web API, on peut ajouter le package NuGet Swashbuckle.AspNetCore en exécutant la commande suivante:
user@debian:~/% dotnet add webapi_example/webapi_example.csproj package swashbuckle.aspnetcore
Pour configurer Swashbuckle, il faut ajouter les lignes suivantes dans StartUp.cs
:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddSwaggerGen(c =>
{
// Permet de préciser de la documentation
c.SwaggerDoc("v1", new Info { Title = "My API", Version = "v1" });
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "My simple API V1");
});
}
}
On ajoute les controllers suivants dans le répertoire Controllers
:
- PizzaFlavour: Controllers/PizzaFlavourController.cs.
- PizzaOrder: Controllers/PizzaOrderController.cs.
On ajoute les services suivants après avoir créé le répertoire Services
:
- PizzaFlavourRepositoryService: Services/PizzaFlavourRepositoryService.cs.
- PizzaOrderRepositoryService: Services/PizzaOrderRepositoryService.cs.
On enregistre les services dans le container d’injection de dépendances en ajoutant ces lignes dans StartUp.ConfigureServices()
:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IPizzaFlavourRepositoryService>(new PizzaFlavourRepositoryService());
services.AddSingleton<IPizzaOrderRepositoryService, PizzaOrderRepositoryService>();
}
}
}
On lance la compilation et l’exécution en exécutant successivement les lignes:
user@debian:~/% cd webapi_example
user@debian:~/webapi_example/% dotnet build
user@debian:~/webapi_example/% dotnet run
On se connecte ensuite, avec un browser à l’adresse: http://localhost:5000/swagger/index.html
.
Il est, ainsi, possible de requêter les fonctions des controllers PizzaFlavour
et PizzaOrder
avec Swagger.
Le code de cet exemple se trouve dans le repository GitHub suivant: https://github.com/msoft/webapi_example.
Routage avec des attributs
L’exemple dans la branche principale permet d’illustrer le routage avec attributs:
- Avec les attributs
RouteAttribute
etApiControllerAttribute
sur les controllers par exemple:[Route("api/[controller]")] [ApiController] public class PizzaOrderController : ControllerBase { // ... }
- Avec les attributs
HttpGetAttribute, HttpDeleteAttribute
etHttpPostAttribute
sur les functions correspondant aux actions, par exemple:[HttpGet] public ActionResult<IEnumerable<OrderedPizza>> GetOrderedPizzas() { // ... } [HttpPost("{pizzaFlavour}")] public ActionResult<int> AddNewOrder(string pizzaFlavour) { // ... }
Routage avec template
La branche “usingRoutes” contient un exemple de routage en utilisant un template. Dans Startup.Configure()
, on définit quelques templates:
app.UseMvc(routes => {
routes.MapRoute("secure", "secure", new { Controller = "Admin", Action="GetOrders"});
routes.MapRoute("admin", "{Controller=Admin}/{Action}/{id?}");
routes.MapRoute("default", "api/{Controller=PizzaOrder}/{Action=GetOrders}/{id?}");
});
Ainsi:
- La route
"secure"
permet d’appeler toujours le controllerAdminController
avec l’actionGetOrders
. Pour l’invoquer il suffit d’effectuer une requêteGET
à l’adresse:http://localhost:5000/secure
- La route
"admin"
permet de définir une route plus générale pour appeler le controllerAdminController
. Par exemple pour appeler l’actionDeleteOrder
, il faut effectuer une requêteGET
à l’adressehttp://localhost:5000/admin/DeleteOrder/1
- La route
"default"
définit une route plus générale pour appeler les controllersPizzaFlavourController
etPizzaOrderController
.Par exemple pour appeler l’action
FindFlavour
dans le controllerPizzaFlavourController
, il faut effectuer une requêteGET
à l’adressehttp://localhost:5000/PizzaFlavour/FindFlavour/Regina
Dans cet exemple, pour illustrer l’utilisation des templates, on a supprimé les attributs dans les controller AdminController
et PizzaFlavourController
. A cause de cette suppression, les fonctions ne sont plus visibles dans Swagger toutefois elles sont toujours fonctionnelles. Pour les invoquer, il faut utiliser:
Définir un route handler spécifique
On peut implémenter un routage plus personnalisé en implémentant une classe satisfaisant IRouter
:
namespace Microsoft.AspNetCore.Routing
{
public interface IRouter
{
VirtualPathData GetVirtualPath(VirtualPathContext context);
Task RouteAsync(RouteContext context);
}
}
Dans la classe:
GetVirtualPath()
permet de générer des url en fonction d’une route.RouteAsync()
permet d’indiquer un routage particulier en fonction d’une logique implémentée.
Dans l’exemple, l’implémentation de la classe satisfaisant IRouter
est:
public class CustomRouter : IRouter
{
private IRouter _defaultRouter;
public CustomRouter(IRouter defaultRouter)
{
_defaultRouter = defaultRouter;
}
public VirtualPathData GetVirtualPath(VirtualPathContext context)
{
return _defaultRouter.GetVirtualPath(context);
}
public async Task RouteAsync(RouteContext context)
{
var path = context.HttpContext.Request.Path.Value;
if (path.Contains("admin"))
{
context.RouteData.Values["controller"] = "Admin";
context.RouteData.Values["action"] = "GetOrders";
await _defaultRouter.RouteAsync(context);
}
}
}
Cette classe va invoquer le controller AdminController
si l’URL contient "admin"
.
Pour que cette classe soit prise en compte, il faut le configurer dans Startup.Configure()
en précisant une autre route par défaut:
app.UseMvc(routes =>
{
routes.Routes.Add(new CustomRouter(routes.DefaultHandler));
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
Le middleware permettant d’effectuer le routage n’est plus le même à partir d’ASP.NET Core 2.2. Jusqu’à ASP.NET Core 2.1, le routage de l’URL vers le controller et l’action était effectué au niveau du middleware MVC. A partir d’ASP.NET Core 2.2, le routage est fait plus en amont et avant l’exécution du middleware MVC dans le pipeline, il est effectué par un middleware spécial appelé “Endpoint Routing”.
Comme on peut voir sur les schémas suivants:
ASP.NET Core ≤ 2.1 | ASP.NET Core ≥ 2.2 |
---|---|
Ainsi, à partir d’ASP.NET Core 2.2, il faut ensuite désactiver le routage par point de terminaison pour que le routage s’effectue avec IRouter
avec la ligne suivante:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(options => options.EnableEndpointRouting = false)
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
// ...
}
- Routing to controller actions in ASP.NET Core: https://docs.microsoft.com/en-us/aspnet/core/mvc/controllers/routing?view=aspnetcore-2.2
- Route template reference: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/routing?view=aspnetcore-2.2#route-template-reference
- ASP.NET Core Middleware: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/middleware/?view=aspnetcore-2.2
- Routing in ASP.NET Core: https://www.tektutorialshub.com/routing-in-asp-net-core/
- Inline Route Constraints in ASP.NET Core MVC: https://blog.mariusschulz.com/2016/03/31/inline-route-constraints-in-asp-net-core-mvc
- Getting started with ASP.NET 5 MVC 6 Web API & Entity Framework 7: http://bitoftech.net/2014/11/18/getting-started-asp-net-5-mvc-6-web-api-entity-framework-7/
- UseMvc and AddMvc – Why doing two calls to get MVC (or any other) layer?RSS: https://forums.asp.net/t/2022139.aspx?UseMvc+and+AddMvc+Why+doing+two+calls+to+get+MVC+or+any+other+layer+
- ASP.NET Core 2.0 MVC Routing: https://tahirnaushad.com/2017/08/20/asp-net-core-mvc-routing/
- [ASP.NET Core] Plongée dans le routage: https://blog.soat.fr/2015/08/asp-net-5-plongee-dans-le-routage/
- [ASP.NET Core MVC Pipeline] Routing Middleware – Custom IRouter: http://azurecoder.net/2017/07/09/routing-middleware-custom-irouter/
- Endpoint Routing in ASP.NET Core 2.2 Explained: https://rolandguijt.com/endpoint-routing-in-asp-net-core-2-2-explained/
- Extended routing in ASP.NET Core 2: https://stackoverflow.com/questions/47787751/extended-routing-in-asp-net-core-2