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..215f94769d 100644
--- a/Src/FluentAssertions/Common/TypeMemberReflector.cs
+++ b/Src/FluentAssertions/Common/TypeMemberReflector.cs
@@ -31,27 +31,45 @@ 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 PropertyInfo[] 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))
+ .Select(property => new
+ {
+ Property = property,
+ IsExplicit = IsExplicitImplementation(property)
+ })
+ .OrderBy(x => x.IsExplicit)
+ .Select(x => x.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 +81,26 @@ from fieldInfo in GetFieldsFromHierarchy(typeToReflect, visibility)
return query.ToArray();
}
- private static List GetFieldsFromHierarchy(Type typeToReflect, MemberVisibility memberVisibility)
+ private static FieldInfo[] 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 +113,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 +146,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 +168,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 481b7b3fcc..41d8a9172c 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)
## 6.11.0