Action Filter Attributes et IoC

Le titre de cet article pourrait aussi bien être « Attributs .NET et IoC » car le sujet de fond est l’injection de dépendances dans des attributs personnalisés. Je m’intéresse ici spécifiquement aux Action Filters MVC/WebApi dont le design oriente le développeur vers une voie qui n’est pas en parfaite cohérence, je le pense, avec l’objectif d’utilisation des attributs tel que décrits originellement sur MSDN, c’est-à-dire comme simples descripteurs, conteneurs de métadonnées, prévus pour être « scannés » (par réflexion).

Dans cet article, j’emploie le terme « service » de façon interchangeable avec « composant », « dépendance », « objet »…, généralement résolu par un conteneur IoC… qui fournit/rend un service.

Avant d’en arriver au problème, je préfère expliquer pourquoi j’en suis arrivé à traiter ce sujet.

J’ai eu à utiliser des action filters de façon très ponctuelle, et il s’agissait de situations assez classiques, typiquement la gestion d’autorisations ou du cache. De plus, si ces filtres dépendaient d’autres services, ces derniers étaient de type singleton. Leur injection via des propriétés d’attributs ne posaient donc pas de problème particulier.

Je n’ai jamais été fan des services dont le cycles de vie est géré par requête. Ils ont sûrement leur place dans des situations que je n’ai pas encore rencontré, cependant très souvent je trouve qu’il s’agit d’un mauvais choix de design. C’est souvent pour gérer une forme de “contexte” partagé par différents objets à différents moments d’une requête. Et malheureusement, on ne choisit pas toujours ses dépendances, ni le cycle de vie qu’elles requièrent.

Quoiqu’il en soit, j’en suis arrivé à ce sujet par hasard, en effectuant une mise à jour de Simple Injector de la version 2 à la version 3. L’injection de propriétés dans des filtres webapi est devenue obsolète. L’excellente documentation de l’excellent Simple Injector mène à cet excellent article.

Le problème

L’article mentionné plus haut, ainsi que la documentation de Simple Injector rappellent que les attributs sont construits par le CLR et que, de ce fait, leur cycle de vie ne peut être contrôlé par le conteneur IoC. Tant que leurs dépendances sont des singletons, il n’y a pas de problème, et c’est pourquoi je n’ai pas été attentif à ce sujet jusqu’à maintenant. Cependant, et c’est ce que l’article souligne, si les dépendances ont un cycle de vie plus particulier, par requête notamment, mais plus généralement n’importe quel cycle de vie autre que singleton, le risque est d’injecter des dépendances dites captives: bien que libérées en fin de requêtes, elles risquent d’être réutilisées par le service dans lequel elles ont été injectées (et même si elles n’ont pas à être libérées, elles ne doivent plus être utilisées).

L’idée de fond est que les attributs .NET sont conçus pour annoter le code. Ils ne devraient contenir que des métadonnées. Alors que les frameworks WebApi et MVC, probablement par compromis “commercial”, orientent le développeur dans le sens opposé, à savoir mêler métadonnées et logique applicative. Cela fonctionne tant que les attributs ne manipulent pas d’état. Une façon très discrète de dire qu’ils ne doivent pas dépendre d’un contexte – d’un cycle de vie par requête. Et quoi que l’on puisse penser de la dépendance à un contexte (qui est aussi très utilisé par d’autres frameworks de Microsoft, comme Entity), cela devrait être proprement supportés par un mécanisme aussi important que les Action Filters.

Une solution

L’article en question propose un code de démonstration.

L’idée est de découper l’attribut en deux parties:

  • un attribut qui sert uniquement de marqueur (appelé attribut passif par Mark Seemann, ce que je trouve être un pléonasme lorsqu’on lit la documentation des attributs)
  • et un service qui implémente une interface générique responsable de la logique associée à cet attribut. Ce dernier étant le paramètre générique de l’interface. Au niveau de l’infrastructure, on inscrit un filtre global – le Dispatcher – à l’initialisation de l’application. Ce filtre est responsable de rechercher les attributs qui marquent l’action qui s’exécute et d’exécuter leur logique. Celle-ci est résolue à partir du conteneur détenu par le Dispatcher. Notons que le Dispatcher n’utilise pas une propriété statique pour obtenir le conteneur IoC. Ce dernier lui est fourni dans son constructeur. C’est bien plus propre.

