Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ _ReSharper*/
Sources/packages
*.nupkg
/Sources/.vs/Equ/v15/Server/sqlite3/*
**/.vs
Binary file added .vs/Equ/v16/TestStore/0/000.testlog
Binary file not shown.
Binary file added .vs/Equ/v16/TestStore/0/testlog.manifest
Binary file not shown.
3 changes: 3 additions & 0 deletions .vs/ProjectSettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"CurrentProjectSetting": null
}
Binary file added .vs/slnx.sqlite
Binary file not shown.
61 changes: 61 additions & 0 deletions Sources/Equ.Test/NestedClassTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using System.Collections.Generic;
using Xunit;

namespace Equ.Test
{
public class NestedClassTest
{
public class Contained
{
public string BasicProperty { get; set; }
public Contained Nested { get; set; }
}

public class Container
{
public Contained Nested { get; set; }
}

public static IEnumerable<object[]> ShouldDetermineCorrectEqualityTests => new List<object[]>
{
new object[] { new Container(), new Container(), MemberwiseEqualityComparer<Container>.ByProperties, true },
new object[] {
new Container { Nested = new Contained() },
new Container { },
MemberwiseEqualityComparer<Container>.ByProperties,
false
},
new object[] {
new Container { Nested = new Contained() },
new Container { Nested = new Contained() },
MemberwiseEqualityComparer<Container>.ByProperties,
false
},
new object[] {
new Container { Nested = new Contained() },
new Container { Nested = new Contained() },
MemberwiseEqualityComparer<Container>.ByPropertiesRecursive,
true
},
new object[] {
new Container { Nested = new Contained { Nested = new Contained() } },
new Container { Nested = new Contained() },
MemberwiseEqualityComparer<Container>.ByPropertiesRecursive,
false
},
new object[] {
new Container { Nested = new Contained { Nested = new Contained() } },
new Container { Nested = new Contained { Nested = new Contained() } },
MemberwiseEqualityComparer<Container>.ByPropertiesRecursive,
true
}
};

[Theory]
[MemberData(nameof(ShouldDetermineCorrectEqualityTests))]
public void ShouldDetermineCorrectEquality(Container x, Container y, MemberwiseEqualityComparer<Container> equalityComparer, bool expected)
{
Assert.Equal(expected, equalityComparer.Equals(x, y));
}
}
}
87 changes: 87 additions & 0 deletions Sources/Equ.Test/NestedCollectionsTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xunit;

