Click here to Skip to main content
Click here to Skip to main content

Applied Use of LinFu/Cecil and Aspect-Oriented Programming Concepts - A Library

, 4 Sep 2008
Rate this:
Please Sign up or sign in to vote.
A library of useful functionality using Aspect-Oriented Programming concepts, and implemented using the LinFu and Cecil.Mono projects/frameworks.

Introduction

The presented library uses the Aspect Oriented Programming related functionality provided by the LinFu project (which itself uses Cecil.Mono) to address a multitude of cross-cutting concerns. These concerns include logging method calls, timing method execution, retrying method execution, restricting or preventing changes to property values, rolling back property changes to a previous state, ensuring single thread execution of methods, asserting global conditions pre- and post-method-execution, allowing methods and properties to be observable, and (last but not least) recording method parameters and playing back method return values.

Example

Background: Why?

I currently work with a small IT team that believes in writing unit tests for code as the code is written. When I first joined the team, I dutifully wrote unit tests for each module I worked on. Soon, though, I began to notice that the unit tests were rarely run, and several times when I opened the solution containing all unit tests, it would not compile due to errors. Thinking this was a problem with developers not updating their unit tests as they modified dependent code, I went about fixing the compile-time errors. When I went to run the unit tests, almost all of the tests failed... What was up?

The majority of the unit tests relied on retrieving test data from the database. When the test data was removed or modified (for instance, when the production database was copied to our test environment), unit tests would fail. Therefore, since nobody could really *use* the unit tests more than a handful of times, developers had little motivation to update their test code as features were added, changed, or removed.

Possible Solutions?

I came up with several solutions, all of which are fairly well documented on the web:

  1. One solution involved restoring a database backup before each test. However, unit tests already were taking long enough to run - and even after stripping down our database (normally 3GB) to create a smaller test database (200-300MB), each database restore operation still took a while to complete.
  2. I explored what it would take to write code that inserted the appropriate data into the database if it did not exist. However, most of the unit tests relied on a multitude of data in many different tables, and furthermore relied on specific flags being set or not set. Writing code to generate the appropriate test data for each unit test was simply impractical.
  3. As another solution, I explored the idea of abstracting the way in which we retrieved our data so that our data source could be swapped transparently. However, implementing this (or a similar solution) would mean rewriting our data access gateway layer. I was certainly not thrilled about the prospect of rewriting such a critical (and large) component that was already functioning just fine in our production environment. However, the concept of modifying the source from which our gateway retrieved data was an interesting one...
  4. I finally stumbled across RhinoMock, NMock, and EasyMock - frameworks built to provide testable objects for unit testing. However, using these frameworks would involve modifying unit test code - something that would be difficult to convince the developers was worthwhile. Plus, if a developer accidentally called a gateway method, we could still run into the situation where expected data could not be retrieved from the database, resulting in likely-still-functional code failing to pass a unit test.

Design by Ideals

I finally decided that, to move forward with any solution, I needed to write down what I really wanted to do. I came up with the following list of requirements:

  1. I didn't want to touch / modify existing gateway code, and I didn't want my solution to alter production code in any way, shape, or form.
  2. I wanted to be able to record values returned from gateway methods. This would involve the following:
    • During an initial execution of the unit test (when the proper test data is in the database), intercept all calls made to any gateway method.
    • Store the parameters used to call the gateway method, and then store any value returned by the gateway method.
    • Save all recorded parameters and return values used during execution of the unit test to disk.
  3. I wanted to be able to replay pre-recorded return values at a later time, bypassing execution of the gateway methods. To do this, I would need to:
    • Intercept all calls made to any gateway method during execution of unit tests.
    • When a gateway method was called during subsequent unit test runs, bypass the method call. Instead, the gateway method would return pre-recorded data - data that was in the database at the time the unit test was written.
  4. Addressing the issue of a unit test possibly getting back stale (unrealistic/old) data, I also wanted the capability of re-recording returned data from the database and then playing back the new data to the unit test.

Recording Parameters and Return Values

When a gateway method is called, the parameter values used and the method's return value should be recorded for later use, as shown in the diagram below:

Example

Playing Back Pre-recorded Return Values

When a gateway method is called during playback, the return value should be retrieved by matching method parameter values, and the original gateway method should be skipped altogether, as shown in the diagram below:

Example

The next step was to determine how I could dynamically modify the program flow to mimic either of the execution paths shown in the diagrams above...

Aspect Oriented Programming (AOP)

