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:
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.
We would like to develop the following custom attributes:
XTraceExceptionAttribute
: write a trace message when the method to which it is applied fails with an exception.
XTraceFieldAccessAttribute
: write a trace message whenever the field to which it is applied is accessed (get or set operation).XTraceMethodBoundaryAttribute
: write a trace message when the method to which it is applied starts or ends (successfully or not).XTraceMethodInvocationAttribute
: write a trace message when the message to which it is applied is invoked (this method may be defined in a different assembly).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
.
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)
{
}
}
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;
}
}
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));
}