Typesafe .Net Object Comparison using ZCompare, A Quick Start Guide

Introduction

 
ZCompare is a powerful autonomous typesafe .NET object comparison tool. The modified data scenario where changes have been made to the original data is a common problem and essentially, as developers, we want to know what has been added, deleted, or modified. I am going to show how easy ZCompare can make detecting these changes and working with the 'before' and 'after' conditions of each item of data.
 
Background
 
I have my original data from the database, and my user has made many changes to this data which reflects in my complex .NET object. I want to know if they have made changes to data or created new data or deleted data. More importantly, I want to know what exactly has changed and what it has changed from. Sounds familiar?
 
The Modified Data Scenario
 
Let's assume we have a large object graph; a list of Suppliers for example. See here for class definitions. Let's assume that originalSuppliers is a list of current suppliers from the database. We are creating them here on the fly for simplicity, using a helper class 'SampleData'.
 
I am using sample data from the Zaybu.Compare.Data assembly is included in the ZCee Project.
  1. List < Supplier > originalSuppliers = SampleData.CreateSuppliers(8);  
  2. // updatedSuppliers contain all of our modifications. // We create them here on the fly and then we are going to modify them.  
  3. List < Supplier > updatedSuppliers = SampleData.CreateSuppliers(8); // We are going to make some modifications to our data - updatedSuppliers  
  4. updatedSuppliers.RemoveAt(0); // Delete a supplier  
  5. updatedSuppliers[0].Products[2].Description = "Description Updated";  
  6. updatedSuppliers[1].Products[1].Price = 19.99 f;  
  7. updatedSuppliers[4].Status = SupplierStatus.Active;  
  8. updatedSuppliers.Add(SampleData.CreateSupplier(9)); // Create a new one // Let's compare them ZCompareResults results = ZCompare.Compare(originalSuppliers, updatedSuppliers);  
'results' is a container for all the results. We will use this object to query for the results you want to work with.
 
Let's get only those suppliers that have changes made to them. We use the results.GetResults<T>() method.
  1. // Pass in a reference object, in this case the top level 'originalSuppliers' list // and also 'true' to say we only want Suppliers that have changes made to them  
  2. var supplierResults = results.GetResults < Supplier > (originalSuppliers, true); // Lets see if we can do something useful with the results  
  3. supplierResults.ForEach(s => {  
  4.     if (s.Status == ResultStatus.Added) {  
  5.         Debug.WriteLine("This supplier has been added: " + s.ChangedToValue.Name);  
  6.     } else if (s.Status == ResultStatus.Deleted) {  
  7.         Debug.WriteLine("This supplier is deleted: " + s.OriginalValue.Name);  
  8.     } else {  
  9.         Debug.WriteLine("This supplier has been changed: " + s.ChangedToValue.Name);  
  10.     }  
  11. });  
  12. --Debug Output--  
  13. This supplier is deleted: Complicated Cow  
  14. This supplier has been changed: Cloudy Cat  
  15. This supplier has been changed: Intelligent Duck  
  16. This supplier has been changed: Stormy Knife Company  
  17. This supplier has been added: The Cheeky Panda Company  
Note how each result has a 'Status' flag indicating if the Supplier has been added, deleted, or modified. Also, each result has an 'OriginalValue' and a 'ChangedToValue' property which references the original object before changes and the modified object respectively.
 
So, perhaps if we were using a micro ORM...
  1. supplierResults.ForEach(s => {  
  2.     if (s.Status == ResultStatus.Added) {  
  3.         ORM.Insert(s.ChangedToValue);  
  4.     } else if (s.Status == ResultStatus.Deleted) {  
  5.         ORM.Delete < Supplier > (s.OriginalValue.ID);  
  6.     } else {  
  7.         ORM.Update(s.ChangedToValue);  
  8.     }  
  9. });  
Of course, things are rarely this simple. We have a Supplier object with a list of Products and a dictionary of Addresses as properties. These properties may have changed and we want to work with them as 'Products' and 'Address' types.
 
So, let's go again and recreate our data and modify it again a bit differently.
  1. originalSuppliers = SampleData.CreateSuppliers(8);  
  2. updatedSuppliers = SampleData.CreateSuppliers(8);  
  3. updatedSuppliers[2].Addresses["Head Office"].PostCode.Inner = "PC99"// Modify an address PostCode for a supplier  
  4. updatedSuppliers[2].Products.RemoveAt(0); // Delete a product from this supplier  
  5. updatedSuppliers[2].Products.Add(SampleData.CreateProduct(20)); // Add a product to this supplier  
  6. updatedSuppliers[2].Products[2].ImageData = new byte[2] {  
  7.     34,  
  8.     36  
  9. }; // Change a property of a product  
  10. updatedSuppliers[4].Addresses.Add("Northern HQ", SampleData.CreateAddressList(3)[0]); // Add an Address  
  11. updatedSuppliers[4].Products.Add(SampleData.CreateProduct(21)); // Add a product to this supplier  
  12. updatedSuppliers[4].Products[1].Code = new ProductCode {  
  13.     Category = 'K', ProductID = 923  
  14. }; // Using the same code from before...  
  15. results = ZCompare.Compare(originalSuppliers, updatedSuppliers);  
  16. supplierResults = results.GetResults < Supplier > (originalSuppliers, true);  
