[PostSharp] How to virtualize all the methods of a class
By Michael DELVA on Wednesday 4 August 2010, 14:00 - C# - Permalink
TweetWhen using NHibernate, it is recommended to declare the public and protected properties and methods of your entities as virtual, to be able to use them with proxies.
But missing a virtual keyword for a member of a POCO is easily forgettable, and unfortunately, you will only be warned of your mistake on runtime, when your mappings are being built.
In this article, I will show you how to 'not care' anymore about that, and let PostSharp do this stuff for us.
What I am going to show you now is a very powerful feature of PostSharp, but unfortunately isn't very well documented (for now?) on PostSharp's site. This article has been of great help, even if targeting the 1.5 version of PostSharp.
In the previous articles, when I used PostSharp to create aspects, I just created a new attribute to be applied on top of the classes to be transformed, and which contained the code to execute or add to the classes.
What we are going to do now is slightly more complex, as we are going to create 2 assemblies to do the work:
- A first assembly with a class defining a PostSharp Task containing the code the PostSharp server will execute, and a PostSharp plugin definition
- A second assembly which will contain the attribute to put on our entities, with a reference to the plugin file of the first assembly
Let's begin by the latter, which is the simplest assembly. In fact, this assembly contains a single simple class, which is, as I said, the attribute which will decorate the class to be altered:
[MulticastAttributeUsage(MulticastTargets.Class, AllowMultiple = false, Inheritance = MulticastInheritance.Strict)]
[RequirePostSharp("MakeVirtualAttributePlugin", "MakeVirtualAttributeTask")]
public class MakeVirtualAttribute : MulticastAttribute
{
}
Here are the meanings of the attributes of the attribute (!):
- We are going to target only classes, disallow the use of this attribute multiple times, and allow the attribute to be inherited by derived classes and overriding members
- We indicate to PostSharp the name of the plug-in it has to load, and the associated Task name, which must also be defined in the plug-in
And that's it for this part. Pretty simple, isn't it? :)
Now is the time to create our second assembly, which will contain both the plugin file, and the task.
The plugin file is a simple XML file, with the psplugin extension, and which must be named accordingly to the string value which has been passed to the RequirePostSharp attribute. So we have here to create a file named: MakeVirtualAttributePlugin.psplugin
In this file, we will add the following content:
<?xml version="1.0" encoding="utf-8" ?>
<PlugIn xmlns="http://schemas.postsharp.org/1.0/configuration">
<SearchPath Directory="bin/{$Configuration}"/>
<TaskType Name="MakeVirtualAttributeTask" Implementation="Emidee.Aspects.MakeVirtualWeaver.MakeVirtualAttributeTask, Emidee.Aspects.MakeVirtualWeaver" Phase="Transform">
<Dependency TaskType="CodeWeaver"/>
</TaskType>
</PlugIn>
And the explanations of this content:
- The SearchPath node specifies a directory where PostSharp will find the assembly containing the task.
- The TaskType node specifies once again the class name of the task, and the assembly where to find this class.
- The Phase attribute, and the Dependency node are both required by PostSharp.
There is still one thing we have to do on this plugin file: we have to click on it in the Solution Explorer in Visual Studio, and in the Properties panel, we have to change the value of ''Copy to Output Directory" to Copy Always, in order to msbuild to copy the file to the output directory.
And now comes the really fun part: the creation of the Task. The main part of this class was taken from this article, but modified to suit our needs.
So here comes the code, followed by the explanations:
public class MakeVirtualAttributeTask : Task
{
public override bool Execute()
{
var annotationRepository = AnnotationRepositoryTask.GetTask(Project);
var customAttributeEnumerator = annotationRepository.GetAnnotationsOfType(typeof(MakeVirtualAttribute), false);
while (customAttributeEnumerator.MoveNext())
{
var wovenType = (TypeDefDeclaration)customAttributeEnumerator.Current.TargetElement;
if (wovenType == null || wovenType.IsInterface)
continue;
if (wovenType.IsSealed)
{
MessageSource.MessageSink.Write(new Message(SeverityType.Error, "UNSL01", string.Format("MakeVirtualAttributeTask cannot process the type '{0}' because it is sealed. Remove either the 'sealed' keyword or the [Unseal] attribute.", wovenType.Name), "Unsealer"));
continue;
}
foreach (var method in wovenType.Methods)
{
if (method.IsStatic || method.Visibility == Visibility.Private || method.Name == ".ctor" || (method.Attributes & MethodAttributes.Virtual) == MethodAttributes.Virtual)
continue;
if ((method.Attributes & MethodAttributes.Final) == MethodAttributes.Final)
{
MessageSource.MessageSink.Write(new Message(SeverityType.Error, "UNSL02", string.Format("MakeVirtualAttributeTask cannot process the method '{0}' in type '{1}' because it is sealed. Remove either the 'sealed' keyword from the method or the [Unseal] attribute from its declaring type.", method.Name, wovenType.Name), "Unsealer"));
continue;
}
method.Attributes |= MethodAttributes.Virtual | MethodAttributes.NewSlot;
}
}
return true;
}
}
We start by creating an AnnotationRepositoryTask, which will be used to iterate through all the objects decorated by the MakeVirtual attribute, thanks to the GetAnnotationsOfType method.
This function will return a result of type IEnumerator<IAnnotationInstance>.
This interface, as said by the documentation, "Exposes the fact that a custom attribute (IAnnotationValue) is applied to an element (MetadataDeclaration)." So, in order to get the classes on which the attribute is applied, we have to use the TargetElement property of this interface, which we will cast in a TypeDefDeclaration.
So, in the loop, we get each class decorated by the MakeVirtualAttribute, and of course each derived class, in the variable named wovenType. But we are still a few steps away from the alteration of the methods.
Indeed, we have to take care of the properties of the woven object, because not all types can be woven. We can't make virtual an interface, or a sealed class, for example. Moreover, all methods can't be made virtual, like static ones. We then have to check the type of the woven object, and the properties of its methods, and manage the corner cases:
- If the woven type is an interface, we just continue without showing an error, because this interface may (and to be useful, should) be implemented. So we just ignore it to let a chance to its derived classes to be woven.
- If the woven type is sealed, we display an error, with the MessageSource.MessageSink.Write method, which will write the error message in the "Error List" panel of Visual Studio.
If those 2 checks are validated, we then iterate through all the methods of the class to check if they match the conditions:
- If the method is the constructor, or is static, or is already virtual, we ignore it
- If the method is sealed, like before, we ignore it, and display an error in VS
And here we come to the end of our validations. We now have a matching method. We (finally) have to make it virtual, which is done by adding the values Virtual and NewSlot to the MethodAttributes flag. As easy as this.
You may wonder why we only take care of altering the methods declaration, and not the properties. This could be a problem, as they have to be virtual too, in order for the class to be used as a proxy.
In fact, we do take care of them too, because properties are automatically transformed by the compiler into methods: one for the getter, and one for the setter.
At this point, we now have our attribute, to decorate the classes, the psplugin file, and the task class.
Let's write some small unit tests to check our aspect is working perfectly. Here is what we want to check:
- The methods are altered, and now are virtual
- The properties become virtual
- The aspect is applied to derived classes
Here are the corresponding tests:
public class MakeVirtualAttributeTaskTests : TestsBase
{
[MakeVirtual]
public class Test
{
public void Foo()
{
}
public int Toto { get; set; }
}
public class TestDerived : Test
{
public void Bar()
{
}
}
[Fact]
public void MakeVirtualAttribute_AddsVirtualToMethods()
{
Test test = new Test();
Type type = test.GetType();
var methodInfo = type.GetMethod("Foo");
Assert.True(methodInfo.IsVirtual);
}
[Fact]
public void MakeVirtualAttribute_AddsVirtualToPropertySetters()
{
Test test = new Test();
Type type = test.GetType();
var methodInfo = type.GetProperty("Toto");
Assert.True(methodInfo.GetSetMethod().IsVirtual);
}
[Fact]
public void MakeVirtualAttribute_AddsVirtualToPropertyGetters()
{
Test test = new Test();
Type type = test.GetType();
var methodInfo = type.GetProperty("Toto");
Assert.True(methodInfo.GetGetMethod().IsVirtual);
}
[Fact]
public void MakeVirtualAttribute_IsPropagated_ToDerivedClasses()
{
TestDerived test = new TestDerived();
Type type = test.GetType();
var methodInfo = type.GetMethod("Bar");
Assert.True(methodInfo.IsVirtual);
}
}
And boom, a compile error...
What? This doesn't compile? What's going on?
What's going on is that we have still one last thing to do: we must provide the psplugin file in a place which is known by PostSharp, as stated here. For my part, I've chosen to modify the .csproj of the test assembly, to add this node:
<PropertyGroup>
<PostSharpSearchPath>..\..\..\app\Aspects\Emidee.Aspects.MakeVirtualWeaver\bin\$(Configuration)\</PostSharpSearchPath>
</PropertyGroup>
This allows me to specify a relative path, which is not possible if you use the Reference Paths panel of the assembly properties.
Now the assembly can be compiled, and the tests executed:
Isn't it a great conclusion for this article? :D
See you soon!
Comments
Great article!