Introduction
There is often a need to compare two complex objects. Particular examples include comparing expected vs. actual objects in unit tests, comparing complex data models, and many others which I currently can't think of. All in all, if you don't want to invent your own bicycle, searching for .NET object comparers will yield 'Compare .NET objects' library as one of the first results.
It's free and open-source, feel free to get via NuGet.
Basic Usage
Let's say we have a class, which we'll call a ComplexClass
:
class ComplexClass
{
public int ID { get; set; }
public string Name { get; set; }
public DateTime CreatedOn { get; set; }
public string OtherStringField { get; set; }
public StatusEnum Status { get; set; }
}
The StatusEnum
looks like this:
enum StatusEnum
{
Inappropriate = 0,
[Description("Slightly Inappropriate")]
SlightlyInappropriate,
Normal,
[Description("Well-off")]
WellOff
}
Now, let's create 2 different class instances and perform a full comparison of them, getting all the properties that are different:
StringBuilder sb = new StringBuilder();
int propertyCount = typeof(ComplexClass).GetProperties().Length;
ComplexClass cc1 = new ComplexClass() { CreatedOn = DateTime.Now, ID = 3,
Name = "the first", OtherStringField = "other", Status = StatusEnum.Inappropriate };
ComplexClass cc2 = new ComplexClass() { CreatedOn = DateTime.Now.AddDays(-1), ID = 5,
Name = "the second", OtherStringField = "osf", Status = StatusEnum.Normal };
CompareLogic basicComparison = new CompareLogic()
{ Config = new ComparisonConfig() { MaxDifferences = propertyCount } };
List<Difference> diffs = basicComparison.Compare(cc1, cc2).Differences;
foreach (Difference diff in diffs)
{
sb.AppendLine("Property name:" + diff.PropertyName);
sb.AppendLine("Old value:" + diff.Object1Value);
sb.AppendLine("New value:" + diff.Object2Value + "\n");
}
using (StreamWriter outfile = new StreamWriter(path + @"\res_cmp.txt"))
{
outfile.Write(sb.ToString());
}
Note that as we want to get all the differences, we get the property count of the class, and set MaxDifferences
parameter, which defaults to 1
, to the number of properties in ComplexClass
class. If you just need to know if the objects are equal, and don't need all the details (e.g. in unit tests) you can leave it as 1
.
If you run the piece of code above, you should get the "res_cmp.txt" file content similar to this:
Property name:.ID
Old value:3
New value:5
Property name:.Name
Old value:the first
New value:the second
Property name:.CreatedOn
Old value:4/3/2014 22:44:04
New value:4/2/2014 22:44:04
Property name:.OtherStringField
Old value:other
New value:osf
Property name:.Status
Old value:Inappropriate
New value:Normal
Advanced Usage
Custom Comparers
There are times when you'd like comparer to behave differently. For example, default enum
comparer outputs enum
's string
value to diff
list. You may want to force it to use numbers, or enum
's description attribute values (e.g. you want to show output to user).
Let's do just that, and output descriptions instead of stringified enum
members. Here is the custom comparer:
class CustomEnumComparer : BaseTypeComparer
{
public CustomEnumComparer() : base(RootComparerFactory.GetRootComparer()) { }
public override void CompareType(KellermanSoftware.CompareNetObjects.ComparisonResult result,
object object1, object object2, string breadCrumb)
{
if (object1.ToString() != object2.ToString())
{
Difference difference = new Difference
{
PropertyName = breadCrumb,
Object1Value = EnumHelper.GetDescription(object1),
Object2Value = EnumHelper.GetDescription(object2),
Object1 = new WeakReference(object1),
Object2 = new WeakReference(object2)
};
AddDifference(result, difference);
}
}
public override bool IsTypeMatch(Type type1, Type type2)
{
return TypeHelper.IsEnum(type1) && TypeHelper.IsEnum(type2);
}
}
We'll use a helper class to extract the attributes we need:
static class EnumHelper
{
public static string GetDescription(object enumMember)
{
FieldInfo fi = enumMember.GetType().GetField(enumMember.ToString());
IEnumerable<DescriptionAttribute> attrs =
fi.GetCustomAttributes<DescriptionAttribute>(false);
return attrs.Any() ? attrs.ElementAt(0).Description : enumMember.ToString();
}
}
And the updated method call:
StringBuilder sb = new StringBuilder();
int propertyCount = typeof(ComplexClass).GetProperties().Length;
ComplexClass cc1 = new ComplexClass() { CreatedOn = DateTime.Now, ID = 3,
Name = "the new first", OtherStringField = "other",
Status = StatusEnum.SlightlyInappropriate };
ComplexClass cc2 = new ComplexClass() { CreatedOn = DateTime.Now.AddDays(-1), ID = 3,
Name = "the new second", OtherStringField = "other", Status = StatusEnum.WellOff };
CompareLogic advanedComparison = new CompareLogic()
{
Config = new ComparisonConfig()
{
MaxDifferences = propertyCount,
CustomComparers = new List<BaseTypeComparer>() { new CustomEnumComparer() }
}
};
List<Difference> diffs = advanedComparison.Compare(cc1, cc2).Differences;
foreach (Difference diff in diffs)
{
sb.AppendLine("Property name:" + diff.PropertyName);
sb.AppendLine("Old value:" + diff.Object1Value);
sb.AppendLine("New value:" + diff.Object2Value + "\n");
}
using (StreamWriter outfile = new StreamWriter(path + @"\res_cmp_adv.txt"))
{
outfile.Write(sb.ToString());
}
You just have to specify your custom comparer in the config
. It will always take precedence over the standard one. Results should be as follows:
Property name:.Name
Old value:the new first
New value:the new second
Property name:.CreatedOn
Old value:4/3/2014 23:38:00
New value:4/2/2014 23:38:00
Property name:.Status
Old value:Slightly Inappropriate
New value:Well-off
Summary
There have been few things to write about the advanced usage of 'Compare .NET Objects', but I really don't want to bloat the article. If this topic is popular, I may write part II, but I believe I've covered the most ways you'll use the 'Compare .NET Objects' lib. Happy programming.