Adding logging using Mono.Cecil

Akos Nagy
May 5, 2018

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.

Akos Nagy
Posted in .NET