Skip to content

Latest commit

History

71 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Logo

Objects Comparer

Introduction

It is quite common situation when complex objects should be compared. Sometimes objects can contain nested elements, or some members should be excluded from comparison (auto generated identifiers, create/update date etc.), or some members can have custom comparison rules (same data in different formats, like phone numbers). This small framework was developed to solve such kind of problems.

Briefly, Objects Comparer is an object-to-object comparer, which allows to compare objects recursively member by member and define custom comparison rules for certain properties, fields or types.

Objects comparer can be considered as ready to use framework or as a starting point for similar solutions.

Installation

Install-Package ObjectsComparer

Basic Examples

Let's suppose that we have 2 classes

publicclassClassA{publicstringStringProperty{get;set;}publicintIntProperty{get;set;}publicSubClassASubClass{get;set;}}publicclassSubClassA{publicboolBoolProperty{get;set;}}

There are some examples below how Objects Comparer can be used to compare instances of these classes.

//Initialize objects and comparervara1=newClassA{StringProperty="String",IntProperty=1};vara2=newClassA{StringProperty="String",IntProperty=1};varcomparer=newComparer<ClassA>();//Compare objectsIEnumerable<Difference>differences;varisEqual=comparer.Compare(a1,a2,outdifferences);//Print resultsDebug.WriteLine(isEqual?"Objects are equal":string.Join(Environment.NewLine,differenses));

Objects are equal

In examples below Compare objects and Print results blocks will be skipped for brevity except some cases.

vara1=newClassA{StringProperty="String",IntProperty=1};vara2=newClassA{StringProperty="String",IntProperty=2};varcomparer=newComparer<ClassA>();

Difference: DifferenceType=ValueMismatch, MemberPath='IntProperty', Value1='1', Value2='2'.

vara1=newClassA{SubClass=newSubClassA{BoolProperty=true}};vara2=newClassA{SubClass=newSubClassA{BoolProperty=false}};varcomparer=newComparer<ClassA>();

Difference: DifferenceType=ValueMismatch, MemberPath='SubClass.BoolProperty', Value1='True', Value2='False'.

Enumerables (arrays)

vara1=new[]{1,2,3};vara2=new[]{1,2,3};varcomparer=newComparer<int[]>();

Objects are equal

vara1=new[]{1,2};vara2=new[]{1,2,3};varcomparer=newComparer<int[]>();

Difference: DifferenceType=ValueMismatch, MemberPath='Length', Value1='2', Value2='3'.

vara1=new[]{1,2,3};vara2=new[]{1,4,3};varcomparer=newComparer<int[]>();

Difference: DifferenceType=ValueMismatch, MemberPath='[1]', Value1='2', Value2='4'.

vara1=newArrayList{"Str1","Str2"};vara2=newArrayList{"Str1",5};varcomparer=newComparer<ArrayList>();

Difference: DifferenceType=TypeMismatch, MemberPath='[1]', Value1='Str2', Value2='5'.

Multidimensional arrays

vara1=new[]{new[]{1,2}};vara2=new[]{new[]{1,3}};varcomparer=newComparer<int[][]>();

Difference: DifferenceType=ValueMismatch, MemberPath='[0][1]', Value1='2', Value2='3'.

vara1=new[]{new[]{1,2}};vara2=new[]{new[]{2,2},new[]{3,5}};varcomparer=newComparer<int[][]>();

Difference: DifferenceType=ValueMismatch, MemberPath='Length', Value1='1', Value2='2'.

vara1=new[]{new[]{1,2},new[]{3,5}};vara2=new[]{new[]{1,2},new[]{3,5,6}};varcomparer=newComparer<int[][]>();

Difference: DifferenceType=ValueMismatch, MemberPath='[1].Length', Value1='2', Value2='3'.

vara1=new[,]{{1,2},{1,3}};vara2=new[,]{{1,3,4},{1,3,8}};varcomparer=newComparer<int[,]>();

