As part of the refactoring I was doing to the load code for crawler projects I needed a way of verifying that new code was loading data correctly. As it would be extremely time consuming to manually compare the objects, I used Reflection to compare the different objects and their properties. This article briefly describes the process and provides a complete helper function you can use in your own projects.

This code is loosely based on a Stack Overflow question, but I have heavily modified and expanded the original concept.

Obtaining a list of properties

The ability to analyze assemblies and their component pieces is directly built into the .NET Framework, and something I really appreciate - I remember the nightmares of trying to work with COM type libraries in Visual Basic many years ago!

The Type class represents a type declaration in your project, such as a class or enumeration. You can either use the GetType method of any object to get its underlying type, or use the typeof keyword to access a type from its type name.

csharp
Type typeA;
Type typeB;
int value;

value = 1;

typeA = value.GetType();
typeB = typeof(int);

Once you have a type, you can call the GetProperties method to return a list of PropertyInfo objects representing the available properties of the type. Several methods, including GetProperties, accept an argument of BindingFlags, these flags allow you to define the type of information return, such as public members or instance members.

In this case, I want all public instance members which can be read from and which are not included in a custom ignore list.

csharp
foreach (PropertyInfo propertyInfo in objectType.GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(p => p.CanRead && !ignoreList.Contains(p.Name)))
{
}

Retrieving the value of a property

The PropertyInfo class has a GetValue method that can be used to read the value of a property. Its most basic usage is to pass in the instance object (or null if you want to read a static property) and any index parameters (or null if no index parameters are supported).

csharp
object valueA;
object valueB;

valueA = propertyInfo.GetValue(objectA, null);
valueB = propertyInfo.GetValue(objectB, null);

The sample function described in this article doesn't currently support indexed properties.

Determining if a property can be directly compared

Some properties are simple types, such as an int or a string and are very easy to compare. What happens if a property returns some other object such as a collection of strings, or a complex class?

In this case, I try and see if the type supports IComparable by calling the IsAssignableFrom method. You need to call this from the type you would like to create, passing in the source type.

csharp
return typeof(IComparable).IsAssignableFrom(type)

I also check the IsPrimitive and IsValueType properties of the source type, although this is possibly redundant as all the base types I've checked so far all support IComparable.

Directly comparing values

Assuming that I can directly compare a value, first I check if one of the values is null - if one value is null and one false, I immediately return a mismatch.

Otherwise, if IComparable is available, then I obtain an instance of it from the first value and call its CompareTo method, passing in the second value.

If IComparable is not supported, then I fallback to object.Equals.

csharp
bool result;
IComparable selfValueComparer;

selfValueComparer = valueA as IComparable;

if (valueA == null && valueB != null || valueA != null && valueB == null)
  result = false; // one of the values is null
else if (selfValueComparer != null && selfValueComparer.CompareTo(valueB) != 0)
  result = false; // the comparison using IComparable failed
else if (!object.Equals(valueA, valueB))
  result = false; // the comparison using Equals failed
else
  result = true; // match

return result;

Comparing objects

If the values could not be directly compared, and do not implement IEnumerable (as described in the next section) then I assume the properties are objects and call the compare objects function again on the properties.

This works nicely, but has one critical flaw - if you have a child object which has a property reference to a parent item, then the function will get stuck in a recursive loop. Currently the only workaround is to ensure that such parent properties are excluded via the ignore list functionality of the compare function.

Comparing collections

If the direct compare check failed, but the property type supports IEnumerable, then some Linq is used to obtain the collection of items.

To save time, a count check is made and if the counts do not match (or one of the collections is null and the other is not), then an automatic mismatch is returned. If the counts do match, then all items are compared in the same manner as the parent objects.

csharp
IEnumerable<object> collectionItems1;
IEnumerable<object> collectionItems2;
int collectionItemsCount1;
int collectionItemsCount2;

collectionItems1 = ((IEnumerable)valueA).Cast<object>();
collectionItems2 = ((IEnumerable)valueB).Cast<object>();
collectionItemsCount1 = collectionItems1.Count();
collectionItemsCount2 = collectionItems2.Count();

I have tested this code on generic lists such as List<string>, and on strongly typed collections which inherit from Collection<TValue> with success.

The code

Below is the comparison code. Please note that it won't handle all situations - as mentioned indexed properties aren't supported. In addition, if you throw a complex object such as a DataReader I suspect it will throw a fit on that. I also haven't tested it on generic properties, it'll probably crash on those too. But it has worked nicely for the original purpose I wrote it for.

Also, as I was running this from a Console application, you may wish to replace the calls to Console.WriteLine with either Debug.WriteLine or even return them as an out parameter.

