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:
- public class Customer : XPObject
- {
- private string _number;
- public string Number
- {
- get { return _number; }
- set { SetPropertyValue("Number", ref _number, value); }
- }
- }
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:
- public class Customer : XPObject
- {
- private string _number;
- public string Number
- {
- get { return _number; }
- set { SetPropertyValue(Property<Customer>.Name(x => x.Number), ref _number, value); }
- }
- }
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:
- public static class Property<T>
- {
- public static string Name(Expression<Func<T, object>> expression)
- {
- if (expression == null) throw new ArgumentNullException("expression");
- if (expression.Body is MemberExpression) return ((MemberExpression)expression.Body).Member.Name;
-
- if (expression.Body is UnaryExpression && ((UnaryExpression)expression.Body).Operand is MemberExpression)
- {
- return ((MemberExpression)((UnaryExpression)expression.Body).Operand).Member.Name;
- }
-
- throw new ArgumentException(string.Format("Could not get property name from expression of type '{0}'",
- expression.GetType()));
- }
- }
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:
- protected void Set<T>(Expression<Func<Customer, object>> property, ref T holder, T value)
- {
- SetPropertyValue(Property<Customer>.Name(property), ref holder, value);
- }
This allows the setter to be even more compact:
- private string _number;
- public string Number
- {
- get { return _number; }
- set { Set(x => x.Number, ref _number, value); }
- }
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.