When dealing with POCOs, a common scenario is to track changes. Look at this interface:
1 2 3 4 5 6 7 8 9 |
namespace DirtyObjects { interface IView { string Name { set;get;} int Weight { set;get;} bool Modified { set;get;} } } |
It is the interface of a simple view class containing the name and weight of a product. The Modified property is used to track whether the object has changed its value, so that we know if we should save the object after it has been edited.
A typical implementation might look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
namespace DirtyObjects { class MyCumbersomeView : IView { private string name; private int weight; private bool modified = false; public int Weight { get { return weight; } set { if (weight != value) { weight = value; modified = true; } } } public string Name { get { return name; } set { if (name != value) { name = value; modified = true; } } } public bool Modified { get { return modified; } set { modified = value; } } } } |
It can be seen above that modified is set to true if a change is detected whenever a setter is called. This is pretty straightforward, yet it seems that an annoying pattern emerges when there is more than a few properties.
How can we then get rid of all the boilerplate code, all the noisy bits in between? Well, there is a way: by using Castle‘s
DynamicProxy and a simple technique from aspect oriented programming called method interception. Read on to see an example.
With
DynamicProxy your view class can look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
namespace DirtyObjects { public class MyCleanView : IView, IDirtyCheckable { private string name; private int weight; private bool modified = false; public virtual int Weight { get { return weight; } set { weight = value; } } public virtual string Name { get { return name; } set { name = value; } } public bool Modified { get { return modified; } set { modified = value; } } } } |
As you can see, it is much cleaner. And definitely more maintainable, as we have shaven off all the annoying bits. So how do we achieve the same behaviour without implementing the dirty tracking inside the view class?
Let’s specify our desired behaviour with a simple test:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
namespace DirtyObjects { [TestFixture] public class TestTrackDirtiness { [Test] public void CumbersomeDirtyChecking() { IView view = new MyCumbersomeView(); TestView(view); } [Test] public void SimpleDirtyChecking() { IView view = DirtyChecker.GetNewDirtyCheckedObject<MyCleanView>(); TestView(view); } private static void TestView(IView view) { Assert.IsFalse(view.Modified); view.Name = "Honey"; view.Weight = 42; Assert.IsTrue(view.Modified); view.Modified = false; view.Name = "Honey"; Assert.IsFalse(view.Modified); view.Name = "Bee Honey"; Assert.IsTrue(view.Modified); view.Modified = false; view.Weight = 42; Assert.IsFalse(view.Modified); view.Weight = 90; Assert.IsTrue(view.Modified); } } } |
As it can be seen, we want our view to start out as “not modified”. When we throw some values in it, it has suddenly become modified. Then we “forget” that it was modified, and set one of the values to the same value as before – still not modified. Then we actually change the name – now it should be modified. Then we do the same thing with a value type to assert the same behaviour applies when the values are boxed.
Note that the IView interface is used only so we can use the TestView method in both tests. Only the IDirtyCheckable interface must be implemented, so that we know that we can use the Modified property.
Let’s get on with the dirty tracking – we do it by creating a proxy class with DynamicProxy.
DynamicProxy is capable of doing what the name implies: dynamically building a proxy. It does some Reflection.Emit magic to dynamically subclass your view class. This subclass will contain overridden methods and properties for each virtual method or property in your base class, thus allowing something to be done each time a property or method is called. This something will be a call to the Intercept method of an implementation of the IInterceptor interface.
This is a phenomenon called method interception which is a key concept in aspect oriented programming.
Please note that it is important that methods and properties are virtual, otherwise the interception will not happen.
To actually do something, we need to supply the interceptor when creating the proxy. And we need some kind of help setting up the interception, because we do not want to do it manually every time we need an instance.
One possible solution is a DirtyChecker class, something like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
namespace DirtyObjects { public class DirtyChecker { public static T GetNewDirtyCheckedObject<T>() where T:IDirtyCheckable { ProxyGenerator generator = new ProxyGenerator(); T t = generator.CreateClassProxy<T>(new DirtyCheckableInterceptor()); return t; } private class DirtyCheckableInterceptor : IInterceptor { #region IInterceptor Members public void Intercept(IInvocation invocation) { string methodName = invocation.Method.Name; object target = invocation.InvocationTarget; if (methodName.StartsWith("set_") && methodName != "set_Modified") { IDirtyCheckable obj = (IDirtyCheckable)target; string propertyName = methodName.Substring(methodName.IndexOf('_') + 1); string nameOfCorrespondingGetter = string.Format("get_{0}", propertyName); MethodInfo getterMethod = invocation.TargetType.GetMethod(nameOfCorrespondingGetter); object currentValue = getterMethod.Invoke(invocation.InvocationTarget, null), newValue = invocation.Arguments[0]; if (!AreEqual(currentValue, newValue)) { obj.Modified = true; invocation.Proceed(); } } else { invocation.Proceed(); } } private bool AreEqual(object v1, object v2) { if (v1 == null && v2 == null) return true; else if (v1 == null && v2 != null) return false; else if (v1 != null && v2 == null) return false; else return v1.Equals(v2); } #endregion } } } |
Note how the type parameter <T> constrains the accepted types to be implementors of the IDirtyCheckable interface.
The static method GetNewDirtyCheckedObject<T> can now be used to create instances of our class. As you can see, the method is very simple, as it simply creates a ProxyGenerator, which in turn creates a proxy of our instance, applying an instance of the private class DirtyCheckableInterceptor to intercept method calls.
When a method call is intercepted, we can do some stuff and then decide whether we want to carry out the actual method call on the original object. This is done by calling Proceed on the supplied invocation argument.
In the example above, we detect if the desired method call is actually someone calling a set property on the object. If this is the case, we check the current value by calling the corresponding getter, comparing it to the desired new value. If they are not equal, we “dirtify” the object, and let the method call proceed.
Conclusion: By using DynamicProxy and method interception it is possible to track changes in a POCO object – completely transparent to the author of the POCOs, reducing the amount of boilerplate code.
Wow… I only have one comment: What happened to “keep it simple”, and what about the increased computation requirements? And I don’t like the implementation of the AreEqual method either 🙂
You are indeed more a computer scientist than a practical computer engineer 😛
DRY. From now on, every time I add a new model class, I save myself the trouble of punching in if (someField != value) { (...).
I don’t think the increased complexity will degrade performance, unless of cource you rely on being able to set the value of your object as fast as possible one million times a second all day long… which you most likely don’t! 🙂
The AreEqual method is weird-looking, I’ll give you that. But it handles objects and boxed value types equally well, which the == operator did not.
Du får en bajer for at være den første, der kommenterer i min nye blog 🙂
Jeg kan god lide tiltag der forsøger at åbne op for C# i en mere dynamisk retning. Måske PostSharp vil interessere dig, hvis du ikke kender det. Det Forsøger at åbne op for at kunne lave aspekter i c#. Du kan finde nogle udemærkede introduktionsvideoer her: http://www.postsharp.org/about/video/