csharp
/// <summary>
/// Compares the properties of two objects of the same type and returns if all properties are equal.
/// </summary>
/// <param name="objectA">The first object to compare.</param>
/// <param name="objectB">The second object to compre.</param>
/// <param name="ignoreList">A list of property names to ignore from the comparison.</param>
/// <returns><c>true</c> if all property values are equal, otherwise <c>false</c>.</returns>
public static bool AreObjectsEqual(object objectA, object objectB, params string[] ignoreList)
{
  bool result;

  if (objectA != null && objectB != null)
  {
    Type objectType;

    objectType = objectA.GetType();

    result = true; // assume by default they are equal

    foreach (PropertyInfo propertyInfo in objectType.GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(p => p.CanRead && !ignoreList.Contains(p.Name)))
    {
      object valueA;
      object valueB;

      valueA = propertyInfo.GetValue(objectA, null);
      valueB = propertyInfo.GetValue(objectB, null);

      // if it is a primitive type, value type or implements IComparable, just directly try and compare the value
      if (CanDirectlyCompare(propertyInfo.PropertyType))
      {
        if (!AreValuesEqual(valueA, valueB))
        {
          Console.WriteLine("Mismatch with property '{0}.{1}' found.", objectType.FullName, propertyInfo.Name);
          result = false;
        }
      }
      // if it implements IEnumerable, then scan any items
      else if (typeof(IEnumerable).IsAssignableFrom(propertyInfo.PropertyType))
      {
        IEnumerable<object> collectionItems1;
        IEnumerable<object> collectionItems2;
        int collectionItemsCount1;
        int collectionItemsCount2;

        // null check
        if (valueA == null && valueB != null || valueA != null && valueB == null)
        {
          Console.WriteLine("Mismatch with property '{0}.{1}' found.", objectType.FullName, propertyInfo.Name);
          result = false;
        }
        else if (valueA != null && valueB != null)
        {
          collectionItems1 = ((IEnumerable)valueA).Cast<object>();
          collectionItems2 = ((IEnumerable)valueB).Cast<object>();
          collectionItemsCount1 = collectionItems1.Count();
          collectionItemsCount2 = collectionItems2.Count();

          // check the counts to ensure they match
          if (collectionItemsCount1 != collectionItemsCount2)
          {
            Console.WriteLine("Collection counts for property '{0}.{1}' do not match.", objectType.FullName, propertyInfo.Name);
            result = false;
          }
          // and if they do, compare each item... this assumes both collections have the same order
          else
          {
            for (int i = 0; i < collectionItemsCount1; i++)
            {
              object collectionItem1;
              object collectionItem2;
              Type collectionItemType;

              collectionItem1 = collectionItems1.ElementAt(i);
              collectionItem2 = collectionItems2.ElementAt(i);
              collectionItemType = collectionItem1.GetType();

              if (CanDirectlyCompare(collectionItemType))
              {
                if (!AreValuesEqual(collectionItem1, collectionItem2))
                {
                  Console.WriteLine("Item {0} in property collection '{1}.{2}' does not match.", i, objectType.FullName, propertyInfo.Name);
                  result = false;
                }
              }
              else if (!AreObjectsEqual(collectionItem1, collectionItem2, ignoreList))
              {
                Console.WriteLine("Item {0} in property collection '{1}.{2}' does not match.", i, objectType.FullName, propertyInfo.Name);
                result = false;
              }
            }
          }
        }
      }
      else if (propertyInfo.PropertyType.IsClass)
      {
        if (!AreObjectsEqual(propertyInfo.GetValue(objectA, null), propertyInfo.GetValue(objectB, null), ignoreList))
        {
          Console.WriteLine("Mismatch with property '{0}.{1}' found.", objectType.FullName, propertyInfo.Name);
          result = false;
        }
      }
      else
      {
        Console.WriteLine("Cannot compare property '{0}.{1}'.", objectType.FullName, propertyInfo.Name);
        result = false;
      }
    }
  }
  else
    result = object.Equals(objectA, objectB);

  return result;
}

/// <summary>
/// Determines whether value instances of the specified type can be directly compared.
/// </summary>
/// <param name="type">The type.</param>
/// <returns>
///   <c>true</c> if this value instances of the specified type can be directly compared; otherwise, <c>false</c>.
/// </returns>
private static bool CanDirectlyCompare(Type type)
{
  return typeof(IComparable).IsAssignableFrom(type) || type.IsPrimitive || type.IsValueType;
}

/// <summary>
/// Compares two values and returns if they are the same.
/// </summary>
/// <param name="valueA">The first value to compare.</param>
/// <param name="valueB">The second value to compare.</param>
/// <returns><c>true</c> if both values match, otherwise <c>false</c>.</returns>
private static bool AreValuesEqual(object valueA, object valueB)
{
  bool result;
  IComparable selfValueComparer;

  selfValueComparer = valueA as IComparable;

  if (valueA == null && valueB != null || valueA != null && valueB == null)
    result = false; // one of the values is null
  else if (selfValueComparer != null && selfValueComparer.CompareTo(valueB) != 0)
    result = false; // the comparison using IComparable failed
  else if (!object.Equals(valueA, valueB))
    result = false; // the comparison using Equals failed
  else
    result = true; // match

  return result;
}

