diff --git a/README.md b/README.md index 36b96da..0793fb2 100644 --- a/README.md +++ b/README.md @@ -25,9 +25,9 @@ The `SolidStack.Core` namespace is the central point of all SolidStack packages, Package | Description ------- | ----------- -[SolidStack.Core.Guards][solidstack.core.guards-page] | `SolidStack.Core.Guards` is an extremely simple, unambiguous and lightweight [*guard clause*][guard-clauses-url] library. +[SolidStack.Core.Guards][solidstack.core.guards-page] | `SolidStack.Core.Guards` is an extremely simple, unambiguous and lightweight [*guard clause*][guard-clauses-url] library that allow you to write pre-conditions and post-conditions for your methods in a readable way. [SolidStack.Core.Flow][solidstack.core.flow-page] | `SolidStack.Core.Flow` focuses on encapsulating the branching logic of your code so you can write a linear and much more readable code flow without having to deal with exceptions, null checks and unnecessary conditions. -SolidStack.Core.Equality (coming soon...) | `SolidStack.Core.Equality` is primarily useful when you have to tweak the equality of an object to implement the [*Value Object Pattern*][value-object-pattern-url]. All you have to do is use one of the provided abstract classes and the complex equality logic will be done for you. +[SolidStack.Core.Equality][solidstack.core.equality-page] | `SolidStack.Core.Equality` is primarily useful when you have to tweak the equality of an object to implement the [*Value Object Pattern*][value-object-pattern-url]. All you have to do is use one of the provided abstract classes and the complex equality logic will be done for you. SolidStack.Core.Construction (coming soon...) | `SolidStack.Core.Construction`'s only responsibility is to help you construct objects. You can use the [*Builder Pattern*][builder-pattern-url] provided implementation to build complex objects in a fluent way. ### SolidStack.Domain (coming soon...) @@ -100,6 +100,7 @@ SolidStack is Copyright © 2018 SoftFrame under the [MIT license][license-url]. [nuget-install-url]: http://docs.nuget.org/docs/start-here/installing-nuget [option-pattern-url]: http://www.codinghelmet.com/?path=howto/understanding-the-option-maybe-functional-type [repository-pattern-url]: https://martinfowler.com/eaaCatalog/repository.html +[solidstack.core.equality-page]: https://github.com/softframe/solidstack/wiki/SolidStack.Core.Equality [solidstack.core.guards-page]: https://github.com/softframe/solidstack/wiki/SolidStack.Core.Guards [solidstack.core.flow-page]: https://github.com/softframe/solidstack/wiki/SolidStack.Core.Flow [unit-of-work-pattern-url]: https://martinfowler.com/eaaCatalog/unitOfWork.html diff --git a/src/SolidStack.Core.Equality.Testing/AssertionExtensions.cs b/src/SolidStack.Core.Equality.Testing/AssertionExtensions.cs new file mode 100644 index 0000000..4e5a2be --- /dev/null +++ b/src/SolidStack.Core.Equality.Testing/AssertionExtensions.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; + +namespace SolidStack.Core.Equality.Testing +{ + public static class AssertionExtensions + { + public static EqualityComparerAssertions Should(this IEqualityComparer equalityComparer) + where T : class => + new EqualityComparerAssertions(equalityComparer); + + public static EquatableAssertions Should(this IEquatable equatable) => + new EquatableAssertions(equatable); + } +} \ No newline at end of file diff --git a/src/SolidStack.Core.Equality.Testing/EqualityComparerAssertions.cs b/src/SolidStack.Core.Equality.Testing/EqualityComparerAssertions.cs new file mode 100644 index 0000000..53a5019 --- /dev/null +++ b/src/SolidStack.Core.Equality.Testing/EqualityComparerAssertions.cs @@ -0,0 +1,79 @@ +using System.Collections.Generic; +using FluentAssertions; +using FluentAssertions.Execution; +using Moq; + +namespace SolidStack.Core.Equality.Testing +{ + public class EqualityComparerAssertions + where T : class + { + public EqualityComparerAssertions(IEqualityComparer equalityComparer) => + Subject = equalityComparer; + + public IEqualityComparer Subject { get; protected set; } + + public AndConstraint> HandleBasicEqualitiesAndInequalites( + string because = "", params object[] becauseArgs) + { + Execute.Assertion + .BecauseOf(because, becauseArgs) + .Given(Mock.Of) + .ForCondition(dummy => Subject.Equals(dummy, dummy)) + .FailWith( + "Expected {context:comparer} to evaluate the equality of the same object as {0}{reason}, but found {1}.", + true, false) + .Then + .ForCondition(dummy => !Subject.Equals(null, dummy) && !Subject.Equals(dummy, null)) + .FailWith( + "Expected {context:comparer} to evaluate the equality of null and a non-null object as {0}{reason}, but found {1}.", + false, true); + + return new AndConstraint>(this); + } + + public AndConstraint> InvalidateEqualityOf( + T x, T y, string because = "", params object[] becauseArgs) + { + var xCopy = x; + var yCopy = y; + + Execute.Assertion + .BecauseOf(because, becauseArgs) + .ForCondition(!Subject.Equals(x, y)) + .FailWith( + "Expected {context:comparer} to evaluate the equality of {0} and {1} as {2}{reason}, but found {3}.", + x, y, false, true) + .Then + .Given(() => new[] {Subject.GetHashCode(x), Subject.GetHashCode(y)}) + .ForCondition(hashCodes => hashCodes[0] != hashCodes[1]) + .FailWith( + "Expected {context:comparer} to return different hash codes for {0} and {1}{reason}, but found {2} and {3}.", + _ => xCopy, _ => yCopy, hashCodes => hashCodes[0], hashCodes => hashCodes[1]); + + return new AndConstraint>(this); + } + + public AndConstraint> ValidateEqualityOf( + T x, T y, string because = "", params object[] becauseArgs) + { + var xCopy = x; + var yCopy = y; + + Execute.Assertion + .BecauseOf(because, becauseArgs) + .ForCondition(Subject.Equals(x, y)) + .FailWith( + "Expected {context:comparer} to evaluate the equality of {0} and {1} as {2}{reason}, but found {3}.", + x, y, true, false) + .Then + .Given(() => new[] {Subject.GetHashCode(x), Subject.GetHashCode(y)}) + .ForCondition(hashCodes => hashCodes[0] == hashCodes[1]) + .FailWith( + "Expected {context:comparer} to return the same hash code for {0} and {1}{reason}, but found {2} and {3}.", + _ => xCopy, _ => yCopy, hashCodes => hashCodes[0], hashCodes => hashCodes[1]); + + return new AndConstraint>(this); + } + } +} \ No newline at end of file diff --git a/src/SolidStack.Core.Equality.Testing/EquatableAssertions.cs b/src/SolidStack.Core.Equality.Testing/EquatableAssertions.cs new file mode 100644 index 0000000..c9c18b2 --- /dev/null +++ b/src/SolidStack.Core.Equality.Testing/EquatableAssertions.cs @@ -0,0 +1,63 @@ +using System; +using System.Reflection; +using FluentAssertions; +using FluentAssertions.Execution; + +namespace SolidStack.Core.Equality.Testing +{ + public class EquatableAssertions + { + public EquatableAssertions(IEquatable equatable) => + Subject = equatable; + + public IEquatable Subject { get; protected set; } + + private TypeInfo SubjectType => + Subject.GetType().GetTypeInfo(); + + public AndConstraint> BeTypeSealed( + string because = "", params object[] becauseArgs) + { + Execute.Assertion + .BecauseOf(because, becauseArgs) + .ForCondition(Subject.GetType().IsSealed) + .FailWith($"Expected {{context:{Subject.GetType().Name}}} to be type sealed{{reason}}."); + + return new AndConstraint>(this); + } + + public AndConstraint> OverrideEquality( + string because = "", params object[] becauseArgs) + { + Execute.Assertion + .BecauseOf(because, becauseArgs) + .ForCondition(OverridesMethod("Equals", new []{typeof(object)})) + .FailWith($"Expected {{context:{Subject.GetType().Name}}} to override Equals(object){{reason}}.", Subject.GetType()) + .Then + .ForCondition(OverridesMethod("GetHashCode", Type.EmptyTypes)) + .FailWith($"Expected {{context:{Subject.GetType().Name}}} to override GetHashCode(){{reason}}.", Subject.GetType()) + .Then + .ForCondition(OverridesOperator("op_Equality")) + .FailWith($"Expected {{context:{Subject.GetType().Name}}} to override equality operator{{reason}}.", Subject.GetType()) + .Then + .ForCondition(OverridesOperator("op_Inequality")) + .FailWith($"Expected {{context:{Subject.GetType().Name}}} to override inequality operator{{reason}}.", Subject.GetType()); + + return new AndConstraint>(this); + } + + private bool OverridesMethod(string methodName, Type[] types) => + SubjectType + .GetMethod(methodName, types) + ?.DeclaringType != typeof(object); + + private bool OverridesOperator(string operatorName) => + SubjectType + .GetMethod( + operatorName, + BindingFlags.Instance | + BindingFlags.Static | + BindingFlags.Public | + BindingFlags.FlattenHierarchy) != null; + } +} \ No newline at end of file diff --git a/src/SolidStack.Core.Equality.Testing/SolidStack.Core.Equality.Testing.csproj b/src/SolidStack.Core.Equality.Testing/SolidStack.Core.Equality.Testing.csproj new file mode 100644 index 0000000..1e96578 --- /dev/null +++ b/src/SolidStack.Core.Equality.Testing/SolidStack.Core.Equality.Testing.csproj @@ -0,0 +1,12 @@ + + + + netstandard2.0 + + + + + + + + diff --git a/src/SolidStack.Core.Equality.Tests/Doubles/DummyByDynamicMembersEqualityComparable.cs b/src/SolidStack.Core.Equality.Tests/Doubles/DummyByDynamicMembersEqualityComparable.cs new file mode 100644 index 0000000..1e8e8dd --- /dev/null +++ b/src/SolidStack.Core.Equality.Tests/Doubles/DummyByDynamicMembersEqualityComparable.cs @@ -0,0 +1,22 @@ +namespace SolidStack.Core.Equality.Tests.Doubles +{ + public class DummyByDynamicMembersEqualityComparable + { + protected dynamic FieldA; + + public DummyByDynamicMembersEqualityComparable(dynamic fieldA, dynamic propertyA) + { + FieldA = fieldA; + PropertyA = propertyA; + } + + /// + /// Constructor used to instantiate the class via reflection. + /// + public DummyByDynamicMembersEqualityComparable() + { + } + + public dynamic PropertyA { get; } + } +} \ No newline at end of file diff --git a/src/SolidStack.Core.Equality.Tests/Doubles/DummyByKeyEqualityComparable.cs b/src/SolidStack.Core.Equality.Tests/Doubles/DummyByKeyEqualityComparable.cs new file mode 100644 index 0000000..c0c7288 --- /dev/null +++ b/src/SolidStack.Core.Equality.Tests/Doubles/DummyByKeyEqualityComparable.cs @@ -0,0 +1,17 @@ +namespace SolidStack.Core.Equality.Tests.Doubles +{ + public class DummyByKeyEqualityComparable + { + public DummyByKeyEqualityComparable(string id) => + Id = id; + + /// + /// Constructor used to instantiate the class via reflection. + /// + public DummyByKeyEqualityComparable() + { + } + + public string Id { get; } + } +} \ No newline at end of file diff --git a/src/SolidStack.Core.Equality.Tests/Doubles/DummyByKeyEqualityComparableChild.cs b/src/SolidStack.Core.Equality.Tests/Doubles/DummyByKeyEqualityComparableChild.cs new file mode 100644 index 0000000..983bc2d --- /dev/null +++ b/src/SolidStack.Core.Equality.Tests/Doubles/DummyByKeyEqualityComparableChild.cs @@ -0,0 +1,18 @@ +namespace SolidStack.Core.Equality.Tests.Doubles +{ + public class DummyByKeyEqualityComparableChild : DummyByKeyEqualityComparable + { + public DummyByKeyEqualityComparableChild(string id) : + base(id) + { + } + + /// + /// + /// Constructor used to instantiate the class via reflection. + /// + public DummyByKeyEqualityComparableChild() + { + } + } +} \ No newline at end of file diff --git a/src/SolidStack.Core.Equality.Tests/Doubles/DummyByMembersEqualityComparable.cs b/src/SolidStack.Core.Equality.Tests/Doubles/DummyByMembersEqualityComparable.cs new file mode 100644 index 0000000..7abe8a5 --- /dev/null +++ b/src/SolidStack.Core.Equality.Tests/Doubles/DummyByMembersEqualityComparable.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; + +namespace SolidStack.Core.Equality.Tests.Doubles +{ + public class DummyByMembersEqualityComparable + { + public int FieldA; + + protected IEnumerable FieldB; + + public DummyByMembersEqualityComparable( + int fieldA, IEnumerable fieldB, + DateTime propertyA, IEnumerable propertyB) + { + FieldA = fieldA; + FieldB = fieldB; + PropertyA = propertyA; + PropertyB = propertyB; + } + + /// + /// Constructor used to instantiate the class via reflection. + /// + public DummyByMembersEqualityComparable() + { + } + + public DateTime PropertyA { get; } + + protected IEnumerable PropertyB { get; set; } + } +} \ No newline at end of file diff --git a/src/SolidStack.Core.Equality.Tests/Doubles/DummyByMembersEqualityComparableChild.cs b/src/SolidStack.Core.Equality.Tests/Doubles/DummyByMembersEqualityComparableChild.cs new file mode 100644 index 0000000..2906935 --- /dev/null +++ b/src/SolidStack.Core.Equality.Tests/Doubles/DummyByMembersEqualityComparableChild.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; + +namespace SolidStack.Core.Equality.Tests.Doubles +{ + public class DummyByMembersEqualityComparableChild : DummyByMembersEqualityComparable + { + protected string FieldC; + + public DummyByMembersEqualityComparableChild( + int fieldA, IEnumerable fieldB, string fieldC, + DateTime propertyA, IEnumerable propertyB, bool propertyC) : + base(fieldA, fieldB, propertyA, propertyB) + { + FieldC = fieldC; + PropertyC = propertyC; + } + + /// + /// + /// Constructor used to instantiate the class via reflection. + /// + public DummyByMembersEqualityComparableChild() + { + } + + public bool PropertyC { get; set; } + } +} \ No newline at end of file diff --git a/src/SolidStack.Core.Equality.Tests/Doubles/DummyByTaggedMembersEqualityComparable.cs b/src/SolidStack.Core.Equality.Tests/Doubles/DummyByTaggedMembersEqualityComparable.cs new file mode 100644 index 0000000..4b8b8e0 --- /dev/null +++ b/src/SolidStack.Core.Equality.Tests/Doubles/DummyByTaggedMembersEqualityComparable.cs @@ -0,0 +1,30 @@ +namespace SolidStack.Core.Equality.Tests.Doubles +{ + public class DummyByTaggedMembersEqualityComparable + { + [EqualityMember] + public int FieldA; + + protected string FieldB; + + public DummyByTaggedMembersEqualityComparable(int fieldA, string fieldB, bool propertyA, char propertyB) + { + FieldA = fieldA; + FieldB = fieldB; + PropertyA = propertyA; + PropertyB = propertyB; + } + + /// + /// Constructor used to instantiate the class via reflection. + /// + public DummyByTaggedMembersEqualityComparable() + { + } + + public bool PropertyA { get; } + + [EqualityMember] + protected char PropertyB { get; set; } + } +} \ No newline at end of file diff --git a/src/SolidStack.Core.Equality.Tests/Doubles/EquatableStub.cs b/src/SolidStack.Core.Equality.Tests/Doubles/EquatableStub.cs new file mode 100644 index 0000000..fdf089e --- /dev/null +++ b/src/SolidStack.Core.Equality.Tests/Doubles/EquatableStub.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; + +namespace SolidStack.Core.Equality.Tests.Doubles +{ + public sealed class EquatableStub : Equatable + { + public EquatableStub(Func> equalityComparerAccessor) => + EqualityComparerAccessor = equalityComparerAccessor; + + private Func> EqualityComparerAccessor { get; } + + protected override IEqualityComparer GetEqualityComparer() => + EqualityComparerAccessor(); + } +} \ No newline at end of file diff --git a/src/SolidStack.Core.Equality.Tests/EqualityComparerTests.cs b/src/SolidStack.Core.Equality.Tests/EqualityComparerTests.cs new file mode 100644 index 0000000..d53eeef --- /dev/null +++ b/src/SolidStack.Core.Equality.Tests/EqualityComparerTests.cs @@ -0,0 +1,391 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using SolidStack.Core.Equality.Testing; +using SolidStack.Core.Equality.Tests.Doubles; +using Xunit; + +namespace SolidStack.Core.Equality.Tests +{ + public class EqualityComparerTests + { + [Fact] + public void ByElement_CreatesComparerBasedOnAllElementsEquality() + { + var comparer = EqualityComparer.ByElements>(); + + comparer.Should() + .HandleBasicEqualitiesAndInequalites() + .And.ValidateEqualityOf( + new[] {"a", "b", "c"}, + new[] {"a", "b", "c"}, + "sequences with equals elements should equals") + .And.ValidateEqualityOf( + new[] {"a", "b", null}, + new[] {"a", "b", null}, + "sequences with equals elements should equals even if some elements are nulls") + .And.ValidateEqualityOf( + new List {"a", "b", "c"}, + new HashSet {"a", "b", "c"}, + "sequences with equals elements should equals even if their types are different") + .And.InvalidateEqualityOf( + new[] {"a", "b", "c"}, + new[] {"d", "e", "f"}, + "sequences with different elements shouldn't equals"); + } + + [Fact] + public void ByFields_CreatesComparerBasedOnAllPropertiesEquality() + { + var comparer = + EqualityComparer.ByFields(); + + comparer.Should() + .HandleBasicEqualitiesAndInequalites() + .And.ValidateEqualityOf( + new DummyByMembersEqualityComparable( + 5, new[] {'a', 'b'}, new DateTime(2018, 07, 30), new[] {true, true}), + new DummyByMembersEqualityComparable( + 5, new[] {'a', 'b'}, new DateTime(2018, 06, 24), new[] {false, false}), + "properties should be ignored") + .And.InvalidateEqualityOf( + new DummyByMembersEqualityComparable( + 5, new[] {'a', 'b'}, new DateTime(2018, 07, 30), new[] {true, true}), + new DummyByMembersEqualityComparable( + 5, new[] {'c', 'd'}, new DateTime(2018, 07, 30), new[] {true, true}), + "objects with different fields shouldn't equals"); + } + + [Fact] + public void ByFields_WithPredicate_CreatesComparerBasedOnSelectedFieldsEquality() + { + var comparer = + EqualityComparer.ByFields( + field => field.Name.EndsWith("A")); + + comparer.Should() + .HandleBasicEqualitiesAndInequalites() + .And.ValidateEqualityOf( + new DummyByMembersEqualityComparable( + 5, new[] {'a', 'b'}, new DateTime(2018, 07, 30), new[] {true, true}), + new DummyByMembersEqualityComparable( + 5, new[] {'c', 'd'}, new DateTime(2018, 06, 24), new[] {false, false}), + "unselected fields and every property should be ignored") + .And.InvalidateEqualityOf( + new DummyByMembersEqualityComparable( + 5, new[] {'a', 'b'}, new DateTime(2018, 07, 30), new[] {true, true}), + new DummyByMembersEqualityComparable( + 3, new[] {'a', 'b'}, new DateTime(2018, 06, 24), new[] {true, true}), + "objects with different selected fields shouldn't equals"); + } + + [Fact] + public void ByFields_WithSelector_CreatesComparerBasedOnSelectedFieldsEquality() + { + var comparer = + EqualityComparer.ByFields( + fields => fields.Where(field => field.Name.EndsWith("A"))); + + comparer.Should() + .HandleBasicEqualitiesAndInequalites() + .And.ValidateEqualityOf( + new DummyByMembersEqualityComparable( + 5, new[] {'a', 'b'}, new DateTime(2018, 07, 30), new[] {true, true}), + new DummyByMembersEqualityComparable( + 5, new[] {'c', 'd'}, new DateTime(2018, 06, 24), new[] {false, false}), + "unselected fields and every property should be ignored") + .And.InvalidateEqualityOf( + new DummyByMembersEqualityComparable( + 5, new[] {'a', 'b'}, new DateTime(2018, 07, 30), new[] {true, true}), + new DummyByMembersEqualityComparable( + 3, new[] {'a', 'b'}, new DateTime(2018, 06, 24), new[] {true, true}), + "objects with different selected fields shouldn't equals"); + } + + [Fact] + public void ByKey_CreatesComparerBasedOnKeyEquality() + { + var comparer = EqualityComparer.ByKey(obj => obj.Id); + + comparer.Should() + .HandleBasicEqualitiesAndInequalites() + .And.ValidateEqualityOf( + new DummyByKeyEqualityComparable("a1b2"), + new DummyByKeyEqualityComparable("a1b2"), + "objects with same key should equals") + .And.InvalidateEqualityOf( + new DummyByKeyEqualityComparable("a1b2"), + new DummyByKeyEqualityComparableChild("a1c3"), + "objects of different types shouldn't equals") + .And.InvalidateEqualityOf( + new DummyByKeyEqualityComparable("a1b2"), + new DummyByKeyEqualityComparable("a1c3"), + "objects with different keys shouldn't equals"); + } + + [Fact] + public void ByMembers_CreatesComparerBasedOnAllMembersEquality() + { + var comparer = EqualityComparer.ByMembers(); + + comparer.Should() + .HandleBasicEqualitiesAndInequalites() + .And.ValidateEqualityOf( + new DummyByMembersEqualityComparable( + 5, new[] {'a', 'b'}, new DateTime(2018, 07, 30), new[] {true, true}), + new DummyByMembersEqualityComparable( + 5, new[] {'a', 'b'}, new DateTime(2018, 07, 30), new[] {true, true}), + "objects with equals members should equals") + .And.ValidateEqualityOf( + new DummyByMembersEqualityComparable( + 5, null, new DateTime(2018, 07, 30), null), + new DummyByMembersEqualityComparable( + 5, null, new DateTime(2018, 07, 30), null), + "objects with equals members should equals even if some members are nulls") + .And.InvalidateEqualityOf( + new DummyByMembersEqualityComparable( + 5, new[] {'a', 'b'}, new DateTime(2018, 07, 30), new[] {true, true}), + new DummyByMembersEqualityComparableChild( + 5, new[] {'a', 'b'}, "abc", new DateTime(2018, 07, 30), new[] {true, true}, false), + "objects of different types shouldn't equals") + .And.InvalidateEqualityOf( + new DummyByMembersEqualityComparable( + 5, new[] {'a', 'b'}, new DateTime(2018, 07, 30), new[] {true, true}), + new DummyByMembersEqualityComparable( + 3, new[] {'a', 'b'}, new DateTime(2018, 06, 24), new[] {true, true}), + "objects with different members shouldn't equals") + .And.InvalidateEqualityOf( + new DummyByMembersEqualityComparable( + 5, new[] {'a', 'b'}, new DateTime(2018, 07, 30), new[] {true, true}), + new DummyByMembersEqualityComparable( + 5, new[] {'a', 'c'}, new DateTime(2018, 07, 30), new[] {true, true}), + "objects with different members shouldn't equals even if the difference is a element contained in a sequence"); + } + + [Fact] + public void ByMembers_WithDerivedType_CreatesComparerBasedOnAllMembersEquality() + { + var comparer = EqualityComparer.ByMembers(); + + comparer.Should() + .HandleBasicEqualitiesAndInequalites() + .And.ValidateEqualityOf( + new DummyByMembersEqualityComparableChild( + 5, new[] {'a', 'b'}, "abc", new DateTime(2018, 07, 30), new[] {true, true}, false), + new DummyByMembersEqualityComparableChild( + 5, new[] {'a', 'b'}, "abc", new DateTime(2018, 07, 30), new[] {true, true}, false), + "objects with inherited members should equals if their members are equals") + .And.ValidateEqualityOf( + new DummyByMembersEqualityComparableChild( + 5, new[] {'a', 'b'}, null, new DateTime(2018, 07, 30), null, false), + new DummyByMembersEqualityComparableChild( + 5, new[] {'a', 'b'}, null, new DateTime(2018, 07, 30), null, false), + "objects with inherited members should equals if their members are equals even if some members are nulls") + .And.InvalidateEqualityOf( + new DummyByMembersEqualityComparableChild( + 5, new[] {'a', 'b'}, "abc", new DateTime(2018, 07, 30), new[] {true, true}, false), + new DummyByMembersEqualityComparableChild( + 5, new[] {'a', 'b'}, "def", new DateTime(2018, 07, 30), new[] {true, true}, false), + "objects with inherited members shouldn't equals if their members aren't equals") + .And.InvalidateEqualityOf( + new DummyByMembersEqualityComparableChild( + 5, new[] {'a', 'b'}, "abc", new DateTime(2018, 07, 30), new[] {true, true}, false), + new DummyByMembersEqualityComparableChild( + 3, new[] {'a', 'b'}, "abc", new DateTime(2018, 07, 30), new[] {true, true}, false), + "objects with inherited members shouldn't equals if their members aren't equals even if the different member is an inherited one"); + } + + [Fact] + public void ByMembers_WithDynamicMembers_CreatesComparerBasedOnAllMembersEquality() + { + var comparer = EqualityComparer.ByMembers(); + + comparer.Should() + .HandleBasicEqualitiesAndInequalites() + .And.ValidateEqualityOf( + new DummyByDynamicMembersEqualityComparable("abc", true), + new DummyByDynamicMembersEqualityComparable("abc", true), + "objects with equals members should equals") + .And.ValidateEqualityOf( + new DummyByDynamicMembersEqualityComparable(null, true), + new DummyByDynamicMembersEqualityComparable(null, true), + "objects with equals members should equals even if some members are nulls") + .And.InvalidateEqualityOf( + new DummyByDynamicMembersEqualityComparable("abc", true), + new DummyByDynamicMembersEqualityComparable("def", true), + "objects with different members shouldn't equals") + .And.InvalidateEqualityOf( + new DummyByDynamicMembersEqualityComparable("abc", true), + new DummyByDynamicMembersEqualityComparable("abc", 5), + "objects with members of different runtime types shouldn't equals"); + } + + [Fact] + public void ByMembers_WithPredicate_CreatesComparerBasedOnSelectedMembersEquality() + { + var comparer = + EqualityComparer.ByMembers( + member => member.Name.EndsWith("A")); + + comparer.Should() + .HandleBasicEqualitiesAndInequalites() + .And.ValidateEqualityOf( + new DummyByMembersEqualityComparable( + 5, new[] {'a', 'b'}, new DateTime(2018, 07, 30), new[] {true, true}), + new DummyByMembersEqualityComparable( + 5, new[] {'c', 'd'}, new DateTime(2018, 07, 30), new[] {false, false}), + "unselected members should be ignored") + .And.InvalidateEqualityOf( + new DummyByMembersEqualityComparable( + 5, new[] {'a', 'b'}, new DateTime(2018, 07, 30), new[] {true, true}), + new DummyByMembersEqualityComparable( + 3, new[] {'a', 'b'}, new DateTime(2018, 07, 30), new[] {true, true}), + "objects with different selected members shouldn't equals"); + } + + [Fact] + public void ByMembers_WithSelector_CreatesComparerBasedOnSelectedMembersEquality() + { + var comparer = + EqualityComparer.ByMembers( + members => members.Where(member => member.Name.EndsWith("A"))); + + comparer.Should() + .HandleBasicEqualitiesAndInequalites() + .And.ValidateEqualityOf( + new DummyByMembersEqualityComparable( + 5, new[] {'a', 'b'}, new DateTime(2018, 07, 30), new[] {true, true}), + new DummyByMembersEqualityComparable( + 5, new[] {'c', 'd'}, new DateTime(2018, 07, 30), new[] {false, false}), + "unselected members should be ignored") + .And.InvalidateEqualityOf( + new DummyByMembersEqualityComparable( + 5, new[] {'a', 'b'}, new DateTime(2018, 07, 30), new[] {true, true}), + new DummyByMembersEqualityComparable( + 3, new[] {'a', 'b'}, new DateTime(2018, 07, 30), new[] {true, true}), + "objects with different selected members shouldn't equals"); + } + + [Fact] + public void ByProperties_CreatesComparerBasedOnAllPropertiesEquality() + { + var comparer = + EqualityComparer.ByProperties(); + + comparer.Should() + .HandleBasicEqualitiesAndInequalites() + .And.ValidateEqualityOf( + new DummyByMembersEqualityComparable( + 5, new[] {'a', 'b'}, new DateTime(2018, 07, 30), new[] {true, true}), + new DummyByMembersEqualityComparable( + 3, new[] {'c', 'd'}, new DateTime(2018, 07, 30), new[] {true, true}), + "fields should be ignored") + .And.InvalidateEqualityOf( + new DummyByMembersEqualityComparable( + 5, new[] {'a', 'b'}, new DateTime(2018, 07, 30), new[] {true, true}), + new DummyByMembersEqualityComparable( + 5, new[] {'a', 'b'}, new DateTime(2018, 06, 24), new[] {true, true}), + "objects with different properties shouldn't equals"); + } + + [Fact] + public void ByProperties_WithPredicate_CreatesComparerBasedOnSelectedPropertiesEquality() + { + var comparer = + EqualityComparer.ByProperties( + property => property.Name.EndsWith("A")); + + comparer.Should() + .HandleBasicEqualitiesAndInequalites() + .And.ValidateEqualityOf( + new DummyByMembersEqualityComparable( + 5, new[] {'a', 'b'}, new DateTime(2018, 07, 30), new[] {true, true}), + new DummyByMembersEqualityComparable( + 3, new[] {'c', 'd'}, new DateTime(2018, 07, 30), new[] {false, false}), + "unselected properties and every field should be ignored") + .And.InvalidateEqualityOf( + new DummyByMembersEqualityComparable( + 5, new[] {'a', 'b'}, new DateTime(2018, 07, 30), new[] {true, true}), + new DummyByMembersEqualityComparable( + 5, new[] {'a', 'b'}, new DateTime(2018, 06, 24), new[] {true, true}), + "objects with different selected properties shouldn't equals"); + } + + [Fact] + public void ByProperties_WithSelector_CreatesComparerBasedOnSelectedPropertiesEquality() + { + var comparer = + EqualityComparer.ByProperties( + properties => properties.Where(property => property.Name.EndsWith("A"))); + + comparer.Should() + .HandleBasicEqualitiesAndInequalites() + .And.ValidateEqualityOf( + new DummyByMembersEqualityComparable( + 5, new[] {'a', 'b'}, new DateTime(2018, 07, 30), new[] {true, true}), + new DummyByMembersEqualityComparable( + 3, new[] {'c', 'd'}, new DateTime(2018, 07, 30), new[] {false, false}), + "unselected properties and every field should be ignored") + .And.InvalidateEqualityOf( + new DummyByMembersEqualityComparable( + 5, new[] {'a', 'b'}, new DateTime(2018, 07, 30), new[] {true, true}), + new DummyByMembersEqualityComparable( + 5, new[] {'a', 'b'}, new DateTime(2018, 06, 24), new[] {true, true}), + "objects with different selected properties shouldn't equals"); + } + + [Fact] + public void ByTaggedFields_CreatesComparerBasedOnTaggedFieldsEquality() + { + var comparer = + EqualityComparer.ByTaggedFields(); + + comparer.Should() + .HandleBasicEqualitiesAndInequalites() + .And.ValidateEqualityOf( + new DummyByTaggedMembersEqualityComparable(5, "abc", true, '!'), + new DummyByTaggedMembersEqualityComparable(5, "def", false, '*'), + "not tagged properties and every field should be ignored") + .And.InvalidateEqualityOf( + new DummyByTaggedMembersEqualityComparable(5, "abc", true, '!'), + new DummyByTaggedMembersEqualityComparable(3, "abc", true, '!'), + "objects with different tagged properties shouldn't equals"); + } + + [Fact] + public void ByTaggedMembers_CreatesComparerBasedOnTaggedMembersEquality() + { + var comparer = + EqualityComparer.ByTaggedMembers(); + + comparer.Should() + .HandleBasicEqualitiesAndInequalites() + .And.ValidateEqualityOf( + new DummyByTaggedMembersEqualityComparable(5, "abc", true, '!'), + new DummyByTaggedMembersEqualityComparable(5, "def", false, '!'), + "not tagged members should be ignored") + .And.InvalidateEqualityOf( + new DummyByTaggedMembersEqualityComparable(5, "abc", true, '!'), + new DummyByTaggedMembersEqualityComparable(3, "def", false, '!'), + "objects with different tagged members shouldn't equals"); + } + + [Fact] + public void ByTaggedProperties_CreatesComparerBasedOnTaggedPropertiesEquality() + { + var comparer = + EqualityComparer.ByTaggedProperties(); + + comparer.Should() + .HandleBasicEqualitiesAndInequalites() + .And.ValidateEqualityOf( + new DummyByTaggedMembersEqualityComparable(5, "abc", true, '!'), + new DummyByTaggedMembersEqualityComparable(3, "def", false, '!'), + "not tagged properties and every field should be ignored") + .And.InvalidateEqualityOf( + new DummyByTaggedMembersEqualityComparable(5, "abc", true, '!'), + new DummyByTaggedMembersEqualityComparable(5, "abc", true, '*'), + "objects with different tagged properties shouldn't equals"); + } + } +} \ No newline at end of file diff --git a/src/SolidStack.Core.Equality.Tests/EquatableTests.cs b/src/SolidStack.Core.Equality.Tests/EquatableTests.cs new file mode 100644 index 0000000..0ad2000 --- /dev/null +++ b/src/SolidStack.Core.Equality.Tests/EquatableTests.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using Moq; +using SolidStack.Core.Equality.Testing; +using SolidStack.Core.Equality.Tests.Doubles; +using Xunit; +namespace SolidStack.Core.Equality.Tests +{ + public class EquatableTests + { + + [Fact] + public void Equatable_OverridesEquality() + { + var comparer = new Mock>(); + comparer.Setup(mock => mock.Equals(It.IsAny(), It.IsAny())).Returns(true); + comparer.Setup(mock => mock.GetHashCode()).Returns(42); + var equatable = new EquatableStub(() => comparer.Object); + + equatable.Should() + .BeTypeSealed() + .And.OverrideEquality(); + } + + } +} \ No newline at end of file diff --git a/src/SolidStack.Core.Equality.Tests/SolidStack.Core.Equality.Tests.csproj b/src/SolidStack.Core.Equality.Tests/SolidStack.Core.Equality.Tests.csproj new file mode 100644 index 0000000..cf92525 --- /dev/null +++ b/src/SolidStack.Core.Equality.Tests/SolidStack.Core.Equality.Tests.csproj @@ -0,0 +1,22 @@ + + + + netcoreapp2.0 + + false + + + + + + + + + + + + + + + + diff --git a/src/SolidStack.Core.Equality/EqualityComparer.cs b/src/SolidStack.Core.Equality/EqualityComparer.cs new file mode 100644 index 0000000..eeac093 --- /dev/null +++ b/src/SolidStack.Core.Equality/EqualityComparer.cs @@ -0,0 +1,266 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Reflection; +using SolidStack.Core.Equality.Internal; +using SolidStack.Core.Guards; + +namespace SolidStack.Core.Equality +{ + public static class EqualityComparer + { + /// + /// Creates an equality comparer that compare the equality of two sequences based on the equality of each element. + /// + /// The type of the sequences to compare. + /// The elementwise equality comparer. + public static IEqualityComparer ByElements() + where T : class, IEnumerable => + For( + (x, y) => x.SequenceEqual(y), + obj => obj.GetSequenceHashCode()); + + /// + /// Creates an equality comparer that compare the equality of two objects based on the equality of each field. + /// + /// The type of the objects to compare. + /// The fieldwise equality comparer. + public static IEqualityComparer ByFields() + where T : class => + ByFields(fields => fields); + + /// + /// Creates an equality comparer that compare the equality of two objects based on the equality of each selected field. + /// + /// The type of the objects to compare. + /// The filter function used to select the fields to compare. + /// The fieldwise equality comparer. + public static IEqualityComparer ByFields(Func predicate) + where T : class + { + Guard.RequiresNonNull(predicate, nameof(predicate)); + + return ByFields(fields => fields.Where(predicate)); + } + + /// + /// Creates an equality comparer that compare the equality of two objects based on the equality of each selected field. + /// + /// The type of the objects to compare. + /// The filter function used to select the fields to compare. + /// The fieldwise equality comparer. + public static IEqualityComparer ByFields( + Func, IEnumerable> selector) + where T : class + { + Guard.RequiresNonNull(selector, nameof(selector)); + + return ByMembers(members => selector(members.OfType())); + } + + /// + /// Creates an equality comparer that compare the equality of two objects based on the equality of a field or a + /// property that act as a key. + /// + /// The type of the objects to compare. + /// The function used to select the key. + /// The keywise equality comparer. + public static IEqualityComparer ByKey(Func keyPath) + where T : class + { + Guard.RequiresNonNull(keyPath, nameof(keyPath)); + + return For( + (x, y) => keyPath(x).Equals(keyPath(y)), + obj => obj.GetType().GetHashCode() ^ keyPath(obj).GetHashCode()); + } + + /// + /// Creates an equality comparer that compare the equality of two objects based on the equality of each field or + /// property. + /// + /// The type of the objects to compare. + /// The memberwise equality comparer. + public static IEqualityComparer ByMembers() + where T : class => + ByMembers(members => members); + + /// + /// Creates an equality comparer that compare the equality of two objects based on the equality of each selected field + /// or property. + /// + /// The type of the objects to compare. + /// The filter function used to select the members to compare. + /// + public static IEqualityComparer ByMembers(Func predicate) + where T : class + { + Guard.RequiresNonNull(predicate, nameof(predicate)); + + return ByMembers(members => members.Where(predicate)); + } + + /// + /// Creates an equality comparer that compare the equality of two objects based on the equality of each selected field + /// or property. + /// + /// The type of the objects to compare. + /// The filter function used to select the members to compare. + /// The memberwise equality comparer. + public static IEqualityComparer ByMembers( + Func, IEnumerable> selector) + where T : class + { + Guard.RequiresNonNull(selector, nameof(selector)); + + return MemberwiseEqualityComparerFactory.Create(selector); + } + + /// + /// Creates an equality comparer that compare the equality of two objects based on the equality of each property. + /// + /// The type of the objects to compare. + /// The propertywise equality comparer. + public static IEqualityComparer ByProperties() + where T : class => + ByProperties(properties => properties); + + /// + /// Creates an equality comparer that compare the equality of two objects based on the equality of each selected + /// property. + /// + /// The type of the objects to compare. + /// The filter function used to select the properties to compare. + /// The propertywise equality comparer. + public static IEqualityComparer ByProperties(Func predicate) + where T : class + { + Guard.RequiresNonNull(predicate, nameof(predicate)); + + return ByProperties(properties => properties.Where(predicate)); + } + + /// + /// Creates an equality comparer that compare the equality of two objects based on the equality of each selected + /// property. + /// + /// The type of the objects to compare. + /// The filter function used to select the properties to compare + /// The propertywise equality comparer. + public static IEqualityComparer ByProperties( + Func, IEnumerable> selector) + where T : class + { + Guard.RequiresNonNull(selector, nameof(selector)); + + return ByMembers(members => selector(members.OfType())); + } + + /// + /// Creates an equality comparer that compare the equality of two objects based on the equality of each field tagged by + /// . + /// + /// The type of the objects to compare. + /// The fieldwise equality comparer. + public static IEqualityComparer ByTaggedFields() + where T : class => + ByTaggedFields(typeof(EqualityMemberAttribute)); + + /// + /// Creates an equality comparer that compare the equality of two objects based on the equality of each field tagged by + /// the specified attributes. + /// + /// The type of the objects to compare. + /// + /// The fieldwise equality comparer. + public static IEqualityComparer ByTaggedFields(params Type[] attributeTypes) + where T : class + { + InnerGuard.RequiresValidAttributeTypes(attributeTypes); + + return ByFields(field => field.HasAnyAttribute(attributeTypes)); + } + + /// + /// Creates an equality comparer that compare the equality of two objects based on the equality of each field or + /// property tagged by . + /// + /// The type of the objects to compare. + /// The memberwise equality comparer. + public static IEqualityComparer ByTaggedMembers() + where T : class => + ByTaggedMembers(typeof(EqualityMemberAttribute)); + + /// + /// Creates an equality comparer that compare the equality of two objects based on the equality of each field or + /// property tagged by the specified attributes. + /// + /// The type of the objects to compare. + /// + /// The memberwise equality comparer. + public static IEqualityComparer ByTaggedMembers(params Type[] attributeTypes) + where T : class + { + InnerGuard.RequiresValidAttributeTypes(attributeTypes); + + return ByMembers(member => member.HasAnyAttribute(attributeTypes)); + } + + /// + /// Creates an equality comparer that compare the equality of two objects based on the equality of each property tagged + /// by . + /// + /// The type of the objects to compare. + /// The propertywise equality comparer. + public static IEqualityComparer ByTaggedProperties() + where T : class => + ByTaggedProperties(typeof(EqualityMemberAttribute)); + + /// + /// Creates an equality comparer that compare the equality of two objects based on the equality of each property tagged + /// by the specified attributes. + /// + /// The type of the objects to compare. + /// + /// The propertywise equality comparer. + public static IEqualityComparer ByTaggedProperties(params Type[] attributeTypes) + where T : class + { + InnerGuard.RequiresValidAttributeTypes(attributeTypes); + + return ByProperties(property => property.HasAnyAttribute(attributeTypes)); + } + + /// + /// Creates a custom equality comparer. + /// + /// The type of the objects to compare. + /// The function used to validate the equality of the object. + /// The function used to generate the hash code of an object. + /// The custom equality comparer. + public static IEqualityComparer For(Func equals, Func getHashCode) + where T : class + { + Guard.RequiresNonNull(equals, nameof(equals)); + Guard.RequiresNonNull(getHashCode, nameof(getHashCode)); + + return new EqualityComparerFunc(equals, getHashCode); + } + + private static bool HasAnyAttribute(this MemberInfo member, IEnumerable attributeTypes) => + attributeTypes.Any(attributeType => Attribute.IsDefined(member, attributeType)); + + private static class InnerGuard + { + [SuppressMessage("ReSharper", "PossibleMultipleEnumeration")] + public static void RequiresValidAttributeTypes(IEnumerable attributeTypes) + { + Guard.RequiresNoNullIn(attributeTypes, nameof(attributeTypes)); + Guard.RequiresAll(attributeTypes, type => type.IsSubclassOf(typeof(Attribute)), + $"Receiving {nameof(attributeTypes)} containing one or more types that are not a subclass of Attribute."); + } + } + } +} \ No newline at end of file diff --git a/src/SolidStack.Core.Equality/EqualityMemberAttribute.cs b/src/SolidStack.Core.Equality/EqualityMemberAttribute.cs new file mode 100644 index 0000000..b4e1225 --- /dev/null +++ b/src/SolidStack.Core.Equality/EqualityMemberAttribute.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; + +namespace SolidStack.Core.Equality +{ + /// + /// + /// Tags a field or a property that should be used to evaluate the equality of the object. + /// + /// + /// To construct a that compare equality of objects based on all members of these + /// objects tagged by this attribute use the method. + /// + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] + public class EqualityMemberAttribute : Attribute + { + } +} \ No newline at end of file diff --git a/src/SolidStack.Core.Equality/Equatable.cs b/src/SolidStack.Core.Equality/Equatable.cs new file mode 100644 index 0000000..2d39bd7 --- /dev/null +++ b/src/SolidStack.Core.Equality/Equatable.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; + +namespace SolidStack.Core.Equality +{ + /// + /// + /// Provides the boilerplate required to alter the equality of an object by overriding the equality methods, the + /// equality operators and the method. + /// + /// + /// The concrete type that should be equatable. + /// This is almost always the type that derives from , + /// e.g. class Foo : Equatable<Foo> + /// + public abstract class Equatable : + IEquatable + where TSelf : Equatable + { + private static IEqualityComparer CachedEqualityComparer { get; set; } + + private IEqualityComparer EqualityComparer => + CachedEqualityComparer ?? (CachedEqualityComparer = GetEqualityComparer()); + + /// + public bool Equals(TSelf other) => + EqualityComparer.Equals(this as TSelf, other); + + public static bool operator ==(Equatable x, Equatable y) => + // True if both references are equal. + ReferenceEquals(x, y) || + // True if both objects are equal. + x?.EqualityComparer.Equals(x as TSelf, y as TSelf) == true; + + public static bool operator !=(Equatable x, Equatable y) => + !(x == y); + + /// + public override bool Equals(object obj) => + Equals(obj as TSelf); + + /// + public override int GetHashCode() => + EqualityComparer.GetHashCode((TSelf) this); + + /// + /// Returns the equality comparer used to define how the equality of the object should be done. + /// + /// + /// The given equality comparer is cached to increase performance. + /// + /// The equality comparer. + protected abstract IEqualityComparer GetEqualityComparer(); + } +} \ No newline at end of file diff --git a/src/SolidStack.Core.Equality/Internal/EnumerableExtensions.cs b/src/SolidStack.Core.Equality/Internal/EnumerableExtensions.cs new file mode 100644 index 0000000..d97f649 --- /dev/null +++ b/src/SolidStack.Core.Equality/Internal/EnumerableExtensions.cs @@ -0,0 +1,32 @@ +using System.Collections; +using System.Linq; + +namespace SolidStack.Core.Equality.Internal +{ + internal static class EnumerableExtensions + { + private const int HashCodeSeed = 17; + + private const int HashCodeMultiplier = 89; + + public static int GetSequenceHashCode(this IEnumerable @this) + { + if (@this is null) + return 0; + + unchecked + { + return @this.Cast().Aggregate(HashCodeSeed, + (current, o) => (current * HashCodeMultiplier) ^ (o?.GetHashCode() ?? 0)); + } + } + + public static bool SequenceEqual(this IEnumerable @this, IEnumerable other) => + // True if both references are equal. + ReferenceEquals(@this, other) || + // False if only one sequence is null. + !(@this is null) && !(other is null) && + // True if all members of both sequences are equal. + Enumerable.SequenceEqual(@this.Cast(), other.Cast()); + } +} \ No newline at end of file diff --git a/src/SolidStack.Core.Equality/Internal/EqualityComparerFunc.cs b/src/SolidStack.Core.Equality/Internal/EqualityComparerFunc.cs new file mode 100644 index 0000000..dfbe2a1 --- /dev/null +++ b/src/SolidStack.Core.Equality/Internal/EqualityComparerFunc.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace SolidStack.Core.Equality.Internal +{ + internal class EqualityComparerFunc : IEqualityComparer + where T : class + { + public EqualityComparerFunc(Func equalsFunc, Func getHashCodeFunc) + { + EqualsFunc = equalsFunc; + GetHashCodeFunc = getHashCodeFunc; + } + + private Func EqualsFunc { get; } + + private Func GetHashCodeFunc { get; } + + public bool Equals(T x, T y) => + // True if both references are equal. + ReferenceEquals(x, y) || + // False if only one object is null. + !(x is null) && !(y is null) && + // True if the delegate function validate the equality. + EqualsFunc(x, y); + + [SuppressMessage("ReSharper", "ConditionIsAlwaysTrueOrFalse")] + public int GetHashCode(T obj) => + obj is null + ? 0 + : GetHashCodeFunc(obj); + } +} \ No newline at end of file diff --git a/src/SolidStack.Core.Equality/Internal/MemberwiseEqualityComparerFactory.cs b/src/SolidStack.Core.Equality/Internal/MemberwiseEqualityComparerFactory.cs new file mode 100644 index 0000000..eb44d4b --- /dev/null +++ b/src/SolidStack.Core.Equality/Internal/MemberwiseEqualityComparerFactory.cs @@ -0,0 +1,141 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; + +namespace SolidStack.Core.Equality.Internal +{ + internal static class MemberwiseEqualityComparerFactory + { + private const int HashCodeSeed = 29; + + private const int HashCodeMultiplier = 103; + + private static BindingFlags AllInstanceMembersBindingFlags => + BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; + + public static IEqualityComparer Create( + Func, IEnumerable> memberSelector) + where T : class => + Create(memberSelector(GetMembers())); + + private static IEqualityComparer Create(IEnumerable members) + where T : class + { + var memberList = members.ToList(); + + return new EqualityComparerFunc( + MakeEqualsMethod(memberList), + MakeGetHashCodeMethod(memberList)); + } + + private static IEnumerable GetMembers() + { + var type = typeof(T).GetTypeInfo(); + + return type + .GetFields(AllInstanceMembersBindingFlags) + .Cast() + .Concat(type.GetProperties(AllInstanceMembersBindingFlags)) + .Where(member => !member.Name.EndsWith(">k__BackingField")); + } + + private static bool IsSequenceType(Type type) => + typeof(IEnumerable).GetTypeInfo().IsAssignableFrom(type) && type != typeof(string); + + private static Expression LinkHashCodeExpression(Expression x, Expression y) + { + var xMultipliedExpr = Expression.Multiply(x, Expression.Constant(HashCodeMultiplier)); + return Expression.ExclusiveOr(xMultipliedExpr, y); + } + + private static Expression MakeEqualsExpression(MemberInfo member, Expression x, Expression y) + { + var xMemberExpr = Expression.MakeMemberAccess(x, member); + var yMemberExpr = Expression.MakeMemberAccess(y, member); + + var memberType = xMemberExpr.Type; + + // If the member is a reference type, \ + if (!memberType.GetTypeInfo().IsValueType) + // we create an expression that invoke Enumerable.SequenceEqual if obj is a sequence or obj.Equals otherwise. + return IsSequenceType(memberType) + ? MakeSequenceTypeEqualExpression(xMemberExpr, yMemberExpr) + : MakeReferenceTypeEqualExpression(xMemberExpr, yMemberExpr); + + var xMemberAsObjExpr = Expression.Convert(xMemberExpr, typeof(object)); + var yMemberAsObjExpr = Expression.Convert(yMemberExpr, typeof(object)); + + return MakeReferenceTypeEqualExpression(xMemberAsObjExpr, yMemberAsObjExpr); + } + + private static Func MakeEqualsMethod(IEnumerable members) + { + // We create the method parameters. + var xParamExpr = Expression.Parameter(typeof(T), "x"); + var yParamExpr = Expression.Parameter(typeof(T), "y"); + + // We create the AND chain expression using short-circuit evaluation. + var equalsExprs = members.Select(member => MakeEqualsExpression(member, xParamExpr, yParamExpr)); + var andChainExpr = equalsExprs.Aggregate((Expression) Expression.Constant(true), Expression.AndAlso); + + // We compile the AND chain expression into a function. + var andChainFunc = Expression.Lambda>(andChainExpr, xParamExpr, yParamExpr).Compile(); + + // We returns false if the types are different or we evaluate the AND chain otherwise. + return (x, y) => x.GetType() == y.GetType() && andChainFunc(x, y); + } + + private static Expression MakeGetHashCodeExpression(MemberInfo member, Expression obj) + { + var memberExpr = Expression.MakeMemberAccess(obj, member); + var memberAsObjExpr = Expression.Convert(memberExpr, typeof(object)); + + var memberType = memberExpr.Type; + + // We create an expression that invoke EnumerableExtensions.GetSequenceHashCode if obj is a sequence or obj.GetHashCode otherwise. + var getHashCodeExpr = IsSequenceType(memberType) + ? MakeSequenceTypeGetHashCodeExpression(memberExpr) + : MakeReferenceTypeGetHashCodeExpression(memberAsObjExpr); + + return Expression.Condition( + // If the argument is null, \ + Expression.ReferenceEqual(Expression.Constant(null), memberAsObjExpr), + // we return 0 \ + Expression.Constant(0), + // otherwise we invoke obj.GetHashCode or GetSequenceHashCode if obj is a sequence. + getHashCodeExpr); + } + + private static Func MakeGetHashCodeMethod(IEnumerable members) + { + // We create the methhod parameter. + var objParamExpr = Expression.Parameter(typeof(T), "obj"); + + // We create the XOR chain expression. + var getHashCodeExprs = members.Select(member => MakeGetHashCodeExpression(member, objParamExpr)); + var xorChainExpr = + getHashCodeExprs.Aggregate((Expression) Expression.Constant(HashCodeSeed), LinkHashCodeExpression); + + // We compile the XOR chain expression into a function. + var xorChainFunc = Expression.Lambda>(xorChainExpr, objParamExpr).Compile(); + + // We apply the XOR operator to the hash code of the given object type and the XOR chain. + return obj => obj.GetType().GetHashCode() ^ xorChainFunc(obj); + } + + private static Expression MakeReferenceTypeEqualExpression(Expression x, Expression y) => + Expression.Call(typeof(object), "Equals", Type.EmptyTypes, x, y); + + private static Expression MakeReferenceTypeGetHashCodeExpression(Expression obj) => + Expression.Call(obj, "GetHashCode", Type.EmptyTypes); + + private static Expression MakeSequenceTypeEqualExpression(Expression x, Expression y) => + Expression.Call(typeof(EnumerableExtensions), "SequenceEqual", Type.EmptyTypes, x, y); + + private static Expression MakeSequenceTypeGetHashCodeExpression(Expression obj) => + Expression.Call(typeof(EnumerableExtensions), "GetSequenceHashCode", Type.EmptyTypes, obj); + } +} \ No newline at end of file diff --git a/src/SolidStack.Core.Equality/SolidStack.Core.Equality.csproj b/src/SolidStack.Core.Equality/SolidStack.Core.Equality.csproj new file mode 100644 index 0000000..4bb4d2e --- /dev/null +++ b/src/SolidStack.Core.Equality/SolidStack.Core.Equality.csproj @@ -0,0 +1,15 @@ + + + + netstandard2.0 + + + + + + + + + + + diff --git a/src/SolidStack.Core.Equality/SolidStack.Core.Equality.nuspec b/src/SolidStack.Core.Equality/SolidStack.Core.Equality.nuspec new file mode 100644 index 0000000..7b23750 --- /dev/null +++ b/src/SolidStack.Core.Equality/SolidStack.Core.Equality.nuspec @@ -0,0 +1,23 @@ + + + + SolidStack.Core.Equality + $version$ + SolidStack.Core.Equality is primarily useful when you have to tweak the equality of an object to implement the Value Object Pattern. All you have to do is use one of the provided abstract classes and the complex equality logic will be done for you. + $authors$ + $title$ + $projectUrl$ + $licenseUrl$ + $iconUrl$ + $requireLicenseAcceptance$ + + $copyright$ + SolidStack Equality + + + + + + + + \ No newline at end of file diff --git a/src/SolidStack.Core.Flow.Tests/OptionAdaptersTests.cs b/src/SolidStack.Core.Flow.Tests/OptionAdaptersTests.cs index 3b4ac6c..2b5f213 100644 --- a/src/SolidStack.Core.Flow.Tests/OptionAdaptersTests.cs +++ b/src/SolidStack.Core.Flow.Tests/OptionAdaptersTests.cs @@ -141,7 +141,9 @@ public void WhereSome_WithPredicate_FiltersTheEmptyOptions() var sequence = new[] {4, -1, 5, -3}; var values = sequence.WhereSome(x => x >= 0 ? Option.Some(x) : Option.None()); - values.Should().HaveCount(2).And.ContainInOrder(4, 5); + values.Should() + .HaveCount(2) + .And.ContainInOrder(4, 5); } } } \ No newline at end of file diff --git a/src/SolidStack.Core.Flow.Tests/SolidStack.Core.Flow.Tests.csproj b/src/SolidStack.Core.Flow.Tests/SolidStack.Core.Flow.Tests.csproj index d33429e..a2a3e32 100644 --- a/src/SolidStack.Core.Flow.Tests/SolidStack.Core.Flow.Tests.csproj +++ b/src/SolidStack.Core.Flow.Tests/SolidStack.Core.Flow.Tests.csproj @@ -7,7 +7,7 @@ - + diff --git a/src/SolidStack.Core.Flow/OptionAdapters.cs b/src/SolidStack.Core.Flow/OptionAdapters.cs index 8ddc21a..af5aa8b 100644 --- a/src/SolidStack.Core.Flow/OptionAdapters.cs +++ b/src/SolidStack.Core.Flow/OptionAdapters.cs @@ -196,7 +196,7 @@ public static IEnumerable WhereSome(this IEnumerable> sequence) { Guard.RequiresNonNull(sequence, nameof(sequence)); - return sequence.SelectMany(option => option.AsEnumerable()).ToList(); + return sequence.SelectMany(option => option.AsEnumerable()); } /// diff --git a/src/SolidStack.Core.Flow/SolidStack.Core.Flow.nuspec b/src/SolidStack.Core.Flow/SolidStack.Core.Flow.nuspec index 1404858..b140e73 100644 --- a/src/SolidStack.Core.Flow/SolidStack.Core.Flow.nuspec +++ b/src/SolidStack.Core.Flow/SolidStack.Core.Flow.nuspec @@ -14,7 +14,7 @@ $copyright$ SolidStack Flow OptionPattern ResultPattern EitherPattern RailwayOrientedProgramming FunctionalProgramming - + diff --git a/src/SolidStack.Core.Guards/SolidStack.Core.Guards.nuspec b/src/SolidStack.Core.Guards/SolidStack.Core.Guards.nuspec index 042cb79..a3d3e96 100644 --- a/src/SolidStack.Core.Guards/SolidStack.Core.Guards.nuspec +++ b/src/SolidStack.Core.Guards/SolidStack.Core.Guards.nuspec @@ -3,7 +3,7 @@ SolidStack.Core.Guards $version$ - SolidStack.Core.Guards is an extremely simple, unambiguous and lightweight guard clause library. + SolidStack.Core.Guards is an extremely simple, unambiguous and lightweight guard clause library that allow you to write pre-conditions and post-conditions for your methods in a readable way. $authors$ $title$ $projectUrl$ diff --git a/src/SolidStack.sln b/src/SolidStack.sln index 48f5e42..2ca9e83 100644 --- a/src/SolidStack.sln +++ b/src/SolidStack.sln @@ -17,6 +17,18 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Core", "Core", "{AD1E680F-C EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Testing", "Testing", "{50EFF3CA-6249-4DEB-9CAC-D71C1DFFD922}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SolidStack.Core.Equality", "SolidStack.Core.Equality\SolidStack.Core.Equality.csproj", "{0B8DC425-446E-4F64-8E08-CDBD9B6B90EB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SolidStack.Core.Equality.Tests", "SolidStack.Core.Equality.Tests\SolidStack.Core.Equality.Tests.csproj", "{5C6B6CCD-49C0-42A3-96F0-244159E192B6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SolidStack.Core.Equality.Testing", "SolidStack.Core.Equality.Testing\SolidStack.Core.Equality.Testing.csproj", "{0F4F6EC6-02AD-4759-B8FD-1530A23DB690}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Equality", "Equality", "{CE6BEC8D-8C71-4F81-8984-D34DE98ECA7A}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Flow", "Flow", "{CB9005C2-1D3F-458B-8456-1EC76B7B737D}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Guards", "Guards", "{3E7B9F09-84C0-4FA8-9E45-1FED01E918E3}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -43,16 +55,34 @@ Global {C0E17594-BC69-4D31-ABD8-E74773D2CAD1}.Debug|Any CPU.Build.0 = Debug|Any CPU {C0E17594-BC69-4D31-ABD8-E74773D2CAD1}.Release|Any CPU.ActiveCfg = Release|Any CPU {C0E17594-BC69-4D31-ABD8-E74773D2CAD1}.Release|Any CPU.Build.0 = Release|Any CPU + {0B8DC425-446E-4F64-8E08-CDBD9B6B90EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0B8DC425-446E-4F64-8E08-CDBD9B6B90EB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0B8DC425-446E-4F64-8E08-CDBD9B6B90EB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0B8DC425-446E-4F64-8E08-CDBD9B6B90EB}.Release|Any CPU.Build.0 = Release|Any CPU + {5C6B6CCD-49C0-42A3-96F0-244159E192B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5C6B6CCD-49C0-42A3-96F0-244159E192B6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5C6B6CCD-49C0-42A3-96F0-244159E192B6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5C6B6CCD-49C0-42A3-96F0-244159E192B6}.Release|Any CPU.Build.0 = Release|Any CPU + {0F4F6EC6-02AD-4759-B8FD-1530A23DB690}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0F4F6EC6-02AD-4759-B8FD-1530A23DB690}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0F4F6EC6-02AD-4759-B8FD-1530A23DB690}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0F4F6EC6-02AD-4759-B8FD-1530A23DB690}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution - {0C7E071B-021D-48F3-9306-A10B38A034D8} = {AD1E680F-CAD7-4F71-A153-010E963A8685} - {3F4308F7-20C6-4E5E-BB94-8681BEEF3E3A} = {AD1E680F-CAD7-4F71-A153-010E963A8685} + {0C7E071B-021D-48F3-9306-A10B38A034D8} = {3E7B9F09-84C0-4FA8-9E45-1FED01E918E3} + {3F4308F7-20C6-4E5E-BB94-8681BEEF3E3A} = {3E7B9F09-84C0-4FA8-9E45-1FED01E918E3} {5AE73D94-930C-4AA3-AA1D-704B9B28FB5C} = {50EFF3CA-6249-4DEB-9CAC-D71C1DFFD922} - {193DDAA2-AD28-4892-B6D9-C155EA6BA698} = {AD1E680F-CAD7-4F71-A153-010E963A8685} - {C0E17594-BC69-4D31-ABD8-E74773D2CAD1} = {AD1E680F-CAD7-4F71-A153-010E963A8685} + {193DDAA2-AD28-4892-B6D9-C155EA6BA698} = {CB9005C2-1D3F-458B-8456-1EC76B7B737D} + {C0E17594-BC69-4D31-ABD8-E74773D2CAD1} = {CB9005C2-1D3F-458B-8456-1EC76B7B737D} + {0B8DC425-446E-4F64-8E08-CDBD9B6B90EB} = {CE6BEC8D-8C71-4F81-8984-D34DE98ECA7A} + {5C6B6CCD-49C0-42A3-96F0-244159E192B6} = {CE6BEC8D-8C71-4F81-8984-D34DE98ECA7A} + {0F4F6EC6-02AD-4759-B8FD-1530A23DB690} = {CE6BEC8D-8C71-4F81-8984-D34DE98ECA7A} + {CE6BEC8D-8C71-4F81-8984-D34DE98ECA7A} = {AD1E680F-CAD7-4F71-A153-010E963A8685} + {CB9005C2-1D3F-458B-8456-1EC76B7B737D} = {AD1E680F-CAD7-4F71-A153-010E963A8685} + {3E7B9F09-84C0-4FA8-9E45-1FED01E918E3} = {AD1E680F-CAD7-4F71-A153-010E963A8685} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {A31C7CBE-244A-4FA0-9D34-B202D205602E}