Repository générique et IoC avec Castle Windsor

Publié le 05 octobre 2009 par Olivier Duval

Repository générique

Lorsque l'on a à interroger plus d'une entité, il convient de factoriser son code afin d'en réduire le nombre de lignes (maintenance, évolution, ...).

Pour cela, on peut utiliser un repository générique qui consiste à factoriser les fonctions les plus communes, à savoir : Get, Delete, Save, GetAll, voire Find en utilisant une expression (lire ce billet sur les find). On peut alors représenter cela au travers d'une interface générique, socle qui sera à compléter pour des besoins plus spécifiques.

public interface IRepository<T>
{
        T GetById(int id);
        void Save(T entite);
        void Delete(T entite);
        List<T> GetAll();
        List<T> Find(Func<T, bool> expression);
}

ou mieux, si l'on souhaite un type generic pour l'ID de nos entités :

public interface IRepository<T,TId>
{
        T GetById(TId id);
        void Save(T entite);
        void Delete(T entite);
        List<T> GetAll();
        List<T> Find(Func<T, bool> expression);
}

[photo]

GetById est optionnel, le Find avec l'expression pourrait suffire pour retrouver une entité par son id : Find(x=>x.Id==4)

On a l'implémentation suivante, on utilise NHibernate comme ORM, cela reste valable avec d'autres :

public class Repository<T,TId> : IRepository<T,TId>
{
   public T GetById(TId id) {  return session.Get<T>(id); }   
 
   public void Save(T entite)
   {
      try {           
          session.BeginTransaction();
       session.SaveOrUpdate(entite);
       session.Transaction.Commit();
      } catch(Exception e)                    
           if(session.Transaction.IsActive)
         session.Transaction.Rollback();
      }
   }
 
   public void Delete(T entite)
   {
            try
            {
                session.BeginTransaction();
                session.Delete(entite);
                session.Transaction.Commit();     
            }
            catch (Exception e)
            {
                if (session.Transaction.IsActive)
                    session.Transaction.Rollback();
            }
        }
 
   public List<T> GetAll()
   {      
         return session.Linq<T>().ToList();
   }
 
   public List<T> Find(Func<T, bool> expression)
   {
     return session.Linq<T>().Where(expression).ToList();
   }
}

IoC

Pour chaque type d'entité, on peut alors se créer un repository en lui indiquant son type.

Pour cela, nous allons utiliser Castle Windsor, un framework IoC qui permet également d'instancier les types génériques. Mais au fait, Castle Windsor, ça sert à quoi en 2 mots exactement ? Ce framework nous permettra, au même titre qu'une factory, d'instancier un objet de type A qui implémente une interface, et ce, lorsque l'on lui demandera. Il permet également et surtout d'instancier des objets qui seront injectés (sous forme d'une propriété ou dans le constructeur) dans notre objet de type A : un objet DAL, de mails, de logging, ...et uniquement à base d'interface afin de découpler les objets entre eux. En gros, le conteneur IoC s'intercale entre la couche présentation (Web par exemple) et nos implémentations (services implémentant des interfaces).

Imaginons la relation suivante : des associations et des responsables

Pour avoir 2 repository, 1 pour chaque entité, dont Assoc qui aura une clé de type string, avec quelques opérations :

IRepository<Assoc, string> repoAssoc = IoC.Resolve<IRepository<Assoc, string>>();
IRepository<Responsable, int> repoResponsable = IoC.Resolve<IRepository<Responsable, int>>();
 
var myrepo = repoAssoc.Get("myassoc1");
var mylistrespons = repoResponsable.Find(a=>a.Prenom.StartsWith("oli"));
var resp = repoResponsable.Get(37);
resp.Nom="dudu";
repoResponsable.Save(resp);

IoC est une classe utilitaire qui encapsule l'appel Windsor (voir aussi ce billet sur le sujet) :

public static class IoC
{           
      private static IWindsorContainer _container = new WindsorContainer(new XmlInterpreter());
      public static T Resolve<T>() { return _container.Resolve<T>(); }
}

Configuration Castle Windsor

Au niveau configuration de Castle Windsor pour représenter les instanciations et les injections, le framework permet de déclarer le type des générics à l'aide du format suivant que cela soit pour le service (le contrat) et le type (l'implémentation) :

mynamespace.IInterface`N[[type1], [type2]{, [typeN]}]], assembly

ou N est le nombre de paramètres de notre générique ici T et TId, donc 2 (s'il il y avait eu que T on aurait 1, trop fort), type, le type à instancier pour les génériques, ici, nos entités Assoc et Responsable.

Pour Assoc, on aurait le repository Repository<Assoc,int>, l'implémentation de IRepository<T,TId> pour l'entité Assoc, qui sera représenté dans la configuration Windsor par (nos POCO / entités et interfaces sont dans l'assembly Connector, et les implémentations, dans l'assembly Impl) :

service="IRepository`2[[Connector.Assoc, Connector],[System.String]], Connector", type="Repository`2[[Connector.Assoc, Connector],[System.Int32]], Impl"

signifie : instancie moi Repository<Assoc, int> de l'assembly Impl, qui implémente IRepository<Assoc, int>, de l'assembly Connector.

Dans l'exemple, on injecte dans les composants RepositoryAssoc et RepositoryResponsable, un objet DALContainer, notre ORM. Le paramètre lifefestyle="transient" demande à Castle Windsor d'instancier à chaque fois nos objets, a contrario d'un singleton du comportement par défaut.

<?xml version="1.0"?>
<configuration>
    <components>
      <!-- container ORM -->
      <component id="DALContainer"
                   type="Impl.DALContainer, Impl"
                   service="Connector.IDALContainer, Connector">        
      </component>
 
      <!-- Repository Assoc -->
      <component id="RepositoryAssoc"
               service="Connector.IRepository`2[[Connector.Assoc, Connector],[System.String]], Connector"
               type="Impl.Repository`2[[Connector.Assoc, Connector],[System.String]], Impl"
               lifestyle="transient">
        <parameters>
          <container>${DALContainer}</container>
        </parameters>
      </component>
 
      <!-- Repository Responsable -->
      <component id="RepositoryResponsable"
               service="Connector.IRepository`2[[Connector.Responsable, Connector],[System.Int32]], Connector"
               type="Impl.Repository`2[[Connector.Responsable, Connector],[System.Int32]], Impl"
               lifestyle="transient">
        <parameters>
          <container>${DALContainer}</container>
        </parameters>
      </component>
    </components>
</configuration>

A base d'une configuration simple et d'un usage accessible, on aura le contrôle de nos repository pour interroger nos entités, Castle Windsor, mangez-en !