-1

I've got a deeply nested private fields chain which I'd like to iterate recursively to get the value of some target field.

How can this be done?

For example:

public class A
{
   private B b;
   public A(B b) { this.b = b; }
}

public class B
{
   private C[] cItems;
   public B(C[] cItems) { this.cItems = cItems; }
}

public class C
{
   private string target; // <-- get this value
   public C(int target) { this.target = val; }
}
public static void GetFieldValueByPath(object targetObj, string targetFieldPath)
{
   // how to do it? I self-answer below 
}

Usage will be:

public void DoSomething(A a)
{
   var val = GetFieldValueByPath(a, "b.cItems[2].target");
}
  • Notes:
    • There is a related question about recursively getting properties, but not fields. But even then, it doesn't support array fields.
    • Related questions such as this one for getting fields are not recursive.
OfirD
  • 9,442
  • 5
  • 47
  • 90
  • While this is something that can be done, I highly recommend not doing it. The only reason I could see you needing to do this was if A were defined in a third party library for which you do not have source code. – Nigel Oct 20 '21 at 22:18
  • @NigelBess, this was excatly my use case, I actually needed to [get Serilog's log file path location](https://github.com/serilog/serilog-sinks-file/issues/170) for debugging purposes. – OfirD Oct 20 '21 at 22:21

3 Answers3

-1

OfirD's answer is on the right track, but it won't work. Not only does it not compile, but C[] does not implement IList<object>.

It also has quite a few scenarios that it does not account for. (I have not updated his code to account for these scenarios)

  • What if the array does not get indexed by an integer?
  • What if it is a jagged array?
  • What if the path points to properties instead of fields?

I've updated his code to work:

    public static object GetFieldValueByPath(object obj, string fieldPath)
    {
        var flags =
            BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
        var splitted = fieldPath.Split('.');

        var current = splitted[0];
        int? index = null;

        // Support getting a certain index in an array field
        var match = Regex.Match(current, @"\[([0-9]+)\]");
        if (match.Groups.Count > 1)
        {
            current = fieldPath.Substring(0, match.Groups[0].Index);
            index = int.Parse(match.Groups[1].Value);
        }

        var value = obj.GetType().GetField(current, flags).GetValue(obj);


        if (value == null)
        {
            return null;
        }

        if (splitted.Length == 1)
        {
            return value;
        }

        if (index != null)
        {
            value = Index(value, index.Value);
        }

        return GetFieldValueByPath(value, string.Join(".", splitted.Skip(1)));
    }

    static object Index(object obj, int index)
    {
        var type = obj.GetType();
        foreach (var property in obj.GetType().GetProperties())
        {
            var indexParams = property.GetIndexParameters();
            if (indexParams.Length != 1) continue;
            return property.GetValue(obj, new object[] { index });
        }

        throw new Exception($"{type} has no getter of the format {type}[int]");
    }
Nigel
  • 2,961
  • 1
  • 14
  • 32
  • Instead of using language that violates [Code of Conduct](https://stackoverflow.com/conduct), you could just have helpfully commented saying that I forgot to replace `Length` with `Count`. After all, my code clearly works on my demo. As for your other comments: (1) Jagged array: your code doesn't work, just run it on my demo and see. (2) Path points to properties: That's not the subject of this question, as there are already questions regarding properties, linked in my question. (3) I guess your code smells the same as mine, since you've pretty much copied it. – OfirD Oct 20 '21 at 23:07
-1

The code works for your example, but you may need to change it in case of having Dictionaries

public static object GetFieldValueByPath(object targetObj, string targetFieldPath)
        {
            var fieldNames = targetFieldPath.Split('.');
            var type = targetObj.GetType();

            foreach (var fieldName in fieldNames)
            {
                string name = fieldName;
                int? objectIndex = default;
                if (name.Contains('['))//getting fieldName without indexer
                {
                    int indexerStart = name.IndexOf('[');
                    int indexerEnd = name.IndexOf(']');

                    objectIndex = int.Parse(name.Substring(indexerStart + 1, indexerEnd-indexerStart - 1));
                    name = name.Substring(0, indexerStart);
                }

                var field = type.GetField(name, BindingFlags.NonPublic | BindingFlags.Instance);
                if (objectIndex.HasValue)//here we know that field is collection
                {
                    targetObj=((IList<object>)field.GetValue(targetObj))[0];//getting item by index
                    type = targetObj.GetType();
                }
                else
                {
                    targetObj = field.GetValue(targetObj);
                    type = field.FieldType;
                }
            }

            return targetObj;
        } 
  • Please try running your code with the demo I provided in my answer, as it currently has several issues, the most prominent is that it's not recursive. – OfirD Oct 21 '21 at 00:00
-3

Here's the way to do it (note the improvement over other answers, achieved using regex to prepare the path-parts ahead of time):

public static object GetFieldValueByPath(object obj, string fieldPath)
{
    var flags = BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
    var parts = Regex.Matches(fieldPath, @"([^.\[]+)(?:\[(.*?)\])?").Cast<Match>().Select(match => match.Groups).ToList();
    return GetFieldValueByPathParts(obj, parts, flags);
}
private static object GetFieldValueByPathParts(object obj, List<GroupCollection> parts, BindingFlags flags)
{
    if (obj == null || parts.Count == 0) return obj;
    var field = new Field(name: parts[0][1].Value, value: (object)null, index: parts[0][2].Value);
    try
    {
        field.Value = obj.GetType().GetField(field.Name, flags).GetValue(obj);
    }
    catch (NullReferenceException ex)
    {
        throw new Exception($"Wrong path provided: field '{field.Name}' does not exist on '{obj}'");
    }
    field = TrySetEnumerableValue(field);
    return GetFieldValueByPathParts(field.Value, parts.Skip(1).ToList(), flags);
}
private static Field TrySetEnumerableValue(Field field)
{
    if (field.Value != null && field.Index != null)
    {
        var enumerable = ((IEnumerable)field.Value).Cast<object>();
        field.Value = field.Index <= enumerable.Count() ? enumerable.ElementAt(field.Index.Value) : null;
    }
    return field;
}

Here's the definition of the Field helper class:

public class Field
{
    public string Name { get; set; }
    public object Value { get; set; }
    public int? Index { get; set; }
    public Field(string name, object value, string index)
    {
        Name = name;
        Value = value;
        Index = int.TryParse(index, out int parsed) ? parsed : (int?)null;
    }
}

Usage (live demo):

public static void Main(string[] s)
{
    var a1 =  new A(new B(new C[] { new C(1), new C(2), new C(3) } ) );
    Console.WriteLine(GetFieldValueByPath(a1, "b.cItems[2].target"));
            
    var a2 =  new A(new B(new C[] { } ) );
    Console.WriteLine(GetFieldValueByPath(a2, "b.cItems[2].target"));
            
    var a3 =  new A(new B(null) );
    Console.WriteLine(GetFieldValueByPath(a3, "b.cItems[2].target"));
}
OfirD
  • 9,442
  • 5
  • 47
  • 90