42

I was doing something like Recursively Get Properties & Child Properties Of An Object, but I wanted to use reflection recursively to get each properties. And I got the code from Recursively Print the properties.

The problem with the code is: it only goes one level down, I wonder how can you automatically get all the properties using reflection? I just made up the following sample Container code:

public class Container
{
    public Bottle MyBottle { get; set; }
    public List<Address> Addresses { get; set; }

    public Container()
    {
        Address a = new Address();
        a.AddressLine1 = "1 Main St";
        a.AddressLine2 = "2 Main St";
        Addresses = new List<Address>();
        Addresses.Add(a);

        MyBottle = new Bottle();
        MyBottle.BottleName = "Big bottle";
        MyBottle.BottageAge = 2;
    }
}

public class Bottle
{
    public string BottleName { get; set; }
    public int BottageAge { get; set; }
}

public class Address
{
    public string AddressLine1 { get; set; }
    public string AddressLine2 { get; set; }
    public List<SpecialFolder> SpecialFolders { get; set; }

    public Address()
    {
        SpecialFolders = new List<SpecialFolder>();
        SpecialFolder sf = new SpecialFolder();
        sf.TemplateFolder = Environment.SpecialFolder.Templates.ToString();
        sf.UserFolder = Environment.SpecialFolder.UserProfile.ToString();
        SpecialFolders.Add(sf);
    }
}

public class SpecialFolder
{
    public string TemplateFolder { get; set; }
    public string UserFolder { get; set; }
}

In the Main method:

static void Main(string[] args)
{
    Container c = new Container();
    PrintProperties(c);
}
public static void PrintProperties(object obj)
{
    PrintProperties(obj, 0);
}
public static void PrintProperties(object obj, int indent)
{

    if (obj == null) return;
    string indentString = new string(' ', indent);
    Type objType = obj.GetType();
    PropertyInfo[] properties = objType.GetProperties();
    foreach (PropertyInfo property in properties)
    {
        object propValue = property.GetValue(obj, null);
        if (property.PropertyType.Assembly == objType.Assembly)
        {
            Console.WriteLine("{0}{1}:", indentString, property.Name);

            PrintProperties(propValue, indent + 2);
        }
        else
        {
            Console.WriteLine("{0}{1}: {2}", indentString, property.Name, propValue);
        }
    }
}

I am hoping to get:

MyBottle:
      BottleName: Big bottle
      BottageAge: 2
Addresses:
      AddressLine1: 1 Main St
      AddressLine2: 2 Main St
      SpecialFolders:
            TemplateFolder: Templates
            UserFolder: UserProfile

The result I get now:

MyBottle:
  BottleName: Big bottle
  BottageAge: 2
