65.9K
CodeProject 正在变化。 阅读更多。
Home

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

2017 年 11 月 2 日

MIT

7分钟阅读

viewsIcon

57676

Objects Comparer 框架提供了一种通过属性(支持数组、列表、各种动态对象等)递归比较复杂对象的机制,允许为特定属性和类型覆盖比较规则。

引言

当需要比较复杂对象时,这种情况相当普遍。有时,对象可能包含嵌套元素,或者某些成员应从比较中排除(自动生成的标识符、创建/更新日期等),或者某些成员可以具有自定义比较规则(例如,不同格式的相同数据,如电话号码)。这个小型框架就是为了解决这类问题而开发的。

简而言之,Objects Comparer 是一个对象到对象的比较器,它允许逐个成员地递归比较对象,并为特定的属性、字段或类型定义自定义比较规则。

Objects Comparer 可用于 .NET Core 项目(.NET Standard 1.6、2.0 或更高版本)和 .NET 项目(版本 4.5 或更高版本)。

支持的对象

  • 基本类型
  • 类和结构
  • 可枚举类型(数组、集合、列表、字典等)
  • 多维数组
  • 枚举
  • 标志
  • 动态对象(ExpandoObjectDynamicObject 和编译器生成的动态对象)

安装

Objects Comparer 可以作为 NuGet 包安装。

Install-Package ObjectsComparer

源代码

源代码可以在 GitHub 上找到。

重要提示!!!

如果您在使用 ObjectsComparer 时遇到任何问题,或者对如何改进它有任何想法,请在 GitHub 上创建一个 issue。

原因

  • CodeProject 过去在发送关于新评论/回复的通知方面存在问题,并且可能仍然存在。我不会每天手动检查这篇文章的评论。
  • CSharpCornerDZoneMedium 上也有几乎相同的文章。那些人看不到您的评论,您也看不到他们的评论。将所有讨论集中在一个地方会更好。
  • GitHub 提供了更多与 issue 交互的功能。

基本示例

为了展示如何使用 Objects Comparer,让我们创建两个类

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; }
}

下面是一些关于如何使用 Objects Comparer 来比较这些类实例的示例。

//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

在下面的示例中,除非特殊情况,否则“比较对象”和“打印结果”块将被省略,以节省篇幅。

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

可枚举类型(数组、集合、列表等)

在这种情况下,可枚举类型可以有不同数量的元素,或者某些元素的值可能不同。可枚举类型可以是泛型或非泛型的。对于非泛型可枚举类型,如果元素的类型相同,则比较具有相同索引的元素;否则,将 DifferenceType=TypeMismatch 的差异添加到差异列表中。

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[]>();
对象相等
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'.

多维数组

var a1 = new[] { new[] { 1, 2 } };
var a2 = new[] { new[] { 1, 3 } };
var comparer = new Comparer<int[][]>();
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'.

动态对象

C# 支持几种类型的对象,这些对象的成员可以在运行时动态添加和删除。

ExpandoObject

如果您不熟悉如何使用 ExpandoObject,可以阅读 这篇文章 或搜索其他示例。

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', 
Difference: DifferenceType=MissedMemberInSecondObject, MemberPath='Field2', 
Value1='5', Value2=''.
Difference: DifferenceType=MissedMemberInFirstObject, MemberPath='Field3', 
Value1='', Value2='False'.

可以通过提供自定义 ComparisonSettings(参见下面的比较设置)来更改成员不存在时的行为。

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 是一个 abstract 类,不能直接实例化。让我们假设我们有 DynamicObject 类的这种实现。必须正确实现 GetDynamicMemberNames 方法,否则 Objects Comparer 将无法正常工作。

如果您不熟悉如何使用 DynamicObject,可以阅读 这篇文章 或搜索其他示例。

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

编译器生成的对象

这种类型的动态对象是最流行且最容易创建的。

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

此示例需要一些额外的解释。对象 a1a2 的类型由编译器生成,并且当且仅当对象 a1a2 具有相同的成员集(相同的名称和相同的类型)时,才被视为同一类型。如果成员集不同时跳过强制转换为 (object),则会引发 RuntimeBinderException

覆盖比较规则

有时某些成员需要自定义比较逻辑。要覆盖比较规则,我们需要创建自定义值比较器或提供一个函数来比较对象以及如何将这些对象转换为 string(可选)和过滤函数(可选)。Value Comparer 应继承自 AbstractValueComparer 或实现 IValueComparer

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

覆盖特定类型对象的比较规则。

//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"));

覆盖特定成员(FieldProperty)的比较规则。如果未提供 toStringFunction 参数,则使用 ToString() 方法将 object 转换为 string

