Low-Level Trace Aspect Sample

Open this sample in Visual Studio

The PostSharp.Samples.Trace family of projects illustrates the implementation of a simple low-level aspect using the PostSharp Class Library and its low-level Code Weaver. It implements the "trace" aspect by injecting MSIL instructions into the method body. The result is a very effective way to trace an application.

The solution is composed of three projects:

Public Interface

The reason what we define the public interface in a different assembly than the implementation is that the implementation needs to reference PostSharp.Core.dll, and we do not want our project to depend on this assembly. The public interface will be deployed with each application that use our aspect.

We want our aspect to be represented as a multicast custom attribute. This will allow users to apply our trace aspect on method using wildcards. So we define a new class TraceAttribute that derives the MulticastAttribute from PostSharp.Public.dll.

In order to make the thing a little more complex, we want to give the possibility to set a trace category. This will be the property Category of our custom attribute.

We have to tell PostSharp what to load and execute our plug-in when it encounters our custom attribute. This is done by implementing the IRequirePostSharp interface, which defines the GetPostSharpRequirements method.

[AttributeUsage( AttributeTargets.Assembly | AttributeTargets.Module |
AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method |
AttributeTargets.Constructor,
AllowMultiple = true, Inherited = false )]
[MulticastAttributeUsage( MulticastTargets.Method | MulticastTargets.Constructor, AllowMultiple=true )]
public sealed class TraceAttribute : MulticastAttribute, IRequirePostSharp
{
string category = null;

public string Category
{
get { return category; }
set { category = value; }
}


PostSharpRequirements IRequirePostSharp.GetPostSharpRequirements()
{
PostSharpRequirements requirements = new PostSharpRequirements();
requirements.PlugIns.Add("PostSharp.Samples.Trace");
requirements.Tasks.Add("PostSharp.Samples.Trace");
return requirements;
}
}

Note that we have decorated the TraceAttribute class with two custom attributes:

Implementation

Overview

The implementation assembly will be deployed on the machine of each developer that works on an application using our aspect. It contains the logic that modifies these applications to add our tracing behavior.

Our implementation will rely on the Low-Level Code Weaver implemented in the PostSharp.CodeWeaver namespace. What we would like to do is to add some instructions before and after each targeted method.

For instance, we want the method to be modified like this:

void MyMethod()
{
Trace.WriteLine( "MyCategory - Entering MyClass.MyMethod" );
Trace.Indent();

try
{
// Initial method body here.
}
finally
{
Trace.Unindent();
Trace.WriteLine( "MyCategory - Leaving MyClass.MyMethod" );

}
}

Each plug-in should have:

We will describe these artifacts in the sections below.

Providing New Aspects

If we want to add aspects to the code, we could create a weaver, add advices to this weaver and execute it. But this is not a very responsible approach: if many plug-ins want to add aspects and execute their own weaver, this would result in suboptimal code generation and higher post-compiling time.

The recommended approach is to add the CodeWeaver task in the project and to implement the IAdviceProvider interface. This way, the module will be woven a single time, but every task has the possibility to provide the aspects it needs.

So our strategy will be to define a task (TraceTask) that implements the IAdviceProvider interface, and this task will be configured to require the CodeWeaver task.

Here is the configuration file:

<PlugIn xmlns="http://schemas.postsharp.org/1.0/configuration">
<SearchPath Directory="bin/{$Configuration}"/>
<TaskType Name="PostSharp.Samples.Trace"
Implementation="PostSharp.Samples.Trace.TraceTask, PostSharp.Samples.Trace.Weaver">
<Dependency TaskType="CodeWeaver"/>
</TaskType>
</PlugIn>

The skeleton of the Trace task will be as follows:

public class TraceTask : Task, IAdviceProvider
{
public void ProvideAdvices(Weaver codeWeaver)
{
// Add advices to codeWeaver.
}
}

Let's now look at the implementation of the ProvideAdvices method.

We have to add an advice for each (multicasted) instance of the Trace custom attribute in the module. This functionality is encapsulated by the CustomAttributeDictionaryTask class, so this concern is solved in a few lines of code.

Then, for each instance of TraceAttribute, we create an instance of our advice (described below) and apply it to the target method of this custom attribute instance.