Cruising through CodeProject, online books, and programming sites, I stumbled across the concept of Aspect Oriented Programming (AOP). Crosscuts, advice, join points, pointcuts, etc., etc., etc... the terminology was all nice and good, but what was important was that, by using an Aspect-Oriented Programming concept called 'weaving', I could execute code before a method call, after a method call, or replace a method call altogether in our test builds, with minimal modification to our original source code. Furthermore, by *not* weaving the production build, I could ensure that the existing code would be executed in an unmodified and unmolested state in our production environment.

Next step - In my usual manner, I promised myself I would read a few academic papers on the subject... later... and then set about writing a library that used AOP to solve our problem. I chose to build atop the LinFu AOP library (which uses Cecil.Mono), and I must give credit to both projects - it would have been a heck of a lot more difficult to do what I wanted to do without having a lot of the dirty work already done for me.

Postweaver / Post-Build Weaving

LinFu uses a process called Weaving to allow programmer-defined code to be executed before, after, and/or instead of a method call. To do this, IL instructions are injected into a compiled assembly, wrapping each original method. The instructions that wrap the original method calls allow conditional branching to programmer-defined code to occur before method execution, after method execution, and/or instead of the original method's execution.

Example

Example Use

Before going further, I'd like to show a few code snippets that demonstrate the various features of the library presented in this article.

The first example shows a method called MakeDeposit (think: bank transaction). By decorating the method with the SingleThreadExecution attribute, the method is guaranteed not to be executed concurrently on multiple threads. By decorating the method with the ObservableMethod attribute, observers can be registered and notified whenever the MakeDeposit method is called:

[SingleThreadExecution("AccountActivity")]
[ObservableMethod]
public double MakeDeposit(double amount)
{
   AccountBalance += amount;
   return AccountBalance;
}

The method WithdrawMoney is presented below. The AssertBeforeAfter attribute denotes that the PreCheckWithdrawl method will be called before WithdrawMoney is executed. If the method returns false or throws an exception, an exception can be raised or the method execution can be skipped. The PostCheckWithdrawl attribute denotes that the PostCheckWithdrawl method should be called after execution to ensure that nothing sneaky went on during the withdrawal. The SingleThreadExecution attribute ensures that:

  1. WithdrawMoney is not executed concurrently on multiple threads, and
  2. that WithdrawMoney is not executed concurrently with any other method that is also marked with the [SingleThreadExecution("AccountActivity")] attribute (i.e. the MakeDeposit method, presented above):
[SingleThreadExecution("AccountActivity")]
[AssertBeforeAfter("PreCheckWithdrawl", 
      "PostCheckWidthdrawl", 
      "Invalid Account Balance", 
      "Withdrawl of ${0} would create a negative balance")]
public double WithdrawMoney(double amount)
{
   AccountBalance -= amount;
   return AccountBalance;
}

After depositing $100, attempting to withdraw $60 twice would then result in output similar to what is shown below (the example below is from the demo app):

Example

In the example below, the property SSN stores a customer's social security number (probably not the best idea...). Because the property is marked with the RestrictPropertyChange attribute, the ValidateSSN method will be called before the property's value is changed. If the ValidateSSN method returns false, the property change will not be allowed - the code within set {...} will be bypassed completely. Furthermore, since the Customer class implements ISupportsPropertyChangeRestrictedNotify, the PropertyChangeRestricted event will be raised if the property change is restricted. (If the class does not implement ISupportsPropertyChangeRestrictedNotify, the change is blocked without notification.) Next, the PropertyChangedAction attribute specifies that some action should be taken whenever the property value is successfully changed. In the example below, the property AccountOwnerSSN of the the BankAccount class instance stored in the property Account will be automatically set to reflect the new SSN value.

[AOPEnabled]
public class Customer : ISupportsPropertyChangeRestrictedNotify, ICanIdentifyAsMockObject
{
   public event EventHandler
       <ObjectEventArgs<
           IInvocationContext, PropertyInfo, object, object, Exception>> 
              PropertyChangeRestricted;

   [LogPropertyChanged, RecordPropertyChange]
   public string FirstName { get; set; }

   [LogPropertyChanged, RecordPropertyChange]
   public string LastName { get; set; }

   [RestrictPropertyChange("ValidateSSN"), 
                  LogPropertyChanged, RecordPropertyChange]
   [PropertyChangedAction("Account", "AccountOwnerSSN")]
   public string SSN { get; set; }
      
   private BankAccount _account;
   public BankAccount Account { get { return Populate.OnDemand(ref _account); } }
   
   public static Exception ValidateSSN(
      string propertyName, object oldVal, object newVal, IInvocationContext context)
   {
      try
      {
         if (newVal != null)
         {
            string newString = newVal.ToString();
            long conversionCheck;

            if (!long.TryParse(newString, out conversionCheck) || 
                 newString.Length != 9)
            {
               return new Exception("SSN must be 9 digits");
            }
         }
      }
      catch (Exception ex)
      {
         return ex;
      }

      return null;
   }
}

