Dans de nombreux cas, il peut être intéressant de suivre l’activité d’une classe quelconque, en particulier pour logger tous les appels à toutes ses méthodes. Quelques exemples :
- Suivre l’activité d’un service Web.
- Enregistrer tous les appels à une couche d’accès aux données.
- Enregistrer tous les appels sur une classe réalisant un accès à une ressource externe, comme une classe réalisant les accès au disque, à un FTP, une classe encapsulant l’envoi de mails, etc.
A priori, il n’existe aucun mécanisme permettant de logger automatiquement tous les appels à une classe quelconque, de préférence sans modifier le code appelant ni le code appelé. Parmi les 4 sections suivantes, les 3 premières présentent des méthodes approchantes. La dernière présente une méthode originale basée sur les Code Contracts permettant d’atteindre le résultat recherché.
IntelliTrace & Rapports de performance
Il s’agit des méthodes les plus génériques. Elles permettent de tout savoir sur l’exécution d’un programme, mais sont limitées à la production de données de trace. Il ne sera donc pas possible de configurer totalement les données produites. De plus, seule la version Ultimate de Visual Studio 2010 possèdent ces fonctionnalités.
Concernant l’utilisation d’IntelliTrace, il est nécessaire d’activer l’option Evènements IntelliTrace et information sur les appels pour obtenir des informations sur les appels de méthodes :

Avec le bout de code suivant en exemple :
static void Main(string[] args)
{
Toto toto = new Toto();
toto.Operation();
}
La fenêtre IntelliTrace affiche à l’exécution :

On a bien une trace de tous les appels à la classe Toto. Bien entendu, dans le cas d’une application professionnelle complexe, le nombre d’appels devient tel que cette méthode n’est pas envisageable.
Concernant l’utilisation des rapports de performance, il suffit de lancer l’assistant performance :

Et de choisir Instrumentation :

Après exécution du programme, le profileur produit un fichier .vsp qui présente un grand nombre d’informations statistiques sur l’exécution. Par exemple, en basculant en mode d’affichage Fonctions, on voit entre autres que la méthode Operation a été appelée une seule fois :

L’IntelliTrace et les rapports de performance ne permettent cependant pas de logger explicitement les appels à une classe en particulier, et il n’est pas raisonnable de les utiliser sur une application de production pendant une durée trop longue. Il s’agit donc seulement de solutions approchantes au problème énoncé.
Méthodes existantes
Bien souvent, s’il y a besoin de suivre l’activité d’une classe en particulier, c’est que le contexte applicatif est propice à ce besoin. Dans ces cas, les technologies existantes anticipent le besoin en proposant les mécanismes nécessaires. Par exemple, pour logger les appels à un Service WCF, il est possible de créer un EndpointBehavior personnalisé dans lequel on passera avant/après chaque requête (methodes AfterReceiveRequest et BeforeSendReply). Autre exemple, en WebForms ASP.NET, tout accès à une page ASPX peut être détecté au moyen de l’évènement Page_BeginRequest. Enfin, pour logger tous les appels aux bases de données, le problème est souvent contourné par l’utilisation de SQL Server Profiler ou de tout autre outil similaire avec d’autres SGBDR.
Toutes ces méthodes sont efficaces, mais elles ne répondent au problème énoncé que dans des cas particuliers.
Design Patterns
Certains design patterns, comme l’Observer ou le Decorator, peuvent être utilisés pour ajouter une fonction de Log à une classe existante. Néanmoins, pour les mettre en place, il faut soit modifier le code de la classe à logger, soit modifier le code appelant. Par exemple, dans le cas du pattern Decorator, il faut :
- extraire une interface ou une classe abstraite de la classe à surveiller,
- implémenter une autre classe de log qui implémente la classe abstraite ou l’interface
- modifier le code appelant la méthode pour ajouter la fonction de log.
On peut bien entendu utiliser le pattern Factory pour ajouter systématiquement la fonction de log, mais cela ne fait que déplacer le problème, puisque le code appelant doit alors solliciter la Factory. En outre, ceci oblige à appliquer le pattern pour chaque classe à surveiller et à “doubler” les méthodes, donc on augmente la complexité et on ajoute un niveau d’abstraction. Tout ceci est disproportionné par rapport à un problème simple a priori.
Utilisation détournée des Code Contracts
Inclus dans mscorlib à partir du Framework .NET 4.0, les Code Contracts proposent des fonctionnalités de validation de code activables par un certain nombre d’options de compilation. A la base des Code Contracts, il y a 3 mécanismes permettant de réaliser cette validation de code :
- La méthode Contract.Requires permettant de spécifier une précondition sur une méthode
- La méthode Contract.Ensures permettant de spécifier une postcondition sur une méthode
- L’attribut ContractInvariantMethod appliqué à une méthode, activant une validation effectuée “à la fin de chaque méthode publique de la classe”.
C’est bien sûr l’attribut ContractInvariantMethod qui va être utilisé comme support pour réaliser la fonction de log. En effet, l’idée est de profiter de ce mécanisme pour réaliser un log plutôt qu’une validation. Pour comprendre comment fonctionnent réellement les Code Contracts, la première étape consiste à activer l’option correspondante dans l’onglet “Code Contracts” (des paramètres du projet) disponible si la fonctionnalité est installée :

