Home > Software design >  Get nested type properties using reflection
Get nested type properties using reflection

Time:02-03

As part of a test I am trying to compare similar objects using reflection. The objects may or may not have multiple levels of nested params. For example:

public class Connection{
    public string Ip{get; set}
    public string Id{get; set}
    public string Key{get; set}
    public string Transport{get; set}
    public Parameters ParametersObj = new Parameters();

    public class Parameters
    {
        public string AssignedName { get; set; }
        public string CategoryType { get; set; }
        public string Status { get; set; }
    }
};

This is just an example of a class, I need this method to deal with any type of object without knowing the number of depth level.

I am doing something like this after I have made sure the two objects are of the same type.

bool result = true;
foreach (var objParam in firstObj.GetType().GetProperties())
{
    var value1 = objParam.GetValue(firstObj);
    var value2 = objParam.GetValue(secondObj);
    if (value1 == null || value2 == null || !value1.Equals(value2))
    {
        logger.Error("Property: "   objParam.Name);
        logger.Error("Values: "   value1?.ToString()   " and "   value2?.ToString());
        result = false;
    }
}
return result;

It works perfectly for the first level of params but it ignores completely any nested objects. In this example I would like it to compare the values inside the parameters object and if they are different the log to print error "Property: Parameters.Status".

CodePudding user response:

The problem is that you only do one loop, without looking at the objects within each object. Here's a quick recursive function I threw together. (untested!)

// You can make it non-generic, this just ensures that both arguments are the same type.
static void Go<T>(T left, T right, Action<object, object, PropertyInfo> onFound, int depth = 2)
{
    foreach (var p in left.GetType().GetProperties())
    {
        var l = p.GetValue(left);
        var r = p.GetValue(right);

        if (l is null || r is null || !l.Equals(r))
            onFound(l, r, p);

        if (depth is not 0)
            Go(l, r, onFound, depth - 1);
    }
}

Usage:

var arg1 = new Connection()
{
    ParametersObj = new() { AssignedName = "foo" }
};

var arg2 = new Connection()
{
    ParametersObj = new() { AssignedName = "bar" }
};

Go(arg1, arg2, Log); // goes 2 layers deep is you don't specify the last parameter

void Log(object l, object r, PropertyInfo p)
{
    logger.Error($"Property: {p.Name}");
    logger.Error($"Values: {l} and {r}");
}

CodePudding user response:

I would recommend to look into some tool which already does that (do not know one which does exactly that but FluentAssertions for example can handle object graph comparisons). But in the nutshell you can check if type is primitive or overrides Equals and call your method recursively. Something like the following:

bool Compare(object firstObj, object secondObj)
{
    if (object.ReferenceEquals(firstObj, secondObj))
    {
        return true;
    }
    var type = firstObj.GetType();
 
    var propertyInfos = firstObj.GetType().GetProperties();
    foreach (var objParam in propertyInfos)
    {
        var methodInfo = objParam.PropertyType.GetMethod(nameof(object.Equals), BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly, new []{typeof(object)});
        var overridesEquals = methodInfo?.GetBaseDefinition().DeclaringType == typeof(object);
        var value1 = objParam.GetValue(firstObj);
        var value2 = objParam.GetValue(secondObj);
        if (value1 == null || value2 == null)
        {
            // Log
            return false;
        }

        if (object.ReferenceEquals(value1, value2))
        {
            continue;
        }
        if (type.IsPrimitive || overridesEquals)
        {
            if (value1.Equals(value2))
            {
                continue;
            }

            // Log?
            return false;
        }

        if (!Compare(value1, value2))
        {
            // log ?
            return false;
        }
    }

    return true;
}

P.S.

  1. Note that Connection.ParametersObj is not a property it is field so it will be ignored by both yours and mine implementations
  2. Consider using source generators instead of reflection.
  3. This does not handle collections.
  • Related