Skip to content

Fail with a descriptive error when path-based rules are used on value-semantic types#3187

Merged
dennisdoomen merged 1 commit intomainfrom
copilot/fix-fluentassertions-member-exclusions
Apr 12, 2026
Merged

Fail with a descriptive error when path-based rules are used on value-semantic types#3187
dennisdoomen merged 1 commit intomainfrom
copilot/fix-fluentassertions-member-exclusions

Conversation

@dennisdoomen
Copy link
Copy Markdown
Member

@dennisdoomen dennisdoomen commented Mar 28, 2026

Summary

Closes #2571

When Excluding() or Including() targets a member of a type that is compared by value, the rule is silently ignored because BeEquivalentTo() never traverses that type’s members. This is surprising and hard to debug.

This PR detects that conflict at comparison time and fails with a descriptive assertion error.

Changes

This PR now reports a descriptive failure when a selection rule targets a type that is being compared by value, including both:

  • Auto-detected value semantics (EqualityStrategy.Equals) when the type overrides Equals()
  • Explicit value semantics (EqualityStrategy.ForceEquals) when the user calls ComparingByValue()

Example message for auto-detected value semantics:

CustomerDto is compared by value (because it overrides Equals), so the Excluding member o.Etag selection rule does not apply. Either call ComparingByMembers<CustomerDto>() to force member-wise comparison, or remove the selection rule.

Example message for explicit ComparingByValue():

CustomerDto is compared by value (because ComparingByValue was configured), so the Excluding member o.Etag selection rule does not apply. Either remove the ComparingByValue configuration, or remove the selection rule.

All user-specified selection rule types are detected:

Rule type Example Detection method
Path-based exclusion Excluding(o => o.Prop) Path matching against the current node
Path-based inclusion Including(o => o.Prop) Path matching against the current node
Predicate exclusion Excluding(m => m.Name == "Etag") Evaluated against the actual member list
Predicate inclusion Including(m => m.Name == "Id") Evaluated from empty set vs. full member list
Type exclusion Excluding<TMemberType>() Evaluated against the actual member list

Design decisions

  • Only selection rules are reported as conflicts; normal value-based equivalency behavior is unchanged otherwise.
  • Supports both implicit and explicit value semantics (Equals and ComparingByValue()).
  • Works at any depth — if a nested type uses value semantics and a selection rule targets one of its members, the conflict is caught when that node is visited.
  • Collection paths are handled correctly — rules such as .For(x => x.Items).Exclude(i => i.Name) are matched against concrete item paths like Items[0].Name.
  • Infrastructure rules (AllPropertiesSelectionRule, AllFieldsSelectionRule, ExcludeNonBrowsableMembersRule) are skipped since they are not user-specified selections.
  • Path-based conflict detection now uses a dedicated path-aware selection rule abstraction so wrapped collection rules can participate without special-case unwrapping.

Tests

Added coverage for:

  • Excluding a member by path on a value-semantic type
  • Including a member by path on a value-semantic type
  • Excluding a nested member by path
  • Excluding a member by predicate
  • Including members by predicate
  • Excluding members of a collection element via .For(...).Exclude(...)
  • Explicit ComparingByValue() with path-based exclusion
  • Explicit ComparingByValue() with path-based inclusion
  • Explicit ComparingByValue() with predicate-based exclusion
  • Explicit ComparingByMembers() continuing to work normally

dennisdoomen pushed a commit that referenced this pull request Mar 28, 2026
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 28, 2026

Qodana for .NET

It seems all right 👌

No new problems were found according to the checks applied

💡 Qodana analysis was run in the pull request mode: only the changed files were checked
☁️ View the detailed Qodana report

Detected 63 dependencies

Third-party software list

This page lists the third-party software dependencies used in FluentAssertions

