Aspect Lifetime
So far we’ve built aspects and we’ve seen the results in action. What we haven’t seen is what happens between those two points. After the build process has been completed by MSBuild, PostSharp starts up and starts processing our aspects and aspect declarations.
Aspects go through two phases, compile time initialization and run time initialization.
Note: For Silverlight, Windows Phone and .NET CF, aspects are initialized at run time and all compile time steps are skipped.
Compile time
One of the benefits of using PostSharp is that it initializes aspects at compile time. Any expensive work that needs to be done related to the aspect can be done at compile time instead of run time. The build process may take a hit on performance, but run time does not suffer.
A new aspect instance is created for every target to which the aspect is applied. Each aspect goes through compile time validation and initialization. Aspect instances are then serialized into a resource for consumption at run time.
Compile time Validation
Last week we covered multicasting which is a way to apply an aspect to multiple targets. Even though multicasting gives us flexibility in applying aspects to targets, there are times when application of an aspect to a target must be considered using logic.
All aspects have a CompileTimeValidate virtual method. When implemented in the aspect, PostSharp will call this method to determine if the application of the aspect should continue on the given target. Depending on the type of aspect being implemented, CompileTimeValidate parameter(s) will be reflection information about the current target PostSharp is asking about. For example, Method based aspects like OnExceptionAspect and OnMethodBoundaryAspect will have a parameter of type MethodBase while LocationInterceptionAspect will have a parameter of type LocationInfo. If the aspect has been applied on an invalid target, implementations of this method must return false so this target will be silently ignored. Implementations can emit errors and warning by using Message.Write.
Compile time Initialization
All aspects also have a CompileTimeInitialize virtual method that can be implemented to perform expensive operations and/or initialize serializable fields so they are available at run time. PostSharp provides CompileTimeInitialize with the reflection information about the current target as well as information about the current aspect. Remember that since this is compile time, you won’t have access to the actual instance of the targets.
Run time
Before any aspect can be executed, PostSharp has to deserialize the aspects and initialize them. Since the serialization process uses a binary serializer, the aspect’s constructor is not called. The only way to perform initialization tasks at run time is to implement the RunTimeInitialize virtual method. When implementing RunTimeInitialize, you have access to the reflection information for the target but unlike CompileTimeInitialize, you have access to the instance of the target, not just the Meta data.
Example: Caching
We’re going to borrow the caching aspect from Matthew Groove’s post 5 Ways That Postsharp Can SOLIDify Your Code: Caching for this example.
[Serializable] public class CacheAttribute : MethodInterceptionAspect { [NonSerialized] private static readonly ICache _cache; private string _methodName; static CacheAttribute() { if (!PostSharpEnvironment.IsPostSharpRunning) { // one minute cache _cache = new StaticMemoryCache(new TimeSpan(0, 1, 0)); // use an IoC container/service locator here in practice } } public override void CompileTimeInitialize(MethodBase method, AspectInfo aspectInfo) { _methodName = string.Format("{0}.{1}", method.DeclaringType.Name, method.Name); } public override void OnInvoke(MethodInterceptionArgs args) { var key = BuildCacheKey(args.Arguments); if (_cache[key] != null) { args.ReturnValue = _cache[key]; } else { var returnVal = args.Invoke(args.Arguments); args.ReturnValue = returnVal; _cache[key] = returnVal; } } private string BuildCacheKey(Arguments arguments) { var sb = new StringBuilder(); sb.Append(_methodName); foreach (object argument in arguments.ToArray()) { sb.Append(argument == null ? "_" : argument.ToString()); } return sb.ToString(); } }
Notice the _cache field is marked as NonSerialized. Fields that are not initialized at compile time or are not serializable should be decorated with NonSerializable. This aspect has a static constructor to instantiate _cache to a new instance of StaticMemoryCache. Static constructors are called even when doing binary serialization.
CompileTimeInitialize is where the cache key prefix is created. In this case it’s just the method name for the target. The values stored in fields/properties at compile time will be serialized into the aspect for consumption at runtime.
The OnInvoke method starts out by building a cache key which consists of the method name of the target method (set at compile time) and the value of each method argument. If the cache contains a valid object for the generated key then the value is returned from cache. If not, then the method is invoked and the return value is stored in cache.
A caching aspect is beneficial on expensive operations such as complex computations or frequently accessed data from a database. We’ll use it on our MD5 hash computation method
class Program { static void Main(string[] args) { TestClass tc = new TestClass(); Console.WriteLine(tc.GetMD5Hash("PostSharp")); Console.WriteLine(tc.GetMD5Hash("SharpCrafters")); Console.WriteLine(tc.GetMD5Hash("PostSharp")); Console.WriteLine(tc.GetMD5Hash("SharpCrafters")); Console.ReadKey(); } } class TestClass { public TestClass() { } [Cache] public string GetMD5Hash(string value) { MD5 md5 = System.Security.Cryptography.MD5.Create(); byte[] inputBytes = System.Text.Encoding.ASCII.GetBytes(value); byte[] hash = md5.ComputeHash(inputBytes); StringBuilder sb = new StringBuilder(); for (int i = 0; i < hash.Length; i++) { sb.Append(hash[i].ToString("X2")); } return sb.ToString(); } }
The first two calls will result in a computation but the second two will return the value from cache without the original method being called.
Just for fun, set break points on the CompileTimeValidate and CompileTimeInitialize methods. Notice they are never hit even if you run the application. Let’s have a look at what happened after we compiled.
No aspects applied |
Aspects applied |
![]() |
![]() |
Looking at the compiled executable with ILSpy, we can see that compiling with our aspect applied produces a Resources node on the namespace tree which contains a binary file. Remember, in CompileTimeInitialize the private field _methodName was populated with the target method name. If you browse around ILSpy you won’t find any trace of that value. If you look at the binary file under Resources however, we see that the value was serialized along with the aspect instance.
PostSharp has initialized the instances of the aspect for each target, and then serialized them into a resource for consumption at run time.
Conclusion
You should now have a good idea about the process between writing an aspect and seeing it in action. Understanding the process of how an aspect is initialized is important when rolling custom aspects. Today we only covered the first half. Tomorrow we’re going to cover aspect scope.
![self573_thumb[1] self573_thumb[1]](/assets/images/2011/2011-07-07-day_9_aspect_lifetime_scope_part_1/self573_thumb[1]_thumb_2.jpg)