|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
IntroductionThe 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.
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:
Design by IdealsI 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:
Recording Parameters and Return ValuesWhen 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:
Playing Back Pre-recorded Return ValuesWhen 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:
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 WeavingLinFu 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 UseBefore 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 [SingleThreadExecution("AccountActivity")]
[ObservableMethod]
public double MakeDeposit(double amount)
{
AccountBalance += amount;
return AccountBalance;
}
The method
[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):
In the example below, the property [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 // 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 ExamplesTo 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 FrameworkUsing the LinFu framework, classes that implement An instance of an Perhaps, the easiest way to see how this all works is to present a well-commented example: The /// <summary>;
/// Hooks into woven assemblies - BeforeInvoke is called before
/// method invocation (duh), and AfterInvoke is called after.
/// </summary>
public class LogWrapper : AroundMethodBase, IAroundInvoke
{
/// <summary>
/// BeforeInvoke is executed before the decorated method is executed
/// </summary>
public override void BeforeInvoke(IInvocationContext context)
{
Execute(context, null, CutpointType.Before);
}
/// <summary>
/// AfterInvoke is executed after the decorated method has been executed
/// </summary>
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
To do this, I needed a way to:
To do this, I had to modify the LinFu- and Cecil-based code that performed post-build weaving.
// 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 IModifiableType mod = (context.Target as IModifiableType);
mod.IsInterceptionDisabled = true;
The class that implements 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:
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]
public class BankAccount : ChangeTrackingBaseClass<BankAccount>
{
...
}
Addressing the Original Issue: Unit TestsMy 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:
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'):
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'):
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 ValuesConsider 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 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:
At the most basic level is the How it's Implemented: Recording and Retrieving Return ValuesThe method 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 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 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);
/// <summary>
/// Substitutes a pre-recorded return value (if one is found)
/// in lieu of original method execution
/// </summary>
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;
}
}
}
/// <summary>
/// Re-enables original method execution
/// </summary>
private void ReEnableMethodExecution(IInvocationContext context)
{
IModifiableType mod = (context.Target as IModifiableType);
mod.IsInterceptionDisabled = Convert.ToBoolean(context.ExtraInfo);
mod.ExtraInfoAdditional = null;
context.ExtraInfo = null;
}
/// <summary>
/// Records param and return values to MockObjectRepository for later playback
/// </summary>
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. InfoThe following class-level attributes are needed to enable AOP post-weaving and to mark all methods in a class as mock-able:
One final attribute allows specific methods to be excluded from mock recording and playback (when the class is marked with the
AOPDemo.MockObjectDemo and Record/ReplayThe AOPDemo.MockObjectDemo project demonstrates recording and playback of return values. Specifically, the method
Solution Contents / Demo ApplicationsThe VS2008 solution contains the following projects and demo applications:
Hope you found the article useful and/or interesting! All comments/suggestions are welcome. Email address: owen@binarynorthwest.com. Website: BrainTechLLC. History
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||