5
\$\begingroup\$

Below I have setup an extension method that takes any object, cycles through its properties, and prints each out to a Console window in the format Name: Value.

Specification / Scope:

  • Handle only single objects (no collections)

code

public static string PropertiesToString<T>(this T obj, int tabs = 0) where T : class
{
    int initTabs = tabs;
    string result = string.Empty;
    PropertyInfo[] propertyInfo = obj.GetType().GetProperties();
    foreach (PropertyInfo property in propertyInfo)
    {
        string name = property.Name;
        object value = property.GetValue(obj, null);
        Type valueType = value.GetType();
        if (valueType.IsValueType || valueType.Name.Equals("String"))
        {
            for (int i = 0; i < tabs; i++)
            {
               result += "    ";
            }
            result += string.Format("{0}: {1}\n", name, value == null ? string.Empty : value.ToString());
        }
        else
        {
            result += string.Format("{0}:\n", name);
            result += value.PropertiesToString(++tabs);
        }
        tabs = initTabs;
    }
   return result;
}

Here is a class I'm using to test this code, along with the lines responsible for creating an instance of the class and writing its properties to the Console:

Class:

public class Circle : IShape
{
    public Circle(double x, double y, double radius)
    {
        Center = new Point
        {
            X = x,
            Y = y
        };
        Radius = radius;
    }

    public Point Center { get; set; }
    public double Radius { get; set; }

    public double Area(int precision = 2)
    {
        return Math.Round(Radius * Radius * Math.PI, precision);
    }
}

Main:

public static void Main(string[] args)
{
    IShape circle = new Circle(5, 5, 10);
    Console.WriteLine(circle.PropertiesToString());
    Console.ReadLine();
}

The above method will also cycle through nested objects and print those to the Console as well, adding in tabs for readability's sake.

I'm kind of unfamiliar with System.Reflection and was wondering if there was a more efficient way I could approach doing something like this.

\$\endgroup\$
  • \$\begingroup\$ The null-references are handled within the method by just adding string.Empty to result if value == null. Otherwise I was only considering using this method for single objects, not collections. \$\endgroup\$ – Delfino Jun 21 at 20:37
6
\$\begingroup\$

Bug

Your code does not handle value == null everywhere.

object value = property.GetValue(obj, null);
Type valueType = value.GetType();   // <- can throw the infamous NRE

Review

  • Use a clear method name RenderProperties.
  • int tabs = 0 does not allow flexibility for rendering indents. Use string indent = "\t" instead.
  • this T obj is fine, but I would prefer this T source.
  • string result = string.Empty; and result += .. use a System.Text.StringBuilder instead; much better memory management.
  • Use var syntax var resultBuilder = new StringBuilder();.
  • obj.GetType().GetProperties(); should be extended to public, instance BindingFlags properties with CanRead, GetGetMethod(false) != null and GetIndexParameters().Length == 0 to only include the publically accessible getter properties of the instance.
  • valueType.Name.Equals("String") should be value is String. But perhaps you need a better strategy for determining which objects are complex ..
  • for (int i = 0; i < tabs; i++) { result += .. gets replaced completely with indent as earlier specified.
  • string.Format("{0}: {1}\n" .. should use Environment.NewLine, or even better use an overload on StringBuilder called AppendFormatLine. Same thing in the else clause.
  • PropertiesToString(value, ++tabs); can be replaced by PropertiesToString(value, indent + indent);.

Your Code Edited

  • I have decoupled retrieving properties from rendering. However, in another answer was suggested to go further and use a tree walker to adhere to best practices. That would be even better.

  • I am asserting array or other collections do not require their items to be visited, and the complete object graph is a tree. You did never specify how to handle cyclic graphs, so they are out of scope :)

Render properties:

public static string RenderProperties<T>(this T source, string indent = "\t") 
            where T : class
{
    if (source == null) return string.Empty;
    indent = indent ?? string.Empty;
    var builder = new StringBuilder();
    var properties = GetAccessibleProperties(source);

    foreach (var property in properties)
    {
        RenderProperty(property, source, builder, indent);
    }

    return builder.ToString();
}

Render property:

private static void RenderProperty(
            PropertyInfo property, object parent, StringBuilder builder, string indent)
{
    Debug.Assert(property != null);
    Debug.Assert(parent != null);
    Debug.Assert(builder != null);
    Debug.Assert(indent != null);

    var name = property.Name;
    var value = property.GetValue(parent, null); // <- need to handle exception?

    if (value == null)
    {
        builder.AppendLine($"{indent}{name}: ");
    }
    else if (value.GetType().IsValueType || value is string)
    {
        builder.AppendLine($"{indent}{name}: {value}");
    }
    else
    {
        builder.AppendLine(RenderProperties(value, indent + indent));
    }
}

Get accessible properties:

private static IEnumerable<PropertyInfo> GetAccessibleProperties(object source)
{
    Debug.Assert(source != null);
    // optimized for readibility over performance ->
    var properties = source.GetType()
            .GetProperties(
                  BindingFlags.Instance             // only instance properties
                | BindingFlags.Public)              // publicly accessible only
            .Where(x =>
                   x.CanRead                             // must have getter
                && x.GetGetMethod(false) != null         // must have public getter
                && x.GetIndexParameters().Length == 0);  // must not be an indexer
    return properties;
}
\$\endgroup\$
  • \$\begingroup\$ Will the Debug.Assert() lines throw exceptions if the conditions inside of them are met? I've not used or seen Debug.Assert() before, and am not sure what its purpose is either. \$\endgroup\$ – Delfino Jun 22 at 22:41
  • \$\begingroup\$ Yes they will, but only in Debug configuration. I use these in private methods to make assertions. You could also throw ArgumentNullException if you want. \$\endgroup\$ – dfhwze Jun 23 at 2:44
  • \$\begingroup\$ In GetAccessibleProperties you mentioned the code was optimized for readability over performance. Could you also add code that is better for performance? \$\endgroup\$ – Delfino Jun 23 at 18:33
  • 1
    \$\begingroup\$ @Delfino If your classes don't have indexers, leave out x.GetIndexParameters().Length == 0. Also, if your classes have only public getters (no internal or private setters etc.), leave out x.GetGetMethod(false) != null. \$\endgroup\$ – dfhwze Jun 23 at 19:26
6
\$\begingroup\$

You should have a guard clause against someone passing in null

PropertyInfo has a PropertyType and you should use that instead of

 Type valueType = value.GetType();

as if value is null you will get a null reference error where PropertyType will give you the type of the property.

You will need to fix this line as well if value is null. Again you will get a null reference error

 result += value.PropertiesToString(++tabs);

Better to compare types then their names. Instead of

 valueType.Name.Equals("String")

use

 valueType == typeof(string)

You should separate out gathering the properties and displaying them. I would have an extension method that returned IEnumerable> and then you could use linq to convert that into the strings and how you want it displayed.

Other things to consider is if you have two object that reference each other you will get a stack overflow.

I would recommend looking at https://docs.microsoft.com/en-us/dotnet/api/system.runtime.serialization.objectmanager?view=netframework-4.8 to see an example of an object walker. I think making the class IEnumerable is a bit confusion when learning and they would have been better making a method that returns IEnumerable. still a good place to start when learning about walking an object graph.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.