Some design patterns require you to add properties, methods or interfaces to your target code. If many components in your codebase need to represent the same construct, repetitively adding those constructs violates the DRY (Don't Repeat Yourself) principle. So how can you add code constructs to your target code without it becoming repetitive?
PostSharp offers a number of ways for you to add different code constructs to your codebase in a controlled and consistent manner. The following sections describe these techniques.
Introducing interfaces
One of the common situations that you will encounter is the need to implement a specific interface on a large number of classes. This may be INotifyPropertyChanged, IDisposable, IEquatable<T> or some custom interface that you have created. If the implementation of the interface is consistent across all of the targets then there is no reason not to centralize its implementation. So how do you add that interface to a class at compile time?
Add the
IIdentifiableinterface to the target code.public interface IIdentifiable { Guid Id { get; } }Create an aspect that inherits from InstanceLevelAspect and add the custom attribute [PSerializableAttribute].
The key to adding an interface to target code is that you must implement that interface on your aspect. Implement the
IIdentifiableinterface on your aspect. The aspect's implementation of the interface is added to the target code, so anything that you include in method or property bodies is added to the target code as you have declared it in the aspect.[PSerializable] public class IdentifiableAspect : InstanceLevelAspect, IIdentifiable { public Guid Id { get; private set; } }Add the IntroduceInterfaceAttribute attribute to the aspect and include the interface type that you want to add to the target code.
[IntroduceInterface(typeof(IIdentifiable))] [PSerializable] public class IdentifiableAspect : InstanceLevelAspect, IIdentifiable { public Guid Id { get; private set; } }Finally, declare where this aspect should be applied in the codebase. In this example, add it as an attribute to a class.
[IdentifiableAspect] public class Customer { public string Name { get; set; } public string Address { get; set; } }After compilation, you can decompile the target code and see that the interface has been added to it.
As you can see in the decompiled code, interfaces are implemented explicitly on the target code. It is also possible to introduce public members to target code. This is covered below.
Note
Interfaces and members introduced by PostSharp are not visible at compile time. To access the dynamically applied interface you must make use of a special PostSharp feature: the Cast<TSource, TTarget>(TSource) pseudo-operator. The Cast<TSource, TTarget>(TSource) method allows you to safely cast the target code to the interface type that was dynamically applied. After that call, you can use the instance through the interface constructs. There is no way to access a dynamically inserted method, property or event, other than through reflection or the dynamic keyword.
Note
When you start adding code constructs to your target code, you need to determine how to initialize them correctly. Because these code constructs are not available for you to work with at compile time, you need to figure out how to deal with them some other way. To learn more about initializing code constructs that you introduce via aspects, see Initializing Aspects.
Introducing methods
The introduction of methods to your target code is very similar to introducing interfaces. The biggest difference is that you will be introducing code at a much more granular level.
Create an aspect that inherits from InstanceLevelAspect and add the custom attribute [PSerializableAttribute].
Add to the aspect the method you want to introduce to the target code.
[PSerializable] public class OurCustomAspect : InstanceLevelAspect { public void TheMethodYouWantToUse(string aValue) { Console.WriteLine("Inside a method that was introduced {0}", aValue); } }Note
The method that you declare must be marked as public. If it is not, you will see an error at compile time.
Decorate the method with the IntroduceMemberAttribute attribute.
[IntroduceMember] public void TheMethodYouWantToUse(string aValue) { Console.WriteLine("Inside a method that was introduced {0}", aValue); }Finally, declare where you want this aspect to be applied in the codebase.
[OurCustomAspect] public class Customer { public string Name { get; set; } }After compilation, you can decompile the target code and see that the method has been added.
Introducing properties
The introduction of properties is almost exactly the same as the introduction of methods. As when introducing a method, you use the IntroduceMemberAttribute attribute. The details follow.
Create an aspect that inherits from InstanceLevelAspect and add the custom attribute [PSerializableAttribute].
Add the property you want to introduce to the aspect.
[PSerializable] public class OurCustomAspect : InstanceLevelAspect { public string Name { get; set; } }Note
The property that you declare must be marked as public. If it is not, you will see a compiler error.
Decorate the property with the IntroduceMemberAttribute attribute.
[IntroduceMember] public string Name { get; set; }Add the aspect attribute to the target code where the aspect should be applied.
[OurCustomAspect] public class Customer { }After you have compiled the codebase you can decompile the target code and see that the property has been added.
As noted for both the introduction of methods and properties, the code being introduced must be declared as public. This is needed to ensure that PostSharp can function. If you look closely at the decompiled targets you will see that the introduced members are actually calling the methods/properties that were declared on the aspect. If the method/property on the aspect is not public, the target code will not be able to call it as it should.
Note
It is possible to introduce properties to target code, but it is not possible to introduce fields to your target code. The reason is that all members are introduced by delegation: the actual implementation of the member always resides in the aspect.
Controlling the visibility of introduced members
You may not want the introduced member to have public visibility once it has been introduced to the target code. PostSharp allows you to control the visibility of the introduced member through the use of the Visibility property on the aspect. To declare that a member should be introduced with private visibility, set this property accordingly.
[IntroduceMember(Visibility = Visibility.Private)]
public string Name { get; set; }
You can introduce members with a number of different visibilities, including public, private, assembly (internal in C#) and others. You can also declare an introduced member as virtual by setting the IsVirtual property to true.
[IntroduceMember(Visibility = Visibility.Private, IsVirtual = true)]
public string Name { get; set; }
Overriding members or interfaces
One thing you need to be aware of is the situation where you are introducing a member that may already exist in the scope of the target code. Perhaps the method you are trying to introduce is available on the target code through inheritance. The method may also be declared explicitly on the target code. The introduction of a member via an aspect needs to take these situations into account. PostSharp allows you to take these situations into account through the use of the OverrideAction property.
The OverrideAction property allows you to declare a rule for how the introduction of a member should behave if the member already exists in the target type or its base type. This property allows you to declare rules such as Fail (any conflict causes a build-time error; this is the default), Ignore (continue on without trying to introduce the member), OverrideOrFail or OverrideOrIgnore. For interfaces, use the OverrideAction property of IntroduceInterfaceAttribute, which accepts Fail or Ignore. It is important to understand how you want to apply your introduced members and interfaces in situations where that member or interface may already exist.
[IntroduceMember(OverrideAction = MemberOverrideAction.Fail)]
public string Name { get; set; }
Introducing interfaces dynamically
The final technique that you can use to introduce code constructs to your target code is the CompositionAspect. When you create a CompositionAspect, you choose at build time which interfaces to add to the target class, and you provide the object that implements them at run time.
Create an aspect that inherits from CompositionAspect and implements its abstract members.
[PSerializable] public class GeneralCompose : CompositionAspect { public override object CreateImplementationObject(AdviceArgs args) { throw new System.NotImplementedException(); } }Next, you need some way to tell the aspect what interface and concrete type you want to implement on the target code. To do that, create a constructor for your aspect that accepts two parameters: one for the interface type and one for the concrete implementation type. Assign those two constructor parameters to fields so that you can use them in the aspect.
[PSerializable] public class GeneralCompose : CompositionAspect { private readonly Type _interfaceType; private readonly Type _implementationType; public GeneralCompose(Type interfaceType, Type implementationType) { _interfaceType = interfaceType; _implementationType = implementationType; } // Details skipped. }There are two methods that you need to implement to complete this aspect. The first is an override of the GetPublicInterfaces(Type) method. This method has a target type parameter, which allows you to filter the application of the interface if you choose to. For this example, return an array that contains the interface type that was provided via the aspect constructor.
protected override Type[] GetPublicInterfaces(Type targetType) { return new[] { _interfaceType }; }Note
The interfaces that are returned from the GetPublicInterfaces(Type) method will be applied to the target code during compilation.
The second method that you need to override is CreateImplementationObject(AdviceArgs). For this example, return an instance of the concrete implementation type that was provided in the aspect constructor. To create the instance, use the CreateInstance(Type) method.
public override object CreateImplementationObject(AdviceArgs args) { return Activator.CreateInstance(_implementationType); }Note
The CreateImplementationObject(AdviceArgs) method is invoked at run time.
Apply the aspect to the target code as an attribute. Provide the attribute with the interface and concrete types that you wish to implement.
[GeneralCompose(typeof(IList), typeof(ArrayList))] public class Fruit { }After compilation, the target class implements the interface and delegates its implementation to an instance of the concrete type. To use the interface from the same project, call the Cast<TSource, TTarget>(TSource) method.
[GeneralCompose(typeof(IList), typeof(ArrayList))] public class Fruit { public Fruit() { IList list = Post.Cast<Fruit,IList>(this); list.Add("apple"); list.Add("orange"); list.Add("banana"); } }
See Also
Reference
CreateImplementationObject(AdviceArgs)
CompositionAspect
Cast<TSource, TTarget>(TSource)
INotifyPropertyChanged
IDisposable
InstanceLevelAspect
PSerializableAttribute
IntroduceInterfaceAttribute
IntroduceMemberAttribute
Visibility
IsVirtual
OverrideAction
Fail
Ignore
OverrideOrFail
OverrideOrIgnore
GetPublicInterfaces(Type)
CreateImplementationObject(AdviceArgs)
CreateInstance(Type)
Other Resources