Dans la classe Toto à surveiller, il suffit d’ajouter une méthode ayant l’attribut ContractInvariantMethod de la façon suivante :
class Toto
{
public void Operation()
{
}
[ContractInvariantMethod]
void aaa()
{
Contract.Invariant(false);
}
}
Après compilation et utilisation de Reflector sur l’assembly produite, on voit que l’activation des Code Contracts modifie la compilation du programme en ajoutant automatiquement, à la fin de chaque méthode publique, un appel à la méthode aaa du bout de code précédent, rebaptisée $InvariantMethod$. Par exemple, pour la méthode Operation qui ne faisait rien, on a en fait :

Par l’utilisation des Code Contracts, on a donc un mécanisme automatisé pour appeler une méthode après n’importe quel appel d’une méthode publique d’une classe. Ceci correspond bien au but recherché, encore faut-il arriver à injecter du code pour effectuer la surveillance de la classe. En effet, si on essaie de modifier la méthode aaa pour y réaliser autre chose que des appels successifs à Contract.Invariant, alors une erreur de compilation survient. Cependant, si on retourne dans les propriétés du projet, on voit qu’il est possible de définir une classe pour Custom Rewriter Methods dans laquelle on va pouvoir injecter du code quelconque :

Les méthodes de cette classe seront appelées uniquement si une validation par les Code Contracts échoue, c’est pourquoi il y a cette instruction dans la méthode aaa permettant de faire échouer systématiquement la validation :
Contract.Invariant(false);
Dans la classe indiquée pour Custom Rewriter Methods sont attendues des méthodes statiques ayant des signatures particulières. Pour celle qui nous intéresse ici, c’est-à-dire Invariant, on pourrait définir la méthode suivante :
public static class RuntimeFailureMethods
{
public static void Invariant(bool cond, string userMsg, string condText)
{
StackFrame[] sf = new StackTrace().GetFrames();
if (sf.Length >= 3)
{
MethodBase mb = sf[2].GetMethod();
Console.WriteLine("Appel de {0}.{1}", mb.DeclaringType.Name, mb.Name);
}
}
}
Ce code détermine quelle méthode a abouti à l’appel à Invariant en accédant au 3e élément de la pile des appels. En effet, le premier élément est toujours la méthode courante, le deuxième est la méthode aaa, et le troisième est bien la méthode d’origine appelée sur notre classe à surveiller. En exécutant toujours notre même Main, on obtient :

Les Code Contracts permettent donc bien de suivre automatiquement l’activité d’une classe quelconque, sans modification du code appelant ni du code appelé.