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:
PostSharp.Samples.Trace
is the
Public Interface.PostSharp.Samples.Trace.Weaver
is
the implementation.PostSharp.Samples.Trace.Test
is a
sample project using the "Trace" aspect.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:
AttributeUsage
is an information
for the compiler:
it specifies that Trace custom attribute can be defined on the level
of assemblies, modules, types and methods.MulticastAttributeUsage
is for the
post-compiler: it
specifies that the custom attribute should be propagated up to the
level of methods. For instance, it tells that when the attribute is
defined on a type, it will be propagated to all the methods of this
time.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:
TraceTask
,
which derives the Task
class.We will describe these artifacts in the sections below.
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);
}
}
}
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)Let's now have a closer look at
{
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));
}
}
WeaveEntry
.
It has three steps:InstructionSequence
,
inserts it to the InstructionBlock
that has been dedicated to our advice, and attach the stock InstructionWriter
.InstructionWriter
,
emit a call to Trace.WriteLine
, then to Trace.Indent
.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)The
{
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();
}
WeaveExit
method is similar.[Trace]However, since our custom attribute is derived from
public static void Traced()
{
System.Diagnostics.Trace.WriteLine( "Traced() should be traced." );
}
MulticastAttribute
,
things are more powerful:AttributeTargetMembers
property.AttributeTargetTypes
and AttributeTargetMembers
properties.[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 )]