jeudi 16 février 2012

Castle Windsor Container Builder

Ci dessous, une petite classe de derrière les fagots permettant de configurer Castle par le code avec une syntaxe fluide.

Cette classe “ContainerBuilder” permet aussi de configurer l’interception par proxy sur utilisation de certains attributs. Rappelons que Castle Windsor permet en plus de l’injection de dépendances de faire de la programmation orientée aspects par interception. Ce n’est pas aussi puissant qu’avec un véritable tisseur comme Postsharp mais c’est déjà pas si mal.

Cette implémentation de la programmation orientée aspect étant très fortement liée à l’injection de dépendance à travers le Framework Castle Windsor, il n’est pas possible :

  • D’avoir un aspect sur une méthode ou un objet qui ne serait pas gérés par l’injection de dépendances
  • Les aspects sont tissés par proxy. En pratique, un appel de méthode sera intercepté, pour une interface par une implémentation de cette méthode d’interface ; et pour une classe, par surcharge de cette méthode à travers un héritage. Par conséquent dans le cas de l’interception sur une méthode de classe, la classe doit pouvoir être héritée (pas de static, ni de sealed) et la méthode être surchargée (virtual en C#).
  • Les Aspects ne fonctionnent pas sur les évènements.
  • Dans le cas d’une interception à travers un proxy d’interface, l’objet final n’étant pas hérité, l’interception ne fonctionne que sur les méthodes portées par l’interface, pas les autres même si elles sont « public virtual ». Dans ce même cas, la méthode ne sera interceptée que si elle est appelée directement depuis le code client, et pas dans le cas où elle est appelée depuis une autre méthode de la classe.
/// <summary>
/// Cette classe encapsule la création d'un container Windsor
/// pour permettre d'enregistrer plus facilement des composants
/// et des intercepteurs déclenchés par des attributs
/// </summary>
public class ContainerBuilder
{
#region Registrations
class ComponentRegistration
{
public Type ServiceType { get; set; }
public Type ImplType { get; set; }
public LifestyleType LifeStyle { get; set; }
public object CtorParams { get; set; }
}

class InterceptorRegistration
{
public Type InterceptorType { get; set; }
public Type AttributeType { get; set; }
}

List<ComponentRegistration> _components = new List<ComponentRegistration>();
List<InterceptorRegistration> _interceptors = new List<InterceptorRegistration>();

void AssertNoRegistrationYetForComponent(Type serviceType)
{
if (_components.Any(c => c.ServiceType == serviceType))
throw new InvalidOperationException(string.Format("Le type de service {0} a déjà été enregistré.", serviceType));
}

void AssertNoRegistrationYetForInterceptor(Type interceptorType)
{
if (_interceptors.Any(c => c.InterceptorType == interceptorType))
throw new InvalidOperationException(string.Format("Le type d'intercepteurs {0} a déjà été enregistré.", interceptorType));
}

/// <summary>
/// Enregistre un composant constitué de l'interface de service
/// le répresentant et de son implémentation
/// </summary>
/// <typeparam name="TService">Interface du service</typeparam>
/// <typeparam name="TImpl">Implémentation du service</typeparam>
/// <param name="lifeStyle">Type de cycle de vie du composant, par défaut le composant sera créé à chaque demande</param>
/// <param name="ctorParamsAsAnonymousType">
/// Type anonyme représentant le dictionnaire des clé/valeurs de paramètres de constructeur
/// par exemple si le TImpl possède un constructeur dont la signature est la suivante : TImpl(string s1, int i2)
/// il faudra passer par exemple le parametre : new { s1 = "test", i2 = 42 }
/// </param>
public ContainerBuilder RegisterComponent<TService, TImpl>(LifestyleType lifeStyle = LifestyleType.Transient, object ctorParamsAsAnonymousType = null)
where TImpl : TService
{
AssertNoRegistrationYetForComponent(typeof(TService));
_components.Add(new ComponentRegistration()
{
ServiceType = typeof(TService),
ImplType = typeof(TImpl),
LifeStyle = lifeStyle,
CtorParams = ctorParamsAsAnonymousType
});
return this;
}

/// <summary>
/// Enregistre un composant directement par son implémentation finale
/// </summary>
/// <typeparam name="TImpl">Implémentation du service</typeparam>
/// <param name="lifeStyle">Type de cycle de vie du composant, par défaut le composant sera créé à chaque demande</param>
/// <param name="ctorParamsAsAnonymousType">
/// Type anonyme représentant le dictionnaire des clé/valeurs de paramètres de constructeur
/// par exemple si le TImpl possède un constructeur dont la signature est la suivante : TImpl(string s1, int i2)
/// il faudra passer par exemple le parametre : new { s1 = "test", i2 = 42 }
/// </param>
public ContainerBuilder RegisterComponent<TImpl>(LifestyleType lifeStyle = LifestyleType.Transient, object ctorParamsAsAnonymousType = null)
{
return RegisterComponent<TImpl, TImpl>(lifeStyle, ctorParamsAsAnonymousType);
}

/// <summary>
/// Enregistre un intercepteur déclenché par un attribut
/// </summary>
/// <typeparam name="TInterceptor">Type de l'intercepteur</typeparam>
/// <typeparam name="TAttribute">Type de l'attribut déclencheur</typeparam>
public ContainerBuilder RegisterInterceptor<TInterceptor, TAttribute>()
where TInterceptor : IInterceptor, new()
where TAttribute : Attribute
{
AssertNoRegistrationYetForInterceptor(typeof(TInterceptor));
_interceptors.Add(new InterceptorRegistration()
{
InterceptorType = typeof(TInterceptor),
AttributeType = typeof(TAttribute)
});
return this;
}

#endregion

#region Nested Classes
/// <summary>
/// Classe ajoutant les intercepteurs passés en constructeur
/// à chaque élément du model
/// </summary>
class AddingInterceptorSelector : IModelInterceptorsSelector
{
private readonly InterceptorRegistration[] _interceptors;

/// <param name="interceptors">Enregistrements d'intercepteurs à ajouter</param>
public AddingInterceptorSelector(InterceptorRegistration[] interceptors)
{
this._interceptors = interceptors;
}

public bool HasInterceptors(global::Castle.Core.ComponentModel model)
{
// On intercepte pas les intercepteurs
if (typeof(IInterceptor).IsAssignableFrom(model.Implementation)) return false;

var interceptorRefs = _interceptors
.Where(i => ReflectionHelper.HasTypeOrMembersAttribute(i.AttributeType, model.Service, model.Implementation));

return interceptorRefs.Any();
}

public InterceptorReference[] SelectInterceptors(global::Castle.Core.ComponentModel model, InterceptorReference[] interceptors)
{
// Selection des reférences d'intercepteur à ajouter
var interceptorRefs = _interceptors
.Where(i => ReflectionHelper.HasTypeOrMembersAttribute(i.AttributeType, model.Service, model.Implementation))
.Select(i => InterceptorReference.ForType(i.InterceptorType));

// Union des deux listes d'intercepteurs
return interceptors.Union(interceptorRefs).ToArray();
}
}

/// <summary>
/// Classe sélectionnant l'exécution des intercepteurs
/// en fonction des attributs de la méthode ou du type
/// </summary>
class AttributesSelectionSelector : IInterceptorSelector
{
readonly IEnumerable<InterceptorRegistration> _interceptors;
/// <param name="interceptors">Enumeration de clée/valeur représentant type d'intercepteur/type d'attribut</param>
public AttributesSelectionSelector(IEnumerable<InterceptorRegistration> interceptors)
{
this._interceptors = interceptors;
}

public IInterceptor[] SelectInterceptors(Type type, MethodInfo method, IInterceptor[] interceptors)
{
// transformation en liste pour supprimer facilement
var interceptorsList = interceptors.ToList();

foreach (var interceptor in interceptors)
{
// Sélection de l'intercepteur, s'il est présent dans la liste gérée par cet objet
var selected = _interceptors.Where(kv => kv.InterceptorType == interceptor.GetType()).ToArray();
if (selected.Length == 0) continue;
var intAndAtt = selected[0];

// Si l'attribut est absent l'intercepteur est supprimé
// pour ne pas être sélectionné
var finalTypeMethod = ReflectionHelper.GetFinalMethodInfo(type, method);
if (!ReflectionHelper.HasContextAttribute(intAndAtt.AttributeType, method, finalTypeMethod))
interceptorsList.Remove(interceptor);
}

// transformation en tableau pour résultat
return interceptorsList.ToArray();
}
}
#endregion

private static Parameter[] GetParametersFromAnonymous(object anonymous)
{
if (anonymous == null) return new Parameter[0]; // early return !

var result = new List<Parameter>();
foreach (var prop in anonymous.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance))
{
var propValue = prop.GetGetMethod().Invoke(anonymous, new object[0]);
if (propValue == null) propValue = "";

result.Add(Parameter.ForKey(prop.Name).Eq(propValue.ToString()));
}
return result.ToArray();
}

/// <summary>
/// Exécute la création d'un container prêt à l'emploi
/// en fonction des différents enregistrement déjà exécutés sur le builder
/// </summary>
public IWindsorContainer CreateContainer()
{
var container = new WindsorContainer();

// Creation du selecteur
var selector = new AttributesSelectionSelector(_interceptors.ToArray());

// Enregistrement des intercepteurs
foreach (var interceptor in _interceptors)
container.Register(Component.For(interceptor.InterceptorType));

// Enregistrement des composants
foreach (var component in _components)
{
var comp =
Component
.For(component.ServiceType) // Pour le service
.ImplementedBy(component.ImplType) // c'est cette implémentation
.SelectInterceptorsWith(selector) // avec la sélection d'intercepteurs
.LifeStyle.Is(component.LifeStyle) // et le cycle de vie
;

// avec ses paramètres statiques de constructeur
if (component.CtorParams != null)
comp = comp.Parameters(GetParametersFromAnonymous(component.CtorParams));

// avec ses paramètres dynamiques
// comp = comp.DynamicParameters .....
// TODO, si besoin

container.Register(comp);
}

// Ajout d'un ajout automatique de tous les intercepteurs sur tous les composants
container.Kernel.ProxyFactory.AddInterceptorSelector(
new AddingInterceptorSelector(_interceptors.ToArray())
);

return container;
}

}


En pratique voici un extrait du code des tests unitaire montrant l’utilisation de la classe :




IWindsorContainer GetContainer()
{
var builder = new ContainerBuilder()
.RegisterComponent<IServiceA, ServiceA>()
.RegisterComponent<IServiceB, ServiceB>()
.RegisterComponent<IServiceC, ServiceC>()
.RegisterComponent<IServiceD, ServiceD>()
.RegisterComponent<IServiceE, ServiceE>()
.RegisterComponent<IServiceF, ServiceF>()
.RegisterComponent<ServiceG>()
.RegisterComponent<IServiceH, ServiceH>(ctorParamsAsAnonymousType: new { stringForInternal = TEST_STRING_I })
.RegisterComponent<IServiceI, ServiceI>()
.RegisterInterceptor<ExceptionIntercept, ExceptionInterceptedAttribute>()
;

return builder.CreateContainer();
}


Le code source complet, comprenant une petite classe d’interrogation de la Reflection et bien sûr les tests unitaires, est téléchargeable ici.

Aucun commentaire: