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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 46 additions & 1 deletion Src/FluentAssertions/Equivalency/EquivalencyValidator.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using FluentAssertions.Common;
using FluentAssertions.Equivalency.Tracing;
using FluentAssertions.Execution;

Expand All @@ -16,7 +17,7 @@ public void AssertEquality(Comparands comparands, EquivalencyValidationContext c
context.ResetTracing();

using var scope = new AssertionScope();

RecursivelyAssertEquivalencyOf(comparands, context);

if (context.TraceWriter is not null)
Expand All @@ -38,6 +39,50 @@ public void AssertEquivalencyOf(Comparands comparands, IEquivalencyValidationCon

if (ShouldContinueThisDeep(context.CurrentNode, context.Options, assertionChain))
{
// Fail when a path-based ExcludeMemberByPathSelectionRule targets this node but its type is compared using value semantics.
foreach (var rule in context.Options.SelectionRules)
{
if (rule is FluentAssertions.Equivalency.Selection.ExcludeMemberByPathSelectionRule excludeRule)
{
try
{
try
{
var nodePath = new MemberPath(context.CurrentNode.Subject.PathAndName);

if (nodePath.IsParentOrChildOf(excludeRule.CurrentPath) || nodePath.IsSameAs(excludeRule.CurrentPath))
{
Type typeToCheck = context.CurrentNode.Type;

// Ignore primitive/value types and strings — excluding these is harmless and expected.
if (typeToCheck.IsValueType || typeToCheck == typeof(string))
{
continue;
}

EqualityStrategy strategy = context.Options.GetEqualityStrategy(typeToCheck);

if ((strategy is EqualityStrategy.Equals or EqualityStrategy.ForceEquals)
&& (typeToCheck.OverridesEquals() || strategy == EqualityStrategy.ForceEquals))
{
assertionChain.FailWith(
"Cannot apply member-level inclusion/exclusion to path {0} because the type {1} is compared using value semantics (Equals). Configure it to be compared by members instead (for example, using ComparingByMembers<{2}>()) before applying member-level rules.",
excludeRule.CurrentPath, typeToCheck, typeToCheck);
}
}
}
catch
{
// Swallow any unexpected errors during inspection to avoid changing behavior in unknown cases.
}
}
catch
{
// Swallow any unexpected errors during inspection to avoid changing behavior in unknown cases.
}
}
}

if (!context.IsCyclicReference(comparands.Expectation))
{
TryToProveNodesAreEquivalent(comparands, context);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1181,6 +1181,21 @@ public void Can_exclude_root_properties_by_name()
.ExcludingMembersNamed("LastName", "Age"));
}

[Fact]
public void Excluding_a_member_on_a_type_with_value_semantics_should_throw()
{
// Arrange
var subject = new ValueSemanticWithProperty { Value = 1 };
var expectation = new ValueSemanticWithProperty { Value = 2 };

// Act
Action act = () => subject.Should().BeEquivalentTo(expectation, opts => opts
.Excluding(x => x.Value));

// Assert
act.Should().Throw<XunitException>().WithMessage("*Cannot apply member-level inclusion/exclusion*");
}

[Fact]
public void Can_exclude_properties_deeper_in_the_graph_by_name()
{
Expand Down
15 changes: 15 additions & 0 deletions Tests/FluentAssertions.Equivalency.Specs/TestTypes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,21 @@ public object ToType(Type conversionType, IFormatProvider provider)
}
}

public class ValueSemanticWithProperty
{
public int Value { get; set; }

public override bool Equals(object obj)
{
return obj is ValueSemanticWithProperty other && other.Value == Value;
}

public override int GetHashCode()
{
return Value.GetHashCode();
}
}

public abstract class Base
{
public abstract string AbstractProperty { get; }
Expand Down