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 StackOverflow 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.
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.
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).
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.
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
.
bool result;
IComparable selfValueComparer;
selfValueComparer = valueA as IComparable;
if (valueA == null && valueB != null || valueA != null && valueB == null)
result = false;
else if (selfValueComparer != null && selfValueComparer.CompareTo(valueB) != 0)
result = false;
else if (!object.Equals(valueA, valueB))
result = false;
else
result = true;
return result;
Comparing Objects
If the values could not be directly compared, and does 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.
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.
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;
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 (CanDirectlyCompare(propertyInfo.PropertyType))
{
if (!AreValuesEqual(valueA, valueB))
{
Console.WriteLine("Mismatch with property '{0}.{1}' found.",
objectType.FullName, propertyInfo.Name);
result = false;
}
}
else if (typeof(IEnumerable).IsAssignableFrom(propertyInfo.PropertyType))
{
IEnumerable<object> collectionItems1;
IEnumerable<object> collectionItems2;
int collectionItemsCount1;
int collectionItemsCount2;
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();
if (collectionItemsCount1 != collectionItemsCount2)
{
Console.WriteLine("Collection counts for property '{0}.{1}' do not match.",
objectType.FullName, propertyInfo.Name);
result = false;
}
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;
}
private static bool CanDirectlyCompare(Type type)
{
return typeof(IComparable).IsAssignableFrom(type) || type.IsPrimitive || type.IsValueType;
}
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;
else if (selfValueComparer != null && selfValueComparer.CompareTo(valueB) != 0)
result = false;
else if (!object.Equals(valueA, valueB))
result = false;
else
result = true;
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.