• 30 hours
  • Medium

Free online content available in this course.

course.header.alt.is_certifying

You can get support and mentoring from a private teacher via videoconference on this course.

Got it!

Last updated on 11/23/17

Les filtres

Log in or subscribe for free to enjoy all this course has to offer!

Nous connaissons les actions de contrôleurs sur le bout des doigts maintenant, avec tout ce que nous avons vu et tout ce que nous avons mis en pratique. Mais savez-vous qu’il est possible de créer des filtres d’actions ? Voire même des filtres de contrôleurs ?
En fait… oui, vous le savez car vous en avez déjà utilisé. :)

Ces filtres peuvent modifier la façon dont une action est exécutée. Voyons à présent ce qu’il faut savoir sur ces filtres.

Les filtres d’ASP.NET MVC

Vous vous rappelez de l’attribut [Authorize]  qui permet de sécuriser nos actions de contrôleurs en empêchant d’invoquer ces actions si jamais l’utilisateur n’est pas authentifié ?
Eh bien il s’agit d’un filtre. En l’occurrence ce filtre est exécuté avant l’action du contrôleur et conditionne son éventuelle exécution. Il s’agit d’un filtre d’autorisation car il implémente IAuthorizationFilter . C’est également le cas du filtre RequireHttpsAttribute  qui oblige une requête à être faite en HTTPS ou du filtre ChildActionOnly  que vous connaissez déjà.

Il existe d’autres filtres, comme les filtres d’exceptions implémentant IExceptionFilter  comme le filtre HandleErrorAttribute  qui permet d’afficher la vue Error si jamais une exception survient lors de l’exécution de l’action.

Notons encore le filtre OutputCacheAttribute  qui permet d’indiquer si l’action doit être mise en cache, de quelle façon et combien de temps. Il s’agit d’un filtre de résultat qui implémente IResultFilter , dont le but est de modifier le contenu de l’ActionResult  retourné par la méthode. En l’occurrence, ce filtre rajoute les headers propres au cache dans la vue retournée.

Enfin, il y a les filtres d’actions implémentant IActionFilter  qui permettent de faire des choses avant que l’action ne démarre et après qu’elle soit exécutée. Très pratique par exemple pour ajouter des logs dans votre application ou pour mesurer combien de temps prennent vos diverses actions.

Vous pouvez cumuler les différents filtres sur vos actions ou contrôleurs, mais prenez garde à l’ordre dans lesquels ils sont exécutés. D’abord il y a les filtres d’autorisations, ensuite les filtres d’actions, puis les filtres de résultats et enfin les filtres d’exceptions.

Créer son propre filtre

Il est possible de créer son propre filtre en implémentant les interfaces qui nous intéressent. Mais il y a bien souvent une classe de base plus pratique que nous pouvons réutiliser. Par exemple, pour créer un filtre d’action il est possible de dériver de la classe ActionFilterAttribute .

Nous allons créer notre propre filtre d’action qui va nous permettre de résoudre un problème que nous avons rencontré lorsque nous avons fait de l’Ajax. En effet, je ne sais pas si vous vous rappelez, mais lorsque nous voulions rafraîchir notre tableau, nous appelions une vue partielle associée à l’action AfficheTableau  du contrôleur Vote . Cependant, cette vue partielle peut également s’afficher via l’URL /Vote/AfficheTableau/1 (pour le sondage numéro 1) ce qui produit un résultat inadapté car il s’affiche hors de sa vue mère. Or, il était impossible de le décorer avec l’attribut ChildActionOnly  car cela provoquait une erreur.
Nous allons écrire un filtre pour résoudre ce problème. Pour cela, il faut arriver à détecter une requête Ajax. Prenez donc votre analyseur de requête préféré et vous pouvez voir qu’il y a un header qui a cette tête :

X-Requested-With:XMLHttpRequest

Le header Ajax
Le header Ajax

Il s’agit d’un header non standard qui est utilisé pour identifier les requêtes Ajax. La plupart des frameworks JavaScript l’envoie avec la valeur XMLHttpRequest  et c’est justement le cas de jQuery.

