Aller plus loin avec les routes et les filtres

Les auteurs de ce contenu recherchent un correcteur. N’hésitez pas à les contacter par MP pour proposer votre aide !

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

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 :

page introuvable
page introuvable

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 :

[Route("Forum/Index/{slug_category}/{slug_sub_category}")]
public ActionResult Index(string slug_category, string slug_sub_category)
{
    return View();
}
[Route("Forum/Index/{slug_category}/{author_email}/")]
///
/// Imaginons que nous voulons avoir la liste des postes par une personne précise dans une catégorie.
/// 
public ActionResult Index(string slug_category, string author_email)
{
    return View();
}
Un conflit possible

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.

public class IsSlugConstraint : IRouteConstraint
{
    public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
    {
        return values[parameterName].toLower() == values[parameterName] && values.Replace("-", "").Replace("_", "").isAlphaNumeric();
    }
}
Notre contrainte de slug.

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 :

routes.MapRoute(
                name: "Default",
                url: ""Forum/Index/{slug_category}/{slug_sub_category}"",
                defaults: new { controller = "Forum", action = "Index"},
                constraints: new { slug_sub_category = new IsSlugConstraint()}
            );
RouteConfig.cs

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.

class SlugRouteAttribute: RouteFactoryAttribute{
    public SlugRouteAttribute(string template, params string[] slugParameters)
        : base(template)
    {
        Slugs = slugParameters;
    }
    public string[] Slugs {
       get;
       private set;
    }
    public override RouteValueDictionary Constraints
    {
        get
        {
            var constraints = new RouteValueDictionary();
                        foreach(string element in Slugs){
                        constraints.Add(element , new IsSlugConstraint());
                        }
                    return constraints;
        }
    }
    }
}
notre attribut personnalisé

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

  1. 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).

public class MyFilterAttribute : ActionFilterAttribute{
    public override void OnActionExecuted(ActionExecutedContext context){
        // appelé lorsque la méthode d'action est exécutée. 
    }
    public override void OnActionExecuting(ActionExecutingContext context){
        //appelé avant la méthode d'action. 
    }
    public override void OnResultExecuted(ResultExecutedContext context){
        // appelé une fois le rendu fait .Vous pouvez "annuler" l'exécution grâce à l'attribut Cancel
    }
    public override void OnResultExecuting(ResultExecutingContext context){
       // appelé avant le rendu. Vous pouvez "annuler" l'exécution grâce à l'attribut Cancel
    }
}
Les méthodes de notre filtre

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.

action lifecycle
L’ordre d’exécution des filtres2

  1. 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.

  2. 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.