In the above example, the class Customer has several properties marked with the RecordPropertyChanged attribute. This allows us to use the library's built-in ChangeTracker to do the following:

// Set up a customer
Customer customer = new Customer() 
{ 
   FirstName = "Fred", 
   LastName = "Flintstone", 
   SSN = "111992222" 
};

customer.ChangeTracker.ResetPropertyChanges();

// Make a few changes to property values
customer.FirstName = "Freddy";
customer.SSN = "111992223";

// Decided the changes are no good. Revert back to the baseline
customer.ChangeTracker.RevertChangesToLastReset(customer);

// Reverted back to old values - customer.FirstName = 
//        "Fred" and SSN = "111992222"

The source code and the solution includes several demo apps which expand upon the code snippets presented above and demonstrate various features of the library.

Implementing AOP - Standard Examples

To familiarize myself with AOP basics, I started by implementing two of the most boring (but still useful) features I could think of - keeping a count of times a method is called, and logging. Specifically, with logging, my goal was to write out a log entry both before and after a specific method was called. The only change I wanted to have to make to my existing code was to add an attribute to the method I wanted logged. Example:

[Logable(LogSeverity.Information, "Before MakeDeposit", "After MakeDeposit")]
public double MakeDeposit(double amount)
{
   ...
}

AOP Using the LinFu Framework

Using the LinFu framework, classes that implement IAroundInvoke are used to hook into woven assemblies. At runtime, the IAroundInvoke methods BeforeInvoke and AfterInvoke are called before and after the original method execution.

An instance of an IInvocationContext-implementing class is used to pass the context information to the BeforeInvoke and AfterInvoke methods. The IInvocationContext parameter contains information such as the method name being called, the parameter values being passed to the method, and other information about the calling context. To weave an assembly and enable this AOP/method-related black magic, LinFu/Cecil's Postweaver is used; as an example, the post-build event "postweaver.exe $(TargetPath)" is run after building the project AOPClasses in the provided solution.

Perhaps, the easiest way to see how this all works is to present a well-commented example: The LogWrapper, presented below, is a class used to write out log entries before and after method execution using an implementation of LinFu's IAroundInvoke interface:

/// <span class="code-SummaryComment"><summary>;</span>
/// Hooks into woven assemblies - BeforeInvoke is called before 
/// method invocation (duh), and AfterInvoke is called after.
/// <span class="code-SummaryComment"></summary></span>
public class LogWrapper : AroundMethodBase, IAroundInvoke
{
   /// <span class="code-SummaryComment"><summary></span>
   /// BeforeInvoke is executed before the decorated method is executed
   /// <span class="code-SummaryComment"></summary>      </span>
   public override void BeforeInvoke(IInvocationContext context)
   {
      Execute(context, null, CutpointType.Before);
   }

   /// <span class="code-SummaryComment"><summary></span>
   /// AfterInvoke is executed after the decorated method has been executed
   /// <span class="code-SummaryComment"></summary></span>
   public override void AfterInvoke(
      IInvocationContext context, object returnValue)
   {
      Execute(context, returnValue, CutpointType.After);
   }

   private void Execute(
      IInvocationContext context, object returnValue, CutpointType when)
   {
      // The "this" object being used for the method call
      object thisObject = context.Target;

      // Get the "Logable" attributes attach to the method being called
      LogableAttribute[] attrs = 
                         context.TargetMethod.ReadAttribute<LogableAttribute>();

      attrs.ForEachAttribute(delegate(LogableAttribute attr)
      {
         // If before method execution, log the specified "before" message
         // The log message can use {0}...{n} to write out the method's 
         // parameter values...
         if (when == CutpointType.Before && 
             !string.IsNullOrEmpty(attr.MessageType))
         {
            GlobalLogger.Log(
               attr.MessageType,
               String.Format(attr.LogMessage, context.Arguments),
               attr.LogLevel);
         }
         // If after method execution, log the specified "after" message.
         // The log message can use {0} to write out the method's return value
         else if (when == CutpointType.After && 
                  !string.IsNullOrEmpty(attr.MessageTypeAfter))
         {
            GlobalLogger.Log(
               attr.MessageTypeAfter,
               String.Format(attr.LogMessageAfter, returnValue),
               attr.LogLevelAfter);
         }
      });
   }
}

Something a Little More Challenging...

This was all fairly easy (and very cool), so I decided to try my hand at implementing a RestrictPropertyChange attribute that, when attached to a property, can skip execution of the "set" code used to change the property value - if the value does not meet certain user-defined criteria. Also, I wanted notification of the restricted change, either in the form of an exception being thrown, or via a raised event:

Example

