Intro
A few months ago, I had to implement a 'locking system' at work.
I will not elaborate to much on this system, but it's intention is that users can prevent that certain properties of certain entities are updated automatically;
The software-system in where I had to implement this functionality, keeps a large database up-to-date by processing and importing lots of data-files that we receive from external sources.
Because of that, in certain circumstances, users want to avoid that data that they've manually changed or corrected, gets overwritten with wrong information next time a file is processed.
The application where I'm talking about, makes heavy use of DataSets and I've been able to create a rather elegant solution for it.
At the same time, I've also been thinking on how I could solve this same problem in a system that is built around POCO's instead of Datasets, and that's what this post will be all about. :)
Enter Aspects
When the idea of implementing such a system first crossed my mind, I already realized that Aspects Oriented Programming could be very helpfull to solve this problem.
A while ago, I already played with Aspect Oriented Programming using Spring.NET.
AOP was very nice and interesing, but I found the runtime-weaving a big drawback. Making use of runtime weaving meant that you could not directly create an instance using it's constructor.
So, instead of:
MyClass c = new MyClass();
you had to instantiate instances via a proxyfactory:
ProxyFactory f = new ProxyFactory (new TestClass());
f.AddAdvice (new MethodInvocationLoggingAdvice());
ITest t = (ITest)f.GetProxy();
I am sure that you agree that this is quite a hassle, just to create a simple instance. (Yes, I know, offcourse you can make abstraction of this by making use of a Factory...).
Recently however, I bumped at an article on Patrick De Boeck's weblog, where he was talking about PostSharp.
PostSharp is an aspect weaver for .NET which weaves at compile-time!
This means that the drawback that I just described when you make use of runtime-weaving has disappeared.
So, I no longer had excuses to start implementing a similar locking system for POCO's.
Bring it on
I like the idea of Test-Driven-Development, so I started out with writing a first simple test:
The advantage of writing your test first, is that you start thinking on how the interface of our class should look like.
This first test tells us that our class should have a Lock
and an IsLocked
method.
The purpose of the Lock
method is to put a 'lock' on a certain property, so that we can avoid that this property is modified at run-time.
The IsLocked
method is there to inform us whether a property is locked or not.
To define this contract, I've created an interface ILockable
which contains these 2 methods.
In order to get this first test working, I've created an abstract class LockableEntity
which inherits from one of my base entity-classes implements this interface.
This LockableEntity
class looks like this:
This is not sufficient to get a green bar on my first test, since I still need an AuditablePerson
class:
These pieces of code are sufficient to make my first test pass, so I continued with writing a second test:
As you can see, in this test-case I define that it should be possible to unlock a property. Unlocking a property means that the value of that property can be modified by the user at runtime.
To implement this simple functionality, it was sufficient to just add an UnLock
method to the LockableEntity
class:
.
Simple, but now, a more challenging feature is coming up.
Now, we can already 'lock' and 'unlock' properties, but there is nothing that really prevents us from changing a locked property.
It's about time to tackle this problem and therefore, I've written the following test:
Running this test obviously gives a red bar, since we haven't implemented any logic yet.
The most simple way to implement this functionality, would be to check in the setter of the Name
property whether there exists a lock on this property or not.
If a lock exists, we should not change the value of the property, otherwise we allow the change.
I think that this is a fine opportunity to use aspects.
Creating the Lockable Aspect
As I've mentionned earlier, I have used PostSharp to create the aspects. Once you've downloaded and installed PostSharp, you can create an aspect rather easy.
There is plenty of documentation to be found on the PostSharp site, so I'm not going to elaborate here on the 'getting started' aspect (no pun intended).
Instead, I'll directly dive into the Lockable
aspect that I've created.
This is how the definition of the class that defines the aspect looks like:
Perhaps I should first elaborate a bit on how I would like to use this Lockable
aspect.
I'd like to be able to decorate the properties of a class that should be 'lockable' with an attribute. Like this:
Decorating a property with the Lockable
attribute, means that the user should be able to 'lock' this property. That is, prevent that it gets changed after it has been locked.
To be able to implement this, I've created a class which inherits from the OnMethodInvocationAspect class (which eventually inherits from Attribute
).
Why did I choose this class to inherit from?
Well, because there exists no OnPropertyInvocation
class or whatsoever.
As you probably know, the getters and setters of a property are actually implemented as get_
and set_
methods, so it is perfectly possible to use the OnMethodInvocationAspect
class to add extra 'concerns' to the property.
This extra functionality is written in the OnInvocation
method that I've overriden in the LockableAttribute
class.
In fact, it does nothing more then checking whether we're in the setter method of the property, and if we are, check whether there exists a lock on the property.
If there exists a lock, we won't allow the property-value to be changed. Otherwise, we just make sure that the implementation of the property itself is called.
The implementation looks like this:
Here, you can see that we use reflection to determine whether we're in the setter-method or in the getter-method of the property; we're only interested if this property is locked if we're about to change the value of the property.
Next, we need to get the name of the property for which we're entering the setter method. This is done via the GetPropertyForSetterMethod
method which uses reflection as well to get the PropertyInfo
object for the given setter-method.
Once this has been done, I can use the IsLocked
method to check whether this property is locked or not.
Note that I haven't checked whether the conversion from eventArgs.Delegate.Target
to ILockable
has succeeded or not. More on that later ...
When the property is locked, I call the OnAttemptToModifyLockedProperty
method (which is declared in ILockable
), and which just raises the LockedPropertyChangeAttempt
event (also declared in the ILockable
interface). By doing so, the programmer can decide what should happen when someone / something attempts to change a locked property. This gives a bit more control to the programmer and is much more flexible then throwing an exception.
When the property is not locked, we let the setter-method execute.
With the creation of this aspect, our third test finally gives a green bar.
Compile time Validation
As I've said a bit earlier, I haven't checked in the OnInvocation
method whether the Target really implemented the ILockable
interface before I called methods of the ILockable
type.
The reason for this , is quite simple: the OnMethodInvocationAspect
class has a method CompileTimeValidate
which you can override to add compile-time validation logic (hm, obvious).
I made use of this to check whether the types where I've applied the Lockable
attribute really are ILockable
types:
Note that it should be possible to make this code more concise, but I could not just call method.DeclaringType.GetInterface("ILockable")
since that gave a NotImplementedException
while compiling. Strange, but trueNow, when I use the Lockable
attribute on a type which is not ILockable
, I'll get the following compiler errors:
Pretty neat, huh ?
Now, what's left is a way to persist the locks in a datastore, but that will be a story for some other time ...