Skip to content

fix: support nested Member after IsTypeOf - #6540

Open
mvanhorn wants to merge 1 commit into
thomhurst:mainfrom
mvanhorn:fix/6528-nested-member-after-istypeof
Open

fix: support nested Member after IsTypeOf#6540
mvanhorn wants to merge 1 commit into
thomhurst:mainfrom
mvanhorn:fix/6528-nested-member-after-istypeof

Conversation

@mvanhorn

@mvanhorn mvanhorn commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Description

Add a small internal production-facing type-erasure contract in MemberAssertion.cs that lets MemberAssertionResult<TObject> expose its already-composed Assertion<object?> without changing the public fluent API or re-running only part of the chain. Update WrapMemberAssertion to recognize that contract before its reflection-based Assertion<T> fallback, preserving existing handling for ordinary transformed assertions and existing diagnostics for unsupported return values. This keeps the nested result on the same execution path used by normal member chaining and avoids making the public result type inherit a different base class.

An outer Member assertion currently selects its object-returning fallback when the assertion lambda ends in another MemberAssertionResult<T>. In the reported chain, IsTypeOf<InnerEx>().And.Member(...) produces that result, but WrapMemberAssertion only recognizes classes derived from Assertion<T> and throws InvalidOperationException before evaluating the assertions. The member result already implements the non-generic assertion contract and contains the combined type and nested-member checks, so rejecting it is inconsistent with the supported chaining model. There are no claims, competing PRs, or closed-unmerged attempts associated with the issue.

Closes #6528

Related Issue

Fixes #

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update
  • Performance improvement
  • Refactoring (no functional changes)

Checklist

Not applicable to this change.

Required

  • I have read the Contributing Guidelines
  • If this is a new feature, I started a discussion first and received agreement
  • My code follows the project's code style (modern C# syntax, proper naming conventions)
  • I have written tests that prove my fix is effective or my feature works

TUnit-Specific Requirements

  • Dual-Mode Implementation: If this change affects test discovery/execution, I have implemented it in BOTH:
    • Source Generator path (TUnit.Core.SourceGenerator)
      Not run: no test command resolved in this workspace, so nothing was executed to pass.
    • Reflection path (TUnit.Engine)
      Not run: no test command resolved in this workspace, so nothing was executed to pass.
  • Snapshot Tests: If I changed source generator output or public APIs:
    • I ran TUnit.Core.SourceGenerator.Tests and/or TUnit.PublicAPI tests
      Not run: no test command resolved in this workspace, so nothing was executed to pass.
    • I reviewed the .received.txt files and accepted them as .verified.txt
      Not run: no test command resolved in this workspace, so nothing was executed to pass.
    • I committed the updated .verified.txt files
      Not run: no test command resolved in this workspace, so nothing was executed to pass.
  • Performance: If this change affects hot paths (test discovery, execution, assertions):
    • I minimized allocations and avoided LINQ in hot paths
    • I cached reflection results where appropriate
  • AOT Compatibility: If this change uses reflection:
    • I added appropriate [DynamicallyAccessedMembers] annotations
      Not run: no test command resolved in this workspace, so nothing was executed to pass.
    • I verified the change works with dotnet publish -p:PublishAot=true
      Not run: no test command resolved in this workspace, so nothing was executed to pass.

Testing

  • All existing tests pass (dotnet test)
    Not run: no test command resolved in this workspace, so nothing was executed to pass.
  • I have added tests that cover my changes
  • I have tested both source-generated and reflection modes (if applicable)
  • The exact ThrowsExactly<OuterEx>().And.Member(... IsTypeOf<InnerEx>().And.Member(Code, IsEqualTo(5))) chain completes successfully for an InnerEx whose code is 5. - The same chain with a different Code value throws the normal assertion exception, proving the nested member assertion is executed instead of being silently skipped or rejected as an unexpected type.

Additional Notes

Nothing beyond what is described above.

@greptile-apps

greptile-apps Bot commented Aug 3, 2026

Copy link
Copy Markdown

Greptile Summary

The PR fixes nested Member assertions after type transformations by exposing and reusing the nested result’s already-composed type-erased assertion.

  • Adds an internal type-erasure contract implemented by MemberAssertionResult<TObject>.
  • Updates the object-returning Member fallback to recognize nested member results before reflection-based wrapping.
  • Adds success and failure coverage proving both the type check and nested member check execute.

Confidence Score: 5/5

The PR appears safe to merge; the nested result preserves the complete assertion chain and the tests cover both successful and failing execution paths.

The new branch extracts the assertion that BuildMemberResult already composed from pending and nested checks, while existing assertion types continue through the unchanged reflection fallback.

Important Files Changed

Filename Overview
src/TUnit.Assertions/Conditions/MemberAssertion.cs Adds an internal interface that exposes the fully composed erased assertion already held by a member result.
src/TUnit.Assertions/Extensions/AssertionExtensions.cs Recognizes nested member results in the object-returning fallback while preserving reflection handling and diagnostics for other values.
tests/TUnit.Assertions.Tests/Bugs/Issue6528Tests.cs Covers the successful chain and independently verifies failures from both the nested value assertion and preceding type assertion.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
    A[Outer Member lambda] --> B[IsTypeOf InnerEx]
    B --> C[And nested Member]
    C --> D[MemberAssertionResult InnerEx]
    D --> E[ITypeErasedMemberAssertion]
    E --> F[Already-composed Assertion object]
    F --> G[Outer BuildMemberResult]
    G --> H[Execute type and nested member checks]
Loading

Reviews (1): Last reviewed commit: "fix: support nested Member after IsTypeO..." | Re-trigger Greptile

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Member assertion throws InvalidOperationException when chained directly after IsTypeOf

1 participant