Introduction
Autonomous .NET object comparison, with control and customisation is something I have never found a satisfactory solution to until now. I am going to introduce ZCompare, a powerful .Net object comparison component and take you through a few simple steps to help you get the results you want.
Background
In my career of 20+ years as a software developer I have, on many occasion needed to compare one data object with another. I have been tasked numerous times to highlight these changes and act on them in one way or another. Implementing interfaces and hand coding comparison routines to do the job can readily become cumbersome, error prone and inconsistent. ZCompare aims to overcome these problems, and more to provide powerful object comparison out of the box with no need to hand crank it to get results.
This tutorial will take you through the basic syntax and results of ZCompare through to the extensibility and customisation features.
Whilst you can read through this and get a good understanding quickly, you may want to download the demo (ZCee) project. This is a very simple C# WPF project which helps see ZCompare in action and the results it gives. Don't worry if you are not familiar with WPF, you only need to modify a simple file - ResultsViewModel.cs
The latest version can be found on the ZCompare project site http://www.zcompare.net
The tutorial uses sample data from the Zaybu.Compare.Data assembly which is included in the ZCee Project.
Contents
Requirements
.NET Framework 4.5 or greater
ZCompare assemblies download from nuget here
or
install the nuget package via Package Manager Console in Visual Studio
PM> Install-Package Zaybu.ZCompare
Step 1 - Compare two simple objects
We are going to compare two products, productA
and productB
. Here is the definition of a Product
public class Product
{
private ProductCode _code;
public int ID { get; set; }
public string Description { get; set; }
public float Price { get; set; }
public Guid InventoryID { get; set; }
public ProductCode Code { get { return _code; } set { _code = value; } }
public byte[] ImageData { get; set; }
}
public struct ProductCode
{
public int ProductID;
public char Category;
}
And here is the code used to compare two instances of a Product
using Zaybu.Compare.Data;
Product productA = SampleData.CreateProduct(1);
Product productB = SampleData.CreateProduct(1);
productB.Description = "Product B"
productB.ImageData[1] = 34;
var results = ZCompare.Compare(productA, productB);
SampleData.CreateProduct(1);
creates a Product with ID = 1 and populates it with some data. productA
and productB
are two separate instances with the same data. We modify productB
by changing the Description and some of it's ImageData
. Pass both objects to the ZCompare.Compare()
static method and assign the returned results.
Step 2 - Understanding the compare results
This looks promising, we have an Identical
property which is false and a NumberOfDifferences
field correctly displaying 2. The Root
is really what is useful though. This is a root tree node and it reflects the object graph tree structure of a Product
Let's expand it and see what's inside.
Hopefully most of these properties are self explanatory.
ChangedToValue
| Object reference to productB |
ChangedToValueAsString
| Display friendly representation of productB |
Children
| Child result nodes |
IsLeaf
| Flag indicating if this is the last object in the branch |
OriginalValue
| Object reference to productA |
OriginalValueAsString
| Display friendly representation of productA |
Parent
| Reference to any parent node |
PropertyName
| The name of the property being compared |
Status
| Comparison result status indicating that the object has changed |
If we expand the Children
node we will see how the results reflect the structure of a Product
There is a node for each public property of the Product
object.
Looking at the Description
node we can see how that property has changed.
Step 3 - Query the results
Quite often we find ourselves dealing with large, deep object trees. In these instances traversing the results tree to find the result you want would be tedious. We can probe and query the results for specific objects we are interested in by using
ZCompare.GetResult()
As the Product class is a little simple, now is a good time to introduce a more complex object, Supplier. The Supplier has a List<Product>
as one of its properties.
public class Supplier
{
public int ID { get; set; }
public string Name { get; set; }
public List<Product> Products { get; set; }
public Dictionary<string, Address> Addresses { get; set; }
public DateTime Created { get; set; }
public SupplierStatus Status { get; set; }
public Array Contacts { get; set; }
public object Logo { get; set; }
}
In the code below we create two Supplier objects. They have seeded sample data populated for them by the CreateSupplier()
method.
Supplier supplierA = SampleData.CreateSupplier(1);
Supplier supplierB = SampleData.CreateSupplier(2);
var results = ZCompare.Compare(supplierA, supplierB);
var productResults = results.GetResult(supplierA.Products);
productResults
above points to the Products
result node.
Another example
var productResult = results.GetResult(supplierA.Products[1]);
productResult
contains the results just for the product at location 1 in the list
And finally, to probe for specific value type properties you can pass in the property name.
var productDescriptionResult = results.GetResult(supplierA.Products[0], "Description");
Step 4 - Display results
There is a useful method, GetSummary()
on every result node
var productResult = results.GetResult(supplierA.Products[1]);
string productSummary = productResult.GetSummary();
This gives a simple string output similar to this.
ListItem Changed
ID Changed - from '2' to '7'
Description Changed - from 'Chocolate Volt Pencil' to 'Emergency Electromagnetic Retrostinker'
Price Changed - from '2.25' to '7.86'
InventoryID Changed - from '22222222-2222-222222222222' to '77777777-7777-777777777777'
Code Changed
ProductID Changed - from '2' to '7'
Category Changed - from 'B' to 'G'
ImageData Changed - from '71-93-C6-E3-6F-7C-38-98' to '4B-63-04-E1-68-FF-41-99-EA'
ZCee Viewer
For full interactive results viewing and validation, the ZCee Project is highly recommended. It displays the results in a tree view, highlighting changes and displays summaries. Source code is included so you can easily get your own objects in there for comparison.
Step 5 - Customisation - Ignore properties, types and namespaces
This step shows us how to specify what gets compared. Large, complex object graphs will potentially produce large, complex comparison results. This obviously has a direct impact on manageability and efficiency. The good news is that ZCompare allows powerful customisation to exclude properties, types and namespaces from the comparison. Let's take another look at the Product type again and it's ImageData
property.
...
ImageData Changed - from '71-93-C6-E3-6F-7C-38-98' to '4B-63-04-E1-68-FF-41-99-EA'
...
Let's assume that we are not really interested in whether it has changed or not, we want to ignore it. We have two options.
1) Ignore a single property declaratively
Use the Zaybu.Compare.IgnorePropertyAttribute
public class Product
{
[IgnoreProperty]
public byte[] ImageData { get; set; }
Or
2) Inject at runtime
This is really useful for objects you don't have source control over!
ZCompare.IgnoreProperty(typeof(Product), "ImageData", typeof(byte[]));
Similarly we have the ability to ignore types and complete namespaces.
ZCompare.IgnoreType(typeof(byte[]));
ZCompare.IgnoreNamespace("System.Runtime");
Step 6 - Extensibility
This step concentrates on how objects and properties get compared. Again this is very powerful and allows complete control over the comparison operation.
Let's assume that we want to take control over how our Supplier class is compared, and that actually we only need to do a much simpler comparison.
We create our own custom comparitor by implementing...
Zaybu.Compare.Comparitors.ComparitorBase<T>
... and overriding the Compare<T>()
method like so...
public class SupplierCustomComparitor : ComparitorBase<Supplier>
{
public override void Compare(Supplier originalObject, Supplier compareToObject, Results results)
{
if (originalObject.ID != compareToObject.ID)
{
results.AddResult("Supplier ID's are different", ResultStatus.Changed, originalObject.ID, compareToObject.ID);
}
}
}
...and registering the custom comparitor with ZCompare.
ZCompare.RegisterComparitor(new SupplierCustomComparitor());
var results = ZCompare.Compare(supplierA, supplierB);
Taking a look at results.Root.GetSummary()
Zaybu.Compare.Data.Supplier Changed
Supplier ID's are different Changed - from '5' to '6'
This demonstrates that we can simplify the comparison operation by implementing a simple rule, if the id's are different then assume the objects are to be different regardless of if the object data is exactly the same.
Note, you can still use ZCompare within the Compare()
override
public class SupplierCustomComparitor : ComparitorBase<Supplier>
{
public override void Compare(Supplier originalObject, Supplier compareToObject, Results results)
{
if (originalObject.ID != compareToObject.ID)
{
results.AddResult("Supplier ID's are different", ResultStatus.Changed, originalObject.ID, compareToObject.ID);
}
var productResults = ZCompare.Compare(originalObject.Products, compareToObject.Products);
}
}
Another method to override is the GetStringValue()
method. This method determines the display format of the properties being compared. For example, we can create a custom comparitor for the DateTime
system type.
public class DateTimeComparitor : ComparitorBase<DateTime>
{
public override string GetStringValue(DateTime value)
{
return String.Format("{0:r}", value);
}
public override void Compare(DateTime baseProperty, DateTime compareToProperty, ZCompareResults results)
{
if (baseProperty.ToShortDateString() != compareToProperty.ToShortDateString())
{
results.CurrentResult.Status = ResultStatus.Changed;
}
}
}
Looking at the results below, we can see the format of the Created
property is displayed as we specified. Also the Status is NoChange
despite the fact that the actual values will differ by a few milliseconds, which we chose to ignore.
var results = ZCompare.Compare(supplierA, supplierB);
var createdResult = results.GetResult(supplierA, "Created";
Another simple example to handle string comparisons how you want.
public class StringCustomComparitor : ComparitorBase<string>
{
public override void Compare(string originalObject, string compareToObject, Results results)
{
if (!String.Equals(originalObject, compareToObject, StringComparison.OrdinalIgnoreCase)
{
results.AddResult("My strings are different!", ResultStatus.Changed, originalObject, compareToObject);
}
}
}
Step 7 - Key Properties
When dealing with arrays, collections and lists etc, you may want to compare items in one with items in the other regardless of if they have changed position.
public class Fruit
{
public int ID { get; set; }
public string Name { get; set; }
public string Colour { get; set; }
}
List<Fruit> originalFruits = new List<Fruit>();
originalFruits.Add(new Fruit { ID = 1, Name = "Apple", Colour = "Green" });
originalFruits.Add(new Fruit { ID = 2, Name = "Banana", Colour = "Yellow" });
originalFruits.Add(new Fruit { ID = 3, Name = "Cherry", Colour = "Red" });
Let us assume that we have another list of fruits but in a different order
List<Fruit> newFruits = new List<Fruit>();
newFruits.Add(new Fruit { ID = 3, Name = "Cherry", Colour = "Red" });
newFruits.Add(new Fruit { ID = 2, Name = "Banana", Colour = "Yellow" });
newFruits.Add(new Fruit { ID = 1, Name = "Apple", Colour = "Green" })
By default, ZCompare will compare the apple in originalFruits with the cherry in newFruits. This is because they are the first object in each of the lists. This may, or not make sense in your application. What you would like though is to have the choice. So for this example we want to compare our apples with another apple and our cherries with other cherries. This is where the KeyPropertyAttribute
comes in.
public class Fruit
{
public int ID { get; set; }
[KeyProperty]
public string Name { get; set; }
public string Colour { get; set; }
}
This can be declared at compile time as above or injected at runtime below. Again really useful for objects that you don't have source control over!
ZCompare.SetKeyProperty(typeof(Fruit), "Name");
You can also have more than one property to make a compound key. So, in the example below we are only going to compare a red apple with another red apple. If we had a green one then that would be treated as something different.
ZCompare.SetKeyProperties(typeof(Fruit), new List<Fruit> { "Name", "Colour" });
public class Fruit
{
public int ID { get; set; }
[KeyProperty]
public string Name { get; set; }
[KeyProperty]
public string Colour { get; set; }
}
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.