Dependency Version Licenses
MSTest.Analyzers 3.11.1 MIT
MSTest.TestAdapter 3.11.1 MIT
MSTest.TestFramework 3.11.1 MIT
Microsoft.NET.Test.Sdk 17.3.3 MS-NET-LIBRARY-2019-06
Microsoft.NETCore.Platforms 2.1.0 MIT
Microsoft.NETCore.Targets 1.0.1 MIT
Microsoft.NETCore.UniversalWindowsPlatform 6.2.14 MS-NET-LIBRARY-2018-11
Microsoft.Net.Native.Compiler 2.2.12-rel-31116-00 MS-NET-LIBRARY-2018-11
Microsoft.Net.UWPCoreRuntimeSdk 2.2.14 MS-NET-LIBRARY-2018-11
Newtonsoft.Json 13.0.1 MIT
System.ComponentModel.Primitives 4.1.0 MIT
System.ComponentModel.TypeConverter 4.1.0 MIT
System.Globalization 4.0.11 MIT
System.IO 4.1.0 MIT
System.Private.Uri 4.0.1 MIT
System.Reflection.Primitives 4.0.1 MIT
System.Reflection 4.1.0 MIT
System.Resources.ResourceManager 4.0.1 MIT
System.Runtime.CompilerServices.Unsafe 4.5.3 MIT
System.Runtime.InteropServices.RuntimeInformation 4.0.0 MIT
System.Runtime 4.1.0 MIT
System.Text.Encoding.Extensions 4.0.11 MIT
System.Text.Encoding 4.0.11 MIT
System.Threading.Tasks.Extensions 4.5.4 MIT
System.Threading.Tasks 4.0.11 MIT
System.Threading 4.0.11 MIT
System.Xml.XPath.XmlDocument 4.7.0 MIT
runtime.any.System.Globalization 4.0.11 MIT
runtime.any.System.IO 4.1.0 MIT
runtime.any.System.Reflection.Primitives 4.0.1 MIT
runtime.any.System.Reflection 4.1.0 MIT
runtime.any.System.Resources.ResourceManager 4.0.1 MIT
runtime.any.System.Runtime 4.1.0 MIT
runtime.any.System.Text.Encoding 4.0.11 MIT
runtime.any.System.Threading.Tasks 4.0.11 MIT
runtime.aot.System.Globalization 4.0.11 MIT
runtime.aot.System.IO 4.1.0 MIT
runtime.aot.System.Reflection.Primitives 4.0.0 MIT
runtime.aot.System.Reflection 4.0.10 MIT
runtime.aot.System.Resources.ResourceManager 4.0.0 MIT
runtime.aot.System.Runtime 4.0.20 MIT
runtime.aot.System.Text.Encoding.Extensions 4.0.11 MIT
runtime.aot.System.Text.Encoding 4.0.11 MIT
runtime.aot.System.Threading.Tasks 4.0.11 MIT
runtime.win10-arm-aot.Microsoft.NETCore.UniversalWindowsPlatform 6.2.14 MS-NET-LIBRARY-2018-11
runtime.win10-arm.Microsoft.NETCore.UniversalWindowsPlatform 6.2.14 MS-NET-LIBRARY-2018-11
runtime.win10-arm.Microsoft.Net.Native.Compiler 2.2.12-rel-31116-00 MS-NET-LIBRARY-2018-11
runtime.win10-arm.Microsoft.Net.Native.SharedLibrary 2.2.8-rel-31116-00 MS-NET-LIBRARY-2018-11
runtime.win10-arm.Microsoft.Net.UWPCoreRuntimeSdk 2.2.14 MS-NET-LIBRARY-2018-11
runtime.win10-arm64-aot.Microsoft.NETCore.UniversalWindowsPlatform 6.2.14 MS-NET-LIBRARY-2018-11
runtime.win10-arm64.Microsoft.Net.Native.Compiler 2.2.12-rel-31116-00 MS-NET-LIBRARY-2018-11
runtime.win10-arm64.Microsoft.Net.Native.SharedLibrary 2.2.8-rel-31116-00 MS-NET-LIBRARY-2018-11
runtime.win10-x64-aot.Microsoft.NETCore.UniversalWindowsPlatform 6.2.14 MS-NET-LIBRARY-2018-11
runtime.win10-x64.Microsoft.NETCore.UniversalWindowsPlatform 6.2.14 MS-NET-LIBRARY-2018-11
runtime.win10-x64.Microsoft.Net.Native.Compiler 2.2.12-rel-31116-00 MS-NET-LIBRARY-2018-11
runtime.win10-x64.Microsoft.Net.Native.SharedLibrary 2.2.8-rel-31116-00 MS-NET-LIBRARY-2018-11
runtime.win10-x64.Microsoft.Net.UWPCoreRuntimeSdk 2.2.14 MS-NET-LIBRARY-2018-11
runtime.win10-x86-aot.Microsoft.NETCore.UniversalWindowsPlatform 6.2.14 MS-NET-LIBRARY-2018-11
runtime.win10-x86.Microsoft.NETCore.UniversalWindowsPlatform 6.2.14 MS-NET-LIBRARY-2018-11
runtime.win10-x86.Microsoft.Net.Native.Compiler 2.2.12-rel-31116-00 MS-NET-LIBRARY-2018-11
runtime.win10-x86.Microsoft.Net.Native.SharedLibrary 2.2.8-rel-31116-00 MS-NET-LIBRARY-2018-11
runtime.win10-x86.Microsoft.Net.UWPCoreRuntimeSdk 2.2.14 MS-NET-LIBRARY-2018-11
runtime.win7.System.Private.Uri 4.0.1 MIT
Contact Qodana team