To do this, I needed a way to:

  1. Specify (at runtime) that execution of the original method should be skipped.
  2. Skip execution of the original method, without affecting concurrent execution of other methods defined by the class.

To do this, I had to modify the LinFu- and Cecil-based code that performed post-build weaving.

  • I added three properties to the definition of (and post-build creation of) IModifiableType: two object properties to store misc./additional pieces of data, and a boolean property, IsInterceptionDisabled, to denote that the original method call should be skipped.
  • It was necessary to modify the LinFu/Cecil-based weaver to generate the appropriate IL to check the value of IsInterceptionDisabled and possibly skip execution of the original method. The IL generated by the following code checks the IsInterceptionDisabled flag (_isDisabled, below) and branches if true, skipping execution of the original method call:
// Checks the IsInterceptionDisabled flag - used to bypass
// original method execution

instructions.Enqueue(IL.Create(OpCodes.Ldarg_0));
instructions.Enqueue(IL.Create(OpCodes.Isinst, _modifiableType));

// if IsInterceptionDisabled == false then continue onward
instructions.Enqueue(IL.Create(OpCodes.Callvirt, _isDisabled));
instructions.Enqueue(IL.Create(OpCodes.Brfalse, skipToEnd));

// otherwise go to the post-method call (skip original method execution)
instructions.Enqueue(IL.Create(OpCodes.Br, JumpForDone));

instructions.Enqueue(skipToEnd);

After making these changes, we can mark that execution of the original method should be skipped in BeforeInvoke by setting the IsInterceptionDisable property to true prior to the original method call:

IModifiableType mod = (context.Target as IModifiableType);
mod.IsInterceptionDisabled = true;

The class that implements IAroundInvoke and executes or skips set_Property based on a value, is shown below:

public class RestrictPropertyChangeWrapper : AroundMethodBase, IAroundInvoke
{
   public override void BeforeInvoke(IInvocationContext context)
   {
      Execute(context, CutpointType.Before);
   }

   public override void AfterInvoke(IInvocationContext context, object returnValue)
   {
      // If we skipped executing property set {...} this time, restore 
      // any previous IsInterceptionDisabled value that may have been set
      if (context.ExtraInfo != null)
         RestoreIsInterceptionDisabledFlag(context);
   }

   private void Execute(IInvocationContext context, CutpointType when)
   {
      object thisObject = context.Target;
      Type t = thisObject.GetType();

      // Method name comes in as set_PropertyName
      // Grab the property name by taking the substring starting at position 4
      string propInfoName = context.TargetMethod.Name.Substring(4);
      
      // Get property info and other details about the property being set
      PropInfo propInfo = t.GetPropInfo(propInfoName);

      // Get RestrictPropertyChangeAttribute attributes attached to the property
      RestrictPropertyChangeAttribute[] attrs = 
         propInfo.GetAttributes<RestrictPropertyChangeAttribute>();

      if (attrs != null && attrs.Length > 0)
      {
         // Read the old property value and record the new one
         object oldValue = propInfo.GetValue(thisObject);
         object newValue = context.Arguments[0];

         for (int n = 0; n < attrs.Length; n++)
         {
            RestrictPropertyChangeAttribute attr = attrs[n];

            // See if the property change is restricted
            Exception ex = attr.IsRestricted(
               thisObject, t, propInfo.Name, oldValue, newValue, context);

            if (ex != null)
            {
               // Send notification regarding the restriction, if possible
               ISupportsPropertyChangeRestrictedNotify notify = 
                  thisObject as ISupportsPropertyChangeRestrictedNotify;
               if (notify != null)
                  notify.NotifyPropertyChangeRestricted(
                     context, propInfo.PropertyInfo, oldValue, newValue, ex);

               // Mark that the original method should NOT be executed
               SetInterceptionDisabledFlag(context, true);

               if (attr.ThrowOnException)
                  throw ex;
            }
         }
      }
   }

   private void RestoreIsInterceptionDisabledFlag(IInvocationContext context)
   {
      IModifiableType mod = context.Target as IModifiableType;

      // Set the flag back to its original (pre-method-call) value
      mod.IsInterceptionDisabled = Convert.ToBoolean(context.ExtraInfo);

      // Blank out ExtraInfo to mark that we're done using it
      context.ExtraInfo = null;
   }

   private void SetInterceptionDisabledFlag(
      IInvocationContext context, bool value)
   {
      IModifiableType mod = (context.Target as IModifiableType);

      // Store the old value of the IsInterceptionDisabled flag
      context.ExtraInfo = mod.IsInterceptionDisabled;

      // Mark that we do not want to execute the original method's code
      mod.IsInterceptionDisabled = true;
   }
}

