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.
Install-Package ObjectsComparer
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'.
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'.
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'.
C# supports several types of dynamic objects.
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'.
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'.
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.
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 hereComparer 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 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);}}varcomparer=newComparer();varisEqual=comparer.Compare(a1,a2);This comparer creates generic implementation of comparer for each comparison.
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).
There are some more complex examples how Objects Comparer can be used.
Check if received message equal to the expected message.
- 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
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'.
Compare persons from different sources.
- 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
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'.
There are files with settings with some differences that need to be found. Json.NET is used to deserialize JSON data.
- 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.
{
"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"
}
}{
"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"
}
}{
"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"
}
}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'.
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!