We have all the modified suppliers in supplierResults. We can now loop through each one and see if they all have changes to their products.
 
Note how we use the results.GetResults<Product>() method and pass in 'Products' property of each individual supplier to get the product results for each supplier.
  1. supplierResults.ForEach(s => { // We can get the product modifications for each supplier like this. // Use the GetResults() method and pass in the reference object var productResults = results.GetResults<Product>(s.OriginalValue.Products, true); Debug.WriteLine("Product changes for Supplier: " + s.OriginalValue.Name); productResults.ForEach(p => Debug.WriteLine(p.GetSummary()));  
  2. });  
The GetSummary() method on each result is useful for verbosely showing changes in a user-readable form.
 
Notice how we are only getting the results for 'Products' and not for 'Addresses'.
  1. -- Debug Output --  
  2. ListItem Deleted - 'Zaybu.Compare.Data.Product' ListItem Changed  
  3. Unique ID Field NoChange  
  4. Description NoChange  
  5. Price NoChange  
  6. Inventory Code NoChange  
  7. Code NoChange ProductID NoChange Category NoChange  
  8. ImageData Changed - from 'F4-DD-85-CF-CE-1E-E2-D1-31-71-65' to '22-24' ListItem Added - 'Zaybu.Compare.Data.Product' ListItem Changed  
  9. Unique ID Field NoChange  
  10. Description NoChange  
  11. Price NoChange  
  12. Inventory Code NoChange  
  13. Code Changed ProductID Changed - from '7' to '923' Category Changed - from 'G' to 'K'  
  14. ImageData NoChange ListItem Added - 'Zaybu.Compare.Data.Product'  
We have another method to get results.
 
The GetResultsDeep() method takes a reference object and will get all the changes in the object tree from the supplied reference object (originalSuppliers in this case) and for the type.
  1. var allProductResults = results.GetResultsDeep < Product > (originalSuppliers, true);  
  2. allProductResults.ForEach(p => {  
  3.     if (p.Status == ResultStatus.Added) Debug.WriteLine("Added: " + p.ChangedToValue.Description);  
  4.     else if (p.Status == ResultStatus.Deleted) Debug.WriteLine("Deleted: " + p.OriginalValue.Description);  
  5.     else Debug.WriteLine("Changed: " + p.ChangedToValue.Description);  
  6. });  
Here we have all the product changes for all suppliers.
  1. -- Debug Output --  
  2. Deleted: Fabulous Elevator  
  3. Changed: Pinball  
  4. Added: Refittable Nailfile  
  5. Changed: Emergency Electromagnetic Retrostinker  
  6. Added: Ultrasonic Home Economics Biotargeter  
This shows that with a combination of GetResults<T>(), GetResultsDeep<T>(), and the reference object passed into these methods, we can work with our results at any level of the object tree we wish.
 
Below are some brief examples of working with different types including a Dictionary and a struct.
  1. // In exactly the same way we can work with Address objects which is a Dictionary var addressDictionaryResults = results.GetResultsDeep<Dictionary<string, Address>>(originalSuppliers, true);  
  2. addressDictionaryResults.ForEach(a => {  
  3.     Debug.WriteLine(a.GetSummary());  
  4. }); // ...also with a deeply nested PostCode type  
  5. var postCodeResults = results.GetResultsDeep(originalSuppliers, true);  
  6. postCodeResults.ForEach(a => {  
  7.     Debug.WriteLine(a.GetSummary());  
  8. }); // ...and also ProductCode which is a struct.  
  9. var productCodeResults = results.GetResultsDeep < ProductCode > (originalSuppliers, true);  
  10. productCodeResults.ForEach(a => {  
  11.     Debug.WriteLine(a.GetSummary());  
  12. });  

Summary

 
Hopefully, the power of the GetResults<T>() and GetResultsDeep<T>() methods is now clear. Combined with the 'OriginalValue', 'ChangedToValue', and the 'Status' flag properties of results, they form the core of ZCompare functionality.
 
I hope, this has shown how you can work with your comparison results in a selective and targeted way. Using the reference object passed into these methods provides a context that allows manageable results from what can be an overwhelming result set. There is much more to ZCompare, such as the ability to Ignore properties, types, and namespaces, as well as custom comparators, and key fields.