After seeing LinFu and Cecil's power in action, I went on a bit of a rampage, and implemented the following attributes and features I thought might be useful:

  • CountCalls - keeps a tally of how many times the decorated method has been called.
  • Logable - adds a log entry before and/or after each call to the decorated method.
  • Retry - retries execution a certain number of times if an exception is thrown during execution of the decorated method.
  • TimeMethod - times how long it takes for the decorated method to execute.
  • SingleThreadExecution - ensures that the method is not executed more than one thread at any given time.
  • IfCase - executes code before method execution only when certain conditions are met.
  • AssertBeforeAfter - checks that specific conditions are either true or false, both before and after method execution.
  • ObservableMethod - observers can be registered dynamically. When the decorated method is called, all registered observers are notified.
  • RecordParameters - both method input parameters and the method return value is recorded when the decorated method is called.
  • ObservableProperty - observers can be registered dynamically. When the decorated property is set, all registered observers are notified.
  • LogPropertyChanged - adds a log entry with the old and new property value whenever the decorated property value is changed.
  • PropertyChangedAction - executes a developer-defined method whenever the decorated property value changes.
  • RecordPropertyChange - records before and after values when the decorated property value changes.
  • RestrictPropertyChange - checks the before/after value before the decorated property value changes, and skips/bypasses changing the property value if the defined conditions are not met.

Performance Overhead?

Yep, there is a performance overhead associated with executing woven assemblies; all woven method calls (including property sets and gets) end up doing extra work, both before and after original method execution. To help limit this overhead to only classes that absolutely need these AOP-related capabilities, I defined the AOPEnabled attribute and modified Postweaver to only weave classes marked with this attribute:

[AOPEnabled]
public class BankAccount : ChangeTrackingBaseClass<BankAccount>
{
   ...
}

Addressing the Original Issue: Unit Tests

My interest in AOP started with the unit test/data inconsistency issue I encountered at work (discussed in the Background section - up top). Basically, unit tests quickly became useless because the data unit tests relied upon frequently disappeared from the database or was modified. I wanted a way to transparently record method return values (in recording mode) and play back the return values (in playback mode). Using LinFu/Cecil, and AOP concepts gave me all the necessary tools - I could now:

  1. intercept methods before execution and record the parameters being passed in,
  2. observe the method's return value before returning execution to the caller, and
  3. choose whether or not to execute the original method.

Here's how it ended up working:

Recording Mode

// Turn on return value recording
AOP.EnableAOP(MockMode.Recording);

// Turn off recording for a specific class type only
typeof(MyClass).EnableMockRecording(false);

// Turn back on recording for MyClass class type
typeof(MyClass).EnableMockRecording(true);

In 'recording' mode (for methods marked as 'Mock-able'):

  • When a method is called, the parameters passed in to the method and the method's return value are recorded.
  • The return value is stored in a lookup table and can be retrieved using the method's name and original parameter values.
  • When recording is done, method names, parameters, and return values are serialized out to a file for later retrieval.

Playback Mode

// Turn on return value playback
AOP.EnableAOP(MockMode.Playback);

// Load previously-recorded return values
MockObjectRepository.AddMockObjectsFromFile("RecordedRetVals.dat");

In 'playback' mode (for methods marked as 'Mock-able'):

  • Previously recorded method names, parameters, and return values can be loaded and deserialized from file and added to a lookup table for quick retrieval.
  • When a mock-able method is called, the lookup table is used to find a previously-recorded return value associated with the method name and the same parameters used to make the original call.
  • If there is a matching return value (same class type, same method name, same parameters), execution of the original method is skipped, and the previously stored return value is used as the method call's return value.

In order to substitute a mock return value for a real one, bypassing the original method call, I had to add the following code to the LinFu/Cecil Postweaver:

// Allows mimicking recorded return values without actually
// executing the original method...
instructions.Enqueue(IL.Create(OpCodes.Ldarg_0));
instructions.Enqueue(IL.Create(OpCodes.Isinst, _modifiableType));

// if ExtraInfoAdditional property is null, continue onward
instructions.Enqueue(IL.Create(OpCodes.Callvirt, _extraInfo));
instructions.Enqueue(IL.Create(OpCodes.Brfalse, skipToEnd));

// otherwise, use the value stored in ExtraInfoAdditional as the 
// return value for the method
instructions.Enqueue(IL.Create(OpCodes.Ldarg_0));
instructions.Enqueue(IL.Create(OpCodes.Isinst, _modifiableType));

// get the return value stored in the ExtraInfoAdditional property
// note: The property's return value will be used as the 
// original method's return value
instructions.Enqueue(IL.Create(OpCodes.Callvirt, _extraInfo));