//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);

按名称覆盖特定成员(字段或属性)的比较规则。

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

按类型覆盖的优先级最高,然后是按成员覆盖,按成员名称覆盖的优先级最低。如果同一成员可以应用多个相同类型的(按类型/按名称/按成员名称)值比较器,则在比较期间会引发 AmbiguousComparerOverrideResolutionException 异常。

示例

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

比较设置

比较器的构造函数有一个可选的 settings 参数,用于配置比较的某些方面。

RecursiveComparison

默认值为 true。如果为 true,则所有非原始类型、没有自定义比较规则且未实现 ICompareble 的成员将使用与根对象相同的规则进行比较。

EmptyAndNullEnumerablesEqual

默认值为 false。如果为 true,则空的可枚举类型(数组、集合、列表等)和 null 值将被视为相等值。

UseDefaultIfMemberNotExist

如果为 true 且成员不存在,Objects Comparer 将认为该成员等于相反成员类型的默认值。仅适用于动态类型比较。默认值为 false

Comparison Settings 类允许存储可用于自定义比较器的自定义值。

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

Factory

Factory 提供了一种封装比较器创建和配置的方式。Factory 应实现 IComparersFactory 或继承自 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);
    }
}

非泛型比较器

此比较器为每次比较创建泛型比较器实现。

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

有用的值比较器

框架包含一些自定义比较器,它们在许多情况下非常有用。

DoNotCompareValueComparer

允许跳过某些字段/类型。具有单例实现(DoNotCompareValueComparer.Instance)。

DynamicValueComparer

接收比较规则作为函数。

NulableStringsValueComparer

null 和空 string 被视为相等值。具有单例实现(NulableStringsValueComparer.Instance)。

DefaultValueValueComparer

允许将提供的默认值和指定类型的默认值视为相等值(参见下面的示例 3)。

IgnoreCaseStringsValueComparer

允许在比较 string 时忽略大小写。具有单例实现(IgnoreCaseStringsValueComparer.Instance)。

UriComparer

允许比较 Uri 对象。

示例

这里有一些更复杂的 Objects Comparer 用法示例。

示例 1:预期消息

挑战

检查收到的消息是否与预期消息相等。

问题

  • 需要跳过 DateCreatedDateSentDateReceived 属性
  • 需要跳过自动生成的 Id 属性
  • 需要跳过 Error 类的 Message 属性

解决方案

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}";
    }
}

配置比较器。

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

示例 2:人员比较

挑战

比较来自不同源的人员。

问题

  • PhoneNumber 格式可能不同。例如:“111-555-8888”和“(111) 555 8888”
  • MiddleName 可能存在于一个源中,而不存在于另一个源中。只有当 MiddleName 在两个源中都有值时才进行比较是有意义的。
  • 需要跳过 PersonId 属性

解决方案

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})";
    }
}

电话号码可以有不同的格式。让我们只比较数字。

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 允许我们不必每次都需要创建比较器时都对其进行配置。

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);
    }
}

配置比较器。

_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);

对象相等

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

示例 3:比较 JSON 配置文件

挑战

这些文件包含一些需要找到的差异的设置。Json.NET 用于反序列化 JSON 数据。

问题

  • URL 可以带或不带 http 前缀。
  • DataCompression 默认情况下为 Off
  • SmartMode1...3 默认情况下禁用
  • 需要跳过 ConnectionStringEmailNotifications
  • 如果跳过了 ProcessTaskTimeoutTotalProcessTimeout 设置,将使用默认值,因此如果在一个文件中缺少该设置,而在另一个文件中该设置具有默认值,那么它们实际上是相同的。

文件

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"
  }
}

配置比较器。

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

示例 4:列表的自定义比较器

挑战

这个例子来自框架用户,所以这是 100% 真实场景。通过内容比较项目列表,即使列表中的项目计数不同。使用 Id 属性作为标识符。

问题

默认情况下,如果列表中的项目计数不同,比较器会将这些列表视为不同,并且不比较项目。

解决方案

解决这个问题的方法是实现自定义 Comparer(它应该继承自 AbstractComparer<TypeOfTheList>),并在该 Comparer 中实现比较列表的逻辑。然后实现 ComparersFactory(如示例 2 中所示),当类型等于 IList 时返回自定义 Comparer,并使用此工厂。

要比较的类。

public class FormulaItem
{
    public long Id { get; set; }
    public int Delay { get; set; }
    public string Name { get; set; }
    public string Instruction { get; set; }
}
public class Formula
{
    public long Id { get; set; }
    public string Name { get; set; }
    public IList<FormulaItem> Items { get; set; }
}

