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.
- List < Supplier > originalSuppliers = SampleData.CreateSuppliers(8);
-
- List < Supplier > updatedSuppliers = SampleData.CreateSuppliers(8);
- updatedSuppliers.RemoveAt(0);
- updatedSuppliers[0].Products[2].Description = "Description Updated";
- updatedSuppliers[1].Products[1].Price = 19.99 f;
- updatedSuppliers[4].Status = SupplierStatus.Active;
- updatedSuppliers.Add(SampleData.CreateSupplier(9));
'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.
-
- var supplierResults = results.GetResults < Supplier > (originalSuppliers, true);
- supplierResults.ForEach(s => {
- if (s.Status == ResultStatus.Added) {
- Debug.WriteLine("This supplier has been added: " + s.ChangedToValue.Name);
- } else if (s.Status == ResultStatus.Deleted) {
- Debug.WriteLine("This supplier is deleted: " + s.OriginalValue.Name);
- } else {
- Debug.WriteLine("This supplier has been changed: " + s.ChangedToValue.Name);
- }
- });
- --Debug Output--
- This supplier is deleted: Complicated Cow
- This supplier has been changed: Cloudy Cat
- This supplier has been changed: Intelligent Duck
- This supplier has been changed: Stormy Knife Company
- 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...
- supplierResults.ForEach(s => {
- if (s.Status == ResultStatus.Added) {
- ORM.Insert(s.ChangedToValue);
- } else if (s.Status == ResultStatus.Deleted) {
- ORM.Delete < Supplier > (s.OriginalValue.ID);
- } else {
- ORM.Update(s.ChangedToValue);
- }
- });
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.
- originalSuppliers = SampleData.CreateSuppliers(8);
- updatedSuppliers = SampleData.CreateSuppliers(8);
- updatedSuppliers[2].Addresses["Head Office"].PostCode.Inner = "PC99";
- updatedSuppliers[2].Products.RemoveAt(0);
- updatedSuppliers[2].Products.Add(SampleData.CreateProduct(20));
- updatedSuppliers[2].Products[2].ImageData = new byte[2] {
- 34,
- 36
- };
- updatedSuppliers[4].Addresses.Add("Northern HQ", SampleData.CreateAddressList(3)[0]);
- updatedSuppliers[4].Products.Add(SampleData.CreateProduct(21));
- updatedSuppliers[4].Products[1].Code = new ProductCode {
- Category = 'K', ProductID = 923
- };
- results = ZCompare.Compare(originalSuppliers, updatedSuppliers);
- 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.
- supplierResults.ForEach(s => {
- });
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'.
- -- Debug Output --
- ListItem Deleted - 'Zaybu.Compare.Data.Product' ListItem Changed
- Unique ID Field NoChange
- Description NoChange
- Price NoChange
- Inventory Code NoChange
- Code NoChange ProductID NoChange Category NoChange
- 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
- Unique ID Field NoChange
- Description NoChange
- Price NoChange
- Inventory Code NoChange
- Code Changed ProductID Changed - from '7' to '923' Category Changed - from 'G' to 'K'
- 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.
- var allProductResults = results.GetResultsDeep < Product > (originalSuppliers, true);
- allProductResults.ForEach(p => {
- if (p.Status == ResultStatus.Added) Debug.WriteLine("Added: " + p.ChangedToValue.Description);
- else if (p.Status == ResultStatus.Deleted) Debug.WriteLine("Deleted: " + p.OriginalValue.Description);
- else Debug.WriteLine("Changed: " + p.ChangedToValue.Description);
- });
Here we have all the product changes for all suppliers.
- -- Debug Output --
- Deleted: Fabulous Elevator
- Changed: Pinball
- Added: Refittable Nailfile
- Changed: Emergency Electromagnetic Retrostinker
- 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.
-
- addressDictionaryResults.ForEach(a => {
- Debug.WriteLine(a.GetSummary());
- });
- var postCodeResults = results.GetResultsDeep(originalSuppliers, true);
- postCodeResults.ForEach(a => {
- Debug.WriteLine(a.GetSummary());
- });
- var productCodeResults = results.GetResultsDeep < ProductCode > (originalSuppliers, true);
- productCodeResults.ForEach(a => {
- Debug.WriteLine(a.GetSummary());
- });
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.