// go to the post-method call (skip original method execution)
instructions.Enqueue(IL.Create(OpCodes.Br, JumpForDone));

instructions.Enqueue(skipToEnd);

MockObjectRepository: Storing Parameter and Return Values

Consider the following code:

public List<Customer> FindCustomerUsingPersonalInfo(string customerSSN)
{
   ...
}

public List<Customer> FindCustomerUsingPersonalInfo(
                     string customerSSN, DateTime birthday, string zipCode)
{
   List<Customer> partialMatches = new List<Customer>(
      FindCustomerUsingPersonalInfo(customerSSN));
                              
   partialMatches.RemoveAll(delegate (Customer c) { 
      return (c.Birthday != birthday || c.ZipCode != zipCode); });
                      
   return partialMatches;
}

List<Customer> c1 = FindCustomerUsingPersonalInfo("536187315");

List<Customer> c2 = FindCustomerUsingPersonalInfo(
                     "111223333", new DateTime(1975, 7, 1), "80111");

List<Customer> c3 = FindCustomerUsingPersonalInfo(
                     "555252193", new DateTime(1976, 7, 19), "98225");

List<Customer> c4 = FindCustomerUsingPersonalInfo(
                     "359252491", new DateTime(1972, 4, 19), "80301");

Each call to FindCustomerUsingPersonalInfo passes in different parameter values, and each call (most likely) returns a different list of customers, depending on the parameter values passed in. To handle variation of return values by parameter values, I use instances of the class StoredParametersAndReturnValue to store hash codes that identify parameter values passed into a method call and the value that was returned from the method, given the specified parameters. The class also defines two methods, PackageMockForStorage and UnpackageMockFromStorage, that can be used to serialize and deserialize return values and parameter hash values to/from a compressed byte array.

In order to locate return values for a specific class type and method, I chose to store parameters and return values using the following data structures:

Example

At the most basic level is the StoredParametersAndReturnValue class, discussed above. At a level up from individual stored parameters and return values, instances of the class MockObjectsForMethod are used to store the parameter hash and return values associated with a single method. At the next level up, MockObjectsForClass is used to store a collection of MockObjectsForMethod for a single class type. Finally, a single instance of MockObjectRepository is used to store a collection of MockObjectsForClass objects. Storing return values by class type and method name lets us quickly drill down into all recorded return values associated with a specific class and method.

How it's Implemented: Recording and Retrieving Return Values

The method RecordMockObjectReturnValue of class MockObjectRepository is used to record a return value associated with a specific class type and method name, when passed a specific set of parameter values:

public static StoredParametersAndReturnValue RecordMockObjectReturnValue(
          this Type t, string methodName, object[] parameters, object returnValue)
{
   // Retrieve the structure that holds all recorded objects for class type t
   MockObjectsForClass mockObjects = RetrieveMockObjectsForClass(t);

   // Calculate a list of hash values that can be used to 
   // uniquely identify parameter values
   List<int> paramHash = GetParametersHash(parameters);
   int[] paramsHashArray = paramHash.ToArray();

   // Retrieve the structure that holds all recorded objects 
   // for a specific method name
   MockObjectsForMethod methodMocks = 
       mockObjects.GetMockObjectsForMethod(methodName);

   // Find any existing stored parameters/return value associated with 
   // this combo of type, method name, and parameters.
   StoredParametersAndReturnValue found = 
      methodMocks.LookupByParameters.FindItem(paramsHashArray);
   
   if (found == null)
      found = new StoredParametersAndReturnValue();
   else
      methodMocks.LookupByParameters.RemoveTerminatingItem(paramsHashArray);

   // Set up values in StoredParametersAndReturnValue
   found.ListOfParameterHash = paramHash;
   found.Parameters = new List<object>(parameters);
   found.ReturnValue = returnValue;

   // Add this instance of StoredParametersAndReturnValue to 
   // the lookup data structure
   methodMocks.LookupByParameters.AddTerminatingItem(found, paramsHashArray);

   return found;
}

The method StoredParametersAndReturnValue of the class MockObjectRepository is used to find a previously-recorded return value associated with a specific class type and method name, given specific parameter values:

public static StoredParametersAndReturnValue GetMockObject(
                 this Type t, 
                 string methodName, 
                 params object[] parameters)
{
   // Retrieve the structure that holds all 
   // recorded objects for class type t
   MockObjectsForClass mockObjects = RetrieveMockObjectsForClass(t);

   // Calculate a list of hash values that can be used to 
   // uniquely identify parameter values
   List<int> paramHash = GetParametersHash(parameters);

   // Retrieve the structure that holds all recorded objects 
   // for a specific method name
   MockObjectsForMethod methodMocks = 
      mockObjects.GetMockObjectsForMethod(methodName);

   // Find a pre-recorded return value for this class type, 
   // method name, and parameter values
   StoredParametersAndReturnValue found = 
      methodMocks.LookupByParameters.FindItem(paramHash.ToArray());

   // The return value may need to be unpacked from 
   // compressed binary storage
   if (found != null && found.IsPacked)
      found.UnpackageMockFromStorage(true, true);

   return found;
}