自定义比较器实现

public class CustomFormulaItemsComparer: AbstractComparer<IList<FormulaItem>>
{
    public CustomFormulaItemsComparer(ComparisonSettings settings, 
    BaseComparer parentComparer, IComparersFactory factory) : base(settings, parentComparer, factory)
    {
    }

    public override IEnumerable<Difference> CalculateDifferences
                (IList<FormulaItem> obj1, IList<FormulaItem> obj2)
    {
        if (obj1 == null && obj2 == null)
        {
            yield break;
        }

        if (obj1 == null || obj2 == null)
        {
            yield return new Difference("", DefaultValueComparer.ToString(obj1) , 
                                        DefaultValueComparer.ToString(obj2));
            yield break;
        }

        if (obj1.Count != obj2.Count)
        {
            yield return new Difference("Count", obj1.Count.ToString(), obj2.Count.ToString(),
                    DifferenceTypes.NumberOfElementsMismatch);
        }

        foreach (var formulaItem in obj1)
        {
            var formulaItem2 = obj2.FirstOrDefault(fi => fi.Id == formulaItem.Id);

            if (formulaItem2 != null)
            {
                var comparer = Factory.GetObjectsComparer<FormulaItem>();

                foreach (var difference in comparer.CalculateDifferences(formulaItem, formulaItem2))
                {
                    yield return difference.InsertPath($"[Id={formulaItem.Id}]");
                }
            }
        }
    }
}

Factory

public class MyComparersFactory : ComparersFactory
{
    public override IComparer<T> GetObjectsComparer<T>(ComparisonSettings settings = null,
        BaseComparer parentComparer = null)
    {
        if (typeof(T) != typeof(IList<FormulaItem>))
        {
            return base.GetObjectsComparer<T>(settings, parentComparer);
        }

        var comparer = new CustomFormulaItemsComparer(settings, parentComparer, this);

        return (IComparer<T>) comparer;

    }
}

配置比较器

_factory = new MyComparersFactory();
_comparer = _factory.GetObjectsComparer<Formula>();
var formula1 = new Formula
{
    Id = 1,
    Name = "Formula 1",
    Items = new List<FormulaItem>
    {
        new FormulaItem
        {
            Id = 1,
            Delay = 60,
            Name = "Item 1",
            Instruction = "Instruction 1"
        }
    }
};

var formula2 = new Formula
{
    Id = 1,
    Name = "Formula 1",
    Items = new List<FormulaItem>
    {
        new FormulaItem
        {
            Id = 1,
            Delay = 80,
            Name = "Item One",
            Instruction = "Instruction One"
        }
    }
};

var isEqual = _comparer.Compare(formula1, formula2, out var differences);
Difference: DifferenceType=ValueMismatch, MemberPath='Items[Id=1].Delay', 
Value1='60', Value2='80'. 
Difference: DifferenceType=ValueMismatch, MemberPath='Items[Id=1].Name', 
Value1='Item 1', Value2='Item One'. 
Difference: DifferenceType=ValueMismatch, MemberPath='Items[Id=1].Instruction', 
Value1='Instruction 1', Value2='Instruction One'.
var formula1 = new Formula
{
    Id = 1,
    Name = "Formula 1",
    Items = new List<FormulaItem>
    {
        new FormulaItem
        {
            Id = 1,
            Delay = 60,
            Name = "Item 1",
            Instruction = "Instruction 1"
        }
    }
};

var formula2 = new Formula
{
    Id = 1,
    Name = "Formula 1",
    Items = new List<FormulaItem>
    {
        new FormulaItem
        {
            Id = 1,
            Delay = 80,
            Name = "Item One",
            Instruction = "Instruction One"
        },
        new FormulaItem
        {
            Id = 2,
            Delay = 30,
            Name = "Item Two",
            Instruction = "Instruction Two"
        }
    }
};

var isEqual = _comparer.Compare(formula1, formula2, out var differences);
Difference: DifferenceType=NumberOfElementsMismatch, MemberPath='Items.Count', 
Value1='1', Value2='2'. Difference: DifferenceType=ValueMismatch, 
MemberPath='Items[Id=1].Delay', Value1='60', Value2='80'. 
Difference: DifferenceType=ValueMismatch, MemberPath='Items[Id=1].Name', 
Value1='Item 1', Value2='Item One'. 
Difference: DifferenceType=ValueMismatch, MemberPath='Items[Id=1].Instruction', 
Value1='Instruction 1', Value2='Instruction One'.

就这样。尽情享受吧!

© . All rights reserved.