La mise en oeuvre de cette solution est assez simple. La vraie charge supplémentaire est causée par le Dispatcher, mais c’est à faire une fois pour tous les attributs, quel que soit leur type, grâce à l’interface générique. Ensuite, que vos 100 lignes de code se trouvent dans une ou dans deux classes ne fait pas de réelle différence en terme de charge de travail. Et au contraire, je trouve que cela nous entraîne naturellement vers un code mieux structuré.

Les limites

Malheureusement, le code proposé dans l’article ne donne pas de solution à tous les types de filtres.

WebApi 2 prévoit ces quatres types de filtres, tous implémentent l’interface la plus générale IFilter :

  • IExceptionFilter
  • IActionFilter
  • IAuthorizationFilter
  • IAuthenticationFilter

L’exécution des filtres est gérée par la classe de base ApiController suivant un modèle en poupées russes. L’ordre d’exécution diffère selon le type de filtre. Les IExceptionFilter sont les plus proches de l’action exécutée, précédés par les IActionFilter, eux-mêmes précédés par les IAuthorizationFilter et enfin les IAuthenticationFilter. Tout cela est relativement bien détaillé dans le livre Designing Evolvable Web APIs with ASP.NET.

Pre / Post Action Filters

Ceux-ci sont encore assez simples à implémenter avec la même approche. Il suffit que notre interface mime les méthodes de ActionFilterAttribute, enrichies avec notre attribut. Evidemment, il faut adapter le Dispatcher pour qu’il prenne en compte ces deux méthodes.

Un exemple est sur GitHub. Celui-ci est une reprise du code de Steven, mais adapté pour le support des méthodes asynchrones et surtout des filtres post-action:


public interface IActionFilter<TAttribute> where TAttribute : Attribute
    {
        Task OnActionExecutingAsync(
                TAttribute attribute, 
                HttpActionContext actionContext, 
                CancellationToken cancellationToken);

        Task OnActionExecutedAsync(
                TAttribute attribute, 
                HttpActionExecutedContext actionExecutedContext, 
                CancellationToken cancellationToken);
    }
 
public class ActionFilterDispatcher : IActionFilter
    {
        public bool AllowMultiple
        {
            get
            {
                return true;
            }
        }

        private readonly Func<Type, IEnumerable<object>> _container;

        public ActionFilterDispatcher(Func<Type, IEnumerable<object>> container)
        {
            if (container == null)
                throw new ArgumentNullException(paramName: "container");
            _container = container;
        }

        public async Task<HttpResponseMessage> ExecuteActionFilterAsync(HttpActionContext context,
            CancellationToken cancellationToken, Func<Task<HttpResponseMessage>> continuation)
        {
            HttpActionDescriptor descriptor = context.ActionDescriptor;
            Attribute[] attributes = descriptor.ControllerDescriptor
                .GetCustomAttributes<Attribute>(inherit: true)
                .Concat(descriptor.GetCustomAttributes<Attribute>(inherit: true))
                .ToArray();

            var attributeAndFilters = new Tuple<dynamic, dynamic[]>[attributes.Length];
            for (int i = 0; i < attributes.Length; i++)
            {
                Attribute attribute = attributes[i];
                Type attrType = attribute.GetType();
                Type filterType = typeof(IActionFilter<>).MakeGenericType(attrType);
                dynamic[] filters = _container.Invoke(filterType).ToArray();
                attributeAndFilters[i] = new Tuple<dynamic, dynamic[]>((dynamic)attribute, filters);
            }

            foreach (var element in attributeAndFilters)
            {
                foreach (dynamic actionFilter in element.Item2)
                {
                    await actionFilter.OnActionExecutingAsync(element.Item1, context, cancellationToken);
                }
            }

            var executedContext = new HttpActionExecutedContext(context, exception: null);
            if (context.Response == null)
            {
                try
                {
                    executedContext.Response = await continuation();
                }
                catch (Exception ex)
                {
                    context.Response = null;
                    if (ex is HttpResponseException)
                        return ((HttpResponseException)ex).Response;

                    return context.Request.CreateErrorResponse(System.Net.HttpStatusCode.InternalServerError, ex);
                }
            }

            foreach (var element in attributeAndFilters)
            {
                foreach (dynamic actionFilter in element.Item2)
                {
                    await actionFilter.OnActionExecutedAsync(element.Item1, executedContext, cancellationToken);
                }
            }

            return executedContext.Response;
        }
    }

