代码人生

使用Objects Comparer来比较c#中的复杂对象

代码人生 http://www.she9.com 2018-09-19 10:58 出处:网络 编辑:@技术狂热粉
IntroductionItisacommonsituationthatcomplexobjectsneedtobecompared.Sometimesobjectscancontainnestedelements,orsomemembersshouldbeexcludedfromthecomparison(auto-generatedidentifiers,create/updatedateet

Introduction

It is a common situation that complex objects need to be compared. Sometimes objects can contain nested elements, or some members should be excluded from the 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 these kinds of problems.

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

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

Now Objects Comparer supports enumerables (arrays, collections, lists), multidimensional arrays, enumerations, flags, and dynamic objects (ExpandoObject, DynamicObject, and compiler generated dynamic objects).

介绍


比较复杂的对象是一种常见的情况。有时候对象可以包含嵌套的元素,或者一些成员应该被排除在比较之外(自动生成标识符、创建/更新日期等),或者一些成员可以有定制的比较规则(相同的数据以不同的格式显示,比如电话号码)。这个小框架是为了解决这类问题而开发的。


简单地说,Objects Comparer是对象到对象比较器,它允许开发人员递归地逐个成员地比较对象,并为某些属性、字段或类型定义定制的比较规则。


对象比较器可以看作是一个现成的框架,也可以看作是类似解决方案的起点。


现在对象比较器支持枚举数(数组、集合、列表)、多维数组、枚举、标记和动态对象(ExpandoObject、DynamicObject和编译器生成的动态对象)。

安装

Objects Comparer 可以用 NuGet package进行安装.

Install-Package ObjectsComparer

源代码

Source code can be found on GitHub.

Version 1.2提供的新功能?

  • StringBuilder support.

  • Sets support.

  • Uri support.

  • Bug fixes.

如果您使用的是版本1.1,那么您不需要做任何更改就可以开始使用版本1.2。

Version 1.1新功能?

  • Dynamic objects support (ExpandoObjectDynamicObject, and compiler generated dynamic objects).

  • Overriding comparison rule by member name.

  • Overriding comparison rule by type and by name with filter.

  • The DifferenceType property was added to the Difference class.

如果你使用1.0版本。x你不需要做任何改变就可以开始使用1.1版.

基本示例

为了演示如何使用对象比较器,让我们创建两个类。

public class ClassA
{
    public string StringProperty { get; set; }

    public int IntProperty { get; set; }

    public SubClassA SubClass { get; set; }
}

public class SubClassA
{
    public bool BoolProperty { get; set; }
}

下面有一些例子来说明如何使用对象比较器来比较这些类的实例。

//Initialize objects and comparer
var a1 = new ClassA { StringProperty = "String", IntProperty = 1 };
var a2 = new ClassA { StringProperty = "String", IntProperty = 1 };
var comparer = new Comparer<ClassA>();

//Compare objects
IEnumerable<Difference> differences;
var isEqual = comparer.Compare(a1, a2, out differences);

//Print results
Debug.WriteLine(isEqual ? "Objects are equal" : string.Join(Environment.NewLine, differenses));

Objects are equal

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

var a1 = new ClassA { StringProperty = "String", IntProperty = 1 };
var a2 = new ClassA { StringProperty = "String", IntProperty = 2 };
var comparer = new Comparer<ClassA>();

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

var a1 = new ClassA { SubClass = new SubClassA { BoolProperty = true } };
var a2 = new ClassA { SubClass = new SubClassA { BoolProperty = false } };
var comparer = new Comparer<ClassA>();

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

var a1 = new StringBuilder("abc");
var a2 = new StringBuilder("abd");
var comparer = new Comparer<StringBuilder>();

 Difference: DifferenceType=ValueMismatch, MemberPath='', Value1='abc', Value2='abd'.

Enumerables (Arrays, Collections, Lists, etc.)

在这种情况下,枚举数可以有不同数量的元素,或者某些元素可以有不同的值。枚举可以是泛型的,也可以是非泛型的。

In the case of non-generic enumerables, elements with the same index will be compared if types of these elements are equal, otherwise, the difference with DifferenceType=TypeMismatch will be added to the list of differences.

var a1 = new[] { 1, 2, 3 };
var a2 = new[] { 1, 2, 3 };
var comparer = new Comparer<int[]>();

Objects are equal

var a1 = new[] { 1, 2 };
var a2 = new[] { 1, 2, 3 };
var comparer = new Comparer<int[]>();

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

var a1 = new[] { 1, 2, 3 };
var a2 = new[] { 1, 4, 3 };
var comparer = new Comparer<int[]>();

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

var a1 = new ArrayList { "Str1", "Str2" };
var a2 = new ArrayList { "Str1", 5 };
var comparer = new Comparer<ArrayList>();

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

Sets

var a1 = new[] { 1, 2, 3 };
var a2 = new[] { 1, 2, 3 };
var comparer = new Comparer<int[]>();

Objects are equal.

var a1 = new HashSet<int> { 1, 2, 3 };
var a2 = new HashSet<int> { 2, 1, 4 };
var comparer = new Comparer<HashSet<int>>();

 Difference: DifferenceType=MissedElementInSecondObject, MemberPath='',Value1='3', Value2=''.

 Difference: DifferenceType=MissedElementInFirstObject, MemberPath='',Value1='', Value2='4'.

Multidimensional Arrays

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

var a1 = new[] { new[] { 1, 2 } };
var a2 = new[] { new[] { 2, 2 }, new[] { 3, 5 } };
var comparer = new Comparer<int[][]>();

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

var a1 = new[] { new[] { 1, 2 }, new[] { 3, 5 } };
var a2 = new[] { new[] { 1, 2 }, new[] { 3, 5, 6 } };
var comparer = new Comparer<int[][]>();

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

var a1 = new[,] { { 1, 2 }, { 1, 3 } };
var a2 = new[,] { { 1, 3, 4 }, { 1, 3, 8 } };
var comparer = new Comparer<int[,]>();

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

var a1 = new[,] { { 1, 2 } };
var a2 = new[,] { { 1, 3 } };
var comparer = new Comparer<int[,]>();

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

Dynamic Objects

C# supports several types of objects, whose members can be dynamically added and removed at runtime.

ExpandoObject

If you are not familiar with how to use ExpandoObject you can read this or search for another example.

dynamic a1 = new ExpandoObject();
a1.Field1 = "A";
a1.Field2 = 5;
a1.Field4 = 4;
dynamic a2 = new ExpandoObject();
a2.Field1 = "B";
a2.Field3 = false;
a2.Field4 = "C";
var comparer = new Comparer();

 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'.

dynamic a1 = new ExpandoObject();
a1.Field1 = "A";
a1.Field2 = 5;
dynamic a2 = new ExpandoObject();
a2.Field1 = "B";
a2.Field3 = false;
var comparer = new Comparer();

 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'.

The behavior, if the member does not exist, could be changed by providing custom ComparisonSettings (see Comparison Settings below).

dynamic a1 = new ExpandoObject();
a1.Field1 = "A";
a1.Field2 = 0;
dynamic a2 = new ExpandoObject();
a2.Field1 = "B";
a2.Field3 = false;
a2.Field4 = "S";
var comparer = new Comparer(new ComparisonSettings { UseDefaultIfMemberNotExist = true });

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

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

DynamicObject

DynamicObject是一个抽象类,不能直接实例化。假设我们有DynamicObject类的这种实现。有必要正确实现GetDynamicMemberNames方法,否则,对象比较器将无法正常工作。

If you are not familiar with how to use  DynamicObject you can read this or search for another example.

private class DynamicDictionary : DynamicObject
{
    public int IntProperty { get; set; }

    private readonly Dictionary<string, object> _dictionary = new Dictionary<string, object>();

    public override bool TryGetMember(GetMemberBinder binder, out object result)
    {
        var name = binder.Name;

        return _dictionary.TryGetValue(name, out result);
    }

    public override bool TrySetMember(SetMemberBinder binder, object value)
    {
        _dictionary[binder.Name] = value;

        return true;
    }

    public override IEnumerable<string> GetDynamicMemberNames()
    {
        return _dictionary.Keys;
    }
}
dynamic a1 = new DynamicDictionary();
a1.Field1 = "A";
a1.Field3 = true;
dynamic a2 = new DynamicDictionary();
a2.Field1 = "B";
a2.Field2 = 8;
a2.Field3 = 1;
var comparer = new Comparer();

 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

This type of dynamic object is most popular and easiest to create.

dynamic a1 = new
{
    Field1 = "A",
    Field2 = 5,
    Field3 = true
};
dynamic a2 = new
{
    Field1 = "B",
    Field2 = 8
};
var comparer = new Comparer();

IEnumerable<Difference> differences;
var isEqual = comparer.Compare((object)a1, (object)a2, out differences);

 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 the compiler and are considered to be the same type if and only if objects a1 and a2 have the same set of members (same name and same type). If casting to (object) is skipped in case of a different set of members,  RuntimeBinderException will be thrown.

Overriding Comparison Rules

Sometimes some of the members require custom comparison logic. To override the comparison rule, we need to create a custom value comparer or provide the function with how to compare objects and how to convert these objects to a string (optional) and filter function (optional). Value Comparer should be inherited from AbstractValueComparer or should implement IValueComparer.

public class MyValueComparer: AbstractValueComparer<string>
{
    public override bool Compare(string obj1, string obj2, ComparisonSettings settings)
    {
        return obj1 == obj2; //Implement comparison logic here
    }
}

Below is the override comparison rule for objects of a particular type:

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

Below is the override comparison rule for a particular member (Field or Property). If the  toStringFunctio parameter is not provided, objects will be converted to a string using the ToString() method.

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

Below is the 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", new MyValueComparer());

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

Example:

var a1 = new ClassA();
var a2 = new ClassA();
comparer.AddComparerOverride<string>(valueComparer1, member => member.Name.StartsWith("String"));
comparer.AddComparerOverride<string>(valueComparer2, member => member.Name.EndsWith("Property"));

var result = 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 the custom comparison rule and do not implement.  IComparable 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 the member does not exist, Objects Comparer will consider this member equal to the default value of the opposite member type. Applicable for dynamic types comparison only. False by default.

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

SetCustomSetting<T>(T value, string key = null)
GetCustomSetting<T>(string key = null)

Factory

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

public class MyComparersFactory: ComparersFactory
{
    public override IComparer<T> GetObjectsComparer<T>(ComparisonSettings settings = null, IBaseComparer parentComparer = null)
    {
        if (typeof(T) == typeof(ClassA))
        {
            var comparer = new Comparer<ClassA>(settings, parentComparer, this);
            comparer.AddComparerOverride<Guid>(new MyCustomGuidComparer());

            return (IComparer<T>)comparer;
        }

        return base.GetObjectsComparer<T>(settings, parentComparer);
    }
}

Non-Generic Comparer

This comparer creates a generic implementation of comparer for each comparison.

var comparer = new Comparer();
var isEqual = comparer.Compare(a1, a2);

Useful Value Comparers

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

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

  • DynamicValueComparer - Receives the comparison rule as a function.

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

  • DefaultValueValueComparer - Allows us to consider the provided value and the default value of a specified type as equal values (see Example 3 below).

  • IgnoreCaseStringsValueComparer - Allows us to compare strings, ignoring case. Has a singleton implementation (IgnoreCaseStringsValueComparer.Instance).

  • UriComparer - Allows us to compare Uri objects.

Examples

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

Example 1: Expected Message

Challenge

Check if the received message is equal to the expected message.

Problems

  • DateCreatedDateSent, and DateReceived properties need to be skipped.

  • Auto-generated Id property needs to be skipped.

  • Message property of Error class needs to be skipped.

Solution

public class Error
{
    public int Id { get; set; }

    public string Messgae { get; set; }
}
public class Message
{
    public string Id { get; set; }

    public DateTime DateCreated { get; set; }

    public DateTime DateSent { get; set; }

    public DateTime DateReceived { get; set; }

    public int MessageType { get; set; }

    public int Status { get; set; }

    public List<Error> Errors { get; set; }

    public override string ToString()
    {
        return $"Id:{Id}, Type:{MessageType}, Status:{Status}";
    }
}

Configuring comparer.

_comparer = new Comparer<Message>(
    new ComparisonSettings
    {
        //Null and empty error lists are equal
        EmptyAndNullEnumerablesEqual = true
    });
//Do not compare Dates
_comparer.AddComparerOverride<DateTime>(DoNotCompareValueComparer.Instance);
//Do not compare Id
_comparer.AddComparerOverride(() => new Message().Id, DoNotCompareValueComparer.Instance);
//Do not compare Message Text
_comparer.AddComparerOverride(() => new Error().Messgae, DoNotCompareValueComparer.Instance);
var expectedMessage = new Message
{
    MessageType = 1,
    Status = 0
};

var actualMessage = new Message
{
    Id = "M12345",
    DateCreated = DateTime.Now,
    DateSent = DateTime.Now,
    DateReceived = DateTime.Now,
    MessageType = 1,
    Status = 0
};

IEnumerable<Difference> differences;
var isEqual = _comparer.Compare(expectedMessage, actualMessage, out differences);

Objects are equal

var expectedMessage = new Message
{
    MessageType = 1,
    Status = 1,
    Errors = new List<Error>
    {
        new Error { Id = 2 },
        new Error { Id = 7 }
    }
};

var actualMessage = new Message
{
    Id = "M12345",
    DateCreated = DateTime.Now,
    DateSent = DateTime.Now,
    DateReceived = DateTime.Now,
    MessageType = 1,
    Status = 1,
    Errors = new List<Error>
    {
        new Error { Id = 2, Messgae = "Some error #2" },
        new Error { Id = 7, Messgae = "Some error #7" },
    }
};

IEnumerable<Difference> differences;
var isEqual = _comparer.Compare(expectedMessage, actualMessage, out differences);

Objects are equal

var expectedMessage = new Message
{
    MessageType = 1,
    Status = 1,
    Errors = new List<Error>
    {
        new Error { Id = 2, Messgae = "Some error #2" },
        new Error { Id = 8, Messgae = "Some error #8" }
    }
};

var actualMessage = new Message
{
    Id = "M12345",
    DateCreated = DateTime.Now,
    DateSent = DateTime.Now,
    DateReceived = DateTime.Now,
    MessageType = 1,
    Status = 2,
    Errors = new List<Error>
    {
        new Error { Id = 2, Messgae = "Some error #2" },
        new Error { Id = 7, Messgae = "Some error #7" }
    }
};

IEnumerable<Difference> differences;
var isEqual = _comparer.Compare(expectedMessage, actualMessage, out differences);

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 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 a value in both sources.

  • PersonId property needs to be skipped.

Solution

public class Person
{
    public Guid PersonId { get; set; }

    public string FirstName { get; set; }

    public string LastName { get; set; }

    public string MiddleName { get; set; }

    public string PhoneNumber { get; set; }

    public override string ToString()
    {
        return $"{FirstName} {MiddleName} {LastName} ({PhoneNumber})";
    }
}

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

public class PhoneNumberComparer: AbstractValueComparer<string>
{
    public override bool Compare(string obj1, string obj2, ComparisonSettings settings)
    {
        return ExtractDigits(obj1) == ExtractDigits(obj2);
    }

    private string ExtractDigits(string str)
    {
        return string.Join(
            string.Empty,
            (str ?? string.Empty)
                .ToCharArray()
                .Where(char.IsDigit));
    }
}

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

public class MyComparersFactory: ComparersFactory
{
    public override IComparer<T> GetObjectsComparer<T>(ComparisonSettings settings = null, IBaseComparer parentComparer = null)
    {
        if (typeof(T) == typeof(Person))
        {
            var comparer = new Comparer<Person>(settings, parentComparer, this);
            //Do not compare PersonId
            comparer.AddComparerOverride<Guid>(DoNotCompareValueComparer.Instance);
            //Sometimes MiddleName can be skipped. Compare only if property has value.
            comparer.AddComparerOverride(
                () => new Person().MiddleName,
                (s1, s2, parentSettings) => string.IsNullOrWhiteSpace(s1) || string.IsNullOrWhiteSpace(s2) || s1 == s2);
            comparer.AddComparerOverride(
                () => new Person().PhoneNumber,
                new PhoneNumberComparer());

            return (IComparer<T>)comparer;
        }

        return base.GetObjectsComparer<T>(settings, parentComparer);
    }
}

Configuring comparer.

_factory = new MyComparersFactory();
_comparer = _factory.GetObjectsComparer<Person>();
var person1 = new Person
{
    PersonId = Guid.NewGuid(),
    FirstName = "John",
    LastName = "Doe",
    MiddleName = "F",
    PhoneNumber = "111-555-8888"
};
var person2 = new Person
{
    PersonId = Guid.NewGuid(),
    FirstName = "John",
    LastName = "Doe",
    PhoneNumber = "(111) 555 8888"
};

IEnumerable<Difference> differences;
var isEqual = _comparer.Compare(person1, person2, out differences);

Objects are equal.

var person1 = new Person
{
    PersonId = Guid.NewGuid(),
    FirstName = "Jack",
    LastName = "Doe",
    MiddleName = "F",
    PhoneNumber = "111-555-8888"
};
var person2 = new Person
{
    PersonId = Guid.NewGuid(),
    FirstName = "John",
    LastName = "Doe",
    MiddleName = "L",
    PhoneNumber = "222-555-9999"
};

IEnumerable<Difference> differences;
var isEqual = _comparer.Compare(person1, person2, out differences);
  • 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 used with or without an HTTP prefix.

  • DataCompression is Off by default.

  • SmartMode1...3 disabled by default.

  • ConnectionStringEmail, and Notifications need to be skipped.

  • If ProcessTaskTimeout or TotalProcessTimeout settings are skipped, default values will be used, so if in one file setting it does not exist and, in another file, this setting has a 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"
  }
}