Bringing all of this functionality together, the class RecordParametersWrapper implements IAroundInvoke, records and/or plays back return values, and has the capability of skipping the original method call while transparently providing a pre-recorded return value to the caller:

// RecordParametersWrapper is able to (1) record parameter and return values and/or 
// (2) play back pre-recorded return values in lieu of original method execution
public class RecordParametersWrapper : AroundMethodBase, IAroundInvoke
{
   // Dummy RecordParametersAttribute associated with methods in 
   // ClassMethodsMockable-attribute marked classes
   private static readonly RecordParametersAttribute RecordParamForMocking = 
                               new RecordParametersAttribute(true, true);

   /// <span class="code-SummaryComment"><summary></span>
   /// Substitutes a pre-recorded return value (if one is found) 
   /// in lieu of original method execution
   /// <span class="code-SummaryComment"></summary></span>
   private void UseMockReturnValue(IInvocationContext context, Type t)
   {
      StoredParametersAndReturnValue mock = 
         t.GetMockObject(context.TargetMethod.Name, context.Arguments);

      if (mock != null)
      {
         // Found a pre-recorded return value
         IModifiableType mod = (context.Target as IModifiableType);
         context.ExtraInfo = mod.IsInterceptionDisabled;
         mod.ExtraInfoAdditional = mock.ReturnValue;

         // Mark that original method execution should be skipped
         mod.IsInterceptionDisabled = true;

         if (mock.ReturnValue != null)
         {
            // If the recorded return object is identifiable as a 
            // pre-recorded return value, mark it as such
            ICanIdentifyAsMockObject mockIndicatable = 
               mock.ReturnValue as ICanIdentifyAsMockObject;

            if (mockIndicatable != null)
               mockIndicatable.IsMockReturnValue = true;
         }
      }
   }

   /// <span class="code-SummaryComment"><summary></span>
   /// Re-enables original method execution
   /// <span class="code-SummaryComment"></summary>      </span>
   private void ReEnableMethodExecution(IInvocationContext context)
   {
      IModifiableType mod = (context.Target as IModifiableType);
      mod.IsInterceptionDisabled = Convert.ToBoolean(context.ExtraInfo);
      mod.ExtraInfoAdditional = null;
      context.ExtraInfo = null;
   }

   /// <span class="code-SummaryComment"><summary></span>
   /// Records param and return values to MockObjectRepository for later playback
   /// <span class="code-SummaryComment"></summary></span>
   private void RecordParametersToMock(IInvocationContext context, Type t, 
      RecordParametersAttribute attr, object returnValue)
   {
      MockObjectsForClass mockObjects = t.RetrieveMockObjectsForClass();
      if (!attr.OnlyWhenRecordingMocks || mockObjects.MockRecordingEnabled)
      {
         StoredParametersAndReturnValue found = 
            t.RecordMockObjectReturnValue(
               context.TargetMethod.Name, 
               context.Arguments, 
               returnValue);
            
         found.PackageMockForStorage(true, true);
      }
   }

