2

In my original introduction to Cirrus framework I drew up a basic method result caching attribute for Delphi Prism. This weekend I thought I’d give it another go and try to create a more general purpose Caching Aspect that integrates with a well known Cache library. I decided to use the opportunity to experiment with the Caching Application Block from the Microsoft Patterns and Practice Enterprise Library which you should definitely download and do some research into if you haven’t already.

Last time, I had created an attribute which could only be attached to methods returning strings and used a home-made caching class, the Aspect looked like this:

namespace AOPCacheLibrary;

interface

uses
  RemObjects.Oxygene.Cirrus.*,
  RemObjects.Oxygene.Aspects;

type
   [AttributeUsage(AttributeTargets.Method)]
  MemCacheAttribute = public class(System.Attribute, IMethodImplementationDecorator)
  private
  public

    method HandleImplementation(Services: IServices; aMethod: IMethodDefinition);
  end;

implementation

method MemCacheAttribute.HandleImplementation(Services: IServices; aMethod: IMethodDefinition);
begin
  // Get a Reference to our Result Value
  var newResult := new ResultValue();

  // Get our MemCache Object
  var cacheType := Services.FindType('AOPCacheLibrary.MemCache');
  // Setup our static methods.
  var getDataProc := new ProcValue(new TypeValue(cacheType), 'getData', [new NamedLocalValue('cacheKey')]);

  // Assign the result of our cache request to a local variable.
  var getDataAssignment := new AssignmentStatement(new NamedLocalValue('strData'), getDataProc);

  // CACHE DATA USED: To assign our Cache'd data to the Method Result
  var useCacheData := new AssignmentStatement(newResult, new NamedLocalValue('strData'));
  // CACHE DATA NOT USED: To assign the result of our OriginalBody to the Method Result.
  var useOriginalMethodResult := new AssignmentStatement(new NamedLocalValue('resultOriginal'), newResult);

  aMethod.SetBody(Services,
    method begin
      var strData: String;
      // Get our Cache Key Name
      var cacheKey := MemCache.getKeyFromMethod(Aspects.MethodName, Aspects.GetParameters);

      try
        // See if we have anything in the cache for this key.
        unquote(getDataAssignment);
        // Replace this
        unquote(useCacheData);
      except
        on E: Exception do
        begin
          var resultOriginal: String;
          // No cache data found, execute the original method body.
          Aspects.OriginalBody;

          // Assign the result of the Original Method to a new variable.
          unquote(useOriginalMethodResult);
          // Send the result of the Original Method to the Cache for next time.
          MemCache.setData(cacheKey, resultOriginal);
        end;
      end;
    end);
end;

end.

Modifying my original code into working with the Microsoft Enterprise Caching black wasn’t particularly hard. If you haven’t worked with the Microsoft P&P Enterprise Library Caching Block before I highly recommend the Quickstart guide. The Enterprise Caching Block allows you to configure your cache using configuration file and change the backing store to suit own your needs. I would certainly recommend tweaking the expiration time and frequency of cache cleaning times before you begin.

Whilst rewriting the code of my Aspect I found it absolutely invaluable to have the Cirrus documentation wiki open and a piece of paper next to my keyboard with the approximate psuedo-code that I was intending to build with Cirrus written on it. I did actually find that despite the fact that the code I was building was not complicated, I did require a reminder of the statements and structure of it.

One additional problem that I did encounter very quickly on my modification was that I had a method which would generate a string key from the method name being called and its parameters which looked like this:

class method CacheKeyFactory.GetKeyFromMethod(aName: String; Args: Array of object): String;
begin
  var argConcat: String;

  argConcat := aName;
  for argTemp in Args do
  begin
    argConcat := argConcat + argTemp.ToString();
  end;

  Result := argConcat;
end;

Which I had intended to inject into the target class using the AutoInjectIntoTargetAttribute which as the name suggests creates the method tagged within the class you are targetting with your attribute. The problem I encountered was that I used this Attribute on a method within an IMethodImplementationDecorator attribute which meant that it worked perfectly when I applied my attribute to just one method within a class but applying my attribute to more than one meant that the AutoInjected method got injected more than once – causing a conflict (Is this a bug? I was told not but it seems odd to me).

[Update: 22/11/2009] I have reported this issue as QC: 79705. [/Update]

Therefore as a workaround, I was forced to move this key generating method into a static class elsewhere in the assembly but maybe someone can work out a way around this limitation.

I also encountered an annoying trait of the Cirrus Framework which is that some errors produce a fairly unhelpful message and lead you to a line in the Project’s build file which whilst being logical can be infuriating when you’re presented with this:

A rather confusing exception and causal code identification.

A rather confusing exception and causal code identification.

After a tiny bit of tweaking, My next iteration of the Caching Attribute looks like this:


type
  [AttributeUsage(AttributeTargets.Method)]
  MethodCacheAttribute = public class(System.Attribute, IMethodImplementationDecorator)
  private
  public
    method HandleImplementation(Services: IServices; aMethod: IMethodDefinition);
  end;

implementation