Difference: DifferenceType=ValueMismatch, MemberPath='Dimension1', Value1='2', Value2='3'.

vara1=new[,]{{1,2}};vara2=new[,]{{1,3}};varcomparer=newComparer<int[,]>();

Difference: DifferenceType=ValueMismatch, MemberPath='[0,1]', Value1='2', Value2='3'.

Dynamic objects

C# supports several types of dynamic objects.

ExpandoObject

dynamica1=newExpandoObject();a1.Field1="A";a1.Field2=5;a1.Field4=4;dynamica2=newExpandoObject();a2.Field1="B";a2.Field3=false;a2.Field4="C";varcomparer=newComparer();

Difference: DifferenceType=ValueMismatch, MemberPath='Field1', Value1='A', Value2='B'.

Difference: DifferenceType=MissedMemberInSecondObject, MemberPath='Field2', Value1='5', Value2=''.

Difference: DifferenceType=TypeMismatch, MemberPath='Field4', Value1='4', Value2='C'.

Difference: DifferenceType=MissedMemberInFirstObject, MemberPath='Field3', Value1='', Value2='False'.

dynamica1=newExpandoObject();a1.Field1="A";a1.Field2=5;dynamica2=newExpandoObject();a2.Field1="B";a2.Field3=false;varcomparer=newComparer();

Difference: DifferenceType=ValueMismatch, MemberPath='Field1', Value1='A', Value2='B'.

Difference: DifferenceType=MissedMemberInSecondObject, MemberPath='Field2', Value1='5', Value2=''.

Difference: DifferenceType=MissedMemberInFirstObject, MemberPath='Field3', Value1='', Value2='False'.

Behavior if member not exists could be changed by providing custom ComparisonSettings (see Comparison Settings below).

dynamica1=newExpandoObject();a1.Field1="A";a1.Field2=0;dynamica2=newExpandoObject();a2.Field1="B";a2.Field3=false;a2.Field4="S";varcomparer=newComparer(newComparisonSettings{UseDefaultIfMemberNotExist=true});

Difference: DifferenceType=ValueMismatch, MemberPath='Field1', Value1='A', Value2='B'.

Difference: DifferenceType=ValueMismatch, MemberPath='Field4', Value1='', Value2='S'.

DynamicObject

Let’s assume that we have such implementation of the DynamicObject class. It is necessary to have a correct implementation of the method GetDynamicMemberNames, otherwise Objects Comparer wouldn't work in a right way.

privateclassDynamicDictionary:DynamicObject{publicintIntProperty{get;set;}privatereadonlyDictionary<string,object>_dictionary=newDictionary<string,object>();publicoverrideboolTryGetMember(GetMemberBinderbinder,outobjectresult){varname=binder.Name;return_dictionary.TryGetValue(name,outresult);}publicoverrideboolTrySetMember(SetMemberBinderbinder,objectvalue){_dictionary[binder.Name]=value;returntrue;}publicoverrideIEnumerable<string>GetDynamicMemberNames(){return_dictionary.Keys;}}
dynamica1=newDynamicDictionary();a1.Field1="A";a1.Field3=true;dynamica2=newDynamicDictionary();a2.Field1="B";a2.Field2=8;a2.Field3=1;varcomparer=newComparer();

Difference: DifferenceType=ValueMismatch, MemberPath='Field1', Value1='A', Value2='B'.

Difference: DifferenceType=TypeMismatch, MemberPath='Field3', Value1='True', Value2='1'.

Difference: DifferenceType=MissedMemberInFirstObject, MemberPath='Field2', Value1='', Value2='8'.

Compiler generated objects

dynamica1=new{Field1="A",Field2=5,Field3=true};dynamica2=new{Field1="B",Field2=8};varcomparer=newComparer();IEnumerable<Difference>differences;varisEqual=comparer.Compare((object)a1,(object)a2,outdifferences);

Difference: DifferenceType=ValueMismatch, MemberPath='Field1', Value1='A', Value2='B'.