I hope you find these helper methods useful, this article will be updated if and when the methods are expanded with new functionality.

Update History

  • 2010-11-05 - First published
  • 2020-11-21 - Updated formatting

Like what you're reading? Perhaps you like to buy us a coffee?

Donate via Buy Me a Coffee

Donate via PayPal


Comments

# DotNetKicks.com

[b]Comparing the properties of two objects via Reflection and C#[/b] You've been kicked (a good thing) - Trackback from DotNetKicks.com - Trackback from DotNetKicks.com

Reply

# DotNetShoutout

[b]Comparing the properties of two objects via Reflection and C#[/b] Thank you for submitting this cool story - Trackback from DotNetShoutout - Trackback from DotNetShoutout

Reply

# Calabonga

That`s cool! Thanks! But reflection is very slow...:)

Reply

# sv

System.StackOverflowException occurs if objects has circular references properties

Reply

# Richard Moss

Hello,

Yes it will - this is why the method supports the ignoreList array - any properties found matching values in this array will be skipped. This is sufficient for the purposes I was using this code for.

Regards; Richard Moss

Reply

# anantharengan

Thanks a lot

Reply

# Jason

How about List? Fails on

valueA = propertyInfo.GetValue( objectA, null );

Reply

# Knoxi

I'm wondering if someone solved the problem about List? This would be great!

Reply

# Richard Moss

Hello,

I haven't looked into this yet, I'll take a look and see what's wrong over the weekend.

Regards; Richard Moss

Reply

# Suri

This is a nice post. Helped a lot with this quick solution I've been looking for: which will be a "object value comparer utility" for my project. Thanks a lot.

Reply

# Richard Moss

Glad you found it helpful!

Reply

# Suri

I just added a few more simple lines of code for my requirement at the beginning to do some extra checks to avoid the loop:

bool result;

        if (objectActual != null && objectModified != null)
        {
            Type objectTypeActual;
            Type objectTypeModified;
            result = true; // assume by default they are equal

            objectTypeActual = objectActual.GetType();    
            objectTypeModified = objectModified.GetType();  

            if(objectTypeActual.FullName !=  objectTypeModified.FullName)
            {
                Console.WriteLine("Objects are of different type. Cannot Compare '{0}' & '{1}'.", objectTypeActual.FullName, objectTypeActual.FullName);
                result = false;
                return result;
            }
            
            var allPropertyInfos = objectTypeActual.GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(p => p.CanRead && !ignoreList.Any(x => x.Contains(p.Name)));
            var allPropertyInfosModified = objectTypeModified.GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(p => p.CanRead && !ignoreList.Any(x => x.Contains(p.Name)));

            if (allPropertyInfos.Count == 0 && allPropertyInfosModified.Count == 0)
            {
                Console.WriteLine("There are no properties in these objects to compare. These are both equal.);
                result = true;
                return result;
            }
            if (allPropertyInfos.Count <= 0)
            {
                Console.WriteLine("Object of type '{0}' does not have any properties to Compare.", objectTypeActual.FullName);
                result = false;
                return result;
            }

            if (allPropertyInfosModified.Count <= 0)
            {
                Console.WriteLine("Object of type '{0}' does not have any properties to Compare.", objectTypeModified.FullName);
                result = false;
                return result;
            }

            foreach (PropertyInfo propertyInfo in allPropertyInfos)

{ .....

Reply

# aadilah

Dictionary not supported.

Reply

# John Campbell

Did you ever get around to taking a look at the problem about List failing on

valueA = propertyInfo.GetValue( objectA, null );

?

Apart from that it's very useful little routine :-)

Reply

# Sujith

Thanks a lot for this code. It saved my time

Reply

# Dmitry

thank! It works for me. I use comparator for unit test

Reply

# cassini

how about to edit one object if there is a missmatch between objects...how can i remove a element from a property list ? :((

Reply

# Vijay

Thank you! Great article. Saved me tons of time.

Reply

# J-S

May I suggest that you exit the loop as soon as possible when results == false as there is absolutely no need to continue since the answer is already found and more importantly because we're using "costly" algorithm (reflection)?

Reply

# Richard Moss

Hello,

You're right of course, but actually I did that deliberately. This was because I wrote this code for use in testing (quite often I don't add IEquatable or IComparable to my classes) and I remember specifically getting really annoyed when tests would fail, I'd fix the first mismatch, but then I'd have to wait for them to run again to get the next. By not breaking out of that loop early I could get all mismatches at once and then fix them at once - I was never worried about the inefficiency as it was "just" testing.

Regards; Richard Moss

Reply

# Babuji Godem

Great article.

Reply

# Software Engineer

Still a great resource for understanding reflection. Thanks for sharing!

Reply