method MethodCacheAttribute.HandleImplementation(Services: IServices; aMethod: IMethodDefinition);
begin
  // Get a Reference to our Result Value
  var methodResult := new ResultValue();

  // Get a reference to our Static Key Factory.
  var cacheKeyGen := Services.FindType('CirrusCachingAspect.CacheKeyFactory');
  // Generate our cache key from the method name and it's parameters
  var getCacheKey := new AssignmentStatement(new NamedLocalValue('cacheKey'), new ProcValue(new TypeValue(cacheKeyGen), 'GetKeyFromMethod', [aMethod.Name, aMethod.GetParameterArrayValue()]));

  // Get our CacheManager Type and our CacheFactory Type
  var cacheType := Services.FindType('Microsoft.Practices.EnterpriseLibrary.Caching.CacheManager');
  var cacheFactoryType := Services.FindType('Microsoft.Practices.EnterpriseLibrary.Caching.CacheFactory');

  // Get a Reference to our Cache Instance.
  var getCacheInstance := new AssignmentStatement(new NamedLocalValue('methodCache'), new ProcValue(new TypeValue(cacheFactoryType), 'GetCacheManager'));

  // Get data from our cache.
  var getDataFromCache := new ProcValue(new NamedLocalValue('methodCache'), 'GetData', [new NamedLocalValue('cacheKey')]);

  // Cast our result from the Cache to the Result Type.
  var valueCastToResultType := new UnaryValue(new NamedLocalValue('cacheData'), UnaryOperator.Cast, aMethod.Result);

  // Assign the result of our cache request to a local variable.
  var getDataAssignment := new AssignmentStatement(new NamedLocalValue('cacheData'), getDataFromCache);

  // Assign our Cache'd data to the Method Result
  var useCacheData := new AssignmentStatement(methodResult, valueCastToResultType);

  // Add our data to the Cache for next time
  var addDataToCache := new StandAloneStatement(new ProcValue(new NamedLocalValue('methodCache'), 'Add', [new NamedLocalValue('cacheKey'), methodResult]));

  // Key not found in the cache, insert the original body and add a call to the Cache.Add() method.
  var notFoundInCache := new BeginStatement();
  notFoundInCache.Add(new PlaceHolderStatement());
  notFoundInCache.Add(addDataToCache);

  // If found in the cache.
  var ifCacheResult := new IfStatement(new BinaryValue(new NamedLocalValue('cacheData'), new NilValue(), BinaryOperator.Equal), notFoundInCache, useCacheData);

  aMethod.SetBody(Services,
    method begin
      var cacheData: Object;
      var methodCache: CacheManager;
      var cacheKey: String;

      // Get out cache instance
      unquote(getCacheInstance);

      // Generate a key for this method call.
      unquote(getCacheKey);

      // Try to retrieve the data from the cache.
      unquote(getDataAssignment);

      // If data from cache = nil then
      //    call original method
      //    add to cache
      // else
      //    return the data from the cache
      unquote(ifCacheResult);

    end);
end;

Which may look quite complex at first but when you look at the actual statements and values being passed around, you can quickly get a feel for how the code works. We can then easily apply this attribute and cache the result of any method in a class automatically like this:

type
  Flickr = public class
  private
    _username: string;
  public
    method Flickr(username: String);
    [Aspect:MethodCache]
    method GetPhotos: XmlDocument;
    [Aspect:MethodCache]
    method GetUsername: String;
    [Aspect:MethodCache]
    method GetAuthLevel(user: String): Integer;
  end;

Where GetPhotos, GetUsername and GetAuthLevel will all have their results cached automatically by our Aspect. You can see the resulting AOP’d code disassembled in the reflector below:

The RedGate .NET Reflector showing our Cirrus doctored caching code.

The RedGate .NET Reflector showing our Cirrus doctored caching code.

There is still one slight limitation in our method of key generation which is that it will currently only generate a unique key for a method with parameters if the Types of the methods parameters provide a proper implementation of ToString() that identifies them. Before you mention it, I had considered using the GetHashCode() method for this purpose but the default implementation of the GetHashCode method does not guarantee unique return values for different objects which makes it no more useful than relying on the user to implement the ToString() method.

I have made my implementation available on GitHub as the CirrusCachingAttribute where you can feel free to download and play with the source but please note that you will need to download and install the Enterprise Library from Microsoft first. I have also added a stub to the Code section on my site for the Cirrus Caching Attribute.

There is also a Standard Library of Aspects over on code.remobjects.com which provides a good set of Aspects from which you can learn more about how Cirrus works.

Tags: , , ,

2 Comments

  1. John Moshakis on the 18th November 2009 remarked #

    Nice article.

    I didn’t know about AutoInjectIntoTarget, it looks like it has a bug. It should really check to see if the method already exists before trying to add it.
    I would add a defect to quality central.

  2. jamiei on the 19th November 2009 remarked #

    Thanks John, not approaching the complexity of your Aspects yet but we all start somewhere!

    I had suspected it was a bug but thought I’d run it past you to check I wasn’t about to submit a caused by lack of user intelligence bug report.

Leave a Comment