namespace Equ.Test
{
public class NestedCollectionsTest
{
public class Level2
{
public string BasicProperty { get; set; }
}

public class Level1
{
public ICollection<Level2> Items { get; set; }
}

public class Container
{
public ICollection<Level1> Items { get; set; }
}

public static IEnumerable<object[]> ShouldDetermineCorrectEqualityTests => new List<object[]>
{
new object[] { new Container(), new Container(), MemberwiseEqualityComparer<Container>.ByProperties, true },
new object[] {
new Container { Items = new List<Level1>() },
new Container { },
MemberwiseEqualityComparer<Container>.ByProperties,
false
},
new object[] {
new Container { Items = new List<Level1>() },
new Container { Items = new List<Level1>() },
MemberwiseEqualityComparer<Container>.ByProperties,
true
},
new object[] {
new Container { Items = new List<Level1>() },
new Container { Items = Array.Empty<Level1>() },
MemberwiseEqualityComparer<Container>.ByProperties,
true
},
new object[] {
new Container { Items = new [] { new Level1() } },
new Container { Items = Array.Empty<Level1>() },
MemberwiseEqualityComparer<Container>.ByProperties,
false
},
new object[] {
new Container { Items = new [] { new Level1() } },
new Container { Items = new [] { new Level1() } },
MemberwiseEqualityComparer<Container>.ByProperties,
false
},
new object[] {
new Container { Items = new [] { new Level1() } },
new Container { Items = new [] { new Level1() } },
MemberwiseEqualityComparer<Container>.ByPropertiesRecursive,
true
},
new object[] {
new Container { Items = new [] { new Level1 { Items = new[] { new Level2() } } } },
new Container { Items = new [] { new Level1 { Items = new[] { new Level2() } } } },
MemberwiseEqualityComparer<Container>.ByProperties,
false
},
new object[] {
new Container { Items = new [] { new Level1 { Items = new[] { new Level2() } } } },
new Container { Items = new [] { new Level1 { Items = new[] { new Level2() } } } },
MemberwiseEqualityComparer<Container>.ByPropertiesRecursive,
true
},
};

[Theory]
[MemberData(nameof(ShouldDetermineCorrectEqualityTests))]
public void ShouldDetermineCorrectEquality(Container x, Container y, MemberwiseEqualityComparer<Container> equalityComparer, bool expected)
{
Assert.Equal(expected, equalityComparer.Equals(x, y));
}
}
}
94 changes: 86 additions & 8 deletions Sources/Equ/ElementwiseSequenceEqualityComparer.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
namespace Equ
{
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
Expand All @@ -13,12 +14,63 @@
/// <typeparam name="T">The type of the enumerable, i.e. a type implementing <see cref="IEnumerable"/></typeparam>
public class ElementwiseSequenceEqualityComparer<T> : EqualityComparer<T> where T : IEnumerable
{
private static readonly Type EnumerableType = typeof(T)
.GetTypeInfo()
.GetInterfaces()
.Where(type => type.GetTypeInfo().IsGenericType && typeof(IEnumerable<>).GetTypeInfo().IsAssignableFrom(type.GetGenericTypeDefinition()))
.SelectMany(type => type.GetTypeInfo().GetGenericArguments())
.SingleOrDefault();

private static readonly MethodInfo SequenceEqualsMethodInfo = (EnumerableType == null) ? null : typeof(Enumerable)
.GetTypeInfo()
.GetMethods(BindingFlags.Static | BindingFlags.Public)
.Where(methodInfo => methodInfo.Name.Equals(nameof(Enumerable.SequenceEqual)) && methodInfo.GetParameters().Length == 3)
.Single()
.MakeGenericMethod(EnumerableType);

private static readonly MethodInfo ScrambledEqualsMethodInfo = (EnumerableType == null) ? null : typeof(ElementwiseSequenceEqualityComparer<T>)
.GetTypeInfo()
.GetMethods(BindingFlags.Static | BindingFlags.NonPublic)
.Where(methodInfo => methodInfo.Name.Equals(nameof(ElementwiseSequenceEqualityComparer<T>.ScrambledEquals)) && methodInfo.IsGenericMethod)
.Single()
.MakeGenericMethod(EnumerableType);

// ReSharper disable once UnusedMember.Global
public new static ElementwiseSequenceEqualityComparer<T> Default => new ElementwiseSequenceEqualityComparer<T>();
public new static ElementwiseSequenceEqualityComparer<T> Default => new ElementwiseSequenceEqualityComparer<T>(MemberwiseEqualityMode.None);
public static ElementwiseSequenceEqualityComparer<T> ByFieldsRecursive => new ElementwiseSequenceEqualityComparer<T>(MemberwiseEqualityMode.ByFieldsRecursive);
public static ElementwiseSequenceEqualityComparer<T> ByPropertiesRecursive => new ElementwiseSequenceEqualityComparer<T>(MemberwiseEqualityMode.ByPropertiesRecursive);

// ReSharper disable once StaticMemberInGenericType
private static readonly bool _typeHasDefinedOrder = !IsDictionaryType() && !IsSetType();


private bool _recursive;
private Lazy<object> _memberwiseEqualityComparer;

public ElementwiseSequenceEqualityComparer() : this(MemberwiseEqualityMode.None) { }

internal ElementwiseSequenceEqualityComparer(MemberwiseEqualityMode mode)
{
_recursive = (mode == MemberwiseEqualityMode.ByFieldsRecursive || mode == MemberwiseEqualityMode.ByPropertiesRecursive);
_memberwiseEqualityComparer = new Lazy<object>(() => CreateMemberwiseEqualityComparer(mode));
}

private object CreateMemberwiseEqualityComparer(MemberwiseEqualityMode mode)
{
var propertyName = mode switch
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think I've seen this syntax before. I haven't paid much attention to the new features for a few years though. What is it doing?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

{
MemberwiseEqualityMode.ByFieldsRecursive => nameof(MemberwiseEqualityComparer<object>.ByFieldsRecursive),
MemberwiseEqualityMode.ByPropertiesRecursive => nameof(MemberwiseEqualityComparer<object>.ByPropertiesRecursive),
_ => nameof(MemberwiseEqualityComparer<object>.ByProperties)
};

return typeof(MemberwiseEqualityComparer<>)
.GetTypeInfo()
.MakeGenericType(EnumerableType)
.GetTypeInfo()
.GetProperty(propertyName, BindingFlags.Static | BindingFlags.Public)
.GetValue(null);
}

public override bool Equals(T left, T right)
{
if (ReferenceEquals(left, right))
Expand All @@ -34,10 +86,21 @@ public override bool Equals(T left, T right)
return false;
}

var leftEnumerable = left.Cast<object>();
var rightEnumerable = right.Cast<object>();

return _typeHasDefinedOrder ? leftEnumerable.SequenceEqual(rightEnumerable) : ScrambledEquals(leftEnumerable, rightEnumerable);
return _typeHasDefinedOrder ? SequenceEqual(left, right) : ScrambledEquals(left, right);
}

private bool SequenceEqual(IEnumerable left, IEnumerable right)
{
if (_recursive && EnumerableType != null && !EnumerableType.GetTypeInfo().IsPrimitive)
{
return (bool)SequenceEqualsMethodInfo.Invoke(null, new [] { left, right, _memberwiseEqualityComparer.Value });
}
else
{
var leftEnumerable = left.Cast<object>();
var rightEnumerable = right.Cast<object>();
return leftEnumerable.SequenceEqual(rightEnumerable);
}
}

public override int GetHashCode(T obj)
Expand Down Expand Up @@ -98,9 +161,24 @@ private static bool IsSetType()
return type.IsGenericType && typeof(ISet<>).GetTypeInfo().IsAssignableFrom(type.GetGenericTypeDefinition());
}

