9
votes

I'm trying to access various parts of a nested class structure using a arbitrary string.

Given the following (contrived) classes:

public class Person
{
   public Address PersonsAddress { get; set; }
}

public class Adddress
{
   public PhoneNumber HousePhone { get; set; }
}

public class PhoneNumber
{
   public string Number { get; set; }
}

I'd like to be able to get the object at "PersonsAddress.HousePhone.Number" from an instance of the Person object.

Currently I'm doing some funky recursive lookup using reflection, but I'm hoping that some ninjas out there have some better ideas.

For reference, here is the (crappy) method I've developed:

private static object ObjectFromString(object basePoint, IEnumerable<string> pathToSearch)
{
   var numberOfPaths = pathToSearch.Count();

   if (numberOfPaths == 0)
     return null;

   var type = basePoint.GetType();
   var properties = type.GetProperties();

   var currentPath = pathToSearch.First();

   var propertyInfo = properties.FirstOrDefault(prop => prop.Name == currentPath);

   if (propertyInfo == null)
     return null;

   var property = propertyInfo.GetValue(basePoint, null);

   if (numberOfPaths == 1)
     return property;

   return ObjectFromString(property, pathToSearch.Skip(1));
}
5
Why do you think you need to do this? - Steve Wellens
@Steve - Because I need to control projection of arbitrary types, and configuration is the best place for that. - Khanzor
This is also useful for implementing a generic data binding mechanism - DataMember property of BindingSource accepts a navigation path string like that. - ducu

5 Answers

15
votes

You could simply use the standard .NET DataBinder.Eval Method, like this:

object result = DataBinder.Eval(myPerson, "PersonsAddress.HousePhone.Number");
5
votes

I've had to some something similar in the past. I went with the lambda approach because after compiling them I can cache them. I've removed the caching in this code.

I included a few unit tests to show the usage of the method. I hope this is helpful.

private static object GetValueForPropertyOrField( object objectThatContainsPropertyName, IEnumerable<string> properties )
  {
     foreach ( var property in properties )
     {
        Type typeOfCurrentObject = objectThatContainsPropertyName.GetType();

        var parameterExpression = Expression.Parameter( typeOfCurrentObject, "obj" );
        Expression memberExpression = Expression.PropertyOrField( parameterExpression, property );

        var expression = Expression.Lambda( Expression.GetDelegateType( typeOfCurrentObject, memberExpression.Type ), memberExpression, parameterExpression ).Compile();

        objectThatContainsPropertyName = expression.DynamicInvoke( objectThatContainsPropertyName );
     }

     return objectThatContainsPropertyName;
  }

  [TestMethod]
  public void TestOneProperty()
  {
     var dateTime = new DateTime();

     var result = GetValueForPropertyOrField( dateTime, new[] { "Day" } );

     Assert.AreEqual( dateTime.Day, result );
  }

  [TestMethod]
  public void TestNestedProperties()
  {
     var dateTime = new DateTime();

     var result = GetValueForPropertyOrField( dateTime,  new[] { "Date", "Day" } );

     Assert.AreEqual( dateTime.Date.Day, result );
  }

  [TestMethod]
  public void TestDifferentNestedProperties()
  {
     var dateTime = new DateTime();

     var result = GetValueForPropertyOrField( dateTime, new[] { "Date", "DayOfWeek" } );

     Assert.AreEqual( dateTime.Date.DayOfWeek, result );
  }
3
votes

Here's a non-recursive version with (almost) the same semantics:

private static object ObjectFromString(object basePoint, IEnumerable<string> pathToSearch)
{
    var value = basePoint;
    foreach (var propertyName in pathToSearch)
    {
        var property = value.GetType().GetProperty(propertyName);
        if (property == null) return null;
        value = property.GetValue(value, null);
    }
    return value;
}
1
votes

Since you are already interested in resolving string property paths, you may benefit from looking into the Dynamic LINQ query library posted as an example by Scott Guthrie @ Microsoft. It parses your string expressions and produces express trees that can be compiled and cached as suggested by @Brian Dishaw.

This would provide you with a wealth of additional options by providing a simple and robust expression syntax you can use in your configuration approach. It supports the common LINQ methods on enumerables, plus simple operator logic, math calculations, property path evaluation, etc.

0
votes

this is based on Brian's code, did some modification to support index addressing for List:

private static object GetValueForPropertyOrField( object objectThatContainsPropertyName, IEnumerable<string> properties )
       {
           foreach ( var property in properties )
           {
               Type typeOfCurrentObject = objectThatContainsPropertyName.GetType();

               var parameterExpression = Expression.Parameter( typeOfCurrentObject, "obj" );
               var arrayIndex = property.IndexOf('[');
               if ( arrayIndex > 0)
               {
                   var property1 = property.Substring(0, arrayIndex);
                   Expression memberExpression1 = Expression.PropertyOrField( parameterExpression, property1 );
                   var expression1 = Expression.Lambda( Expression.GetDelegateType( typeOfCurrentObject, memberExpression1.Type ), memberExpression1, parameterExpression ).Compile();
                   objectThatContainsPropertyName = expression1.DynamicInvoke( objectThatContainsPropertyName );
                   var index = Int32.Parse(property.Substring(arrayIndex+1, property.Length-arrayIndex-2));
                   typeOfCurrentObject = objectThatContainsPropertyName.GetType(); 
                   parameterExpression = Expression.Parameter( typeOfCurrentObject, "list" );
                   Expression memberExpression2 =  Expression.Call(parameterExpression, typeOfCurrentObject.GetMethod("get_Item"), new Expression[] {Expression.Constant(index)});
                   var expression2 = Expression.Lambda( Expression.GetDelegateType( typeOfCurrentObject, memberExpression2.Type ), memberExpression2, parameterExpression ).Compile();
                    objectThatContainsPropertyName = expression2.DynamicInvoke( objectThatContainsPropertyName );
               }
               else
               {
                   Expression memberExpression = Expression.PropertyOrField( parameterExpression, property );  
                   var expression = Expression.Lambda( Expression.GetDelegateType( typeOfCurrentObject, memberExpression.Type ), memberExpression, parameterExpression ).Compile(); 
                   objectThatContainsPropertyName = expression.DynamicInvoke( objectThatContainsPropertyName );
               }

           }