Nous allons donc écrire un filtre d’action qui va vérifier si ce header est présent dans la requête. Créez donc un répertoire Filters et ajoutez la classe suivante :

public class AjaxFilterAttribute : ActionFilterAttribute
{
}

Vous pouvez substituer la méthode OnActionExecuting qui va être appelée juste avant l’action et qui contient le contexte de la requête. Il suffit de regarder s’il y a ce header, et s’il n’existe pas, alors on renvoie que la requête n’est pas trouvée :

public override void OnActionExecuting(ActionExecutingContext filterContext)
{
    if (filterContext.HttpContext.Request.Headers != null && filterContext.HttpContext.Request.Headers["X-Requested-With"] != "XMLHttpRequest")
        filterContext.Result = new HttpNotFoundResult();
    base.OnActionExecuting(filterContext);
}

Et voilà. Tout simple. Il ne reste plus qu’à décorer la méthode AfficheTableau  du contrôleur avec ce filtre :

[AjaxFilter]
public ActionResult AfficheTableau(int id)
{
    List<Resultats> resultats = dal.ObtenirLesResultats(id);
    return PartialView(resultats.OrderByDescending(r => r.NombreDeVotes).ToList());
}

Ainsi, l’action ne renverra rien si elle n’est pas appelée en Ajax. Pas mal non ?

Nous pouvons d’ailleurs simplifier cette méthode en utilisant une méthode d’extension qui vérifie justement si ce header est présent. Il s’agit de la méthode IsAjaxRequest  et qui fait à peu près le même boulot :

public override void OnActionExecuting(ActionExecutingContext filterContext)
{
    if (!filterContext.HttpContext.Request.IsAjaxRequest())
        filterContext.Result = new HttpNotFoundResult();
    base.OnActionExecuting(filterContext);
}

Ce filtre pose par contre un petit problème. En effet, l’action AfficheTableau  est appelée par le helper RenderAction  qui se trouve dans la vue AfficheResultat :

<p>Résultats du sondage :</p>
<div id="tableauResultat">
@{
Html.RenderAction("AfficheTableau", new { id = ViewBag.Id });
}
</div>
<p>Vue normale : @DateTime.Now.ToLongTimeString()</p>

Ce qu’il va se passer c’est que lorsque RenderAction  va invoquer la méthode AfficheTableau , ce ne sera pas un appel Ajax. Cet appel est donc devenu inutile et vous pouvez alors vider la balise <div> :

<div id="tableauResultat">
</div>
<p>Vue normale : @DateTime.Now.ToLongTimeString()</p>

Par contre, le tableau ne s’affichera qu’au bout de 10 secondes, temps à partir duquel la mise à jour Ajax opère. Vous pouvez simplement forcer un appel Ajax la toute première fois, au moment de déclencher le timer :

<script type="text/javascript">
    var timer;
    function ChargeVuePartielle() {
        $.ajax({
            url: '@Url.Action("AfficheTableau", new {id = ViewBag.Id })',
            type: 'GET',
            dataType: 'html',
            success: function (result) {
                $('#tableauResultat').html(result);
            }
        });
    }

    $(function () {
        timer = window.setInterval("ChargeVuePartielle()", 10000);
        ChargeVuePartielle();
    });
</script>

Mais avec tout cela, ce qui est magique, c’est que maintenant l’appel direct à /Vote/AfficheTableau/1 n’affichera plus rien. :)

Tester son filtre

Ahhh… ça faisait longtemps qu’on n'avait pas fait des tests. :-°
Le problème avec les filtres c’est qu’ils sont exécutés par le moteur d’ASP.NET MVC, donc lorsque l’on teste une action d’un contrôleur, on ne peut pas bénéficier du traitement du filtre. Mais en même temps, ce n’est pas trop grave car ce que l’on veut tester, c’est le filtre et seulement lui.

Et pour cela, nous avons besoin de bouchonner le contexte d’exécution. Qui a dit Moq ? ^^
Bien vu !

Cela n’a plus de secret pour vous désormais. Voici les deux tests qui permettent de valider le fonctionnement de mon filtre :

