High-Level Trace Aspect Sample

Open this sample in Visual Studio

The PostSharp.Samples.XTrace project demonstrates the use of PostSharp Laos to implement tracing.

It provides more advanced functionalities than the low-level trace aspect:

  1. It allows not only to trace method boundaries but also exceptions, field access and method invocations.
  2. It traces also the concrete values of parameters.

Note that, since it is based on PostSharp Laos, it is much more demanding than the low-level trace aspect. Using it too aggressively could result in significantly higher CPU consumption, principally because your program would because GC unfriendly. But it seems the price to pay  if you want to trace also the parameter values.

This example illustrates also the use of compile-time initialization.

Objective

We would like to develop the following custom attributes:

We want to trace as richly as possible, i.e. we would like to write the values of parameters, field values and so on. Also, we would like to take advantage of the compile-time initialization to avoid computing the formatting strings at runtime.

Since all these 4 aspects are very similar, we will comment only the most commonly used, XTraceMethodBoundaryAttribute.

Implementation of XTraceMethodBoundaryAttribute

Since we want to insert code before and after the method body, we derive our custom attribute from OnMethodBoundaryAspect and we implement the OnEntry, OnSuccess and OnException methods.

The skeleton of our custom attribute is the following:

[Serializable]
public sealed class XTraceMethodBoundaryAttribute : OnMethodBoundaryAspect
{

public override void OnEntry(MethodExecutionEventArgs context)
{
}

public override void OnSuccess(MethodExecutionEventArgs eventArgs)
{
}

public override void OnException(MethodExecutionEventArgs eventArgs)
{
}

}

Compile-Time Initialization

Writing the method name with all its parameters and generic type parameters is not trivial. It requires a computing power that we cannot neglect if we want our trace aspect to have acceptable runtime performance.

Fortunately, a lot of things are known at compile-time: the name of the type and the method, the number of generic parameters, and the number and the type of parameters. So the approach we chose is, at compile-time, to compose a template (formatting string) and, at runtime, to format these templates using the concretely received parameters and generic parameters.

For instance, if we have a method 

MyType<T1>::MyMethod<T2>(T2 arg1, int arg2)

the template may be

MyType<{0}>::MyMethod<{1}>({2}, {3})

At runtime, we would provide an array with the parameters.

The functionality of preparing templates and applying them is encapsulated in the classes Formatter and MethodFormatStrings. Their implementation is not interesting for our discussion, so we will not describe them here.

How to realize this with PostSharp Laos?

First, we define instance fields containing formatting strings and other things we don't want to compute at runtime. Then, implement the CompileTimeInitialize method to set up these fields. During post-compilation, PostSharp Laos will serialize the object. That means that fields that are initialized at compile-time to used at runtime should be serializable. Since it is the default behavior, we do not have to care much about that. But if there is a field that is used only at runtime or only at compile-time, we can safely mark it as non serializable. There is no such field in our example.

Finally here is the code of compile-time initialization:

string prefix;
MethodFormatStrings formatStrings;
bool isVoid;

public string Prefix
{
get { return this.prefix; }
set { this.prefix = value; }
}

public override void CompileTimeInitialize(MethodBase method)
{
this.prefix = Formatter.NormalizePrefix(this.prefix);
this.formatStrings = Formatter.GetMethodFormatStrings(method);
MethodInfo methodInfo = method as MethodInfo;
if (methodInfo != null)
{
this.isVoid = methodInfo.ReturnType == typeof(void);
}
else
{
this.isVoid = true;
}
}

Runtime Methods

Let's go back to the skeleton of our XTraceMethodBoundaryAttribute class. We have to implement the OnEntry, OnSuccess and OnException classes. All we have to do is to format the templates with the concrete parameters and to call the Trace.TraceInformation method.
public override void OnEntry(MethodExecutionEventArgs context)
{
Trace.TraceInformation(
this.prefix + "Entering " +
this.formatStrings.Format(
context.Instance,
context.Method,
context.GetArguments()));
Trace.Indent();

}

public override void OnSuccess(MethodExecutionEventArgs eventArgs)
{
Trace.Unindent();
Trace.TraceInformation(
this.prefix + "Leaving " +
this.formatStrings.Format(
eventArgs.Instance,
eventArgs.Method,
eventArgs.GetArguments()) +
(this.isVoid ? "" : Formatter.FormatString(" : {{{0}}}.", eventArgs.ReturnValue)));
}

public override void OnException(MethodExecutionEventArgs eventArgs)
{
Trace.Unindent();
Trace.TraceWarning(
this.prefix + "Leaving " +
this.formatStrings.Format(
eventArgs.Instance,
eventArgs.Method,
eventArgs.GetArguments()
) +
Formatter.FormatString(" with exception {0} : {{{1}}}.", eventArgs.Exception.GetType().Name,
eventArgs.Exception.Message));

}