Adding logging using Mono.Cecil
A while back (actually, it's been a long while) I found an interesting question on Stackoverflow.
The question was about adding logging to method calls automatically. Or to rephrase: the question was basically about handling aspect-oriented programming using Mono.Cecil in .NET.
So given this piece of code:
public class Target
{
// Target method that needs to be logged
public void Run(int arg0, string arg1)
{
Console.WriteLine("Run method body");
}
}
public static class Trace
{
// This is the logger method that must be called at the beginning of other methods
public static void LogEntry(string methodName, object[] parameters)
{
Console.WriteLine("******Entered in " + methodName + " method.***********");
Console.WriteLine(parameters[0]);
Console.WriteLine(parameters[1]);
}
}
This is the piece of code (complete with comments) that achieves the goal:
public class Sample
{
private readonly string _targetFileName;
private readonly ModuleDefinition _module;
public ModuleDefinition TargetModule { get { return _module; } }
public Sample(string targetFileName)
{
_targetFileName = targetFileName;
// Read the module with default parameters
_module = ModuleDefinition.ReadModule(_targetFileName);
}
public void Run(string type, string method)
{
// Retrive the target class.
var targetType = _module.Types.Single(t => t.Name == type);
// Retrieve the target method.
var runMethod = targetType.Methods.Single(m => m.Name == method);
// Get log entry method ref to create instruction
var logEntryMethodReference = _module.Types.Single(t => t.Name == "Trace").Methods.Single(m => m.Name == "LogEntry");
var newInstructions = new List<Instruction>();
// Create variable to hold the array to be passed to the LogEntry() method
var arrayDef = new VariableDefinition(new ArrayType(_module.TypeSystem.Object));
// Add variable to the method
runMethod.Body.Variables.Add(arrayDef);
// Get a ILProcessor for the Run method
var processor = runMethod.Body.GetILProcessor();
// Load to the stack the number of parameters
newInstructions.Add(processor.Create(OpCodes.Ldc_I4, runMethod.Parameters.Count));
// Create a new object[] with the number loaded to the stack
newInstructions.Add(processor.Create(OpCodes.Newarr, _module.TypeSystem.Object));
// Store the array in the local variable
newInstructions.Add(processor.Create(OpCodes.Stloc, arrayDef));
// Loop through the parameters of the method to run
for (int i = 0; i < runMethod.Parameters.Count; i++)
{
// Load the array from the local variable
newInstructions.Add(processor.Create(OpCodes.Ldloc, arrayDef));
// Load the index
newInstructions.Add(processor.Create(OpCodes.Ldc_I4, i));
// Load the argument of the original method (note that parameter 0 is 'this', that's omitted)
newInstructions.Add(processor.Create(OpCodes.Ldarg, i+1));
if (runMethod.Parameters[i].ParameterType.IsValueType)
{
// Boxing is needed for value types
newInstructions.Add(processor.Create(OpCodes.Box, runMethod.Parameters[i].ParameterType));
}
else
{
// Casting for reference types
newInstructions.Add(processor.Create(OpCodes.Castclass, _module.TypeSystem.Object));
}
// Store in the array
newInstructions.Add(processor.Create(OpCodes.Stelem_Ref));
}
// Load the method name to the stack
newInstructions.Add(processor.Create(OpCodes.Ldstr, method));
// Load the array to the stack
newInstructions.Add(processor.Create(OpCodes.Ldloc, arrayDef));
// Call the LogEntry() method
newInstructions.Add(processor.Create(OpCodes.Call, logEntryMethodReference));
// Add the new instructions in referse order
foreach (var newInstruction in newInstructions.Reverse<Instruction>())
{
var firstInstruction = runMethod.Body.Instructions[0];
processor.InsertBefore(firstInstruction, newInstruction);
}
// Write the module with default parameters
_module.Write(_targetFileName);
}
}
Hopefully the comments give you a general idea about how to use Mono.Cecil to implement AOP, but if you have any questions, feel free to comment.