Monday 4 October 2010

Replacing Strings in XPObject.SetPropertyValue with Lambdas

I came across a post on Aussie Alf’s blog today about removing magic strings from the persistent property setters in the XPO ORM from DevExpress

The Aussie Alf blog is written by Michael Proctor who is a member of the DevExpress community DXSquad, specialising in XPO.  The blog is a superb resource for any XPO developers and really worth reading.  Michael is also a very active member on the DevExpress forums and may well come to your help with an XPO issue there.

XPO uses a classic INotifyPropertyChanged pattern and passes a string of the changed property name into SetPropertyValue method.  You can see this below:  

  1. public class Customer : XPObject
  2. {
  3.    private string _number;
  4.    public string Number
  5.    {
  6.       get { return _number; }
  7.       set { SetPropertyValue("Number", ref _number, value); }
  8.    }
  9. }

The problem with this approach is that the compiler can’t determine whether the string is correct or not, leaving you open to potential runtime errors.  Michael’s solution to this to generate a helper class that can be used in the property setter and he has a Visual Studio plugin that generates and refreshes the helper class for you based on your domain model. 

This is a great approach to the problem and you can read more about it on the homepage for Michael’s plugin, XPO_EasyFields.

Michael’s XPO_EasyFields plugin uses the free DXCore Visual Studio plugin from DevExpress.  Personally, I use Resharper and don’t have DXCore installed, so I thought I would share my approach to removing the magic strings - I use a helper class and lambdas to do this.  It looks like this:

  1. public class Customer : XPObject
  2. {
  3.    private string _number;
  4.    public string Number
  5.    {
  6.       get { return _number; }
  7.       set { SetPropertyValue(Property<Customer>.Name(x => x.Number), ref _number, value); }
  8.    }
  9. }

This relies on the use of a generic Property class and a helper method that gleans the property name from a Linq Expression.  The property class looks like this:

  1. public static class Property<T>
  2. {
  3.    public static string Name(Expression<Func<T, object>> expression)
  4.    {
  5.       if (expression == null) throw new ArgumentNullException("expression");
  6.       if (expression.Body is MemberExpression) return ((MemberExpression)expression.Body).Member.Name;
  7.  
  8.       if (expression.Body is UnaryExpression && ((UnaryExpression)expression.Body).Operand is MemberExpression)
  9.       {
  10.          return ((MemberExpression)((UnaryExpression)expression.Body).Operand).Member.Name;
  11.       }
  12.  
  13.       throw new ArgumentException(string.Format("Could not get property name from expression of type '{0}'",
  14.                                                 expression.GetType()));
  15.    }
  16. }

The magic comes from translating the little lambda expression x => x.Name to a string.  If I remember right, I originally based this on some code from Jeremy Miller, but there are various implementations out there.  Here’s and elegant one from Paul Stovell that’s focused purely on INotifyPropertyChanged. 

As Paul mentions in the above link, it’s worth noting that there is a performance hit when using this approach.  Michael’s approach of generating the code does not have any performance hit.  You may need to consider the performance issue if you have very high rates of properties being set. 

I did some simple performance tests that show a 10-15x overhead with my approach compared to Michael’s.  However, this only becomes relevant with a very large number of property sets.  For my usage scenarios the added quality benefit outweighs the performance hit, but you will need to carefully consider your scenario.

Lastly, to take this further, and get an even tighter syntax, I add this helper method to my persistent classes:

  1. protected void Set<T>(Expression<Func<Customer, object>> property, ref T holder, T value)
  2. {
  3.    SetPropertyValue(Property<Customer>.Name(property), ref holder, value);
  4. }

This allows the setter to be even more compact:

  1. private string _number;
  2. public string Number
  3. {
  4.    get { return _number; }
  5.    set { Set(x => x.Number, ref _number, value); }
  6. }

The downside is that you need the helper method in each class.  Down to taste that really, but I always go for the tighter syntax wherever possible! 

You can get the code from bitbucket and browse the salient parts here.

2 comments:

Unknown said...

Hi Sean,

Out of interest how does this handle nested properties? for example if you have MyObject.MyReferencedObject.MyProperty will this return "MyReferencedObject.MyProperty" ?

Great post and good alternative, just a pity about the performance hit and the .NET 3 dependancy.

Just going to post a blog linking to yours as this is a good alternative if you don't have CodeRush available (however will just note that the XPO_EasyFields plugin works with the Free CodeRush Xpress version so you don't need a CodeRush license to use XPO_EasyFields, however I can understand there might be conflicts between other addons and CodeRush)

Sean Kearon said...

Hi Michael

Thanks for your kind words.

I don't think that you can nest using this technique. Expression> is trying to access a member of the type T expressed as a lamda, and this will not traverse nested classes I'm afraid.

You are quite right about the .Net 3 dependency - I had not thought of that either. There's a Linq implementation for .Net 2 in VS 2008 from Jo Albahari's work here:

http://www.albahari.com/nutshell/linqbridge.aspx

Not sure if that covers Expressions (bet it doesn't!). Jo is the author of the awesome LinqPad tool (http://www.linqpad.net/).

I don't think there's any conflict with Resharper and CodeRush - I've read some posts that say they happily coexist. The only reason I don't use CodeRush is just that I've been with Resharper since they started and it's like a second skin for me now. It'd take too long to get to that level in CodeRush (although I have a full DXperience licence for CodeRush!).

Cheers for now

Sean