VI. Plus loin avec Unity : l'interception▲
Pour cette partie, je suggère l'utilisation de l'application console.
VI-A. L'interception▲
La notion d'interception est un peu plus complexe, au lieu de nous fournir une implémentation d'une interface, Unity nous retourne un objet proxy qui enrobe notre objet réel. Cet objet possède exactement les méthodes de l'interface, mais on peut ajouter des comportements à celles-ci. Exemple : On me remonte que ma couche d'accès aux données est lente, comment puis je mesurer les performances de celle-ci ? Soit je la modifie en conséquence et ajoute des traces d'audit un peu partout ce qui est assez coûteux, soit j'intercepte toutes les méthodes via un proxy : lors de l'interception je peux enregistrer le temps d'exécution. Ça a l'avantage de laisser intact mon code de la couche d'accès aux données et de s'activer via quelques lignes dans le fichier de configuration.
Nous allons créer une classe StopwatchInterceptionBehavior pour auditer nos performances, mais tout d'abord, il faut configurer Unity! Pour activer l'interception de manière générale, il faut déclarer l'extension via une balise sectionExtension dans la section unity puis enregistrer l'extension dans notre container via une balise extension. On peut enfin activer notre interception sur des objets désirés.
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section
name
=
"unity"
type
=
"Microsoft.Practices.Unity.Configuration.UnityConfigurationSection, Microsoft.Practices.Unity.Configuration"
/>
</configSections>
<unity
xmlns
=
"http://schemas.microsoft.com/practices/2010/unity"
>
<sectionExtension
type
=
"Microsoft.Practices.Unity.InterceptionExtension.Configuration.InterceptionConfigurationExtension, Microsoft.Practices.Unity.Interception.Configuration"
/>
<assembly
name
=
"MovieBase.Business.Interfaces"
/>
<assembly
name
=
"MovieBase.Business"
/>
<assembly
name
=
"MovieBase.DataAccess.Interfaces"
/>
<assembly
name
=
"MovieBase.XmlDataAccess"
/>
<assembly
name
=
"MovieBase.Tools"
/>
<namespace
name
=
"MovieBase.Business.Interfaces"
/>
<namespace
name
=
"MovieBase.Business"
/>
<namespace
name
=
"MovieBase.DataAccess.Interfaces"
/>
<namespace
name
=
"MovieBase.XmlDataAccess"
/>
<namespace
name
=
"MovieBase.Tools"
/>
<container>
<extension
type
=
"Interception"
/>
<register
type
=
"IDataAccess"
mapTo
=
"DataAccess"
>
<lifetime
type
=
"singleton"
/>
<constructor />
<property
name
=
"StoreFile"
value
=
"Store.xml"
/>
<interceptor
type
=
"InterfaceInterceptor"
/>
<interceptionBehavior
type
=
"StopwatchInterceptionBehavior"
/>
</register>
<register
type
=
"IBusinessLogic"
mapTo
=
"BusinessLogic"
/>
</container>
</unity>
</configuration>
Ici, on a activé l'interception via le Behavior StopwatchInterceptionBehavior. Pour ça, Unity va créer un proxy de notre couche d'accès, j'ai choisi un proxy de type InterfaceInterceptor. Voici les types de proxy différents :
- Transparent Proxy Interceptor : permet d'intercepter toutes les méthodes et plusieurs interfaces de l'objet. Revers de la médaille : il est très lent ! ;
- Interface Interceptor : il permet d'intercepter toutes les méthodes de l'interface. Revers de la médaille : ne fonctionne que pour ladite interface et pas les autres méthodes ;
- Virtual Method Interceptor : permet d'intercepter tous les membres déclarés virtual. Revers de la médaille : impose de déclarer ses membres virtual. Quid des assemblies tierces ?
Un excellent tableau dans la documentation de Unity détaille un peu plus les différences, cas d'utilisation et avantages/inconvénients.
Notre classe d'interception est assez simple, elle doit implémenter l'interface IInterceptionBehavior de Unity et donc implémenter trois membres : GetRequiredInterfaces qui renvoie les interfaces que l'objet intercepté doit implémenter, WillExecute qui renvoie si le behavior fait vraiment quelque chose (si non, il n'est pas inclus dans la chaine d'interception) et Invoke qui est appelé lors de l'interception. Cette méthode prend deux paramètres : un IMethodInvocation qui contient les paramètres de l'interception (méthode interceptée, paramètres, objet doté d'un proxy, etc.) et un délégué vers le prochain maillon de la chaine d'interception.
Voici donc l'exemple :
public
class
StopwatchInterceptionBehavior :
IInterceptionBehavior
{
public
IEnumerable<
Type>
GetRequiredInterfaces
(
)
{
return
new
HashSet<
Type>(
);
}
public
bool
WillExecute
{
get
{
return
true
;
}
}
public
IMethodReturn Invoke
(
IMethodInvocation input,
GetNextInterceptionBehaviorDelegate getNext)
{
var
stopwatch =
Stopwatch.
StartNew
(
);
try
{
return
getNext
(
)(
input,
getNext);
}
finally
{
Debug.
WriteLine
(
String.
Format
(
"Executing {0} took {1} ms"
,
input.
MethodBase.
Name,
stopwatch.
ElapsedMilliseconds));
}
}
}
Lors de l'exécution en Debug, on aperçoit dans la fenêtre Output de Visual Studio le message avec la durée d'exécution. Il est évidemment possible d'écrire dans un fichier journal plutôt que dans cette fenêtre. Ainsi, il est possible d'instrumenter un environnement de production.
VI-B. L'injection de Policy▲
Créer un Interception Behavior est pratique lorsque l'on veut ajouter un comportement à toutes les méthodes. Ça impose de configurer pour chaque élément les comportements à ajouter et l'interception se fait sur toutes les méthodes possibles (selon le type du proxy).
Il existe un moyen d'être un peu plus fin : il est possible de définir des règles par exemple « Si l'objet doté d'un proxy fait partie de telle assembly, applique ces comportements. S’il possède telle méthode, applique également ce comportement sur cette méthode uniquement ». C'est l'injection de Policy.
Pour l'exemple, nous allons retravailler notre couche Business : actuellement elle s'occupe de valider les paramètres d'entrée et de remonter une erreur si l'année n'est pas dans la plage attendue, etc. Nous allons mettre au point un système de validation des paramètres par attribut.
Il faut tout d'abord changer légèrement changer l'interface IBusinessLogic :
public
interface
IBusinessLogic
{
List<
Movie>
GetMovies
(
);
List<
Movie>
GetMoviesByCountry
(
[RequiredValidator]
[InValidator(
"UK"
,
"USA"
,
"France"
)]
String country);
List<
Movie>
GetMoviesByYear
(
[RangeValidator(Min =
1950
, Max =
2010
)]
ushort
year);
List<
Movie>
GetMoviesByYearAndCountry
(
[RangeValidator(Min =
1950
, Max =
2010
)]
ushort
year,
[RequiredValidator]
[InValidator(
"UK"
,
"USA"
,
"France"
)]
String country);
Movie GetMovie
(
[RangeValidator(Min =
0
, Max = Int32.MaxValue)]
int
movieId);
void
InsertMovie
(
[RequiredValidator]
String title,
[RequiredValidator]
[InValidator(
"UK"
,
"USA"
,
"France"
)]
String country,
[RangeValidator(Min =
1950
, Max =
2010
)]
ushort
year);
}
On remarque l'apparition de trois validateurs (RequiredValidatorAttribute, InValidatorAttribute et RangeValidatorAttribute) qui héritent tous les trois de BaseValidatorAttribute.
Il faut donc maintenant écrire un bout de code qui déclenche effectivement la validation des paramètres à chaque appel. Pour ceci, il faut créer une classe ValidationCallHandler qui se charge de faire le travail. Cette classe implémente ICallHandler qui ressemble à IInterceptionBehavior, car elle possède également la méthode Invoke comme ci-dessus. Le principe est simple : à chaque interception, on récupère les paramètres et pour chaque paramètre on récupère la valeur et ses attributs héritant de BaseValidatorAttribute. Pour chaque attribut, on vérifie la valeur et si une exception de validation est rencontrée, on le renvoie. Cette exception arrête la chaine des comportements d'interception (on n'appelle pas getNext).
public
class
ValidationCallHandler :
ICallHandler
{
public
IMethodReturn Invoke
(
IMethodInvocation input,
GetNextHandlerDelegate getNext)
{
try
{
for
(
int
i =
0
;
i <
input.
Arguments.
Count;
i++
)
{
var
parameterValue =
input.
Arguments[
i];
var
parameterInfo =
input.
Arguments.
GetParameterInfo
(
i);
var
attributes =
parameterInfo.
GetCustomAttributes
(
typeof
(
BaseValidatorAttribute),
false
);
foreach
(
var
attribute in
attributes.
OfType<
BaseValidatorAttribute>(
))
{
attribute.
Validate
(
parameterInfo.
Name,
parameterValue);
}
}
return
getNext
(
)(
input,
getNext);
}
catch
(
Exception exc)
{
return
input.
CreateExceptionMethodReturn
(
exc);
}
}
public
int
Order {
get
;
set
;
}
}
Il faut également configurer les objets à intercepter dans le fichier de configuration. Il faut définir le comportement de proxification et ajouter la balise policyInjection. On utilise également l'implémentation AttributeValidatedBusinessLogic qui, contrairement à la version originale, ne procède à aucune vérification des paramètres.
<register
type
=
"IBusinessLogic"
mapTo
=
"AttributeValidatedBusinessLogic"
>
<interceptor
type
=
"InterfaceInterceptor"
/>
<policyInjection/>
</register>
Désormais, pour utiliser notre CallHandler, il y a deux possibilités : continuer à procéder par attributs ou procéder par configuration.
VI-B-1. Injection de Policy par attribut▲
Plus simple à mettre en œuvre, elle nécessite cependant une recompilation du code pour l'activation/désactivation. Il suffit de créer un attribut ValidationCallHandlerAttribute héritant de HandlerAttribute.
[AttributeUsage(AttributeTargets.Method)]
public
class
ValidationCallHandlerAttribute :
HandlerAttribute
{
public
override
ICallHandler CreateHandler
(
IUnityContainer container)
{
return
new
ValidationCallHandler
(
);
}
}
Il suffit désormais de décorer les méthodes à qui il faut appliquer la vérification avec l'attribut.
[ValidationCallHandler]
List<
Movie>
GetMoviesByYear
(
[RangeValidator(Min =
1950
, Max =
2010
)]
ushort
year);
Désormais, si on essaie de saisir une date comme 1920, on obtient une exception.
La technique par attributs peut être intéressante pour, par exemple, créer une méthode transactionnelle. On peut créer un attribut Transaction qui enrôle toute la méthode dans une transaction. Il est ici utile de bénéficier de l'information que la méthode est transactionnelle directement dans le C#, ce que la technique par configuration ne permet pas.
VI-B-2. Injection de Policy par configuration▲
Légèrement plus complexe à mettre en place, mais plus flexible, elle permet de définir des règles pour appliquer plusieurs CallHandlers. Bien sûr, il faut auparavant enlever les attributs ValidationCallHandler sous peine de conflits.
Voici la configuration que nous allons utiliser :
<unity
xmlns
=
"http://schemas.microsoft.com/practices/2010/unity"
>
<sectionExtension
type
=
"Microsoft.Practices.Unity.InterceptionExtension.Configuration.InterceptionConfigurationExtension, Microsoft.Practices.Unity.Interception.Configuration"
/>
<assembly
name
=
"MovieBase.Business.Interfaces"
/>
<assembly
name
=
"MovieBase.Business"
/>
<assembly
name
=
"MovieBase.DataAccess.Interfaces"
/>
<assembly
name
=
"MovieBase.XmlDataAccess"
/>
<assembly
name
=
"MovieBase.Tools"
/>
<namespace
name
=
"MovieBase.Business.Interfaces"
/>
<namespace
name
=
"MovieBase.Business"
/>
<namespace
name
=
"MovieBase.DataAccess.Interfaces"
/>
<namespace
name
=
"MovieBase.XmlDataAccess"
/>
<namespace
name
=
"MovieBase.Tools"
/>
<namespace
name
=
"MovieBase.Tools.Validation"
/>
<container>
<extension
type
=
"Interception"
/>
<interception>
<policy
name
=
"ParametersValidationPolicy"
>
<callHandler
name
=
"ValidationCallHandler"
type
=
"ValidationCallHandler"
/>
<matchingRule
name
=
"AssemblyMatchingRule"
type
=
"AssemblyMatchingRule"
>
<constructor>
<param
name
=
"assemblyName"
value
=
"MovieBase.Business"
/>
</constructor>
</matchingRule>
</policy>
</interception>
<register
type
=
"IDataAccess"
mapTo
=
"DataAccess"
>
<lifetime
type
=
"singleton"
/>
<constructor />
<property
name
=
"StoreFile"
value
=
"Store.xml"
/>
<interceptor
type
=
"InterfaceInterceptor"
/>
<interceptionBehavior
type
=
"StopwatchInterceptionBehavior"
/>
</register>
<register
type
=
"IBusinessLogic"
mapTo
=
"AttributeValidatedBusinessLogic"
>
<interceptor
type
=
"InterfaceInterceptor"
/>
<policyInjection/>
</register>
</container>
</unity>
On remarque plusieurs choses : il faut créer une Policy appelée ici ParametersValidationCallHandler, indiquer le ou les CallHandler à appliquer et enfin une règle d'application. Ici on veut que cette règle ne soit appliquée que pour les éléments de ma couche Business. Il existe plusieurs MatchingRules par défaut : matcher sur la signature de méthode, un tag, le namespace, une propriété, etc.
De manière identique, avec 1920, une exception est levée.
VI-C. Conclusions sur l'interception▲
Unity se branche très bien sur les Enterprise Library de Microsoft. Dans cette bibliothèque, il y a plusieurs CallHandler disponibles : pour la sécurité un AuthorizationCallHandler chargé de vérifier les autorisations, pour l'instrumentation un LogCallHandler et un PerformanceCounterCallHandler, pour la gestion d'exception un ExceptionCallHandler pour la gestion d'exception et enfin un ValidationCallHandler un peu plus complexe que l'exemple ci-dessus.
Ces méthodes sont un bon moyen de rajouter des comportements communs plutôt techniques à des briques métier. Cela permet de nettoyer par exemple la couche Business et n'y laisser que du code métier.