private static bool ScrambledEquals<TElem>(IEnumerable<TElem> list1, IEnumerable<TElem> list2)
private bool ScrambledEquals(IEnumerable left, IEnumerable right)
{
if (_recursive && EnumerableType != null && !EnumerableType.GetTypeInfo().IsPrimitive)
{
return (bool)ScrambledEqualsMethodInfo.Invoke(null, new [] { left, right, _memberwiseEqualityComparer.Value });
}
else
{
var leftEnumerable = left.Cast<object>();
var rightEnumerable = right.Cast<object>();

return ScrambledEquals(leftEnumerable, rightEnumerable, EqualityComparer<object>.Default);
}
}

private static bool ScrambledEquals<TElem>(IEnumerable<TElem> list1, IEnumerable<TElem> list2, IEqualityComparer<TElem> equalityComparer)
{
var counters = new Dictionary<TElem, int>();
var counters = new Dictionary<TElem, int>(equalityComparer);
foreach (var element in list1)
{
if (counters.ContainsKey(element))
Expand Down
1 change: 1 addition & 0 deletions Sources/Equ/Equ.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
<PackageReleaseNotes>See https://github.com/thedmi/Equ/blob/master/ReleaseNotes.md</PackageReleaseNotes>
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>..\key.snk</AssemblyOriginatorKeyFile>
<LangVersion>8.0</LangVersion>
</PropertyGroup>

</Project>
48 changes: 48 additions & 0 deletions Sources/Equ/EqualityFunctionContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using System;
using System.Collections.Generic;

namespace Equ
{
public class EqualityFunctionContext
{
private readonly Dictionary<Type, MemberwiseEqualityComparer> _equalityComparers;

public EqualityFunctionContext(MemberwiseEqualityMode mode)
{
_equalityComparers = new Dictionary<Type, MemberwiseEqualityComparer>();

Mode = mode;

MemberwiseEqualityComparerProperty = mode switch
{
MemberwiseEqualityMode.ByFields => nameof(MemberwiseEqualityComparer<object>.ByFields),
MemberwiseEqualityMode.ByFieldsRecursive => nameof(MemberwiseEqualityComparer<object>.ByFieldsRecursive),
MemberwiseEqualityMode.ByProperties => nameof(MemberwiseEqualityComparer<object>.ByProperties),
MemberwiseEqualityMode.ByPropertiesRecursive => nameof(MemberwiseEqualityComparer<object>.ByPropertiesRecursive),
_ => string.Empty
};

ElementwiseSequenceEqualityComparerProperty = mode switch
{
MemberwiseEqualityMode.ByFieldsRecursive => nameof(ElementwiseSequenceEqualityComparer<IEnumerable<object>>.ByFieldsRecursive),
MemberwiseEqualityMode.ByPropertiesRecursive => nameof(ElementwiseSequenceEqualityComparer<IEnumerable<object>>.ByPropertiesRecursive),
_ => nameof(ElementwiseSequenceEqualityComparer<IEnumerable<object>>.Default)
};
}

public bool TryGetEqualityComparer(Type type, out MemberwiseEqualityComparer equalityComparer)
{
return _equalityComparers.TryGetValue(type, out equalityComparer);
}

public void Add<T>(MemberwiseEqualityComparer<T> equalityComparer)
{
_equalityComparers.Add(typeof(T), equalityComparer);
}

public MemberwiseEqualityMode Mode { get; }
public bool IsRecursive => Mode == MemberwiseEqualityMode.ByFieldsRecursive || Mode == MemberwiseEqualityMode.ByPropertiesRecursive;
public string MemberwiseEqualityComparerProperty { get; }
public string ElementwiseSequenceEqualityComparerProperty { get; }
}
}
Loading