9

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));
}
Khanzor
  • 4,830
  • 3
  • 25
  • 41
  • Why do you think you need to do this? – Steve Wellens May 04 '11 at 02:11
  • @Steve - Because I need to control projection of arbitrary types, and configuration is the best place for that. – Khanzor May 04 '11 at 02:19
  • This is also useful for implementing a generic data binding mechanism - DataMember property of BindingSource accepts a navigation path string like that. – ducu Jan 29 '13 at 20:01

5 Answers5

15

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

object result = DataBinder.Eval(myPerson, "PersonsAddress.HousePhone.Number");
Simon Mourier
  • 132,049
  • 21
  • 248
  • 298
  • This is probably closer to the no-code approach I was looking for! – Khanzor May 25 '11 at 03:26
  • @Khanzor, well it is exactly same as your method, I am still unable to understand that you already have working answer, what alternative you are looking for? In terms of performance or anything else? Reflection is the only way, otherwise there is another alternative of generating dynamic method and use it, but its too much of coding for small issue. – Akash Kava May 27 '11 at 10:22
  • 1
    Just note that you'll be have to reference - **System.Web.dll** – Maxim May 28 '11 at 00:20
  • 3
    @Maxim: Note also that you have to give up the _.NET Framework 4 Client Profile_: http://msdn.microsoft.com/en-us/library/cc656912.aspx – Rick Sladkey May 29 '11 at 00:56
5

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 );
  }
Brian Dishaw
  • 5,767
  • 34
  • 49
  • The biggest reason is that Expressions utilize AST of the properties where reflection does not. The only faster way to do this would be to use Reflection.Emit and write it using IL, but that seems like it would be more trouble than it's worth. Please note that if you can cashe the delegate generated by the .Compile() step, it will help keep execution time low. You'll need to leverate the property as well as the type for the cache to retrieve your delegate that you want to invoke. – Brian Dishaw May 24 '11 at 12:30
  • I think this post might help further explain this as I'm not sure I did a good job. http://stackoverflow.com/questions/2697655/lambda-expression-based-reflection-vs-normal-reflection – Brian Dishaw May 24 '11 at 12:32
  • I like your answer, but it didn't support property of array or list. so I modify it a little bit to support [index]: – yww325 May 15 '20 at 17:26
3

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;
}
BrandonAGr
  • 5,827
  • 5
  • 47
  • 72
Rick Sladkey
  • 33,988
  • 6
  • 71
  • 95
  • Why is a non-recursive implementation necessarily better? The GetProperty method is a good tip though. – Khanzor May 04 '11 at 02:25
  • 1
    @Khanzor: Recursion prevents us from using the natural foreach iterator on the incoming IEnumerable. That's what foreach is for! – Rick Sladkey May 04 '11 at 02:36
  • well, I guess I am just using recursion for recursion's sake. There's not really that much wrong with list chomping though, I don't think. I take your point though, the CLR doesn't do tail recursion, so it makes more sense to implement using foreach :). – Khanzor May 04 '11 at 02:46
1

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.

Jamie Thomas
  • 1,513
  • 1
  • 10
  • 16
0

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 );
               }

           }

yww325
  • 315
  • 4
  • 12