Difference: DifferenceType=TypeMismatch, MemberPath='Field2', Value1='5', Value2='8'.

Difference: DifferenceType=MissedMemberInSecondObject, MemberPath='Field3', Value1='True', Value2=''.

This example requires some additional explanations. Types of the objects a1 and a2 were generated by compiler and are considered as the same type if and only if objects a1 and a2 have same set of members (same name and same type). If casting to (object) is skipped in case of different set of members RuntimeBinderException will be thrown.

Overriding Comparison Rules

To override comparison rule we need to create custom value comparer or provide function how to compare objects and how to convert these objects to string(optional) and filter function(optional). Value Comparer should be inherited from AbstractValueComparer or should implement IValueComparer.

publicclassMyValueComparer:AbstractValueComparer<string>{publicoverrideboolCompare(stringobj1,stringobj2,ComparisonSettingssettings){returnobj1==obj2;//Implement comparison logic here}}

Override comparison rule for objects of particular type.

//Use MyComparer to compare all members of type string comparer.AddComparerOverride<string>(newMyValueComparer());comparer.AddComparerOverride(typeof(string),newMyValueComparer());//Use MyComparer to compare all members of type string except members which name starts with "Xyz"comparer.AddComparerOverride(typeof(string),newMyValueComparer(), member =>!member.Name.StartsWith("Xyz"));comparer.AddComparerOverride<string>(newMyValueComparer(), member =>!member.Name.StartsWith("Xyz"));

Override comparison rule for particular member (Field or Property).

//Use MyValueComparer to compare StringProperty of ClassAcomparer.AddComparerOverride(()=>newClassA().StringProperty,newMyValueComparer());comparer.AddComparerOverride(typeof(ClassA).GetTypeInfo().GetMember("StringProperty").First(),newMyValueComparer());//Compare StringProperty of ClassA by length. If length equal consider that values are equalcomparer.AddComparerOverride(()=>newClassA().StringProperty,(s1,s2,parentSettings)=>s1?.Length==s2?.Length,
s =>s.ToString());comparer.AddComparerOverride(()=>newClassA().StringProperty,(s1,s2,parentSettings)=>s1?.Length==s2?.Length);

Override comparison rule for particular member(s) (Field or Property) by name.

//Use MyValueComparer to compare all members with name equal to "StringProperty"comparer.AddComparerOverride("StringProperty",newMyValueComparer());

Overrides by type have highest priority, then overrides by member and overrides by member name have lowest priority. If more than one value comparers of the same type (by type/by name/by member name) could be applied to the same member, exception AmbiguousComparerOverrideResolutionException will be thrown during comparison.

Example:

vara1=newClassA();vara2=newClassA();comparer.AddComparerOverride<string>(valueComparer1, member =>member.Name.StartsWith("String"));comparer.AddComparerOverride<string>(valueComparer2, member =>member.Name.EndsWith("Property"));varresult=comparer.Compare(a1,a2);//Exception here

Comparison Settings

Comparer constructor has an optional settings parameter to configure some aspects of comparison.

RecursiveComparison

True by default. If true, all members which are not primitive types, do not have custom comparison rule and do not implement ICompareble will be compared using the same rules as root objects.

EmptyAndNullEnumerablesEqual

False by default. If true, empty enumerables (arrays, collections, lists etc.) and null values will be considered as equal values.

UseDefaultIfMemberNotExist

If true and member does not exist, objects comparer will consider that this member is equal to default value of opposite member type. Applicable for dynamic types comparison only. False by default.

Comparison Settings class allows to store custom values that can be used in custom comparers.

SetCustomSetting<T>(Tvalue,stringkey=null)GetCustomSetting<T>(stringkey=null)

Factory

Factory provides a way to encapsulate comparers creation and configuration. Factory should implement IComparersFactory or should be inherited from ComparersFactory.

publicclassMyComparersFactory:ComparersFactory{publicoverrideIComparer<T>GetObjectsComparer<T>(ComparisonSettingssettings=null,IBaseComparerparentComparer=null){if(typeof(T)==typeof(ClassA)){varcomparer=newComparer<ClassA>(settings,parentComparer,this);comparer.AddComparerOverride<Guid>(newMyCustomGuidComparer());return(IComparer<T>)comparer;}returnbase.GetObjectsComparer<T>(settings,parentComparer);}}

Non-generic comparer

varcomparer=newComparer();varisEqual=comparer.Compare(a1,a2);

This comparer creates generic implementation of comparer for each comparison.

Useful Value Comparers

Framework contains several custom comparers that can be useful in many cases.

DoNotCompareValueComparer

Allows to skip some fields/types. Has singleton implementation (DoNotCompareValueComparer.Instance).

DynamicValueComparer

Receives comparison rule as a function.

NulableStringsValueComparer

Null and empty strings are considered as equal values. Has singleton implementation (NulableStringsValueComparer.Instance).

DefaultValueValueComparer

Allows to consider provided value and default value of specified type as equal values (see Example 3 below).

IgnoreCaseStringsValueComparer

Allows to compare string ignoring case. Has singleton implementation (IgnoreCaseStringsValueComparer.Instance).

Examples

There are some more complex examples how Objects Comparer can be used.

Example 1: Expected Message

Challenge

Check if received message equal to the expected message.

Problems

  • DateCreated, DateSent and DateReceived properties need to be skipped
  • Auto generated Id property need to be skipped
  • Message property of Error class need to be skipped

Solution

publicclassError{publicintId{get;set;}publicstringMessgae{get;set;}}
publicclassMessage{publicstringId{get;set;}publicDateTimeDateCreated{get;set;}publicDateTimeDateSent{get;set;}publicDateTimeDateReceived{get;set;}publicintMessageType{get;set;}publicintStatus{get;set;}publicList<Error>Errors{get;set;}publicoverridestringToString(){return$"Id:{Id}, Date:{DateCreated}, Type:{MessageType}, Status:{Status}";}}

Configuring comparer.

_comparer=newComparer<Message>(newComparisonSettings{//Null and empty error lists are equalEmptyAndNullEnumerablesEqual=true});//Do not compare DateCreated _comparer.AddComparerOverride<DateTime>(DoNotCompareValueComparer.Instance);//Do not compare Id_comparer.AddComparerOverride(()=>newMessage().Id,DoNotCompareValueComparer.Instance);//Do not compare Message Text_comparer.AddComparerOverride(()=>newError().Messgae,DoNotCompareValueComparer.Instance);
varexpectedMessage=newMessage{MessageType=1,Status=0};varactualMessage=newMessage{Id="M12345",DateCreated=DateTime.Now,DateSent=DateTime.Now,DateReceived=DateTime.Now,MessageType=1,Status=0};IEnumerable<Difference>differences;varisEqual=_comparer.Compare(expectedMessage,actualMessage,outdifferences);

Objects are equal

varexpectedMessage=newMessage{MessageType=1,Status=1,Errors=newList<Error>{newError{Id=2},newError{Id=7}}};varactualMessage=newMessage{Id="M12345",DateCreated=DateTime.Now,DateSent=DateTime.Now,DateReceived=DateTime.Now,MessageType=1,Status=1,Errors=newList<Error>{newError{Id=2,Messgae="Some error #2"},newError{Id=7,Messgae="Some error #7"},}};IEnumerable<Difference>differences;varisEqual=_comparer.Compare(expectedMessage,actualMessage,outdifferences);

Objects are equal

varexpectedMessage=newMessage{MessageType=1,Status=1,Errors=newList<Error>{newError{Id=2,Messgae="Some error #2"},newError{Id=8,Messgae="Some error #8"}}};varactualMessage=newMessage{Id="M12345",DateCreated=DateTime.Now,DateSent=DateTime.Now,DateReceived=DateTime.Now,MessageType=1,Status=2,Errors=newList<Error>{newError{Id=2,Messgae="Some error #2"},newError{Id=7,Messgae="Some error #7"}}};IEnumerable<Difference>differences;varisEqual=_comparer.Compare(expectedMessage,actualMessage,outdifferences);

Difference: DifferenceType=ValueMismatch, MemberPath='Status', Value1='1', Value2='2'.

Difference: DifferenceType=ValueMismatch, MemberPath='Errors[1].Id', Value1='8', Value2='7'.

Example 2: Persons comparison

Challenge

Compare persons from different sources.

Problems

  • PhoneNumber format can be in different. Example: "111-555-8888" and "(111) 555 8888"
  • MiddleName can exist in one source but does not exist in another source. It makes a sense to compare MiddleName only if it has value in both sources.
  • PersonId property need to be skipped

Solution

publicclassPerson{publicGuidPersonId{get;set;}publicstringFirstName{get;set;}publicstringLastName{get;set;}publicstringMiddleName{get;set;}publicstringPhoneNumber{get;set;}publicoverridestringToString(){return$"{FirstName}{MiddleName}{LastName} ({PhoneNumber})";}}

Phone number can have different formats. Let’s compare only digits.

publicclassPhoneNumberComparer:AbstractValueComparer<string>{publicoverrideboolCompare(stringobj1,stringobj2,ComparisonSettingssettings){returnExtractDigits(obj1)==ExtractDigits(obj2);}privatestringExtractDigits(stringstr){returnstring.Join(string.Empty,(str??string.Empty).ToCharArray().Where(char.IsDigit));}}

Factory allows not to configure comparer every time we need to create it.

publicclassMyComparersFactory:ComparersFactory{publicoverrideIComparer<T>GetObjectsComparer<T>(ComparisonSettingssettings=null,IBaseComparerparentComparer=null){if(typeof(T)==typeof(Person)){varcomparer=newComparer<Person>(settings,parentComparer,this);//Do not compare PersonIdcomparer.AddComparerOverride<Guid>(DoNotCompareValueComparer.Instance);//Sometimes MiddleName can be skipped. Compare only if property has value.comparer.AddComparerOverride(()=>newPerson().MiddleName,(s1,s2,parentSettings)=>string.IsNullOrWhiteSpace(s1)||string.IsNullOrWhiteSpace(s2)||s1==s2);comparer.AddComparerOverride(()=>newPerson().PhoneNumber,newPhoneNumberComparer());return(IComparer<T>)comparer;}returnbase.GetObjectsComparer<T>(settings,parentComparer);}}

Configuring comparer.

_factory=newMyComparersFactory();_comparer=_factory.GetObjectsComparer<Person>();
varperson1=newPerson{PersonId=Guid.NewGuid(),FirstName="John",LastName="Doe",MiddleName="F",PhoneNumber="111-555-8888"};varperson2=newPerson{PersonId=Guid.NewGuid(),FirstName="John",LastName="Doe",PhoneNumber="(111) 555 8888"};IEnumerable<Difference>differences;varisEqual=_comparer.Compare(person1,person2,outdifferences);

Objects are equal

varperson1=newPerson{PersonId=Guid.NewGuid(),FirstName="Jack",LastName="Doe",MiddleName="F",PhoneNumber="111-555-8888"};varperson2=newPerson{PersonId=Guid.NewGuid(),FirstName="John",LastName="Doe",MiddleName="L",PhoneNumber="222-555-9999"};IEnumerable<Difference>differences;varisEqual=_comparer.Compare(person1,person2,outdifferences);

Difference: DifferenceType=ValueMismatch, MemberPath='FirstName', Value1='Jack', Value2='John'.

Difference: DifferenceType=ValueMismatch, MemberPath='MiddleName', Value1='F', Value2='L'.

Difference: DifferenceType=ValueMismatch, MemberPath='PhoneNumber', Value1='111-555-8888', Value2='222-555-9999'.

Example 3: Comparing JSON configuration files

Challenge

There are files with settings with some differences that need to be found. Json.NET is used to deserialize JSON data.

Problems

  • URLs can be with or without http prefix.
  • DataCompression is Off by default
  • SmartMode1...3 disabled by default 
  • ConnectionString, Email and Notifications need to be skipped
  • If ProcessTaskTimeout or TotalProcessTimeout settings skipped default values will be used, so if in one file setting does not exists and in another file this setting has default value it is actually the same.

Files

Settings0
{
"ConnectionString": "USER ID=superuser;PASSWORD=superpassword;DATA SOURCE=localhost:1111",
"Email": {
"Port": 25,
"Host": "MyHost.com",
"EmailAddress": "test@MyHost.com"
},
"Settings": {
"DataCompression": "On",
"DataSourceType": "MultiDataSource",
"SomeUrl": "http://MyHost.com/VeryImportantData",
"SomeOtherUrl": "http://MyHost.com/NotSoImportantData/",
"CacheMode": "Memory",
"MaxCacheSize": "1GB",
"SuperModes": {
"SmartMode1": "Enabled",
"SmartMode2": "Disabled",
"SmartMode3": "Enabled"
}
},
"Timeouts": {
"TotalProcessTimeout": 500,
"ProcessTaskTimeout": 100
},
"BackupSettings": {
"BackupIntervalUnit": "Day",
"BackupInterval": 100
},
"Notifications": [
{
"Phone": "111-222-3333"
},
{
"Phone": "111-222-4444"
},
{
"EMail": "support@MyHost.com"
}
],
"Logging": {
"Enabled": true,
"Pattern": "Logs\\MyApplication.%data{yyyyMMdd}.log",
"MaximumFileSize": "20MB",
"Level": "ALL"
}
}
Settings1
{
"ConnectionString": "USER ID=admin;PASSWORD=*****;DATA SOURCE=localhost:22222",
"Email": {
"Port": 25,
"Host": "MyHost.com",
"EmailAddress": "test@MyHost.com"
},
"Settings": {
"DataCompression": "On",
"DataSourceType": "MultiDataSource",
"SomeUrl": "MyHost.com/VeryImportantData",
"SomeOtherUrl": "MyHost.com/NotSoImportantData/",
"CacheMode": "Memory",
"MaxCacheSize": "1GB",
"SuperModes": {
"SmartMode1": "enabled",
"SmartMode3": "enabled"
}
},
"BackupSettings": {
"BackupIntervalUnit": "Day",
"BackupInterval": 100
},
"Notifications": [
{
"Phone": "111-222-3333"
},
{
"EMail": "support@MyHost.com"
}
],
"Logging": {
"Enabled": true,
"Pattern": "Logs\\MyApplication.%data{yyyyMMdd}.log",
"MaximumFileSize": "20MB",
"Level": "ALL"
}
}
Settings2
{
"ConnectionString": "USER ID=superuser;PASSWORD=superpassword;DATA SOURCE=localhost:1111",
"Email": {
"Port": 25,
"Host": "MyHost.com",
"EmailAddress": "test@MyHost.com"
},
"Settings": {
"DataSourceType": "MultiDataSource",
"SomeUrl": "http://MyHost.com/VeryImportantData",
"SomeOtherUrl": "http://MyHost.com/NotSoImportantData/",
"CacheMode": "Memory",
"MaxCacheSize": "1GB",
"SuperModes": {
"SmartMode3": "Enabled"
}
},
"Timeouts": {
"TotalProcessTimeout": 500,
"ProcessTaskTimeout": 200
},
"BackupSettings": {
"BackupIntervalUnit": "Week",
"BackupInterval": 2
},
"Notifications": [
{
"EMail": "support@MyHost.com"
}
],
"Logging": {
"Enabled": false,
"Pattern": "Logs\\MyApplication.%data{yyyyMMdd}.log",
"MaximumFileSize": "40MB",
"Level": "ERROR"
}
}

Solution

Configuring comparer.

_comparer=newComparer(newComparisonSettings{UseDefaultIfMemberNotExist=true});//Some fields should be ignored_comparer.AddComparerOverride("ConnectionString",DoNotCompareValueComparer.Instance);_comparer.AddComparerOverride("Email",DoNotCompareValueComparer.Instance);_comparer.AddComparerOverride("Notifications",DoNotCompareValueComparer.Instance);//Smart Modes are disabled by default. These fields are not case sensitivevardisabledByDefaultComparer=newDefaultValueValueComparer<string>("Disabled",IgnoreCaseStringsValueComparer.Instance);_comparer.AddComparerOverride("SmartMode1",disabledByDefaultComparer);_comparer.AddComparerOverride("SmartMode2",disabledByDefaultComparer);_comparer.AddComparerOverride("SmartMode3",disabledByDefaultComparer);//http prefix in URLs should be ignoredvarurlComparer=newDynamicValueComparer<string>((url1,url2,settings)=>url1.Trim('/').Replace(@"http://",string.Empty)==url2.Trim('/').Replace(@"http://",string.Empty));_comparer.AddComparerOverride("SomeUrl",urlComparer);_comparer.AddComparerOverride("SomeOtherUrl",urlComparer);//DataCompression is Off by default._comparer.AddComparerOverride("DataCompression",newDefaultValueValueComparer<string>("Off",NulableStringsValueComparer.Instance));//ProcessTaskTimeout and TotalProcessTimeout fields have default values._comparer.AddComparerOverride("ProcessTaskTimeout",newDefaultValueValueComparer<long>(100,DefaultValueComparer.Instance));_comparer.AddComparerOverride("TotalProcessTimeout",newDefaultValueValueComparer<long>(500,DefaultValueComparer.Instance));
varsettings0Json=LoadJson("Settings0.json");varsettings0=JsonConvert.DeserializeObject<ExpandoObject>(settings0Json);varsettings1Json=LoadJson("Settings1.json");varsettings1=JsonConvert.DeserializeObject<ExpandoObject>(settings1Json);IEnumerable<Difference>differences;varisEqual=_comparer.Compare(settings0,settings1,outdifferences);

Objects are equal

varsettings0Json=LoadJson("Settings0.json");varsettings0=JsonConvert.DeserializeObject<ExpandoObject>(settings0Json);varsettings2Json=LoadJson("Settings2.json");varsettings2=JsonConvert.DeserializeObject<ExpandoObject>(settings2Json);IEnumerable<Difference>differences;varisEqual=_comparer.Compare(settings0,settings2,outdifferences);

Difference: DifferenceType=ValueMismatch, MemberPath='Settings.DataCompression', Value1='On', Value2='Off'.

Difference: DifferenceType=ValueMismatch, MemberPath='Settings.SuperModes.SmartMode1', Value1='Enabled', Value2='Disabled'.

Difference: DifferenceType=ValueMismatch, MemberPath='Timeouts.ProcessTaskTimeout', Value1='100', Value2='200'.

Difference: DifferenceType=ValueMismatch, MemberPath='BackupSettings.BackupIntervalUnit', Value1='Day', Value2='Week'.

Difference: DifferenceType=ValueMismatch, MemberPath='BackupSettings.BackupInterval', Value1='100', Value2='2'.

Difference: DifferenceType=ValueMismatch, MemberPath='Logging.Enabled', Value1='True', Value2='False'.

Difference: DifferenceType=ValueMismatch, MemberPath='Logging.MaximumFileSize', Value1='20MB', Value2='40MB'.

Difference: DifferenceType=ValueMismatch, MemberPath='Logging.Level', Value1='ALL', Value2='ERROR'.

Contributing

Any useful changes are welcomed.

Feel free to report any defects or ideas how this framework can be improved.

Create an issue, contact me directly or fork the code and submit a pull request!

About

C# Framework provides mechanism to compare complex objects, allows to override comparison rules for specific properties and types.

Resources

Stars

0 stars

Watchers

6 watching

Forks

Releases

Packages

Contributors

Languages