public void ProvideAdvices(Weaver codeWeaver)
{
// Gets the dictionary of custom attributes.
CustomAttributeDictionaryTask customAttributeDictionary =
CustomAttributeDictionaryTask.GetTask(this.Project);

// Requests an enumerator of all instances of our TraceAttribute.
IEnumerator<ICustomAttributeInstance> customAttributeEnumerator =
customAttributeDictionary.GetCustomAttributesEnumerator(typeof(TraceAttribute), false);

// For each instance of our TraceAttribute.
while (customAttributeEnumerator.MoveNext())
{
// Gets the method to which it applies.
MethodDefDeclaration methodDef = customAttributeEnumerator.Current.TargetElement as MethodDefDeclaration;
if (methodDef != null)
{
// Constructs a custom attribute instance.
TraceAttribute attribute = (TraceAttribute)CustomAttributeHelper.ConstructRuntimeObject( customAttributeEnumerator.Current.Value, this.Project.Module);

// Build an advice based on this custom attribute.
TraceAdvice advice = new TraceAdvice(this, attribute);

codeWeaver.AddMethodLevelAdvice(advice,
new Singleton<MethodDefDeclaration>(methodDef),
JoinPointKinds.BeforeMethodBody | JoinPointKinds.AfterMethodBodyAlways,
null);
}
}
}

Inside the Aspect

Let's now look inside the TraceAdvice class, which implements the code injection. The most interesting top-level method is Weave. It is called in two opportunities: before and after the method body. In order to make the code more readable, we have split it in two methods: WeaveEntry and WeaveExit.

public void Weave(WeavingContext context, InstructionBlock block)
{
switch (context.JoinPoint.JoinPointKind)
{
case JoinPointKinds.BeforeMethodBody:
this.WeaveEntry(context, block);
break;

case JoinPointKinds.AfterMethodBodyAlways:
this.WeaveExit(context, block);
break;

default:
throw new ArgumentException(string.Format("Unexpected join point kind: {0}",
context.JoinPoint.JoinPointKind));
}
}
Let's now have a closer look at WeaveEntry. It has three steps:
  1. Create an InstructionSequence, inserts it to the InstructionBlock that has been dedicated to our advice, and attach the stock InstructionWriter.
  2. Using this InstructionWriter, emit a call to Trace.WriteLine, then to Trace.Indent.
  3. Detach the InstructionWriter from the InstructionSequence, which has the effect to commit the changes and to release the InstructionWriter so that it is available to the next advice weaver.
private void WeaveEntry(WeavingContext context, InstructionBlock block)
{
string methodName = context.Method.DeclaringType.ToString() + "/" + context.Method.ToString();

// Create a new instruction sequence and add it to the block
// dedicated to our advice. Attach the InstructionWriter.
InstructionSequence entrySequence = context.Method.Body.CreateInstructionSequence();
block.AddInstructionSequence(entrySequence, NodePosition.Before, null);
context.InstructionWriter.AttachInstructionSequence(entrySequence);


// Call Trace.WriteLine
context.InstructionWriter.EmitInstructionString(OpCodeNumber.Ldstr, (LiteralString)("Entry - " + methodName));
if (this.attribute.Category == null)
{
context.InstructionWriter.EmitInstruction(OpCodeNumber.Ldnull);
}
else
{
context.InstructionWriter.EmitInstructionString(OpCodeNumber.Ldstr, (LiteralString)this.attribute.Category);
}

context.InstructionWriter.EmitInstructionMethod(OpCodeNumber.Call, this.parent.traceWriteLineMethod);

// Call Trace.Indent()
if (context.Method.Name != ".ctor")
{
context.InstructionWriter.EmitInstructionMethod(OpCodeNumber.Call, this.parent.traceIndentMethod);
}

// Commit changes and detach the instruction sequence.
context.InstructionWriter.DetachInstructionSequence();
}
The WeaveExit method is similar.

Using the Trace Custom Attribute

The Trace custom attribute is as easy to use as any other custom attribute. For instance, if you want to trace a method, you can apply the custom attribute to this method:
[Trace]
public static void Traced()
{
System.Diagnostics.Trace.WriteLine( "Traced() should be traced." );
}
However, since our custom attribute is derived from MulticastAttribute, things are more powerful:
Here is some example of attributes multicasted to many methods using filters:
[assembly: Trace( Category = "BaseCategory" )]
[assembly: Trace( AttributeTargetTypes = "TestTargetProject.FirstNamespace.*",
Category = "FirstCategory" )]
[assembly: Trace( AttributeTargetTypes = "TestTargetProject.SecondNamespace.*",
Category = "SecondNamespace", AttributePriority = 10 )]
[assembly: Trace( AttributeTargetTypes = "TestTargetProject.SecondNamespace.*",
AttributeTargetMembers = "*NotTrace", AttributeExclude = true, AttributePriority = 20 )]