Addresses: System.Collections.Generic.List`1[TreeViewReflectionExample.Address]

Can someone help me with the PrintProperties method? Thank you very much.

Community
  • 1
  • 1
HoKy22
  • 4,057
  • 8
  • 33
  • 54

5 Answers5

71

You have two problems with your code:

  1. because of condition if (property.PropertyType.Assembly == objType.Assembly) you will omit System.Collections like List<>
  2. you do not treat differently propValue that are collections. Hence it will print List properties, not its elements properties.

You can change that for example into:

public void PrintProperties(object obj, int indent)
{    
    if (obj == null) return;
    string indentString = new string(' ', indent);
    Type objType = obj.GetType();
    PropertyInfo[] properties = objType.GetProperties();
    foreach (PropertyInfo property in properties)
    {
        object propValue = property.GetValue(obj, null);
        var elems = propValue as IList;
        if (elems != null)
        {
            foreach (var item in elems)
            {
                PrintProperties(item, indent + 3);
            }
        }
        else
        {
            // This will not cut-off System.Collections because of the first check
            if (property.PropertyType.Assembly == objType.Assembly)
            {
                Console.WriteLine("{0}{1}:", indentString, property.Name);

                PrintProperties(propValue, indent + 2);
            }
            else
            {
                Console.WriteLine("{0}{1}: {2}", indentString, property.Name, propValue);
            }
        }
    }
}
Konrad Kokosa
  • 16,563
  • 2
  • 36
  • 58
  • 3
    Exactly what I was looking for, not only worked for this example, but for all the other case I had – HoKy22 Dec 12 '13 at 21:57
  • This was helpful. I made a small addition to make sure the property can be read before calling GetValue to prevent an exception on a write only property - just wrapped it with "if (property.CanRead) {...". – thephez Jan 25 '17 at 18:35
  • 2
    Nice little tidbit of C# 6 syntactic sugar: `if (propValue is IList elems)` works and saves a line. – CaptainMarvel May 16 '17 at 18:05
  • 1
    I get a `System.Reflection.TargetParameterCountException` with this setup: `var person = new Person { Name = "Name", Vorname = "Firstname", Alter = 27, Adresse = new Adresse { Strasse = "Street", Ort = "Place" }, Hobbies = new List { "Tennis", "Salsa", "blub" } };` – xeraphim Nov 24 '17 at 15:27
  • Nice code, but it doesn't print List of built-in types, e.g `List`, also `Array`s – Mehdi Dehghani Jan 31 '19 at 08:51
  • @CaptainMarvel saving a line is not the point. Making the code more error safe is. Every cast is in some way a risk that a programmer makes a mistake. This makes it type-safe all the way. – Preza8 May 24 '19 at 07:44
  • @Preza8 Unless you're paid per line of code, concise beats verbose every time. [See here](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/is#type-pattern). – CaptainMarvel May 29 '19 at 12:30
  • @CaptainMarvel My point was that the biggest advantage of using this kind of pattern matching is not shortening code but avoiding human mistakes. Every cast might fail in theory. If the variable is type no longer nullable, it is safer. – Preza8 May 29 '19 at 15:16
  • @Konrad Kokosa Good day, help me with something like a notation example address.city = tokyo Thank you. – Aljohn Yamaro Jan 18 '20 at 15:49
  • I can't use IList like this. It's giving an error "Using the generic type 'IList' requires 1 type arguments" – AsIndeed Nov 01 '22 at 13:40
16

You want to handle primitive types and strings separately, and loop over enumerables instead of just taking their ToString() value. So your code could be updated to:

public void PrintProperties(object obj, int indent)
{

    if (obj == null) return;
    string indentString = new string(' ', indent);
    Type objType = obj.GetType();
    PropertyInfo[] properties = objType.GetProperties();
    foreach (PropertyInfo property in properties)
    {   
        object propValue = property.GetValue(obj, null);
        if(property.PropertyType.IsPrimitive || property.PropertyType == typeof(string))
            Console.WriteLine("{0}{1}: {2}", indentString, property.Name, propValue);
        else if (typeof(IEnumerable).IsAssignableFrom(property.PropertyType))
        {
            Console.WriteLine("{0}{1}:", indentString, property.Name);
            IEnumerable enumerable = (IEnumerable)propValue;
            foreach(object child in enumerable)
                PrintProperties(child, indent + 2);
        }
        else 
        {
            Console.WriteLine("{0}{1}:", indentString, property.Name);
            PrintProperties(propValue, indent + 2);
        }
    }
}
driis
  • 161,458
  • 45
  • 265
  • 341
  • This code works very well for this example, when I added some more properties and classes, I got infinite loop. I was looking for a generic way to get all the properties. – HoKy22 Dec 12 '13 at 21:59
  • 1
    After some small modifications this code worked for me. – Daniel Dušek Nov 05 '15 at 10:38
3

Based on Konrad Kokosa's answer:

private string ObjectToString(object obj, int indent = 0)
{
    if (obj is null)
    {
        return "";
    }

    var sb = new StringBuilder();
    string indentString = new string(' ', indent);
    Type objType = obj.GetType();

    foreach (PropertyInfo property in objType.GetProperties())
    {
        object propValue = property.GetValue(obj);
        var elems = propValue as IList;

        if (elems != null)
        {
            foreach (var item in elems)
            {
                sb.Append($"{indentString}- {property.Name}\n");
                sb.Append(ObjectToString(item, indent + 4));
            }
        }
        else if (property.Name != "ExtensionData")
        {
            sb.Append($"{indentString}- {property.Name}={propValue}\n");

            if (property.PropertyType.Assembly == objType.Assembly)
            {
                sb.Append(ObjectToString(propValue, indent + 4));
            }
        }
    }

    return sb.ToString();
}

UPDATE

Edit the code based on this older question: TargetParameterCountException when enumerating through properties of string

private string ObjectToString(object obj, int indent = 0)
    {
        var sb = new StringBuilder();

        if (obj != null)
        {
            string indentString = new string(' ', indent);

            if (obj is string)
            {
                sb.Append($"{indentString}- {obj}\n");
            }
            else if (obj is Array)
            {
                var elems = obj as IList;
                sb.Append($"{indentString}- [{elems.Count}] :\n");

                for (int i = 0; i < elems.Count; i++)
                {
                    sb.Append(ObjectToString(elems[i], indent + 4));
                }
            }
            else
            {
                Type objType = obj.GetType();
                PropertyInfo[] props = objType.GetProperties();

                foreach (PropertyInfo prop in props)
                {
                    if (prop.GetIndexParameters().Length == 0)
                    {
                        object propValue = prop.GetValue(obj);
                        var elems = propValue as IList;

                        if (elems != null)
                        {
                            foreach (var item in elems)
                            {
                                sb.Append($"{indentString}- {prop.Name} :\n");
                                sb.Append(ObjectToString(item, indent + 4));
                            }
                        }
                        else if (prop.Name != "ExtensionData")
                        {
                            sb.Append($"{indentString}- {prop.Name} = {propValue}\n");

                            if (prop.PropertyType.Assembly == objType.Assembly)
                            {
                                sb.Append(ObjectToString(propValue, indent + 4));
                            }
                        }
                    }
                    else
                    {
                        sb.Append($"{indentString}- {prop.Name} ({prop.PropertyType.Name}): <Indexed>\n");
                    }
                }
            }
        }

        return sb.ToString();
    }

UPDATE 2

public static string ObjectToString(object obj, int indent = 0)
    {
        var sb = new StringBuilder();

        if (obj != null)
        {
            string indentString = new string(' ', indent);

            if (obj is string || obj.IsNumber())
            {
                sb.Append($"{indentString}- {obj}\n");
            }
            else if (obj.GetType().BaseType == typeof(Enum))
            {
                sb.Append($"{indentString}- {obj.ToString()}\n");
            }
            else if (obj is Array)
            {
                var elems = obj as IList;
                sb.Append($"{indentString}- [{elems.Count}] :\n");

                for (int i = 0; i < elems.Count; i++)
                {
                    sb.Append(ObjectToString(elems[i], indent + 4));
                }
            }
            else
            {
                Type objType = obj.GetType();
                PropertyInfo[] props = objType.GetProperties();

                foreach (PropertyInfo prop in props)
                {
                    if (prop.GetIndexParameters().Length == 0)
                    {
                        object propValue = prop.GetValue(obj);
                        var elems = propValue as IList;

                        if (elems != null)
                        {
                            foreach (var item in elems)
                            {
                                sb.Append($"{indentString}- {prop.Name} :\n");
                                sb.Append(ObjectToString(item, indent + 4));
                            }
                        }
                        else if (prop.Name != "ExtensionData")
                        {
                            sb.Append($"{indentString}- {prop.Name} = {propValue}\n");

                            if (prop.PropertyType.Assembly == objType.Assembly)
                            {
                                sb.Append(ObjectToString(propValue, indent + 4));
                            }
                        }
                    }
                    else if (objType.GetProperty("Item") != null)
                    {
                        int count = -1;

                        if (objType.GetProperty("Count") != null &&
                            objType.GetProperty("Count").PropertyType == typeof(int))
                        {
                            count = (int)objType.GetProperty("Count").GetValue(obj, null);
                        }

                        for (int i = 0; i < count; i++)
                        {
                            object val = prop.GetValue(obj, new object[] { i });
                            sb.Append(ObjectToString(val, indent + 4));
                        }
                    }
                }
            }
        }

        return sb.ToString();
    }

    public static bool IsNumber(this object value)
    {
        return value is sbyte
            || value is byte
            || value is short
            || value is ushort
            || value is int
            || value is uint
            || value is long
            || value is ulong
            || value is float
            || value is double
            || value is decimal;
    }
2

I've invested a lot of time to solve this problem. There is a lot of type-specific formatting required to properly dump all property types.

I can recommend you having a look at FormatValue method in this file on github. It is the main logic that writes property values to string: https://github.com/thomasgalliker/ObjectDumper/blob/ada64c7e51fedf57731006959358aa890b5e4344/ObjectDumper/Internal/ObjectDumperCSharp.cs#L98

You can also use this code as a nuget package: https://www.nuget.org/packages/ObjectDumper.NET

thomasgalliker
  • 1,279
  • 13
  • 19
1

It works for all cases except propValue is string[]. You will get the exception "Parameter Count Mismatch" in line: object propValue = property.GetValue(obj, null);

To fix this issue you can use this code with a little fix:

private void PrintProperties(object obj, int indent)
    {
        if (obj == null) return;
        string indentString = new string(' ', indent);
        Type objType = obj.GetType();
        PropertyInfo[] properties = objType.GetProperties();
        foreach (PropertyInfo property in properties)
        {
            object propValue = property.GetValue(obj, null);

            var elems = propValue as IList;
            if ((elems != null) && !(elems is string[]) )
            {
                foreach (var item in elems)
                {
                    PrintProperties(item, indent + 3);
                }
            }
            else
            {
                // This will not cut-off System.Collections because of the first check
                if (property.PropertyType.Assembly == objType.Assembly)
                {
                    LogToWindow(String.Format("{0}{1}:", indentString, property.Name));
                    PrintProperties(propValue, indent + 2);
                }
                else
                {
                    if (propValue is string[])
                    {
                        var str = new StringBuilder();
                        foreach (string item in (string[])propValue)
                        {
                            str.AppendFormat("{0}; ", item);
                        }
                        propValue = str.ToString();
                        str.Clear();
                    }
                    LogToWindow(String.Format("{0}{1}: {2}", indentString, property.Name, propValue));
                }
            }
        }
    }