Il y a quelque temps, j’ai voulu développer un moteur d’injection pour résoudre une problématique simple. J’adore Ninject mais il est lourd.

Après réflexion, j’ai remarqué que j’utilisais très peu les fonctionnalités d’injection conditionnelles et pas du tout l’injection par propriété. En revanche, je trouve la syntaxe de binding très agréable.

J’ai donc commencé à développer un framework d’injection pour le fun. Et aujourd’hui, c’est le seul framework que j’utilise pour mes projets. Je l’ai nommé Pattern car j’utilise ce framework pour différentes problématiques liées à Web API, Xamarin, Log, etc.

L’idée de ce framework est de pouvoir me constituer un ensemble de librairies qui me permettent de développer mes applications.

 

Pattern.Core

Tout vient de Pattern.Core, c’est la librairie qui permet d’initialiser un kernel d’injection.

var kernel = new Kernel();

Sur un kernel, il n’existe que 3 méthodes :

  • void Bind(Type @from, IFactory toFactory)
  • object Get(Type parentType, Type @from)
  • bool CanResolve(Type parentType, Type @from)

Tous les bindings peuvent se faire via la méthode “Bind” du dessus. Mais j’ai décidé d’extraire des helpers dans la librairie “Pattern.Config“. Grâce à cette librairie, je peux travailler sur la configuration de mon injection sans impacter l’architecture du kernel.

Des méthodes d’extension permettent de faire un Get<> générique. Ce qui permet de récupérer un objet déjà typé.

La méthode CanResolve est utile dans certains cas, lorsque l’on veut savoir si notre type peut être résolu avec le kernel.

Première étape, le binding

La première étape consiste à associer une factory à chaque type que l’on souhaite injecter. Ainsi, le kernel utilisera cette factory pour instancier le type concret à injecter.

Le plus simple est encore de faire une lambda factory. L’idée est de spécifier une lambda expression qui va permettre d’instancier le type concret :

    public class LambdaFactory : IFactory
    {
        private readonly Func<CallContext, object> create;

        public LambdaFactory(Func<CallContext, object> create)
        {
            this.create = create;
        }

        public virtual object Create(CallContext callContext)
        {
            return this.create(callContext);
        }
    }

Au moment du bind, il faut ajouter la factory dans la liste des factories associées à notre type. Le kernel ressemble à cela :

    public class Kernel : IKernel
    {
        private readonly Dictionary<Type, IList<IFactory>> binds;
        ...
        public void Bind(Type @from, IFactory toFactory)
        {
            if (!this.binds.ContainsKey(@from))
            {
                this.binds.Add(@from, new List<IFactory> { toFactory });
            }
            else
            {
                this.binds[@from].Add(toFactory);
            }
        }

Deuxième étape, le resolve

Pour résoudre notre type, il suffit de prendre le list de factory associé au type que l’on souhaite résoudre. Mais voilà, tout n’est pas si simple, nous allons devoir commencer par créer un CallContext :

            var callContext = CreateCallContext(parentType, @from);

Le CallContext va contenir un certain nombre de propriétés :

        public Type[] GenericTypes { get; }

        public Type InstanciatedType { get; }

        public Type Parent { get; }

        public bool AutomaticInstance { get; }

        public Type EnumerableType { get; }

Toutes ces propriétés sont là pour savoir comment instancier le type demandé :

  • si le type demandé contient des paramètres génériques.
  • s’il est instancié automatiquement pas le kernel (c’est-à-dire pas de binding référencé).
  • si le type est une liste.

La suite du get consistera à récupérer les factories associées au CallContext, puis, d’appeler ces factories pour instancier nos types.

            var factories = this.GetFactories(ref callContext);

            if (callContext.EnumerableType != null)
            {
                var instanciateValues = factories.Select(t => t.Create(callContext));
                var list = CreateList(callContext);

                foreach (var value in instanciateValues)
                {
                    list.Add(value);
                }

                return list;
            }

            if (factories.Count > 1)
            {
                throw new FactoryException(callContext.InstanciatedType);
            }

            if (factories.Count == 0)
            {
                return null;
            }

            return factories.Select(t => t.Create(callContext)).Single();

On doit donc procéder dans l’ordre suivant :

  • si c’est un type énumérable qui est demandé, alors on doit créer une liste et on appelle chaque factory.
  • si le type n’est pas énumérable, alors on vérifie que l’on n’a pas plus d’une factory.
  • si l’on a seulement une factory alors on l’appelle.
  • dans les autres cas, on renvoie en null.

 

Troisième étape, l’injection

Notre moteur est capable d’instancier des types en utilisant des factories. Mais à aucun moment il n’y a d’injection automatique par constructeur. La solution est simple, le TypeFactory.

Pour résumer, le type factory permet de :

  • choisir le constructeur le plus approprié (celui avec le plus de paramètres) et tester si vous pouvez résoudre tous les types passés en paramètre.
  • ensuite, on utilise le Kernel pour instancier tous les types qui doivent être injectés dans le constructeur choisi.
  • on appelle enfin le constructeur avec les paramètres instanciés.

C’est TypeFactory qui permet de créer cette injection. Si je voulais faire de l’injection par propriétés, je pourrais créer une autre factory qui parcourrait toutes les propriétés à injecter.

 

.Dispose()

Pour résumer, pour créer un Kernel il suffit de créer trois parties :

  • le moteur d’association
  • le moteur de factory
  • et la factory pour résoudre le constructeur

J’ai développé ce moteur d’injection en TDD. Il y a donc de nombreux tests qui me garantissent les règles métier d’injection, ce qui m’a permis de faire évoluer le code de mon Kernel et des factories. Aujourd’hui, j’utilise ce moteur d’injection dans tous mes projets personnels. Il est disponible en package NuGet afin de le faire évoluer rapidement.