Tuesday, February 3, 2009

Static typed property names

String names of properties is common problem in dynamically developed software.

Common usages of property names literals
Common usages of such literal are:
  • Property names in Nhibernate queries
    Suppose you want to query all user for specific value of property “LastProjectCode” with detached criteria class:
    DetachedCriteria query = DetachedCriteria.For(typeof(User))
    .Add(Expression.Eq("LastProjectCode", 12345));
    
  • Implementing INotifyPropertyChanged interface
    Standard implementation unfortunately have to pass property name as literal when event is raised. It looks like below:

    public int LastProjectCode
    {
        get { return this.lastProjectCode; }
        set{
            if (this.lastProjectCode != value)
            {
                this.lastProjectCode = value;
                OnPropertyChanged(
                  new PropertyChangedEventArgs("LastProjectCode"));
            }
        }
    }
    
  • Providing error information with IDataErrorInfo
    Component using entity (like user) queries it for error by calling property this[string columnName]. Implementation is usually based on many if condition checking for column name like below:
    public string this[string columnName]
    {
        get{
            string result = null;
            if (columnName == "LastProjectCode" && LastProjectCode < 0)
            {
                result = "Last project code cannot be negative.";
            } 
    
            return result;
        }
    }
    

Disadvantages
All mentioned situation have at least 2 major disadvantages
  • Possibility to mistype name
  • Hard refactoring (since refactoring cannot determine if name of property in literal is really name of property)
To avoid it I have wrote TypeHelper class where you can find out property name in static way like this:  

Solution
string propertyName = TypeHelper.GetPropertyName(u => u.LastProjectCode);
Now both disadvantages disappeared
  • You cannot misspell word since compiler checks it
  • Refactoring is possible because refactoring engine knows all type names at compile type and none of them is "hidden" in literals
Usage
Here is how you can extract property names and PropertyInfo object in staticly typed way:
string propertyName = TypeHelper.GetPropertyName(u => u.LastProjectCode);

PropertyInfo property1 = TypeHelper.GetProperty((SomeClass o) => o.InstanceProperty.Length);

PropertyInfo property2 = TypeHelper.GetProperty(() => SomeClass.StaticProperty.Length);

Implementation
public class TypeHelper
{
    private static PropertyInfo GetPropertyInternal(LambdaExpression p)
    {
        MemberExpression memberExpression; 

        if (p.Body is UnaryExpression)
        {
            UnaryExpression ue = (UnaryExpression)p.Body;
            memberExpression = (MemberExpression)ue.Operand;
        }
        else
        {
            memberExpression = (MemberExpression)p.Body;
        } 

        return (PropertyInfo)(memberExpression).Member;
    } 

    public static string GetPropertyName<TObject>(Expression<Func<TObject, object>> p)
    {
        return GetPropertyNameInternal(p);
    } 

    public static string GetPropertyName<TObject, T>(Expression<Func<TObject, T>> p)
    {
        return GetPropertyNameInternal(p);
    } 

    public static string GetPropertyName<T>(Expression<Func<T>> p)
    {
        return GetPropertyNameInternal(p);
    } 

    private static string GetPropertyNameInternal(LambdaExpression p)
    {
        return GetPropertyInternal(p).Name;
    } 

    public static PropertyInfo GetProperty<TObject>(Expression<Func<TObject, object>> p)
    {
        return GetPropertyInternal(p);
    } 

    public static PropertyInfo GetProperty<TObject, T>(Expression<Func<TObject, T>> p)
    {
        return GetPropertyInternal(p);
    } 

    public static PropertyInfo GetProperty<T>(Expression<Func<T>> p)
    {
        return GetPropertyInternal(p);
    }
}

5 comments:

Anonymous said...

Maybe you should look at BuildProviders :)

Anonymous said...

Nice use of Linq, Miroslav. Well explained too!

Have you looked at the implementation of ActiveRecord that is part of the Caste Project? In particular, the use of the ActiveWriter modelling tool? This creates an inner class .Properties for your class under consideration, containing a string member for all of your persisted properties. Your query would look like

DetachedCriteria query = DetachedCriteria.For(typeof (User)).Add(Expression.Eq(User.Properties.LastProjectCode, 12345));

but even better it would just be

User foo = User.Find(User.Properties.LastProjectCode, 12345);

And you get the INotifyPropertyChanged interface implemented for free :-)

Regards
\m

Mirosław Jedynak said...

To Anonymous:
Unfortunatelly build providers can only be used in ASP.NET Web Site project, what is not what I expect, since most of project sensitive code is in lower layers (Class Library projects)

Duncan Smart said...

Nice, how about making it an extension method of Type, e.g.

typeof(MyType).PropertyName(p => p.MyProp)

Anonymous said...

This is most-certainly a recurring theme (as a set of problems that more than one person is trying to solve).

Check out both my own post on this topic as well as the links in the comments to the same ideas from several others...

http://unhandled-exceptions.com/blog/index.php/2008/11/22/the-continuing-quest-for-death-of-string-literals-in-my-code/

If this many of us are solving the problem, it MUST be real :D

String-literals SUCK. Period.