diff --git a/Src/FluentAssertions/FluentAssertions.csproj b/Src/FluentAssertions/FluentAssertions.csproj
index 1a3d1f3de8..c12fb3b555 100644
--- a/Src/FluentAssertions/FluentAssertions.csproj
+++ b/Src/FluentAssertions/FluentAssertions.csproj
@@ -40,6 +40,9 @@
<_Parameter1>FluentAssertions.Specs, PublicKey=00240000048000009400000006020000002400005253413100040000010001002d25ff515c85b13ba08f61d466cff5d80a7f28ba197bbf8796085213e7a3406f970d2a4874932fed35db546e89af2da88c194bf1b7f7ac70de7988c78406f7629c547283061282a825616eb7eb48a9514a7570942936020a9bb37dca9ff60b778309900851575614491c6d25018fadb75828f4c7a17bf2d7dc86e7b6eafc5d8f
+
+ <_Parameter1>FluentAssertions.Equivalency.Specs, PublicKey=00240000048000009400000006020000002400005253413100040000010001002d25ff515c85b13ba08f61d466cff5d80a7f28ba197bbf8796085213e7a3406f970d2a4874932fed35db546e89af2da88c194bf1b7f7ac70de7988c78406f7629c547283061282a825616eb7eb48a9514a7570942936020a9bb37dca9ff60b778309900851575614491c6d25018fadb75828f4c7a17bf2d7dc86e7b6eafc5d8f
+
<_Parameter1>Benchmarks, PublicKey=00240000048000009400000006020000002400005253413100040000010001002d25ff515c85b13ba08f61d466cff5d80a7f28ba197bbf8796085213e7a3406f970d2a4874932fed35db546e89af2da88c194bf1b7f7ac70de7988c78406f7629c547283061282a825616eb7eb48a9514a7570942936020a9bb37dca9ff60b778309900851575614491c6d25018fadb75828f4c7a17bf2d7dc86e7b6eafc5d8f
diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt
index adda36a474..b449218afb 100644
--- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt
+++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt
@@ -1,5 +1,6 @@
[assembly: System.Reflection.AssemblyMetadata("RepositoryUrl", "https://github.com/fluentassertions/fluentassertions")]
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo(@"Benchmarks, PublicKey=00240000048000009400000006020000002400005253413100040000010001002d25ff515c85b13ba08f61d466cff5d80a7f28ba197bbf8796085213e7a3406f970d2a4874932fed35db546e89af2da88c194bf1b7f7ac70de7988c78406f7629c547283061282a825616eb7eb48a9514a7570942936020a9bb37dca9ff60b778309900851575614491c6d25018fadb75828f4c7a17bf2d7dc86e7b6eafc5d8f")]
+[assembly: System.Runtime.CompilerServices.InternalsVisibleTo(@"FluentAssertions.Equivalency.Specs, PublicKey=00240000048000009400000006020000002400005253413100040000010001002d25ff515c85b13ba08f61d466cff5d80a7f28ba197bbf8796085213e7a3406f970d2a4874932fed35db546e89af2da88c194bf1b7f7ac70de7988c78406f7629c547283061282a825616eb7eb48a9514a7570942936020a9bb37dca9ff60b778309900851575614491c6d25018fadb75828f4c7a17bf2d7dc86e7b6eafc5d8f")]
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo(@"FluentAssertions.Specs, PublicKey=00240000048000009400000006020000002400005253413100040000010001002d25ff515c85b13ba08f61d466cff5d80a7f28ba197bbf8796085213e7a3406f970d2a4874932fed35db546e89af2da88c194bf1b7f7ac70de7988c78406f7629c547283061282a825616eb7eb48a9514a7570942936020a9bb37dca9ff60b778309900851575614491c6d25018fadb75828f4c7a17bf2d7dc86e7b6eafc5d8f")]
[assembly: System.Runtime.Versioning.TargetFramework(".NETFramework,Version=v4.7", FrameworkDisplayName=".NET Framework 4.7")]
namespace FluentAssertions
diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp2.1.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp2.1.verified.txt
index 6540a4e08b..d5e90c248e 100644
--- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp2.1.verified.txt
+++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp2.1.verified.txt
@@ -1,5 +1,6 @@
[assembly: System.Reflection.AssemblyMetadata("RepositoryUrl", "https://github.com/fluentassertions/fluentassertions")]
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo(@"Benchmarks, PublicKey=00240000048000009400000006020000002400005253413100040000010001002d25ff515c85b13ba08f61d466cff5d80a7f28ba197bbf8796085213e7a3406f970d2a4874932fed35db546e89af2da88c194bf1b7f7ac70de7988c78406f7629c547283061282a825616eb7eb48a9514a7570942936020a9bb37dca9ff60b778309900851575614491c6d25018fadb75828f4c7a17bf2d7dc86e7b6eafc5d8f")]
+[assembly: System.Runtime.CompilerServices.InternalsVisibleTo(@"FluentAssertions.Equivalency.Specs, PublicKey=00240000048000009400000006020000002400005253413100040000010001002d25ff515c85b13ba08f61d466cff5d80a7f28ba197bbf8796085213e7a3406f970d2a4874932fed35db546e89af2da88c194bf1b7f7ac70de7988c78406f7629c547283061282a825616eb7eb48a9514a7570942936020a9bb37dca9ff60b778309900851575614491c6d25018fadb75828f4c7a17bf2d7dc86e7b6eafc5d8f")]
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo(@"FluentAssertions.Specs, PublicKey=00240000048000009400000006020000002400005253413100040000010001002d25ff515c85b13ba08f61d466cff5d80a7f28ba197bbf8796085213e7a3406f970d2a4874932fed35db546e89af2da88c194bf1b7f7ac70de7988c78406f7629c547283061282a825616eb7eb48a9514a7570942936020a9bb37dca9ff60b778309900851575614491c6d25018fadb75828f4c7a17bf2d7dc86e7b6eafc5d8f")]
[assembly: System.Runtime.Versioning.TargetFramework(".NETCoreApp,Version=v2.1", FrameworkDisplayName="")]
namespace FluentAssertions
diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp3.0.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp3.0.verified.txt
index d3f72b2cbf..ee25dd44c0 100644
--- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp3.0.verified.txt
+++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp3.0.verified.txt
@@ -1,5 +1,6 @@
[assembly: System.Reflection.AssemblyMetadata("RepositoryUrl", "https://github.com/fluentassertions/fluentassertions")]
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo(@"Benchmarks, PublicKey=00240000048000009400000006020000002400005253413100040000010001002d25ff515c85b13ba08f61d466cff5d80a7f28ba197bbf8796085213e7a3406f970d2a4874932fed35db546e89af2da88c194bf1b7f7ac70de7988c78406f7629c547283061282a825616eb7eb48a9514a7570942936020a9bb37dca9ff60b778309900851575614491c6d25018fadb75828f4c7a17bf2d7dc86e7b6eafc5d8f")]
+[assembly: System.Runtime.CompilerServices.InternalsVisibleTo(@"FluentAssertions.Equivalency.Specs, PublicKey=00240000048000009400000006020000002400005253413100040000010001002d25ff515c85b13ba08f61d466cff5d80a7f28ba197bbf8796085213e7a3406f970d2a4874932fed35db546e89af2da88c194bf1b7f7ac70de7988c78406f7629c547283061282a825616eb7eb48a9514a7570942936020a9bb37dca9ff60b778309900851575614491c6d25018fadb75828f4c7a17bf2d7dc86e7b6eafc5d8f")]
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo(@"FluentAssertions.Specs, PublicKey=00240000048000009400000006020000002400005253413100040000010001002d25ff515c85b13ba08f61d466cff5d80a7f28ba197bbf8796085213e7a3406f970d2a4874932fed35db546e89af2da88c194bf1b7f7ac70de7988c78406f7629c547283061282a825616eb7eb48a9514a7570942936020a9bb37dca9ff60b778309900851575614491c6d25018fadb75828f4c7a17bf2d7dc86e7b6eafc5d8f")]
[assembly: System.Runtime.Versioning.TargetFramework(".NETCoreApp,Version=v3.0", FrameworkDisplayName="")]
namespace FluentAssertions
diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.0.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.0.verified.txt
index 49034c7df4..e9d45913dc 100644
--- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.0.verified.txt
+++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.0.verified.txt
@@ -1,5 +1,6 @@
[assembly: System.Reflection.AssemblyMetadata("RepositoryUrl", "https://github.com/fluentassertions/fluentassertions")]
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo(@"Benchmarks, PublicKey=00240000048000009400000006020000002400005253413100040000010001002d25ff515c85b13ba08f61d466cff5d80a7f28ba197bbf8796085213e7a3406f970d2a4874932fed35db546e89af2da88c194bf1b7f7ac70de7988c78406f7629c547283061282a825616eb7eb48a9514a7570942936020a9bb37dca9ff60b778309900851575614491c6d25018fadb75828f4c7a17bf2d7dc86e7b6eafc5d8f")]
+[assembly: System.Runtime.CompilerServices.InternalsVisibleTo(@"FluentAssertions.Equivalency.Specs, PublicKey=00240000048000009400000006020000002400005253413100040000010001002d25ff515c85b13ba08f61d466cff5d80a7f28ba197bbf8796085213e7a3406f970d2a4874932fed35db546e89af2da88c194bf1b7f7ac70de7988c78406f7629c547283061282a825616eb7eb48a9514a7570942936020a9bb37dca9ff60b778309900851575614491c6d25018fadb75828f4c7a17bf2d7dc86e7b6eafc5d8f")]
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo(@"FluentAssertions.Specs, PublicKey=00240000048000009400000006020000002400005253413100040000010001002d25ff515c85b13ba08f61d466cff5d80a7f28ba197bbf8796085213e7a3406f970d2a4874932fed35db546e89af2da88c194bf1b7f7ac70de7988c78406f7629c547283061282a825616eb7eb48a9514a7570942936020a9bb37dca9ff60b778309900851575614491c6d25018fadb75828f4c7a17bf2d7dc86e7b6eafc5d8f")]
[assembly: System.Runtime.Versioning.TargetFramework(".NETStandard,Version=v2.0", FrameworkDisplayName="")]
namespace FluentAssertions
diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt
index 4a79a41215..84a9aa9c4e 100644
--- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt
+++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt
@@ -1,5 +1,6 @@
[assembly: System.Reflection.AssemblyMetadata("RepositoryUrl", "https://github.com/fluentassertions/fluentassertions")]
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo(@"Benchmarks, PublicKey=00240000048000009400000006020000002400005253413100040000010001002d25ff515c85b13ba08f61d466cff5d80a7f28ba197bbf8796085213e7a3406f970d2a4874932fed35db546e89af2da88c194bf1b7f7ac70de7988c78406f7629c547283061282a825616eb7eb48a9514a7570942936020a9bb37dca9ff60b778309900851575614491c6d25018fadb75828f4c7a17bf2d7dc86e7b6eafc5d8f")]
+[assembly: System.Runtime.CompilerServices.InternalsVisibleTo(@"FluentAssertions.Equivalency.Specs, PublicKey=00240000048000009400000006020000002400005253413100040000010001002d25ff515c85b13ba08f61d466cff5d80a7f28ba197bbf8796085213e7a3406f970d2a4874932fed35db546e89af2da88c194bf1b7f7ac70de7988c78406f7629c547283061282a825616eb7eb48a9514a7570942936020a9bb37dca9ff60b778309900851575614491c6d25018fadb75828f4c7a17bf2d7dc86e7b6eafc5d8f")]
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo(@"FluentAssertions.Specs, PublicKey=00240000048000009400000006020000002400005253413100040000010001002d25ff515c85b13ba08f61d466cff5d80a7f28ba197bbf8796085213e7a3406f970d2a4874932fed35db546e89af2da88c194bf1b7f7ac70de7988c78406f7629c547283061282a825616eb7eb48a9514a7570942936020a9bb37dca9ff60b778309900851575614491c6d25018fadb75828f4c7a17bf2d7dc86e7b6eafc5d8f")]
[assembly: System.Runtime.Versioning.TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName="")]
namespace FluentAssertions
diff --git a/Tests/FluentAssertions.Equivalency.Specs/.editorconfig b/Tests/FluentAssertions.Equivalency.Specs/.editorconfig
new file mode 100644
index 0000000000..436717d4cf
--- /dev/null
+++ b/Tests/FluentAssertions.Equivalency.Specs/.editorconfig
@@ -0,0 +1,141 @@
+[*.cs]
+
+# IDE0051: Private member is unused
+dotnet_diagnostic.IDE0051.severity = none
+# IDE0070: GetHashCode implementation can be simplified
+dotnet_diagnostic.IDE0070.severity = none
+
+# CA1002 Change 'List' to use 'Collection', 'ReadOnlyCollection' or 'KeyedCollection'
+dotnet_diagnostic.CA1002.severity = none
+# CA1003 Change the event to replace the type with a generic EventHandler
+dotnet_diagnostic.CA1003.severity = none
+# CA1008: Add a member to Color that has a value of zero with a suggested name of 'None'
+dotnet_diagnostic.CA1008.severity = none
+# CA1014 Mark assemblies with CLSCompliant
+dotnet_diagnostic.CA1014.severity = none
+# CA1017 Mark assembly with ComVisisble(false)
+dotnet_diagnostic.CA1017.severity = none
+# CA1024 Use properties where appropriate
+dotnet_diagnostic.CA1024.severity = none
+# CA1028: Enum Storage should be Int32
+dotnet_diagnostic.CA1028.severity = none
+# CA1032: Implement standard exception constructors
+dotnet_diagnostic.CA1032.severity = none
+# CA1036: Override methods on comparable types
+dotnet_diagnostic.CA1036.severity = none
+# CA1040: Avoid empty interfaces
+dotnet_diagnostic.CA1040.severity = none
+# CA1044: Properties should not be write only
+dotnet_diagnostic.CA1044.severity = none
+# CA1051: Do not declare visible instance fields
+dotnet_diagnostic.CA1051.severity = none
+# CA1062: Validate arguments of public methods
+dotnet_diagnostic.CA1062.severity = none
+# CA1064: Exceptions should be public
+dotnet_diagnostic.CA1064.severity = none
+# CA1307: Specify StringComparison
+dotnet_diagnostic.CA1307.severity = none
+# CA1506 Rewrite or refactor the code to decrease its class coupling
+dotnet_diagnostic.CA1506.severity = none
+# CA1707: Remove the underscores from member name
+dotnet_diagnostic.CA1707.severity = none
+# CA1711: Rename type name so that it does not end in 'Enum'
+dotnet_diagnostic.CA1711.severity = none
+# CA1714: Flags enums should have plural names
+dotnet_diagnostic.CA1714.severity = none
+# CA1716: Identifiers should not match keywords
+dotnet_diagnostic.CA1716.severity = none
+# CA1813 Avoid unsealed attributes
+dotnet_diagnostic.CA1813.severity = none
+# CA1814: Prefer jagged arrays over multidimensional
+dotnet_diagnostic.CA1814.severity = none
+# CA1818: Type is an internal class that is apparently never instantiated.
+dotnet_diagnostic.CA1812.severity = none
+# CA1822: Member does not access instance data and can be marked as static
+dotnet_diagnostic.CA1822.severity = none
+# CA2000: Dispose objects before losing scope
+dotnet_diagnostic.CA2000.severity = none
+# CA2201: Exception type System.Exception is not sufficiently specific
+dotnet_diagnostic.CA2201.severity = none
+# CA2208: Call the ArgumentNullException constructor that contains a message and/or paramName parameter
+dotnet_diagnostic.CA2208.severity = none
+# CA2227: Collection properties should be read only
+dotnet_diagnostic.CA2227.severity = none
+# CA5394 Random is an insecure random number generator
+dotnet_diagnostic.CA5394.severity = none
+
+# AV1000: Type contains the word 'and', which suggests it has multiple purposes
+dotnet_diagnostic.AV1000.severity = none
+# AV1008: Class should be non-static or its name should be suffixed with Extensions
+dotnet_diagnostic.AV1008.severity = none
+# AV1115: Member or local function contains the word 'and', which suggests doing multiple things
+dotnet_diagnostic.AV1115.severity = none
+# AV1135: Do not return null for strings, collections or tasks
+dotnet_diagnostic.AV1135.severity = none
+# AV1505: Namespace should match with assembly name
+dotnet_diagnostic.AV1505.severity = none
+# AV1564: Parameter in public or internal member is of type bool or bool?
+dotnet_diagnostic.AV1564.severity = none
+# AV1708: Type name contains term that should be avoided
+dotnet_diagnostic.AV1708.severity = none
+# AV1225: Method raises event, so it should be named
+dotnet_diagnostic.AV1225.severity = none
+# AV1250; Method returns the result of a query, which uses deferred execution
+dotnet_diagnostic.AV1250.severity = none
+# AV1507: File contains additional type
+dotnet_diagnostic.AV1507.severity = none
+# AV1532: Loop statement contains nested loop
+dotnet_diagnostic.AV1532.severity = none
+# AV1555: Parameter in the call to is invoked with a named argument
+dotnet_diagnostic.AV1555.severity = none
+# AV1704: Type contains one or more digits in its name
+dotnet_diagnostic.AV1704.severity = none
+# AV1706: Parameter should have a more descriptive name
+dotnet_diagnostic.AV1706.severity = none
+# AV1710: Property contains the name of its containing type
+dotnet_diagnostic.AV1710.severity = none
+# AV1755: Name of async method should end with Async or TaskAsync
+dotnet_diagnostic.AV1755.severity = none
+
+# SA0001: XmlCommentAnalysisDisabled
+dotnet_diagnostic.SA0001.severity = none
+# SA1001: CommasMustBeSpacedCorrectly
+dotnet_diagnostic.SA1001.severity = none
+# SA1009: Closing parenthesis should not be preceded by a space
+dotnet_diagnostic.SA1009.severity = none
+# SA1111: Closing parenthesis should be on line of the last parameter
+dotnet_diagnostic.SA1111.severity = none
+# SA1118: The parameter spans multiple lines
+dotnet_diagnostic.SA1118.severity = none
+# SA1122: Use string.Empty for empty strings
+dotnet_diagnostic.SA1122.severity = none
+# SA1124: Regions should not be used
+dotnet_diagnostic.SA1124.severity = none
+# SA1312: variable should begin with lower-case letter
+dotnet_diagnostic.SA1312.severity = none # re-enable if using statements can be discarded
+# SA1313: parameter should begin with lower-case letter
+dotnet_diagnostic.SA1313.severity = none # re-enable when parameters discards are available
+# SA1401: Field should be private
+dotnet_diagnostic.SA1401.severity = none
+# SA1402: File may only contain a single type
+dotnet_diagnostic.SA1402.severity = none
+# SA1403: File may only contain a single namespace
+dotnet_diagnostic.SA1403.severity = none
+# SA1404: Remove unused locals
+dotnet_diagnostic.SA1404.severity = none
+# SA1502: Element should not be on a single line
+dotnet_diagnostic.SA1502.severity = none
+# SA1600: Elements should be documented
+dotnet_diagnostic.SA1600.severity = none
+# SA1602: Enumeration items should be documented
+dotnet_diagnostic.SA1602.severity = none
+# SA1611: The documentation for parameter is missing
+dotnet_diagnostic.SA1611.severity = none
+# SA1615: Element return value should be documented
+dotnet_diagnostic.SA1615.severity = none
+
+# SA1005: Single line comments should begin with single space
+dotnet_diagnostic.SA1005.severity = suggestion
+
+# ReSharper/Rider
+resharper_expression_is_always_null_highlighting=none
diff --git a/Tests/FluentAssertions.Equivalency.Specs/AssertionRuleSpecs.cs b/Tests/FluentAssertions.Equivalency.Specs/AssertionRuleSpecs.cs
new file mode 100644
index 0000000000..a7c523b2c8
--- /dev/null
+++ b/Tests/FluentAssertions.Equivalency.Specs/AssertionRuleSpecs.cs
@@ -0,0 +1,274 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using FluentAssertions.Extensions;
+using Xunit;
+using Xunit.Sdk;
+
+namespace FluentAssertions.Equivalency.Specs
+{
+ public class AssertionRuleSpecs
+ {
+ [Fact]
+ public void When_two_objects_have_the_same_property_values_it_should_succeed()
+ {
+ // Arrange
+ var subject = new { Age = 36, Birthdate = new DateTime(1973, 9, 20), Name = "Dennis" };
+
+ var other = new { Age = 36, Birthdate = new DateTime(1973, 9, 20), Name = "Dennis" };
+
+ // Act / Assert
+ subject.Should().BeEquivalentTo(other);
+ }
+
+ [Fact]
+ public void When_two_objects_have_the_same_nullable_property_values_it_should_succeed()
+ {
+ // Arrange
+ var subject = new { Age = 36, Birthdate = (DateTime?)new DateTime(1973, 9, 20), Name = "Dennis" };
+
+ var other = new { Age = 36, Birthdate = (DateTime?)new DateTime(1973, 9, 20), Name = "Dennis" };
+
+ // Act / Assert
+ subject.Should().BeEquivalentTo(other);
+ }
+
+ [Fact]
+ public void When_two_objects_have_the_same_properties_but_a_different_value_it_should_throw()
+ {
+ // Arrange
+ var subject = new { Age = 36 };
+
+ var expectation = new { Age = 37 };
+
+ // Act
+ Action act = () => subject.Should().BeEquivalentTo(expectation, "because {0} are the same", "they");
+
+ // Assert
+ act.Should().Throw().WithMessage(
+ "Expected*Age*to be 37 because they are the same, but found 36*");
+ }
+
+ [Fact]
+ public void
+ When_subject_has_a_valid_property_that_is_compared_with_a_null_property_it_should_throw_with_descriptive_message()
+ {
+ // Arrange
+ var subject = new { Name = "Dennis" };
+
+ var other = new { Name = (string)null };
+
+ // Act
+ Action act = () => subject.Should().BeEquivalentTo(other, "we want to test the failure {0}", "message");
+
+ // Assert
+ act.Should().Throw().WithMessage(
+ "Expected property subject.Name to be *we want to test the failure message*, but found \"Dennis\"*");
+ }
+
+ [Fact]
+ public void When_two_collection_properties_dont_match_it_should_throw_and_specify_the_difference()
+ {
+ // Arrange
+ var subject = new { Values = new[] { 1, 2, 3 } };
+
+ var other = new { Values = new[] { 1, 4, 3 } };
+
+ // Act
+ Action act = () => subject.Should().BeEquivalentTo(other);
+
+ // Assert
+ act.Should().Throw().WithMessage(
+ "Expected*Values[1]*to be 4, but found 2*");
+ }
+
+ [Fact]
+ [SuppressMessage("ReSharper", "StringLiteralTypo")]
+ public void When_two_string_properties_do_not_match_it_should_throw_and_state_the_difference()
+ {
+ // Arrange
+ var subject = new { Name = "Dennes" };
+
+ var other = new { Name = "Dennis" };
+
+ // Act
+ Action act = () => subject.Should().BeEquivalentTo(other, options => options.Including(d => d.Name));
+
+ // Assert
+ act.Should().Throw().WithMessage(
+ "Expected*Name to be \"Dennis\", but \"Dennes\" differs near \"es\" (index 4)*");
+ }
+
+ [Fact]
+ public void When_two_properties_are_of_derived_types_but_are_equal_it_should_succeed()
+ {
+ // Arrange
+ var subject = new { Type = new DerivedCustomerType("123") };
+
+ var other = new { Type = new CustomerType("123") };
+
+ // Act
+ Action act = () => subject.Should().BeEquivalentTo(other);
+
+ // Assert
+ act.Should().NotThrow();
+ }
+
+ [Fact]
+ public void
+ When_two_properties_have_the_same_declared_type_but_different_runtime_types_and_are_equivalent_according_to_the_declared_type_it_should_succeed()
+ {
+ // Arrange
+ var subject = new { Type = (CustomerType)new DerivedCustomerType("123") };
+
+ var other = new { Type = new CustomerType("123") };
+
+ // Act
+ Action act = () => subject.Should().BeEquivalentTo(other);
+
+ // Assert
+ act.Should().NotThrow();
+ }
+
+ [Fact]
+ public void When_a_nested_property_is_equal_based_on_equality_comparer_it_should_not_throw()
+ {
+ // Arrange
+ var subject = new { Timestamp = 22.March(2020).At(19, 30) };
+
+ var expectation = new { Timestamp = 1.January(2020).At(7, 31) };
+
+ // Act
+ Action act = () => subject.Should().BeEquivalentTo(expectation,
+ opt => opt.Using());
+
+ // Assert
+ act.Should().NotThrow();
+ }
+
+ [Fact]
+ public void When_a_nested_property_is_unequal_based_on_equality_comparer_it_should_throw()
+ {
+ // Arrange
+ var subject = new { Timestamp = 22.March(2020) };
+
+ var expectation = new { Timestamp = 1.January(2021) };
+
+ // Act
+ Action act = () => subject.Should().BeEquivalentTo(expectation,
+ opt => opt.Using(new DateTimeByYearComparer()));
+
+ // Assert
+ act.Should()
+ .Throw()
+ .WithMessage("Expected*equal*2021*DateTimeByYearComparer*2020*");
+ }
+
+ [Fact]
+ public void When_the_subjects_property_type_is_different_from_the_equality_comparer_it_should_throw()
+ {
+ // Arrange
+ var subject = new { Timestamp = 1L };
+
+ var expectation = new { Timestamp = 1.January(2021) };
+
+ // Act
+ Action act = () => subject.Should().BeEquivalentTo(expectation,
+ opt => opt.Using(new DateTimeByYearComparer()));
+
+ // Assert
+ act.Should()
+ .Throw()
+ .WithMessage("Expected*Timestamp*1L*");
+ }
+
+ private class DateTimeByYearComparer : IEqualityComparer
+ {
+ public bool Equals(DateTime x, DateTime y)
+ {
+ return x.Year == y.Year;
+ }
+
+ public int GetHashCode(DateTime obj) => obj.GetHashCode();
+ }
+
+ [Fact]
+ public void When_an_invalid_equality_comparer_is_provided_it_should_throw()
+ {
+ // Arrange
+ var subject = new { Timestamp = 22.March(2020) };
+
+ var expectation = new { Timestamp = 1.January(2021) };
+
+ IEqualityComparer equalityComparer = null;
+
+ // Act
+ Action act = () => subject.Should().BeEquivalentTo(expectation,
+ opt => opt.Using(equalityComparer));
+
+ // Assert
+ act.Should()
+ .Throw()
+ .WithMessage("*comparer*");
+ }
+
+ [Fact]
+ public void When_the_compile_time_type_does_not_match_the_equality_comparer_type_it_should_use_the_default_mechanics()
+ {
+ // Arrange
+ var subject = new { Property = (IInterface)new ConcreteClass("SomeString") };
+
+ var expectation = new { Property = (IInterface)new ConcreteClass("SomeOtherString") };
+
+ // Act
+ Action act = () => subject.Should().BeEquivalentTo(expectation, opt =>
+ opt.Using());
+
+ // Assert
+ act.Should().NotThrow();
+ }
+
+ [Fact]
+ public void When_the_runtime_type_does_match_the_equality_comparer_type_it_should_use_the_default_mechanics()
+ {
+ // Arrange
+ var subject = new { Property = (IInterface)new ConcreteClass("SomeString") };
+
+ var expectation = new { Property = (IInterface)new ConcreteClass("SomeOtherString") };
+
+ // Act
+ Action act = () => subject.Should().BeEquivalentTo(expectation, opt => opt
+ .RespectingRuntimeTypes()
+ .Using());
+
+ // Assert
+ act.Should().Throw().WithMessage("*ConcreteClassEqualityComparer*");
+ }
+
+ private interface IInterface
+ {
+ }
+
+ private class ConcreteClass : IInterface
+ {
+ private readonly string property;
+
+ public ConcreteClass(string propertyValue)
+ {
+ property = propertyValue;
+ }
+
+ public string GetProperty() => property;
+ }
+
+ private class ConcreteClassEqualityComparer : IEqualityComparer
+ {
+ public bool Equals(ConcreteClass x, ConcreteClass y)
+ {
+ return x.GetProperty() == y.GetProperty();
+ }
+
+ public int GetHashCode(ConcreteClass obj) => obj.GetProperty().GetHashCode();
+ }
+ }
+}
diff --git a/Tests/FluentAssertions.Equivalency.Specs/BasicEquivalencySpecs.cs b/Tests/FluentAssertions.Equivalency.Specs/BasicEquivalencySpecs.cs
deleted file mode 100644
index fd7d8027f6..0000000000
--- a/Tests/FluentAssertions.Equivalency.Specs/BasicEquivalencySpecs.cs
+++ /dev/null
@@ -1,5758 +0,0 @@
-using System;
-using System.Collections;
-using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
-using System.Globalization;
-using System.Linq;
-using System.Net;
-using FluentAssertions.Common;
-using FluentAssertions.Equivalency;
-using FluentAssertions.Extensions;
-using FluentAssertions.Specs.Primitives;
-using Xunit;
-using Xunit.Sdk;
-
-namespace FluentAssertions.Specs.Equivalency
-{
- public class BasicEquivalencySpecs
- {
- #region General
-
- [Fact]
- public void When_expectation_is_null_it_should_throw()
- {
- // Arrange
- var subject = new
- {
- };
-
- // Act
- Action act = () => subject.Should().BeEquivalentTo