[TestClass]
public class AjaxFilterAttributeTests
{
    [TestMethod]
    public void AjaxFilterOnActionExecuting_AvecAjaxHeader_LaissePasserLaRequete()
    {
        NameValueCollection fausseCollection = new NameValueCollection { { "X-Requested-With", "XMLHttpRequest" } };
        Mock<ActionExecutingContext> context = new Mock<ActionExecutingContext>();
        context.Setup(r => r.HttpContext.Request.Headers).Returns(fausseCollection);

        AjaxFilterAttribute filtre = new AjaxFilterAttribute();
        filtre.OnActionExecuting(context.Object);
            
        Assert.IsNull(context.Object.Result);
    }

    [TestMethod]
    public void AjaxFilterOnActionExecuting_SansAjaxHeader_RenvoiHttpNotFoundResult()
    {
        NameValueCollection fausseCollection = new NameValueCollection();
        Mock<ActionExecutingContext> context = new Mock<ActionExecutingContext>();
        context.Setup(r => r.HttpContext.Request.Headers).Returns(fausseCollection);

        AjaxFilterAttribute filtre = new AjaxFilterAttribute();
        filtre.OnActionExecuting(context.Object);

        Assert.IsInstanceOfType(context.Object.Result, typeof(HttpNotFoundResult));
    }
}

Le principe est de bouchonner le contexte de filtre et de mettre (ou non) dans la requête le header Ajax. Lorsque le header n’est pas présent, je dois retrouver un HttpNotFoundResult  dans l’objet Result du contexte.

Un filtre global

Il est également possible de définir des filtres globaux. Imaginez par exemple que vous ayez réalisé un site énorme avec pléthore de contrôleurs et que vous ayez décidé de tout sécuriser et de ne permettre l’accès qu’à des gens authentifiés.
Allez-vous vraiment rajouter un [Authorize]  sur tous les contrôleurs ? Vraiment ?

Eh bien non… vous allez créer un filtre global qui s’applique à toutes les requêtes. Si vous avez généré une application non empty, vous avez déjà dans votre projet un fichier FilterConfig.cs du répertoire App_Start (si non, créez-le). Dedans, il y a déjà le code suivant :

public class FilterConfig
{
    public static void RegisterGlobalFilters(GlobalFilterCollection filters)
    {
        filters.Add(new HandleErrorAttribute());
    }
}

On y découvre que le filtre HandleErrorAttribute  dont nous avons parlé un peu plus haut est défini globalement dans notre site. Pour rappel, ce filtre permet d’afficher la vue Error (définie dans le répertoire Shared) si une exception est levée dans une méthode. En fait, pour être un peu plus précis, c’est le cas uniquement s’il y a une configuration particulière dans le web.config, à savoir :

<customErrors mode="On" defaultRedirect="Error" />

que l’on positionne dans la section <system.web> .
Ceci permet d’éviter que vos utilisateurs finaux voient toute la pile d’appel de votre code si jamais il y a une erreur, ce qui ne fait pas très classe et surtout qui peut renseigner vos utilisateurs sur le contenu de votre code et pourquoi pas sur des éventuelles failles…

Revenons à notre besoin de sécuriser tous les contrôleurs. Il vous suffit de rajouter la ligne suivante dans cette méthode qui définit les filtres globaux :

public class FilterConfig
{
    public static void RegisterGlobalFilters(GlobalFilterCollection filters)
    {
        filters.Add(new HandleErrorAttribute());
        filters.Add(new AuthorizeAttribute());
    }
}

Et le tour est joué.

Vous devrez bien sûr permettre au moins à l’utilisateur non authentifié de pouvoir s'authentifier, en décorant les actions adéquates avec l’attribut AllowAnonymous .

FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);

En résumé

  • Les filtres permettent de modifier la façon dont les actions des contrôleurs sont exécutées.

  • Il existe quatre types de filtres, s’exécutant dans l’ordre suivant : les filtres d’autorisations, les filtres d’actions, les filtres de résultats et les filtres d’exceptions.

  • Pour utiliser un filtre, on décore l’action ou le contrôleur de ce filtre.

  • Un filtre peut également se déclarer de manière globale et s’exécuter ainsi sur toutes les requêtes.

Example of certificate of achievement
Example of certificate of achievement