Contact us at qodana-support@jetbrains.com

@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 28, 2026

Test Results

    37 files  ± 0      37 suites  ±0   5m 57s ⏱️ -11s
 6 363 tests +10   6 362 ✅ +10  1 💤 ±0  0 ❌ ±0 
39 524 runs  +60  39 518 ✅ +60  6 💤 ±0  0 ❌ ±0 

Results for commit 580ef79. ± Comparison against base commit efe9635.

This pull request removes 10 and adds 18 tests. Note that renamed tests count towards both.
FluentAssertions.Specs.Streams.StreamAssertionSpecs+HaveLength ‑ When_a_throwing_stream_should_have_a_length_it_should_fail(exception: System.ObjectDisposedException: Cannot access a disposed object.
FluentAssertions.Specs.Streams.StreamAssertionSpecs+HaveLength ‑ When_a_throwing_stream_should_have_a_length_it_should_fail(exception: System.ObjectDisposedException: Cannot access a disposed object.
FluentAssertions.Specs.Streams.StreamAssertionSpecs+HavePosition ‑ When_a_throwing_stream_should_have_a_position_it_should_fail(exception: System.ObjectDisposedException: Cannot access a disposed object.
FluentAssertions.Specs.Streams.StreamAssertionSpecs+HavePosition ‑ When_a_throwing_stream_should_have_a_position_it_should_fail(exception: System.ObjectDisposedException: Cannot access a disposed object.
FluentAssertions.Specs.Streams.StreamAssertionSpecs+NotHaveLength ‑ When_a_throwing_stream_should_not_have_a_length_it_should_fail(exception: System.ObjectDisposedException: Cannot access a disposed object.
FluentAssertions.Specs.Streams.StreamAssertionSpecs+NotHaveLength ‑ When_a_throwing_stream_should_not_have_a_length_it_should_fail(exception: System.ObjectDisposedException: Cannot access a disposed object.
FluentAssertions.Specs.Streams.StreamAssertionSpecs+NotHavePosition ‑ When_a_throwing_stream_should_not_have_a_position_it_should_fail(exception: System.ObjectDisposedException: Cannot access a disposed object.
FluentAssertions.Specs.Streams.StreamAssertionSpecs+NotHavePosition ‑ When_a_throwing_stream_should_not_have_a_position_it_should_fail(exception: System.ObjectDisposedException: Cannot access a disposed object.
Object name: 'GetLengthExceptionMessage'.)
Object name: 'GetPositionExceptionMessage'.)
FluentAssertions.Equivalency.Specs.SelectionRulesSpecs+Excluding ‑ Excluding_a_member_by_path_and_then_forcing_member_comparison_does_not_fail
FluentAssertions.Equivalency.Specs.SelectionRulesSpecs+Excluding ‑ Excluding_a_member_by_path_on_a_type_with_value_semantics_fails_with_a_descriptive_error
FluentAssertions.Equivalency.Specs.SelectionRulesSpecs+Excluding ‑ Excluding_a_member_by_path_when_forcing_value_semantics_explicitly_fails_with_a_descriptive_error
FluentAssertions.Equivalency.Specs.SelectionRulesSpecs+Excluding ‑ Excluding_a_member_by_predicate_on_a_type_with_value_semantics_fails_with_a_descriptive_error
FluentAssertions.Equivalency.Specs.SelectionRulesSpecs+Excluding ‑ Excluding_a_member_by_predicate_when_forcing_value_semantics_explicitly_fails_with_a_descriptive_error
FluentAssertions.Equivalency.Specs.SelectionRulesSpecs+Excluding ‑ Excluding_a_member_of_a_collection_element_with_value_semantics_via_For_and_Exclude_fails_with_a_descriptive_error
FluentAssertions.Equivalency.Specs.SelectionRulesSpecs+Excluding ‑ Excluding_a_nested_member_by_path_on_a_type_with_value_semantics_fails_with_a_descriptive_error
FluentAssertions.Equivalency.Specs.SelectionRulesSpecs+Excluding ‑ Including_a_member_by_path_when_forcing_value_semantics_explicitly_fails_with_a_descriptive_error
FluentAssertions.Equivalency.Specs.SelectionRulesSpecs+Excluding ‑ Including_members_by_predicate_on_a_type_with_value_semantics_fails_with_a_descriptive_error
FluentAssertions.Equivalency.Specs.SelectionRulesSpecs+Including ‑ Including_a_member_by_path_on_a_type_with_value_semantics_fails_with_a_descriptive_error
…

♻️ This comment has been updated with latest results.

@coveralls
Copy link
Copy Markdown

coveralls commented Mar 28, 2026

Coverage Report for CI Build 24278160846

Coverage decreased (-0.04%) to 97.159%

Details

  • Coverage decreased (-0.04%) from the base build.
  • Patch coverage: 1 uncovered change across 1 file (86 of 87 lines covered, 98.85%).
  • 9 coverage regressions across 2 files.

Uncovered Changes

File Changed Covered %
Src/FluentAssertions/Equivalency/Selection/SelectMemberByPathSelectionRule.cs 14 13 92.86%

Coverage Regressions

9 previously-covered lines in 2 files lost coverage.

File Lines Losing Coverage Coverage
Src/FluentAssertions/Collections/GenericCollectionAssertions.cs 5 99.17%
Src/FluentAssertions/AssertionExtensions.cs 4 94.74%

Coverage Stats

Coverage Status
Relevant Lines: 13239
Covered Lines: 13014
Line Coverage: 98.3%
Relevant Branches: 4328
Covered Branches: 4054
Branch Coverage: 93.67%
Branches in Coverage %: Yes
Coverage Strength: 940548.09 hits per line

💛 - Coveralls

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses surprising behavior in BeEquivalentTo() where path-based selection rules (Excluding() / Including()) can be silently ignored when a type is auto-compared by value semantics (EqualityStrategy.Equals). It introduces conflict detection to fail fast with a descriptive assertion message.

Changes:

  • Detect and report conflicts between auto-detected value-semantics comparison and path-based selection rules during equivalency comparison.
  • Add SelectsMembersOf(INode) to path-based selection rules to encapsulate overlap detection logic.
  • Add/extend selection rule specs and document the behavior change in the release notes.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
docs/_pages/releases.md Adds a release note describing the new fail-fast behavior for conflicting path rules + value semantics.
Tests/FluentAssertions.Equivalency.Specs/SelectionRulesSpecs.Including.cs Adds a test asserting Including() fails with a descriptive message on value-semantic types.
Tests/FluentAssertions.Equivalency.Specs/SelectionRulesSpecs.Excluding.cs Adds tests asserting Excluding() fails (and explicit overrides do/don’t fail).
Src/FluentAssertions/Equivalency/Steps/ValueTypeEquivalencyStep.cs Adds conflict detection and emits a dedicated assertion failure instead of silently ignoring path rules.
Src/FluentAssertions/Equivalency/Selection/SelectMemberByPathSelectionRule.cs Implements overlap detection (SelectsMembersOf) and normalizes current node paths.
Src/FluentAssertions/Equivalency/Selection/IncludeMemberByPathSelectionRule.cs Refactors to expose the rule MemberPath via the base class for conflict detection.
Src/FluentAssertions/Equivalency/Selection/ExcludeMemberByPathSelectionRule.cs Refactors to expose the rule MemberPath via the base class for conflict detection.

Comment thread Src/FluentAssertions/Equivalency/Selection/SelectMemberByPathSelectionRule.cs Outdated
Comment thread Src/FluentAssertions/Equivalency/Steps/ValueTypeEquivalencyStep.cs Outdated
@dennisdoomen dennisdoomen marked this pull request as draft April 9, 2026 19:25
@dennisdoomen dennisdoomen force-pushed the copilot/fix-fluentassertions-member-exclusions branch 4 times, most recently from 6aa97a2 to 32702e1 Compare April 11, 2026 07:58
Detect conflicts between Including/Excluding selection rules and value-semantic
comparison so Fluent Assertions reports a descriptive error instead of silently
ignoring user intent. This now covers both auto-detected Equals-based value
semantics and explicit ComparingByValue() configuration.

Use a path-aware selection-rule abstraction so collection decorators can forward
path conflict checks without special-case unwrapping, and add coverage for path,
predicate, nested, collection, and explicit configuration scenarios.

Fixes #2571

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@dennisdoomen dennisdoomen force-pushed the copilot/fix-fluentassertions-member-exclusions branch from 32702e1 to 580ef79 Compare April 11, 2026 07:59
@dennisdoomen dennisdoomen marked this pull request as ready for review April 11, 2026 08:00
@dennisdoomen dennisdoomen requested a review from jnyrup April 11, 2026 08:00
@dennisdoomen dennisdoomen merged commit ca64123 into main Apr 12, 2026
14 checks passed
@dennisdoomen dennisdoomen deleted the copilot/fix-fluentassertions-member-exclusions branch April 12, 2026 12:55
@dennisdoomen dennisdoomen added this to the 8.10 milestone Apr 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Excluding and Including options should fail when applied on types with value semantics

4 participants