Configuring comparer.

_comparer = new Comparer(new ComparisonSettings { 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 sensitive
var disabledByDefaultComparer = new DefaultValueValueComparer<string>("Disabled", IgnoreCaseStringsValueComparer.Instance);
_comparer.AddComparerOverride("SmartMode1", disabledByDefaultComparer);
_comparer.AddComparerOverride("SmartMode2", disabledByDefaultComparer);
_comparer.AddComparerOverride("SmartMode3", disabledByDefaultComparer);
//http prefix in URLs should be ignored
var urlComparer = new DynamicValueComparer<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", new DefaultValueValueComparer<string>("Off", NulableStringsValueComparer.Instance));
//ProcessTaskTimeout and TotalProcessTimeout fields have default values.
_comparer.AddComparerOverride("ProcessTaskTimeout", new DefaultValueValueComparer<long>(100, DefaultValueComparer.Instance));
_comparer.AddComparerOverride("TotalProcessTimeout", new DefaultValueValueComparer<long>(500, DefaultValueComparer.Instance));
var settings0Json = LoadJson("Settings0.json");
var settings0 = JsonConvert.DeserializeObject<ExpandoObject>(settings0Json);
var settings1Json = LoadJson("Settings1.json");
var settings1 = JsonConvert.DeserializeObject<ExpandoObject>(settings1Json);

IEnumerable<Difference> differences;
var isEqual = _comparer.Compare(settings0, settings1, out differences);

Objects are equal

var settings0Json = LoadJson("Settings0.json");
var settings0 = JsonConvert.DeserializeObject<ExpandoObject>(settings0Json);
var settings2Json = LoadJson("Settings2.json");
var settings2 = JsonConvert.DeserializeObject<ExpandoObject>(settings2Json);

IEnumerable<Difference> differences;
var isEqual = _comparer.Compare(settings0, settings2, out differences);

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'.


请关注公众号:程序你好
0

精彩评论

暂无评论...
验证码 换一张
取 消