Notre application grandit à chaque fois et nous aimerions ajouter du code qui se répète à chaque action.
Ce code peut avoir pour but de tracer le fonctionnement de l’application, générer des métriques, ajouter de la sécurité, prégénérer des éléments de la vue…
De même nous aimerions pouvoir utiliser des URL un peu plus "user friendly".
Bienvenu dans ce chapitre.
- Les routes personnalisées
- Les filtres personnalisés
- Les filtres en pratique : mini TP calculer le temps de chargement
Les routes personnalisées
Reprenons l’exemple de zeste de savoir, mais cette fois-ci, allons du côté des forums.
Ces derniers sont organisés d’une manière spéciale, en effet, vous avez la liste des forums qui est divisée en deux catégories. Puis, dans ces catégories vous avez une liste de sous catégorie.
Pour lister tous les messages, vous n’avez pas une url du style souscategorie/un_numero/un_titre
mais forums/titre_categorie/titre_sous_categorie
.
Ce genre d’url est très complexe à généraliser, alors il faudra personnaliser ça au niveau du contrôleur.
Je vous invite à créer un contrôleur que nous appelleront ForumControlleur.
Dans ce contrôleur, nous allons créer une action Index qui prend deux paramètres de type string slug_category
, slug_sub_category
et une vue Index.cshtml qui est vide.
Essayons d’accéder à l’url http://llocalhost:port/Forum/Index/cat/subcat
:
Par contre, il est capable de trouver http://localhost:port/Forum/Index/?slug_category=cat&slug_sub_category=subcat
.
Comme il n’a pas trouvé de paramètre qui colle avec sa route par défaut, il a interprété tous les autres paramètres de votre fonction comme des query string.
Pour faire plus beau, nous allons lui dire qu’il doit router l’url par segment grâce à un attribut qui s’appelle Route
et qui se place au dessus de la fonction :
[Route("Forum/Index/{slug_category}/{slug_sub_category}")]
public ActionResult Index(string slug_category, string slug_sub_category)
{
return View();
}
Puis, dans RouteConfig.cs, nous allons lui dire que quand il voit l’attribut Route
, il doit utiliser la route définie :
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapMvcAttributeRoutes();
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
Si deux routes se ressemblent, comment je fais pour les départager?
Si au sein d’un même contrôleur deux routes possèdent le même nombre de paramètres, les choses peuvent devenir assez compliquées.
Néanmoins il existe un cas assez simple à régler, qu’on peut trouver dans cet exemple :
On se rend compte dans cet exemple que le routeur aura du mal à décider entre les deux routes. En effet, nous attendons à chaque fois Forum/Index/un_string/un_string/
.
Pourtant, un humain y arriverait facilement car il détecterait tout de suite que le second argument dans chacune des vues est totalement différent : le premier est un simple slug, le second est une adresse email.
Pour résoudre ce problème on peut proposer à notre route un nouveau type de paramètres : les contraintes. Elles permettent de s’assurer qu’un ensemble de préconditions est respecté ou bien que le format de l’url correspond à ce qu’on attend.
En ce qui concerne le format, la méthode simple est souvent basée sur les regex, je vous la donnerai en fin de chapitre. Ici, nous allons apprendre à créer une contrainte complètement nouvelle. L’avantage est que cette méthode est plus souple et peut s’intégrer au mieux à vos besoins1.
La première étape est de définir une contrainte : cela prend la forme d’une classe qui hérite de IRouteConstraint
. Cette interface nous force à surcharger la méthode Match
qui nous permettra de donner notre contrainte.
Une fois cette contrainte construite, il suffira de préciser à notre route que notre champ doit la respecter.
Pour cela deux méthodes s’offrent à vous :
La première consiste à réutiliser le fichier de configuration du routage pour lui donner l’ensemble des contraintes :
Cette méthode nous fait perdre l’utilisation des attributs Route
, par soucis de cohérence n’utilisez-la que si vous définissez toutes vos routes dans RouteConfig.cs
.
La seconde méthode vous permettra de tirer une nouvelle fois partie des attributs. Cependant, nous ne pourront plus nous suffire de l’attribut Route
. Il vous faudra créer votre propre attribut personnalisé qui permettra de générer les bonnes contraintes. Pour cela il doit hériter de RouteFactoryAttribute
.
Une fois cet attribut créé, il suffira de remplacer l’ancien attribut "Route" par le nôtre.
[SlugRouteAttribute("Forum/Index/{slug_category}/{slug_sub_category}", "slug_sub_category")]
public ActionResult Index(string slug_category, string slug_sub_category)
{
return View();
}
-
Un exemple souvent donné est forcer la requête à venir soit du localhost soit d’un réseau particulier. Vous pouvez trouver l’exemple sur developpez.com.
↩
Les filtres personnalisés
Nous l’avons vu précédemment, le système permet l’exécution de filtres à différentes étapes de la requête.
Typiquement, cela sera exécuté avant d’entrer dans votre Action
, avant d’entrer dans la génération du template, en en sortant et juste avant d’envoyer la réponse au client.
Les filtres peuvent ajouter de nouvelles fonctionnalités à votre programme, en enrichissant le ViewBag
par exemple. Leur utilité première —qui leur vaut leur nom— reste néanmoins de filtrer les requêtes (par exemple en rejetant un utilisateur anonyme quand on est sur une page du backoffice).
La majorité du temps les filtres fournis avec ASP.NET sont suffisants, mais il serait dommage de se passer de la possibilité de créer vos propres filtres voire d’étendre les filtres proposés par ASP.NET.
La première chose à faire est de créer une classe qui hérite de ActionFilterAttribute
(ou d’une classe qui hérite de System.Web.Mvc.ActionFilterAttribute
).
Pour filtrer certaines requêtes, le mieux est de surcharger OnActionExecuting
voire OnActionExecuted
. Comme nous allons le voir dans le TP qui suit, cela peut aussi être un bon moyen de générer un système qui va générer des métriques1 quant à votre code. On peut imaginer la mesure du temps d’exécution — puisque c’est le sujet du TP — mais aussi, par exemple dans le cas d’une réponse où vous dépendez d’une enum
, la proportion que représente chaque valeur de l’énumération.
Notons que le fait de surcharger ActionFilterAttribute permet d’avoir une classe parente qui implémente déjà IFilter
, IActionFilter
et IResultFilter
.
Ce sont en fait ces trois classes qui sont primordiales.
-
on peut par exemple utiliser influxdb ou prometheus pour stocker les métriques et les afficher dans des beaux dashboard. C’est très pratique quand votre application grandit.
↩ -
Source http://www.dotnetexpertguide.com/2013/02/aspnet-mvc-action-filter-life-cycle.html
↩
Les filtres en pratique : mini TP calculer le temps de chargement
Le but de notre filtre sera assez simple : calculer le temps que le serveur a passé à exécuter notre action et envoyer ce temps dans un fichier de log.
L’idée est ici de mettre à chaque action importante pour notre site un marqueur d’efficacité afin que plus tard on sache quels sont les endroits qui nécessitent une optimisation.
Quelques indications si l’énoncé ne vous suffit pas :
Si vous êtes un peu perdu, voici quelques indications que vous pourrez suivre pour coder le TP :
- notre but est de retenir à quelle heure a commencé l’action et à quelle heure elle a terminé
- une fois ces deux données obtenues, il suffit de faire la différence pour savoir combien de temps l’action a mis pour s’exécuter.
- pour obtenir le nom de l’action qu’on vient d’exécuter, vous pouvez faire
actionExecutedContext.ActionDescriptor.ActionName
. - pour retenir une date, les objets
DateTime
sont très performants.
La correction
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Http.Controllers;
using System.Web.Http.Filters;
namespace Blog.Tools
{
public class TimeTracingFilterAttribute:System.Web.MVC.ActionFilterAttribute
{
private DateTime start;
private DateTime end;
public override void OnActionExecuted(ActionExecutedContextactionExecutedContext)
{
base.OnActionExecuted(actionExecutedContext);
end = DateTime.Now;
double milliseconds = end.Subtract(start).TotalMilliseconds;
log4net.LogManager.GetLogger(actionExecutedContext.ActionDescriptor.ActionName)
.Info(String.Format("temps_de_chargement=%sms", milliseconds));
}
public override void OnActionExecuting(ActionExecutingContext actionContext)
{
base.OnActionExecuting(actionContext);
start = DateTime.Now;
}
}
}
et sur nos actions il suffit de faire :
[TimeTracingFilter]
public ActionResult MonAction(){
}
Les attributs forment une grande partie de la "magie" d’ASP.NET MVC, explorer la documentation pour améliorer le tout peut être vraiment intéressant, notamment, on peut imaginer que vous désirez personnaliser la page d’erreur lorsqu’une exception d’un type précis est levé. Eh bien ça sera à un ExceptionFilterAttribute
de faire ce travail.