   private void Execute(
      IInvocationContext context, CutpointType when, object returnValue)
   {
      object thisObject = context.Target;
      Type t = thisObject.GetType();
      RecordParametersAttribute[] attrs = 
         context.TargetMethod.ReadAttribute<RecordParametersAttribute>();

      DoNotMockMeAttribute[] attrsDoNotMock = 
         context.TargetMethod.ReadAttribute<DoNotMockMeAttribute>();

      bool classIsMockable = t.GetClassIsMockable();

      // Determine if the current method call is mockable

      if (classIsMockable || (attrs != null && attrs.Length > 0))
      {
         bool noMock = false;

         // Check the method for the DoNotMockMe attribute
         if (attrsDoNotMock != null && attrsDoNotMock.Length > 0)
         {
            foreach (DoNotMockMeAttribute doNotMock in attrsDoNotMock)
            {
               if (doNotMock.DoNotMock)
               {
                  noMock = true;
                  break;
               }
            }
         }

         RecordParametersAttribute attr;
         int count = classIsMockable ? 1 : 0;
         int total = (attrs == null) ? 0 : attrs.Length;

         for (int n = 0; n < total + count; n++)
         {
            // If the CLASS is marked with the ClassMethodsMockable attribute,
            // consider the method for recording/playback...
            attr = (classIsMockable && n == total) ? RecordParamForMocking : attrs[n];

            // unless the method is marked with the DoNotMockMe attribute
            if (!noMock)
            {
               // Replaying mock objects?  If so, set the
               // return value to the mock object and mark the 
               // context to skip execution of the original method
               if (when == CutpointType.Before && 
                  attr.AllowMockReplay && 
                  MockObjectRepository.MockReplayEnabled)
               {
                  UseMockReturnValue(context, t);
               }

               // Replaying mock objects?  Afterwards, re-enable execution of 
               // the original method
               if (when == CutpointType.After && 
                  attr.AllowMockReplay && 
                  MockObjectRepository.MockReplayEnabled && 
                  context.ExtraInfo != null)
               {
                  ReEnableMethodExecution(context);
               }

               // Recording parameters?
               if (when == CutpointType.After && 
                  (!attr.OnlyWhenRecordingMocks || 
                   MockObjectRepository.MockRecordingEnabled))
               {
                  RecordParametersToMock(context, t, attr, returnValue);
               }
            }

            MethodInfo methodBefore = attr.GetMethodBefore(t);
            MethodInfo methodAfter = attr.GetMethodAfter(t);

            // Optionally execute programmer-specified before and after methods 
            // when the method is called
            if (when == CutpointType.Before)
            {
               if (methodBefore != null)
                  methodBefore.Invoke(thisObject, new object[] { context });
            }
            else
            {
               if (methodAfter != null)
                  methodAfter.Invoke(thisObject, new object[] { context, returnValue });
            }
         }
      }
   }

   public override void AfterInvoke(IInvocationContext context, object returnValue)
   {
      Execute(context, CutpointType.After, returnValue);
   }

   public override void BeforeInvoke(IInvocationContext context)
   {
      Execute(context, CutpointType.Before, null);
   }
}

Addendum / Misc. Info

The following class-level attributes are needed to enable AOP post-weaving and to mark all methods in a class as mock-able:

  • AOPEnabled - enables AOP post-build weaving in the decorated class.
  • ClassMethodsMockable - enables the recording and replaying of mock objects for methods defined by the decorated class.

One final attribute allows specific methods to be excluded from mock recording and playback (when the class is marked with the ClassMethodsMockable attribute):

  • DoNotMockMe - Do not allow mock object recording and playback for the decorated method.

AOPDemo.MockObjectDemo and Record/Replay

The AOPDemo.MockObjectDemo project demonstrates recording and playback of return values. Specifically, the method FindLogin returns a randomly-generated first/last name when passed any username and password. In recording mode, the randomly-generated first/last name return values are stored, along with the parameter values used when calling the FindLogin method. When playing back recorded values, you'll note that you do not get back random names, but instead, are presented with the previously-recorded return values associated with the given username/password parameter combination.

Example

Example

Example

Example

Solution Contents / Demo Applications

The VS2008 solution contains the following projects and demo applications:

  • AOPLibrary - AOP-related library - contains implementation of the features discussed in this article.
  • BrainTechLLC.Core - common functions I use across multiple applications.
  • AOPClasses - AOP-enabled classes used in the demo applications.
  • LinFu.AOP.Weavers.Cecil, LinFu.AOP.CecilExtensions, PostWeaver, Simple.IoC.VS2008, Simple.IoC.Loaders.VS2008 - LinFu projects.
  • AOPDemo.PropertyChangeTracking - a Windows app that demonstrates property change tracking, and rollback of properties to a previous state / prior values.
  • AOPDemo.PropertyRestrictionsAndAssertions - a Windows app that demonstrates restricting property changes, and asserting method pre- and post-conditions.
  • AOPDemo.Other - demonstrates counting calls made to a method and method execution timing.
  • AOPDemo.MockObjectDemo - demonstrates recording of parameters and return values, and playback of return values based on method parameters.
  • AOPDemo.Combined - this application includes all of the demo features listed above.

Hope you found the article useful and/or interesting! All comments/suggestions are welcome.

Email address: owen@binarynorthwest.com. Website: BrainTechLLC.

History

  • Sept 4, 2008 - Article posted.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

owen654321
Software Developer (Senior) Troppus Software
United States United States
Currently working as a Senior Silverlight Developer with Troppus Software in Superior, CO. I enjoy statistics, programming, new technology, playing the cello, and reading codeproject articles. Smile | :)

Comments and Discussions

 
GeneralOh, Em, Gee... PinmvpPhilip Laureano5-Sep-08 3:17 
GeneralRe: Oh, Em, Gee... Pinmembersefstrat5-Sep-08 4:33 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web01 | 2.8.140709.1 | Last Updated 5 Sep 2008
Article Copyright 2008 by owen654321
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid