Skip to content

fix: chain projected values across nested WhichNodes#958

Merged
vbreuss merged 4 commits into
mainfrom
topic/fix-chained-which-projection
May 17, 2026
Merged

fix: chain projected values across nested WhichNodes#958
vbreuss merged 4 commits into
mainfrom
topic/fix-chained-which-projection

Conversation

@vbreuss
Copy link
Copy Markdown
Member

@vbreuss vbreuss commented May 17, 2026

ExpectationBuilder.ForWhich (since #955) drains a pending _whichNode into the graph before creating a new one, so two consecutive ForWhich calls produce two sibling WhichNodes. At evaluation time both nodes received the original outer subject - including the inner one, whose TSource is the outer node's TMember. That made any chained ForWhich whose accessor consumes the previous projection (e.g. Which.X.Which.Y or Which.X.And.WhoseParent) throw "The member type for the actual value in the which node did not match".

WhichNode<TSource, TMember>.IsMetBy now, when the outer value is not itself a TSource, asks the parent's ConstraintResult.TryGetValue for a TSource. WhichConstraintResult.TryGetValue already exposes the projection produced by the previous WhichNode, so the chained accessor consumes that value instead of the unrelated outer subject. The original type-mismatch error is still thrown when neither the outer value nor the parent chain can supply a TSource.

Adds coverage in WhichNodeTests (direct, two- and three-level chains plus a regression test for the fallback path) and in ExpectationBuilderTests (ForWhich and ForWhich-async called twice where the second projection consumes the first's result, plus a three-level integration variant).

`ExpectationBuilder.ForWhich` (since #955) drains a pending `_whichNode`
into the graph before creating a new one, so two consecutive `ForWhich`
calls produce two sibling `WhichNode`s. At evaluation time both nodes
received the original outer subject — including the inner one, whose
`TSource` is the *outer* node's `TMember`. That made any chained
`ForWhich` whose accessor consumes the previous projection (e.g.
`Which.X.Which.Y` or `Which.X.And.WhoseParent`) throw "The member type
for the actual value in the which node did not match".

`WhichNode<TSource, TMember>.IsMetBy` now, when the outer value is not
itself a `TSource`, asks the parent's `ConstraintResult.TryGetValue`
for a `TSource`. `WhichConstraintResult.TryGetValue` already exposes
the projection produced by the previous `WhichNode`, so the chained
accessor consumes that value instead of the unrelated outer subject.
The original type-mismatch error is still thrown when neither the
outer value nor the parent chain can supply a `TSource`.

Adds coverage in `WhichNodeTests` (direct, two- and three-level chains
plus a regression test for the fallback path) and in
`ExpectationBuilderTests` (`ForWhich` and `ForWhich`-async called
twice where the second projection consumes the first's result, plus a
three-level integration variant).
@vbreuss vbreuss self-assigned this May 17, 2026
Copilot AI review requested due to automatic review settings May 17, 2026 07:02
@vbreuss vbreuss added the bug Something isn't working label May 17, 2026
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 fixes chained ForWhich evaluation so nested WhichNodes can consume the projection produced by a parent node when the original outer subject is not assignable to the inner source type.

Changes:

  • Updates WhichNode.IsMetBy to fall back to a parent result’s projected value before throwing a type mismatch.
  • Adds direct WhichNode regression coverage for two-level, three-level, and fallback projection scenarios.
  • Adds ExpectationBuilder integration tests for sync and async chained ForWhich projections.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.

File Description
Source/aweXpect.Core/Core/Nodes/WhichNode.cs Adds parent-result projection fallback for nested WhichNode source resolution.
Tests/aweXpect.Core.Tests/Core/Nodes/WhichNodeTests.cs Covers nested WhichNode projection chaining and preserves type-mismatch behavior.
Tests/aweXpect.Core.Tests/Core/ExpectationBuilderTests.cs Adds sync/async integration coverage for repeated ForWhich projection chains.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 17, 2026

Test Results

     23 files   - 27       23 suites   - 27   7m 37s ⏱️ ±0s
 19 823 tests  - 16   19 822 ✅  - 16  1 💤 ±0  0 ❌ ±0 
102 440 runs   - 45  102 439 ✅  - 45  1 💤 ±0  0 ❌ ±0 

Results for commit 260c070. ± Comparison against base commit 3b2559e.

This pull request removes 3180 and adds 3164 tests. Note that renamed tests count towards both.
aweXpect.Core.Tests.Core.Exceptions.FailExceptionTests ‑ Message_ShouldBeSet(message: "message326d8b4b-a408-4d51-a6d3-b34764c17aa9")
aweXpect.Core.Tests.Core.Exceptions.FailExceptionTests ‑ Message_ShouldBeSet(message: "message399e9239-c575-4980-bf8f-417d6b097eb2")
aweXpect.Core.Tests.Core.Exceptions.FailExceptionTests ‑ Message_ShouldBeSet(message: "message601fdf41-1765-45ed-a87a-511ce1053353")
aweXpect.Core.Tests.Core.Exceptions.FailExceptionTests ‑ Message_ShouldBeSet(message: "message7406bd3f-4fac-4f18-9a39-9d57c4e24a00")
aweXpect.Core.Tests.Core.Exceptions.FailExceptionTests ‑ Message_ShouldBeSet(message: "messageb9eb8f1d-cacd-4179-9c46-be574d3db12b")
aweXpect.Core.Tests.Core.Exceptions.FailExceptionTests ‑ Message_ShouldBeSet(message: "messagec21490a0-6889-466a-9ab8-ce0850ff63dc")
aweXpect.Core.Tests.Core.Exceptions.SkipExceptionTests ‑ Message_ShouldBeSet(message: "message1562e72a-b5d0-46f3-81e5-c13a353649fa")
aweXpect.Core.Tests.Core.Exceptions.SkipExceptionTests ‑ Message_ShouldBeSet(message: "message3033f425-69ad-454b-ba23-75dcf80f06a6")
aweXpect.Core.Tests.Core.Exceptions.SkipExceptionTests ‑ Message_ShouldBeSet(message: "message76c7a599-3635-47b8-9ec3-21ced0e73745")
aweXpect.Core.Tests.Core.Exceptions.SkipExceptionTests ‑ Message_ShouldBeSet(message: "message8f9f1fe1-dc83-43f1-a1b3-9a0e7be1587a")
…
aweXpect.Core.Tests.Core.Exceptions.FailExceptionTests ‑ Message_ShouldBeSet(message: "message266c5799-d5e2-4e5a-9d1a-9469c1a6a1f9")
aweXpect.Core.Tests.Core.Exceptions.FailExceptionTests ‑ Message_ShouldBeSet(message: "message8e83822c-ed17-4383-ada0-8a857d2e9cef")
aweXpect.Core.Tests.Core.Exceptions.FailExceptionTests ‑ Message_ShouldBeSet(message: "message9dd2e2ba-fa50-4232-afad-a557d4586a16")
aweXpect.Core.Tests.Core.Exceptions.FailExceptionTests ‑ Message_ShouldBeSet(message: "messagea35a1ccd-49a0-47c2-ac3d-468d8fdcf528")
aweXpect.Core.Tests.Core.Exceptions.FailExceptionTests ‑ Message_ShouldBeSet(message: "messageadf89b77-5ca9-477b-b64e-1b760e7541bb")
aweXpect.Core.Tests.Core.Exceptions.FailExceptionTests ‑ Message_ShouldBeSet(message: "messaged57a13ff-7a3d-4971-aafe-370680f76072")
aweXpect.Core.Tests.Core.Exceptions.SkipExceptionTests ‑ Message_ShouldBeSet(message: "message062aabbf-c5ca-4dc3-92ba-4f899eccc317")
aweXpect.Core.Tests.Core.Exceptions.SkipExceptionTests ‑ Message_ShouldBeSet(message: "message23bd4b4b-5fba-4765-a06f-870890b2087b")
aweXpect.Core.Tests.Core.Exceptions.SkipExceptionTests ‑ Message_ShouldBeSet(message: "message6b0811ce-161b-490a-8f9e-fe899dca406d")
aweXpect.Core.Tests.Core.Exceptions.SkipExceptionTests ‑ Message_ShouldBeSet(message: "message8c7bacff-f496-4d9d-bdc4-e03e0309213b")
…

♻️ This comment has been updated with latest results.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 17, 2026

🚀 Benchmark Results

Details

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 9V74 2.87GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.300
[Host] : .NET 8.0.27 (8.0.27, 8.0.2726.22922), X64 RyuJIT x86-64-v3

Job=InProcess Toolchain=InProcessEmitToolchain IterationCount=15
LaunchCount=1 WarmupCount=10

Method Mean Error StdDev Gen0 Gen1 Allocated
Bool_aweXpect 286.2 ns 4.10 ns 3.83 ns 0.0415 - 696 B
Bool_FluentAssertions 271.8 ns 0.86 ns 0.72 ns 0.0567 - 952 B
Equivalency_aweXpect 324,579.6 ns 3,157.77 ns 2,799.28 ns 20.0195 0.4883 335444 B
Equivalency_FluentAssertions 2,650,887.9 ns 24,243.66 ns 22,677.54 ns 285.1563 46.8750 4804906 B
Int_GreaterThan_aweXpect 284.0 ns 3.14 ns 2.62 ns 0.0515 - 864 B
Int_GreaterThan_FluentAssertions 270.4 ns 1.17 ns 1.04 ns 0.0730 - 1224 B
ItemsCount_AtLeast_aweXpect 492.0 ns 3.66 ns 3.42 ns 0.0811 - 1360 B
ItemsCount_AtLeast_FluentAssertions 556.9 ns 4.17 ns 3.70 ns 0.1192 - 2008 B
String_aweXpect 478.5 ns 3.47 ns 2.71 ns 0.0668 - 1128 B
String_FluentAssertions 1,263.7 ns 19.94 ns 17.67 ns 0.2346 - 3944 B
StringArray_aweXpect 1,908.3 ns 10.52 ns 9.32 ns 0.1564 - 2624 B
StringArray_FluentAssertions 1,422.1 ns 12.34 ns 10.94 ns 0.2480 - 4152 B
StringArrayInAnyOrder_aweXpect 2,518.9 ns 7.65 ns 6.79 ns 0.1678 - 2816 B
StringArrayInAnyOrder_FluentAssertions 63,841.4 ns 360.82 ns 337.51 ns 3.4180 - 57481 B

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 17, 2026

👽 Mutation Results

Mutation testing badge

aweXpect

Details
File Score Killed Survived Timeout No Coverage Ignored Compile Errors Total Detected Total Undetected Total Mutants

The final mutation score is NaN%

Coverage Thresholds: high:80 low:60 break:0

aweXpect.Core

Details
File Score Killed Survived Timeout No Coverage Ignored Compile Errors Total Detected Total Undetected Total Mutants
Core/Nodes/WhichNode.cs 96.36% 53 1 0 1 17 8 53 2 80

The final mutation score is 96.36%

Coverage Thresholds: high:80 low:60 break:0

Copilot AI review requested due to automatic review settings May 17, 2026 07:42
@vbreuss vbreuss force-pushed the topic/fix-chained-which-projection branch from a0ec6d8 to 6ef5fba Compare May 17, 2026 07:42
@vbreuss vbreuss enabled auto-merge (squash) May 17, 2026 07:42
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

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.

Comment thread Source/aweXpect.Core/Core/Nodes/WhichNode.cs
Comment thread Source/aweXpect.Core/Core/Nodes/WhichNode.cs Outdated
Comment thread Tests/aweXpect.Core.Tests/Core/ExpectationBuilderTests.cs
Comment thread Tests/aweXpect.Core.Tests/Core/Nodes/WhichNodeTests.cs
@sonarqubecloud
Copy link
Copy Markdown

@vbreuss vbreuss merged commit ef8b9dd into main May 17, 2026
13 checks passed
@vbreuss vbreuss deleted the topic/fix-chained-which-projection branch May 17, 2026 09:12
github-actions Bot added a commit that referenced this pull request May 17, 2026
github-actions Bot added a commit that referenced this pull request May 17, 2026
@github-actions
Copy link
Copy Markdown
Contributor

This is addressed in release v2.34.0.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working state: released The issue is released

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants