Introduction
In many scenario, you will want to generate fields in an existing class of an existing assembly. Let's imagine for example that you want to do some lazy loading in your classes. As a consequence, in all of your properties you will have some code checking if your private backing field has already been loaded or not. For that, you will have two solutions : look the nullity of your backing field, or look a boolean variable "hasBeenLoaded". This second solution is the best. Indeed the nullity can be a valid value and retreiving a "null" from your database shall prevent you from doing another get in your database.
Of course, this solution will result in a lots of cumbersome code that you will need to propagate in all of your classes. So you will probably think "aspect" and so use an AOP framework like PostSharp to do all of this plumbing code. You will find many posts on Internet speaking of dealing with LazyLoading functionality in PostSharp, but they usually deal with the nullity of the backing field.
So how can we use PostSharp (I will use the 1.5 version for this example. The version 2.0 of PostSharp will probably simplify this solution) to generate a boolean field ?
This example (that should be seen more like a proof of concept) will probably make you think of many other cases where you will need to generate some code in an existing class. Here we will generate a simple variable in a class, but the code can be easily adapted to generate a field per existing property.
Let's create an aspect
With PostSharp, many high-level aspects already exist and they will be sufficient for many of your scenarios. However for code generation, you will need to go a bit more low level.
So in your project you will need to add a reference to PostSharp.Public (in the GAC) and to PostSharp.Core (that can be found in the installation folder of PostSharp). Now you can create your attribute :
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
[MulticastAttributeUsage(MulticastTargets.Class, AllowMultiple = false)]
[RequirePostSharp("InjectCodeAttributePlugin", "InjectCodeAttributeTask")]
public class InjectCodeAttribute : MulticastAttribute { }
With the AttributeUsage attribute, I specify that this attribute will target only classes, and the MulticastAttributeUsage attribute tells that only the tagged class will be "aspected" (I could here decide that when I put my attribute on a class, it will get automatically propagated to all properties). I also specify the RequirePostSharp attribute. Indeed as we work low-level, we need to tell PostSharp what to do when seeing this attribute. So in this atribute, we specify the plugin file and the task related to our attribute.
The plugin file can be seen like a configuration file, listing tasks to perform and dependencies between them. Let's see the file we will create in our case. In our attribute, we have specified InjectCodeAttributePlugin, so it means that PostSharp will look for a file named InjectCodeAttributePlugin.psplugin. Let's create such a file in our project:
<?xml version="1.0" encoding="utf-8" ?>
<PlugIn xmlns="http://schemas.postsharp.org/1.0/configuration">
<SearchPath Directory="bin/{$Configuration}"/>
<TaskType Name="InjectCodeAttributeTask"
Implementation="LowLevelAspect.InjectCodeAttributeTask, LowLevelAspect">
<Dependency TaskType="CodeWeaver"/>
</TaskType>
</PlugIn>
What does it say ? Simply that the task InjectCodeAttributeTask (the name we specified in the RequirePostSharp attribute) will be implemented by a class of the same name, in a specific assembly and namespace. We also specify that our task will depend on a task called CodeWeaver. This is an out-of-the-box task of PostSharp that allow us to re-use the current weaver process. It's a best practice for nearly all the low level tasks you will create.
Let's create a task
So what does the task ? In my view, it can be seen as an orchestrator (or provider) of advices. And an advice is the object that will inject (weave) code in our class. Remember that we are doing low level aspects. So when we speak of code, we mean IL code.
Our task is an IAdviceProvider, meaning that it will provide PostSharp with the different advices we want to define (only one in our case : the one responsible for generating a new field). So the task's responsability is limited to two things:
- As the task knows on which specific member it's weaving (type, method, ...), it can extract everything our advices will need for injection (types, methods, ...)
- Iterate thru all the attributes (and attribute's targets) we have specified in our code to create the advices. Here is how we deal with that :
internal class InjectCodeAttributeTask : Task, IAdviceProvider
{
protected override void Initialize()
{
base.Initialize();
}
void IAdviceProvider.ProvideAdvices(Weaver codeWeaver)
{
var annotationRepository = AnnotationRepositoryTask.GetTask(this.Project);
var customAttributeEnumerator
= annotationRepository.GetAnnotationsOfType(typeof(InjectCodeAttribute), false);
while ( customAttributeEnumerator.MoveNext() )
{
InjectCodeAttribute attribute
= (InjectCodeAttribute)
CustomAttributeHelper.ConstructRuntimeObject(customAttributeEnumerator.Current.Value,
this.Project.Module);
var type = (TypeDefDeclaration)customAttributeEnumerator.Current.TargetElement;
codeWeaver.AddTypeLevelAdvice(new AddFieldAdvice(this, attribute, type),
JoinPointKinds.AfterInstanceInitialization,
new Singleton<TypeDefDeclaration>(type));
}
}
Note that we will complete later on the Initialize method. The JoinPointKinds enum of the AddTypeLevelAdvice is useless in our case. Its goal is to tell PostSharp where we want to inject code in the methods or classes. In our case creating our fields is independant of the existing code so we can be agnostic of the present code and so we just choose one of type-level JoinPointKinds.
So what do we want to generate ? We said a boolean field that can logically be decorated with an attribute CompilerGenerated. This is typically the kind of information we need for the Initialize method. Let's extract these informations.
internal ITypeSignature SystemBoolean;
internal IMethod CompilerGeneratedAttributeCtor;
protected override void Initialize()
{
base.Initialize();
var module = this.Project.Module;
this.SystemBoolean = module.FindType(typeof(bool), BindingOptions.Default);
var compilerGeneratedAttribute = module.FindType(typeof(CompilerGeneratedAttribute), BindingOptions.Default);
this.CompilerGeneratedAttributeCtor = module.FindMethod(compilerGeneratedAttribute, ".ctor");
}
Note that for an attribute, we are not interested by the type itself, but by its constructor.
Let's create our advice
So let's go to our advice ! This is the class that will generate the field. This is the easiest part of our job !
internal class AddFieldAdvice : IAdvice
{
private InjectCodeAttributeTask task;
private InjectCodeAttribute attribute;
private TypeDefDeclaration type;
public AddFieldAdvice(InjectCodeAttributeTask task, InjectCodeAttribute attribute, TypeDefDeclaration type)
{
this.task = task;
this.attribute = attribute;
this.type = type;
}
int IAdvice.Priority
{
get { return this.attribute.AttributePriority; }
}
bool IAdvice.RequiresWeave(WeavingContext context)
{
return true;
}
void IAdvice.Weave(WeavingContext context, InstructionBlock block)
{
FieldDefDeclaration field = new FieldDefDeclaration();
field.FieldType = this.task.SystemBoolean;
field.Name = "HasBeenLoaded";
field.Attributes = FieldAttributes.Public;
type.Fields.Add(field);
var ctor = this.task.CompilerGeneratedAttributeCtor;
var compilerGenerated = new CustomAttributeDeclaration(ctor);
field.CustomAttributes.Add(compilerGenerated);
}
}
Note that we must add the field to the class' fields before we can add the attribute to our field. Otherwise we would get an exception telling that the field's parent is not known.
Ready to use our attribute !
Our attribute is now finished and we can test it !
So I just create a separate project (our attribute will be typically created in a framework DLL and not in your business DLL) referencing the DLL where my attribute reside and I add a basic class like this one :
[InjectCode]
public class MyAspectedClass { }
and I build it. But it does not work ! I have the following compilation error : Error 2 PostSharp: The plug-in "InjectCodeAttributePlugin" required by the type "LowLevelAspect.InjectCodeAttribute" was not found.
What happen is simple. PostSharp is searching for the file ".psplugin" but it cannot find it. So what to do ? You must know that by default PostSharp knows about the plugin files that are deployed on the machine under the "Plugin" folder (typically in C:\Program Files\PostSharp 1.5\PlugIns). But you can also give him a hint of where to find it. To do so, just edit your project file to add this property :
<PropertyGroup>
<PostSharpSearchPath>..\LowLevelAspect\bin\Debug</PostSharpSearchPath>
</PropertyGroup>
In my case, LowLevelAspect is the name of the project where I defined my aspect. Note that you could also replace "Debug" with $(Configuration) to target either the Debug or the Release folder depending of the configuration you are building in
For this to work, you need of course to deploy your psplugin file. So you can just update the properties of your file to specify Copy to Ouput Directory : Copy Always.
So let's try again to compile. This time, it's ok !
We can now look to our class in the reflector and here what we get :
public class MyAspectedClass
{
[CompilerGenerated]
public bool HasBeenLoaded;
public MyAspectedClass();
}
Pretty cool no ?