L’utilisation est simple :


    public class TestController : ApiController
    {
        [MeasureTimeFilter("some metadata")]
        public string Get()
        {
            return "Test OK";
        }
    }


    public class MeasureTimeFilterAttribute : Attribute
    {
        public string Label { get; set; }

        public MeasureTimeFilterAttribute(string label)
        {
            Label = label;
        }
    }


    public class MeasureTimeFilter : IActionFilter<MeasureTimeFilterAttribute>
    {
        private readonly ILogger _logger;
        private DateTime _startedAt;

        public MeasureTimeFilter(ILogger logger)
        {
            if (logger == null)
                throw new ArgumentNullException(paramName: "logger");
            _logger = logger;
        }

        public Task OnActionExecutingAsync(MeasureTimeFilterAttribute attribute, HttpActionContext actionContext, CancellationToken cancellationToken)
        {
            _startedAt = DateTime.UtcNow;
            _logger.Debug("Executing {0}.{1} with '{2}'...", actionContext.ActionDescriptor.ControllerDescriptor.ControllerName, actionContext.ActionDescriptor.ActionName, attribute.Label);
            return Task.CompletedTask;
        }

        public Task OnActionExecutedAsync(MeasureTimeFilterAttribute attribute, HttpActionExecutedContext actionExecutedContext, CancellationToken cancellationToken)
        {
            _logger.Debug("{0}.{1} executed in {2:F02} ms.", actionExecutedContext.ActionContext.ActionDescriptor.ControllerDescriptor.ControllerName, actionExecutedContext.ActionContext.ActionDescriptor.ActionName, DateTime.UtcNow.Subtract(_startedAt).TotalMilliseconds);
            return Task.CompletedTask;
        }
    }

La solution complète est sur GitHub. Le filtre mesure le temps d’exécution de l’action côté serveur, et affiche la mesure dans la sortie Debug. L’API de démonstration est accessible à l’URL /api/test du site.

Authentication, Authorization et Exception Filters

Une difficulté supplémentaire apparaît sur des filtres particuliers tels que les filtres d’autorisations et les filtres d’exception. En effet, ceux-ci sont traités de manière particulière par les frameworks MVC/webapi. Je ne dis pas qu’il n’est pas possible de les supporter avec cette approche, mais cela nous force à recopier les mécanismes internes du framework dans notre Dispatcher qui devient de ce fait plus compliqué et donc plus fragile, en particulier en cas d’évolution dans l’implémentation native de ces mécanismes (que l’on souhaite mimer dans notre Dispatcher).

Je n’ai pas fourni d’exemple car je pense que c’est une pente glissante pour une appproche générique. En revanche, si la situation est bien définie, il est probablement possible de reprendre l’idée générale de séparer l’attribut et le service responsable de sa logique. Ce qui va différer est la mise en oeuvre de cette mécanique.

On voit ici les limites du design actuel des frameworks WebApi et MVC. Tout reste possible bien entendu, mais pas de façon simple.

L’alternative est d’éliminer ce problème, en utilisant des filtres qui n’ont pas d’état. Ce n’est pas toujours possible, selon ce que l’on souhaite faire.

Références

Si ce n’est déjà fait, vous pouvez lire l’article à l’origine de celui-ci:
Dependency Injection in Attributes: don’t do it!

Understanding Action Filters (MSDN)
Attributes (MSDN)