diff --git a/Src/FluentAssertions/Common/TypeExtensions.cs b/Src/FluentAssertions/Common/TypeExtensions.cs index 25d3a58931..a913bd83c3 100644 --- a/Src/FluentAssertions/Common/TypeExtensions.cs +++ b/Src/FluentAssertions/Common/TypeExtensions.cs @@ -15,9 +15,6 @@ internal static class TypeExtensions private const BindingFlags PublicInstanceMembersFlag = BindingFlags.Public | BindingFlags.Instance; - private const BindingFlags AllInstanceMembersFlag = - BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; - private const BindingFlags AllStaticAndInstanceMembersFlag = PublicInstanceMembersFlag | BindingFlags.NonPublic | BindingFlags.Static; @@ -176,24 +173,21 @@ public static bool OverridesEquals(this Type type) } /// - /// Finds the property by a case-sensitive name. + /// Finds the property by a case-sensitive name and with a certain visibility. /// + /// + /// If both a normal property and one that was implemented through an explicit interface implementation with the same name exist, + /// then the normal property will be returned. + /// /// /// Returns if no such property exists. /// - public static PropertyInfo FindProperty(this Type type, string propertyName) + public static PropertyInfo FindProperty(this Type type, string propertyName, MemberVisibility memberVisibility) { - while (type != typeof(object)) - { - if (type.GetProperty(propertyName, AllInstanceMembersFlag | BindingFlags.DeclaredOnly) is { } property) - { - return property; - } - - type = type.BaseType; - } + var properties = type.GetNonPrivateProperties(memberVisibility); - return null; + return Array.Find(properties, p => + p.Name == propertyName || p.Name.EndsWith("." + propertyName, StringComparison.OrdinalIgnoreCase)); } /// @@ -202,19 +196,12 @@ public static PropertyInfo FindProperty(this Type type, string propertyName) /// /// Returns if no such property exists. /// - public static FieldInfo FindField(this Type type, string fieldName) + public static FieldInfo FindField(this Type type, string fieldName, MemberVisibility memberVisibility) { - while (type != typeof(object)) - { - if (type.GetField(fieldName, AllInstanceMembersFlag | BindingFlags.DeclaredOnly) is { } field) - { - return field; - } + var fields = type.GetNonPrivateFields(memberVisibility); - type = type.BaseType; - } - - return null; + return Array.Find(fields, p => + p.Name == fieldName || p.Name.EndsWith("." + fieldName, StringComparison.OrdinalIgnoreCase)); } public static MemberInfo[] GetNonPrivateMembers(this Type typeToReflect, MemberVisibility visibility) @@ -318,8 +305,11 @@ public static bool IsIndexer(this PropertyInfo member) public static ConstructorInfo GetConstructor(this Type type, IEnumerable parameterTypes) { + const BindingFlags allInstanceMembersFlag = + BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; + return type - .GetConstructors(AllInstanceMembersFlag) + .GetConstructors(allInstanceMembersFlag) .SingleOrDefault(m => m.GetParameters().Select(p => p.ParameterType).SequenceEqual(parameterTypes)); } diff --git a/Src/FluentAssertions/Common/TypeMemberReflector.cs b/Src/FluentAssertions/Common/TypeMemberReflector.cs index 1706e4e32e..9f74929ea1 100644 --- a/Src/FluentAssertions/Common/TypeMemberReflector.cs +++ b/Src/FluentAssertions/Common/TypeMemberReflector.cs @@ -31,27 +31,39 @@ private static PropertyInfo[] LoadNonPrivateProperties(Type typeToReflect, Membe { IEnumerable query = from propertyInfo in GetPropertiesFromHierarchy(typeToReflect, visibility) - where HasNonPrivateGetter(propertyInfo) + where HasNonPrivateGetter(propertyInfo, visibility) where !propertyInfo.IsIndexer() select propertyInfo; return query.ToArray(); } - private static List GetPropertiesFromHierarchy(Type typeToReflect, MemberVisibility memberVisibility) + private static IEnumerable GetPropertiesFromHierarchy(Type typeToReflect, MemberVisibility memberVisibility) { - bool includeInternals = memberVisibility.HasFlag(MemberVisibility.Internal); + bool includeInternal = memberVisibility.HasFlag(MemberVisibility.Internal); return GetMembersFromHierarchy(typeToReflect, type => { return type .GetProperties(AllInstanceMembersFlag | BindingFlags.DeclaredOnly) - .Where(property => property.GetMethod?.IsPrivate == false) - .Where(property => includeInternals || property.GetMethod is { IsAssembly: false, IsFamilyOrAssembly: false }) + .Where(property => includeInternal || !IsInternal(property)) + .OrderBy(property => IsExplicitImplementation(property)) .ToArray(); }); } + private static bool IsInternal(PropertyInfo property) + { + return property.GetMethod is { IsAssembly: true } or { IsFamilyOrAssembly: true }; + } + + private static bool IsExplicitImplementation(PropertyInfo property) + { + return property.GetMethod?.IsPrivate == true && + property.SetMethod?.IsPrivate == true && + property.Name.Contains('.', StringComparison.Ordinal); + } + private static FieldInfo[] LoadNonPrivateFields(Type typeToReflect, MemberVisibility visibility) { IEnumerable query = @@ -63,21 +75,26 @@ from fieldInfo in GetFieldsFromHierarchy(typeToReflect, visibility) return query.ToArray(); } - private static List GetFieldsFromHierarchy(Type typeToReflect, MemberVisibility memberVisibility) + private static IEnumerable GetFieldsFromHierarchy(Type typeToReflect, MemberVisibility memberVisibility) { - bool includeInternals = memberVisibility.HasFlag(MemberVisibility.Internal); + bool includeInternal = memberVisibility.HasFlag(MemberVisibility.Internal); return GetMembersFromHierarchy(typeToReflect, type => { return type .GetFields(AllInstanceMembersFlag) .Where(field => !field.IsPrivate) - .Where(field => includeInternals || (!field.IsAssembly && !field.IsFamilyOrAssembly)) + .Where(field => includeInternal || !IsInternal(field)) .ToArray(); }); } - private static List GetMembersFromHierarchy( + private static bool IsInternal(FieldInfo field) + { + return field.IsAssembly || field.IsFamilyOrAssembly; + } + + private static TMemberInfo[] GetMembersFromHierarchy( Type typeToReflect, Func> getMembers) where TMemberInfo : MemberInfo @@ -90,7 +107,7 @@ private static List GetMembersFromHierarchy( return GetClassMembers(typeToReflect, getMembers); } - private static List GetInterfaceMembers(Type typeToReflect, + private static TMemberInfo[] GetInterfaceMembers(Type typeToReflect, Func> getMembers) where TMemberInfo : MemberInfo { @@ -123,10 +140,10 @@ private static List GetInterfaceMembers(Type typeToRef members.InsertRange(0, newPropertyInfos); } - return members; + return members.ToArray(); } - private static List GetClassMembers(Type typeToReflect, + private static TMemberInfo[] GetClassMembers(Type typeToReflect, Func> getMembers) where TMemberInfo : MemberInfo { @@ -145,12 +162,18 @@ private static List GetClassMembers(Type typeToReflect typeToReflect = typeToReflect.BaseType; } - return members; + return members.ToArray(); } - private static bool HasNonPrivateGetter(PropertyInfo propertyInfo) + private static bool HasNonPrivateGetter(PropertyInfo propertyInfo, MemberVisibility visibility) { MethodInfo getMethod = propertyInfo.GetGetMethod(nonPublic: true); + + if (visibility.HasFlag(MemberVisibility.ExplicitlyImplemented)) + { + return getMethod is { IsPrivate: false, IsFamily: false } or { IsPrivate: true, IsFinal: true }; + } + return getMethod is { IsPrivate: false, IsFamily: false }; } } diff --git a/Src/FluentAssertions/Equivalency/Matching/MustMatchByNameRule.cs b/Src/FluentAssertions/Equivalency/Matching/MustMatchByNameRule.cs index e06f7ea6cd..62a68d4fb9 100644 --- a/Src/FluentAssertions/Equivalency/Matching/MustMatchByNameRule.cs +++ b/Src/FluentAssertions/Equivalency/Matching/MustMatchByNameRule.cs @@ -15,19 +15,20 @@ public IMember Match(IMember expectedMember, object subject, INode parent, IEqui if (options.IncludedProperties != MemberVisibility.None) { - PropertyInfo propertyInfo = subject.GetType().FindProperty(expectedMember.Name); + PropertyInfo propertyInfo = subject.GetType().FindProperty( + expectedMember.Name, + options.IncludedProperties | MemberVisibility.ExplicitlyImplemented); + subjectMember = propertyInfo is not null && !propertyInfo.IsIndexer() ? new Property(propertyInfo, parent) : null; } if (subjectMember is null && options.IncludedFields != MemberVisibility.None) { - FieldInfo fieldInfo = subject.GetType().FindField(expectedMember.Name); - subjectMember = fieldInfo is not null ? new Field(fieldInfo, parent) : null; - } + FieldInfo fieldInfo = subject.GetType().FindField( + expectedMember.Name, + options.IncludedFields); - if ((subjectMember is null || !options.UseRuntimeTyping) && ExpectationImplementsMemberExplicitly(subject, expectedMember)) - { - subjectMember = expectedMember; + subjectMember = fieldInfo is not null ? new Field(fieldInfo, parent) : null; } if (subjectMember is null) @@ -49,11 +50,6 @@ public IMember Match(IMember expectedMember, object subject, INode parent, IEqui return subjectMember; } - private static bool ExpectationImplementsMemberExplicitly(object expectation, IMember subjectMember) - { - return subjectMember.DeclaringType.IsInstanceOfType(expectation); - } - /// /// 2 public override string ToString() diff --git a/Src/FluentAssertions/Equivalency/Matching/TryMatchByNameRule.cs b/Src/FluentAssertions/Equivalency/Matching/TryMatchByNameRule.cs index e44c0e6779..079aecb556 100644 --- a/Src/FluentAssertions/Equivalency/Matching/TryMatchByNameRule.cs +++ b/Src/FluentAssertions/Equivalency/Matching/TryMatchByNameRule.cs @@ -10,14 +10,17 @@ internal class TryMatchByNameRule : IMemberMatchingRule { public IMember Match(IMember expectedMember, object subject, INode parent, IEquivalencyAssertionOptions options) { - PropertyInfo property = subject.GetType().FindProperty(expectedMember.Name); + PropertyInfo property = subject.GetType().FindProperty(expectedMember.Name, + options.IncludedProperties | MemberVisibility.ExplicitlyImplemented); if (property is not null && !property.IsIndexer()) { return new Property(property, parent); } - FieldInfo field = subject.GetType().FindField(expectedMember.Name); + FieldInfo field = subject.GetType() + .FindField(expectedMember.Name, options.IncludedFields); + return field is not null ? new Field(field, parent) : null; } diff --git a/Src/FluentAssertions/Equivalency/MemberFactory.cs b/Src/FluentAssertions/Equivalency/MemberFactory.cs index 7f12299e4d..6fed0af560 100644 --- a/Src/FluentAssertions/Equivalency/MemberFactory.cs +++ b/Src/FluentAssertions/Equivalency/MemberFactory.cs @@ -18,14 +18,14 @@ public static IMember Create(MemberInfo memberInfo, INode parent) internal static IMember Find(object target, string memberName, INode parent) { - PropertyInfo property = target.GetType().FindProperty(memberName); + PropertyInfo property = target.GetType().FindProperty(memberName, MemberVisibility.Public | MemberVisibility.ExplicitlyImplemented); if (property is not null && !property.IsIndexer()) { return new Property(property, parent); } - FieldInfo field = target.GetType().FindField(memberName); + FieldInfo field = target.GetType().FindField(memberName, MemberVisibility.Public); return field is not null ? new Field(field, parent) : null; } } diff --git a/Src/FluentAssertions/Equivalency/MemberVisibility.cs b/Src/FluentAssertions/Equivalency/MemberVisibility.cs index fd341a0346..e64b798a81 100644 --- a/Src/FluentAssertions/Equivalency/MemberVisibility.cs +++ b/Src/FluentAssertions/Equivalency/MemberVisibility.cs @@ -11,5 +11,6 @@ public enum MemberVisibility { None = 0, Internal = 1, - Public = 2 + Public = 2, + ExplicitlyImplemented = 4 } diff --git a/Tests/Approval.Tests/ApiApproval.cs b/Tests/Approval.Tests/ApiApproval.cs index a1f0bbd9ca..c9b3726f6f 100644 --- a/Tests/Approval.Tests/ApiApproval.cs +++ b/Tests/Approval.Tests/ApiApproval.cs @@ -28,10 +28,12 @@ public Task ApproveApi(string frameworkVersion) string assemblyPath = Uri.UnescapeDataString(uri.Path); var containingDirectory = Path.GetDirectoryName(assemblyPath); var configurationName = new DirectoryInfo(containingDirectory).Parent.Name; + var assemblyFile = Path.GetFullPath( Path.Combine( GetSourceDirectory(), - Path.Combine("..", "..", "Src", "FluentAssertions", "bin", configurationName, frameworkVersion, "FluentAssertions.dll"))); + Path.Combine("..", "..", "Src", "FluentAssertions", "bin", configurationName, frameworkVersion, + "FluentAssertions.dll"))); var assembly = Assembly.LoadFile(Path.GetFullPath(assemblyFile)); var publicApi = assembly.GeneratePublicApi(options: null); @@ -53,6 +55,7 @@ public static Task OnlyIncludeChanges(string received, string ver var diff = InlineDiffBuilder.Diff(verified, received); var builder = new StringBuilder(); + foreach (var line in diff.Lines) { switch (line.Type) @@ -79,9 +82,12 @@ private class TargetFrameworksTheoryData : TheoryData { public TargetFrameworksTheoryData() { - var csproj = Path.Combine(GetSourceDirectory(), Path.Combine("..", "..", "Src", "FluentAssertions", "FluentAssertions.csproj")); + var csproj = Path.Combine(GetSourceDirectory(), + Path.Combine("..", "..", "Src", "FluentAssertions", "FluentAssertions.csproj")); + var project = XDocument.Load(csproj); var targetFrameworks = project.XPathSelectElement("/Project/PropertyGroup/TargetFrameworks"); + foreach (string targetFramework in targetFrameworks!.Value.Split(';')) { Add(targetFramework); diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt index 5bf1ccb1d5..a1a9e8cb1e 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt @@ -972,6 +972,7 @@ namespace FluentAssertions.Equivalency None = 0, Internal = 1, Public = 2, + ExplicitlyImplemented = 4, } public class NestedExclusionOptionBuilder { diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net6.0.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net6.0.verified.txt index 316bd5d444..bad3c151b9 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net6.0.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net6.0.verified.txt @@ -985,6 +985,7 @@ namespace FluentAssertions.Equivalency None = 0, Internal = 1, Public = 2, + ExplicitlyImplemented = 4, } public class NestedExclusionOptionBuilder { diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp2.1.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp2.1.verified.txt index 16fec8c0b5..b6f3b6ce87 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp2.1.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp2.1.verified.txt @@ -972,6 +972,7 @@ namespace FluentAssertions.Equivalency None = 0, Internal = 1, Public = 2, + ExplicitlyImplemented = 4, } public class NestedExclusionOptionBuilder { diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp3.0.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp3.0.verified.txt index 16fec8c0b5..b6f3b6ce87 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp3.0.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp3.0.verified.txt @@ -972,6 +972,7 @@ namespace FluentAssertions.Equivalency None = 0, Internal = 1, Public = 2, + ExplicitlyImplemented = 4, } public class NestedExclusionOptionBuilder { diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.0.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.0.verified.txt index ddfde1e102..d237a9c6f5 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.0.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.0.verified.txt @@ -965,6 +965,7 @@ namespace FluentAssertions.Equivalency None = 0, Internal = 1, Public = 2, + ExplicitlyImplemented = 4, } public class NestedExclusionOptionBuilder { diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt index 16fec8c0b5..b6f3b6ce87 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt @@ -972,6 +972,7 @@ namespace FluentAssertions.Equivalency None = 0, Internal = 1, Public = 2, + ExplicitlyImplemented = 4, } public class NestedExclusionOptionBuilder { diff --git a/Tests/FluentAssertions.Equivalency.Specs/MemberMatchingSpecs.cs b/Tests/FluentAssertions.Equivalency.Specs/MemberMatchingSpecs.cs index 7808394542..231c68de0a 100644 --- a/Tests/FluentAssertions.Equivalency.Specs/MemberMatchingSpecs.cs +++ b/Tests/FluentAssertions.Equivalency.Specs/MemberMatchingSpecs.cs @@ -62,8 +62,8 @@ public void Nested_properties_can_be_mapped_using_a_nested_expression() subject.Should() .BeEquivalentTo(expectation, opt => opt .WithMapping( - e => e.Parent[0].Property2, - s => s.Parent[0].Property1)); + e => e.Children[0].Property2, + s => s.Children[0].Property1)); } [Fact] @@ -83,6 +83,25 @@ public void Nested_properties_can_be_mapped_using_a_nested_type_and_property_nam .WithMapping("Property2", "Property1")); } + [Fact] + public void Nested_explicitly_implemented_properties_can_be_mapped_using_a_nested_type_and_property_names() + { + // Arrange + var subject = new ParentOfSubjectWithExplicitlyImplementedProperty(new[] { new SubjectWithExplicitImplementedProperty() }); + + ((IProperty)subject.Children[0]).Property = "Hello"; + + var expectation = new ParentOfExpectationWithProperty2(new[] + { + new ExpectationWithProperty2 { Property2 = "Hello" } + }); + + // Act / Assert + subject.Should() + .BeEquivalentTo(expectation, opt => opt + .WithMapping("Property2", "Property")); + } + [Fact] public void Nested_fields_can_be_mapped_using_a_nested_type_and_field_names() { @@ -144,6 +163,25 @@ public void Properties_can_be_mapped_by_name() .WithMapping("Property2", "Property1")); } + [Fact] + public void Properties_can_be_mapped_by_name_to_an_explicitly_implemented_property() + { + // Arrange + var subject = new SubjectWithExplicitImplementedProperty(); + + ((IProperty)subject).Property = "Hello"; + + var expectation = new ExpectationWithProperty2 + { + Property2 = "Hello" + }; + + // Act / Assert + subject.Should() + .BeEquivalentTo(expectation, opt => opt + .WithMapping("Property2", "Property")); + } + [Fact] public void Fields_can_be_mapped_by_name() { @@ -515,21 +553,31 @@ private class EntityDto internal class ParentOfExpectationWithProperty2 { - public ExpectationWithProperty2[] Parent { get; } + public ExpectationWithProperty2[] Children { get; } - public ParentOfExpectationWithProperty2(ExpectationWithProperty2[] parent) + public ParentOfExpectationWithProperty2(ExpectationWithProperty2[] children) { - Parent = parent; + Children = children; } } internal class ParentOfSubjectWithProperty1 { - public SubjectWithProperty1[] Parent { get; } + public SubjectWithProperty1[] Children { get; } + + public ParentOfSubjectWithProperty1(SubjectWithProperty1[] children) + { + Children = children; + } + } + + internal class ParentOfSubjectWithExplicitlyImplementedProperty + { + public SubjectWithExplicitImplementedProperty[] Children { get; } - public ParentOfSubjectWithProperty1(SubjectWithProperty1[] parent) + public ParentOfSubjectWithExplicitlyImplementedProperty(SubjectWithExplicitImplementedProperty[] children) { - Parent = parent; + Children = children; } } @@ -538,6 +586,16 @@ internal class SubjectWithProperty1 public string Property1 { get; set; } } + internal class SubjectWithExplicitImplementedProperty : IProperty + { + string IProperty.Property { get; set; } + } + + internal interface IProperty + { + string Property { get; set; } + } + internal class ExpectationWithProperty2 { public string Property2 { get; set; } diff --git a/Tests/FluentAssertions.Equivalency.Specs/SelectionRulesSpecs.cs b/Tests/FluentAssertions.Equivalency.Specs/SelectionRulesSpecs.cs index 04296b8f86..1ae4bf2455 100644 --- a/Tests/FluentAssertions.Equivalency.Specs/SelectionRulesSpecs.cs +++ b/Tests/FluentAssertions.Equivalency.Specs/SelectionRulesSpecs.cs @@ -1763,7 +1763,7 @@ public void When_a_reference_to_an_explicit_interface_impl_is_provided_it_should } [Fact] - public void When_respecting_declared_types_explicit_interface_member_on_interfaced_subject_should_be_used() + public void Explicitly_implemented_subject_properties_are_ignored_if_a_normal_property_exists_with_the_same_name() { // Arrange IVehicle expected = new Vehicle @@ -1779,10 +1779,33 @@ public void When_respecting_declared_types_explicit_interface_member_on_interfac subject.VehicleId = 1; // interface member // Act - Action action = () => subject.Should().BeEquivalentTo(expected, opt => opt.RespectingDeclaredTypes()); + Action action = () => subject.Should().BeEquivalentTo(expected); // Assert - action.Should().NotThrow(); + action.Should().Throw(); + } + + [Fact] + public void Excluding_missing_members_does_not_affect_how_explicitly_implemented_subject_properties_are_dealt_with() + { + // Arrange + IVehicle expected = new Vehicle + { + VehicleId = 1 + }; + + IVehicle subject = new ExplicitVehicle + { + VehicleId = 2 // instance member + }; + + subject.VehicleId = 1; // interface member + + // Act + Action action = () => subject.Should().BeEquivalentTo(expected, opt => opt.ExcludingMissingMembers()); + + // Assert + action.Should().Throw(); } [Fact] @@ -1854,29 +1877,6 @@ public void When_respecting_runtime_types_explicit_interface_member_on_interface action.Should().Throw(); } - [Fact] - public void When_respecting_declared_types_explicit_interface_member_on_subject_should_not_be_used() - { - // Arrange - var expected = new Vehicle - { - VehicleId = 1 - }; - - var subject = new ExplicitVehicle - { - VehicleId = 2 - }; - - ((IVehicle)subject).VehicleId = 1; - - // Act - Action action = () => subject.Should().BeEquivalentTo(expected, opt => opt.RespectingDeclaredTypes()); - - // Assert - action.Should().Throw(); - } - [Fact] public void When_respecting_declared_types_explicit_interface_member_on_expectation_should_not_be_used() { @@ -1894,56 +1894,31 @@ public void When_respecting_declared_types_explicit_interface_member_on_expectat }; // Act - Action action = () => subject.Should().BeEquivalentTo(expected, opt => opt.RespectingDeclaredTypes()); + Action action = () => subject.Should().BeEquivalentTo(expected); // Assert action.Should().Throw(); } [Fact] - public void When_respecting_runtime_types_explicit_interface_member_on_subject_should_not_be_used() + public void Can_find_explicitly_implemented_property_on_the_subject() { // Arrange - var expected = new Vehicle - { - VehicleId = 1 - }; - - var subject = new ExplicitVehicle - { - VehicleId = 2 - }; - - ((IVehicle)subject).VehicleId = 1; - - // Act - Action action = () => subject.Should().BeEquivalentTo(expected, opt => opt.RespectingRuntimeTypes()); + IPerson person = new Person(); + person.Name = "Bob"; - // Assert - action.Should().Throw(); + // Act / Assert + person.Should().BeEquivalentTo(new { Name = "Bob" }); } - [Fact] - public void When_respecting_runtime_types_explicit_interface_member_on_expectation_should_not_be_used() + private interface IPerson { - // Arrange - var expected = new ExplicitVehicle - { - VehicleId = 2 - }; - - ((IVehicle)expected).VehicleId = 1; - - var subject = new Vehicle - { - VehicleId = 1 - }; - - // Act - Action action = () => subject.Should().BeEquivalentTo(expected, opt => opt.RespectingRuntimeTypes()); + string Name { get; set; } + } - // Assert - action.Should().Throw(); + private class Person : IPerson + { + string IPerson.Name { get; set; } } [Fact] diff --git a/docs/_pages/releases.md b/docs/_pages/releases.md index 7809068db1..beb138f8f5 100644 --- a/docs/_pages/releases.md +++ b/docs/_pages/releases.md @@ -14,6 +14,7 @@ sidebar: ### Fixes * `because` and `becauseArgs` were not included in the error message when collections of enums were not equivalent - [#2214](https://github.com/fluentassertions/fluentassertions/pull/2214) +* `BeEquivalentTo` will now find and can map subject properties that are implemented through an explicitly-implemented interface - [2152](https://github.com/fluentassertions/fluentassertions/pull/2152) * Improve caller identification for tests written in Visual Basic - [#2254](https://github.com/fluentassertions/fluentassertions/pull/2254) * Improved auto conversion to enums for objects of different integral type - [#2261](https://github.com/fluentassertions/fluentassertions/pull/2261) * Fixed exceptions when trying to auto convert strings or enums of different type to enums- [#2261](https://github.com/fluentassertions/fluentassertions/pull/2261)