Introduction
No, not the “Hello, my name is Dustin!” kind of introduction, but the “injection” type. What does that mean? PostSharp gives us the power to implement an interface on a class…at build time. We can also add (introduce) members to that class such as fields/properties, events and methods too. These members are injected at build time and are available at run time.
Why would you want to do this? As in most cases when applying aspect-oriented programming, you would use this to implement required interfaces that are little more than boilerplate code. One of the most popular examples of interface introduction is the NotifyPropertyChanged aspect which automatically introduces the INotifyPropertyChanged interface and required members. Anyone who has worked with WPF and the MVVM pattern would love to not have to write all of that scaffolding code just to get change notification. Since that aspect uses features we have not yet covered, we will not cover it today. If you’re feeling adventurous, you can check it out here.
Member Introduction
Member introduction allows us to add properties, events and methods to a class. Let’s start off by creating an aspect to introduce a property and a method.
[Serializable] public class IntroduceAspect : InstanceLevelAspect { [IntroduceMember] public int ID { get; set; } [IntroduceMember] public void SomeMethod(int param1) { Console.WriteLine("Inside of introduced method"); } }
And now our target class
[IntroduceAspect] public class TargetClass { }
You might be laughing at our test class, but don’t worry, our aspect will do the work for us. When we look at the compiled assembly with ILSpy, we see that instead of a blank class we have a few more members than we started with, including the members we wanted to introduce.
Amongst the aspect related code, we have our ID property and our SomeMethod method. Notice that the getter and setter of ID are delegated to our aspect and so does our method. This is important to keep in mind because when implementing members, they must be marked as public inside of the aspect (because our target class has to access them). However, if you happen to forget, PostSharp will remind you with a compiler error
But what happens if you don’t want the introduced members to be public in the target class? Have no fear, PostSharp thought of that too. Let’s have a look at the IntroduceMember attribute.
IntroduceMember attribute
By default, using IntroduceMember by itself will use public visibility and will cause compiler errors if a member with the same signature is already part of the class. We can control the behavior of how the member is implemented by changing the following parameters.
Visibility
By default, PostSharp will introduce the member to the target class with public visibility. We can specify one of the enumerations from PostSharp.Reflection.Visibility to control what visibility the member will have in the target class. Available values are
· Public (Default) – Is publically available.
· Family – Is available to the class and any derived classes. Same as protected.
· Assembly – Is publicly available within the assembly. Same as internal.
· FamilyOrAssembly – Is available to the class and any derived classes, but only within the assembly. Same as protected internal.
· FamilyAndAssembly – Protected types inside the assembly. There is no C# equivalent.
· Private – Only visible to the class.
OverrideAction
There is a chance that the target class already has a member with the same signature. By default, there will be a compiler error if this scenario is encountered. To change the behavior, we can provide one of the enumerations from PostSharp.Aspects.Advices.MemberOverrideAction.
· Default – Fails with a compiler error.
· Fail – Fails with a compiler error.
· Ignore – Continues on, without trying to introduce the member.
· OverrideOrFail – Tries to override the member with our own implementation. If the existing member is defined in a base class and is sealed or non-virtual, it will fail with a compiler error.
· OverrideOrIgnore – Tries to override the member with our own implementation. If the existing member is defined in a base class and is sealed or non-virtual, it will ignore the member introduction and continue on.
IsVirtual
If you would like to introduced member to be virtual (overridable in derived classes) then you can set IsVirtual to true. The member signature in the base class will be marked as virtual.
CopyCustomAttributesAttribute
Sometimes members need to be decorated with attributes. An example of this would be decorating members of a DataContract with DataMember. However, when introducing members from an aspect, any attributes applied to the member in the aspect will not be introduced along with the member in the target. We can use CopyCustomAttributes attribute in addition to the IntroduceMember attribute to introduce the attributes along with the member. Let’s look at an example.
[Serializable] public class IntroduceAspect : TypeLevelAspect { [IntroduceMember] [DataMember(IsRequired=true)] public int ID { get; set; } } [IntroduceAspect] [DataContract] public class TargetClass { [DataMember] public string FirstName { get; set; } }
Our aspect is introducing a member, ID, which is decorated with DataMember. Let’s look at the result in ILSpy
The DataMember attribute is not present on ID. Let’s update the aspect to use CopyCustomAttributes.
[Serializable] public class IntroduceAspect : TypeLevelAspect { [IntroduceMember, CopyCustomAttributes(typeof(DataMemberAttribute), OverrideAction = CustomAttributeOverrideAction.MergeReplaceProperty)] [DataMember(IsRequired=true)] public int ID { get; set; } }
In the constructor for CopyCustomAttributes we pass in the base type for the desired attribute and then we set the override action with a value from the CustomAttributeOverrideAction enumeration. When we look at the end result in ILSpy, we see that the attribute was introduced along with the member.
CustomAttributeOverrideAction
CustomAttributeOverrideAction is an enum that lets us tell PostSharp how to handle a situation when an attribute of the same type already exists on the target member.
· Default – Fails with a compile time error.
· Fail – Fails with a compile time error.
· Ignore – Ignores the attribute introduction and does not generate an error.
· Add – Adds the attribute as defined, even if it already exists on the target. This could cause duplicate attributes on the target.
· MergeAddProperty – Combines the existing attribute with the attribute being introduced. Any properties defined by the existing attribute will remain. No override will occur. Any properties defined by the introduced attribute will be added to the existing attribute.
· MergeReplaceProperty – Same as MergeAddProperty except that any properties defined by the existing attribute will overridden by the introduced attribute.
Interface Introduction
When introducing an interface via an aspect, the interface must be implemented on the aspect itself. The type will expose the interface at run time, but the aspect actually implements it. Let’s have a look at our interface:
public interface IFriendlyName { string Name { get; set; } void PrintName(); }
And now our aspect:
[Serializable] [IntroduceInterface(typeof(IFriendlyName))] public class IntroduceAspect : InstanceLevelAspect, IFriendlyName { #region IFriendlyName Members public string Name { get; set; } public void PrintName() { Console.WriteLine(this.Name); } #endregion }
Our test class remains the same, empty
[IntroduceAspect] public class TestClass { }
When we look at the compiled result we see our interface has been implemented
Notice that we didn’t use the IntroduceMember attribute on the interface members. Also notice that the resulting implementations of the interface members are private. To make the interface members public we have to apply the IntroduceMember attribute to the members
[Serializable] [IntroduceInterface(typeof(IFriendlyName))] public class IntroduceAspect : InstanceLevelAspect, IFriendlyName { #region IFriendlyName Members [IntroduceMember] public string Name { get; set; } [IntroduceMember] public void PrintName() { Console.WriteLine(this.Name); } #endregion }
And now the compiled result shows two implementations of our members
Looking at the PrintName method, the explicit interface implementation is private, but we’ve introduced a public version which the interface method calls.
IntroduceInterface attribute
To tell PostSharp that we want to introduce an interface, we decorate the aspect with the IntroduceInterface attribute. To tell PostSharp which interface to implement, we pass in a type using typeof(IFriendlyName). Just like the IntroduceMember attribute, there are parameters to control the behavior of the introduction.
· OverrideAction – Exactly the same as IntroduceMember. Determines what to do when the target already implements the interface. Default is to fail with a compiler error.
· IsProtected – If set to true, the interface is not directly implemented by the type. Instead, the type exposes the interface through the IProtectedInterface<T>. Since protected interfaces are considered obsolete, you should leave this as false (default).
· AncestorOverrideAction – Defines the behavior of the introduction when and ancestor of the interface is already applied to the target class. See example below. Available enumerations in the PostSharp.Aspects.Advices.InterfaceOverrideAction are Default (Fail), Fail and Ignore.
Extended Example
Let’s finish up with a bit more in-depth example using some of the behavior parameters.
public interface IIdentifiable { Guid ID { get; set; } } public interface IFriendlyName : IIdentifiable { string Name { get; set; } void PrintName(); } [IntroduceAspect] public class TargetClass : IIdentifiable { #region IFriendlyNameBase Members public Guid ID { get; set; } #endregion string Name { get; set; } public void PrintName() { throw new NotImplementedException(); } } [Serializable] [IntroduceInterface(typeof(IFriendlyName),
AncestorOverrideAction=InterfaceOverrideAction.Ignore)] public class IntroduceAspect : InstanceLevelAspect, IFriendlyName { #region IFriendlyName Members [IntroduceMember(OverrideAction=MemberOverrideAction.Ignore)] public Guid ID { get; set; } [IntroduceMember(OverrideAction=MemberOverrideAction.Ignore)] public string Name { get; set; } [IntroduceMember(OverrideAction=MemberOverrideAction.OverrideOrFail)] public void PrintName() { Console.WriteLine(this.Name); } #endregion }
We define two interfaces. IFriendlyName implements IIdentifiable. Our test class implements IIdentifiable and also has a PrintName method which throws an exception. Our aspect specifies the introduction of IFriendlyName and also implements the required interface members. We specify that we should ignore any implementation of an ancestor (IIdentifiable) of the introduced interface (IFriendlyName). We also specify that we want to ignore the member introduction on the two properties if they exist in the target class. We mark PrintName with the OverrideOrFail because we want to force our own implementation of the PrintName method. The end result looks like this
First take a look at the PrintName method. Instead of the original method body, which threw an exception, we see that there is a call to our aspect which invokes our implementation of that method.
Next we see that both interfaces are implemented, but we only have get/set methods for the Name property, not the ID property. This is because PostSharp ignored the implementation of IIdentifiable since it was already implemented on the target class. If we remove InterfaceOverride.Ignore from the IntroduceInterface attribute, we would get a compiler error.
If we removed the implementation of IIdentifiable from our test class, we would see get/set methods for ID in the compiled results.
Conclusion
Today we covered some good ground on introducing members and interfaces along with some of the nuances that you have to be aware of. Tomorrow we’ll continue with importing members and accessing introduced members at compile time.