-
-
Notifications
You must be signed in to change notification settings - Fork 109
fix IAsyncInitializer order when using nested property injection #4036
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Code Review - PR #4036SummaryThis PR fixes issue #4032 where nested IAsyncInitializer dependencies weren't being initialized in the correct order, breaking patterns like WebApplicationFactory + Testcontainers. Strengths
Code Quality ObservationsObjectGraphDiscoverer.cs (Lines 497-514)Correctness: The logic is sound. Filtering by testClassPrefix ensures only direct properties are added at depth 0. Performance: The StartsWith check with StringComparison.Ordinal is appropriate and efficient. Edge Case Question: What happens if testDetails.ClassType.FullName is null? (Unlikely but worth considering) The current implementation should handle this correctly (null would create prefix "null." which wouldn't match real properties), but consider adding a defensive null-check. NestedAsyncInitializerTests.csTest Design: Excellent use of static counters, [NotInParallel], [Before(Class)], and shared instances. Suggestions:
Testing RecommendationsSuggested Additional Test Cases:
Dual-Mode Testing (Per CLAUDE.md Rule 1): Security & PerformanceSecurity: No security concerns. The fix is contained to object graph discovery logic. Performance:
AOT Compatibility: No new reflection usage; existing suppressions are appropriate. Pre-Commit Checklist VerificationBased on CLAUDE.md:
Final VerdictRecommendation: Approve with minor suggestions This is a well-crafted fix for a critical initialization ordering bug. The code change is minimal and surgical, the tests are comprehensive, and the documentation is clear. Suggested improvements before merge:
Nice work! The test coverage is particularly impressive - the mock classes clearly demonstrate the failure scenario and verify the fix. |
There was a problem hiding this 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 issue #4032 where nested IAsyncInitializer dependencies weren't being initialized before their parent's InitializeAsync was called. This broke real-world patterns like WebApplicationFactory + Testcontainers where a container must be fully initialized before the factory tries to access its connection string.
Key Changes:
- Modified
ObjectGraphDiscoverer.CollectRootObjectsto filter property injection so only direct test class properties are added at depth 0 - Nested properties are now discovered through normal graph traversal at depth 1+, ensuring proper initialization order
- Added comprehensive regression tests simulating the WebApplicationFactory + Testcontainers pattern
- Updated CLAUDE.md with guidance about running TUnit.TestProject tests with filters
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 11 comments.
| File | Description |
|---|---|
| TUnit.Core/Discovery/ObjectGraphDiscoverer.cs | Core fix: Filters injected properties to only include direct test class properties at depth 0, allowing nested properties to be initialized in correct order through graph traversal |
| TUnit.TestProject/Bugs/4032/NestedAsyncInitializerTests.cs | Comprehensive regression tests with mock classes simulating database containers and web application factories, including both 2-level and 3-level deep nesting scenarios |
| CLAUDE.md | Added important guidance about avoiding OR patterns in treenode-filter commands to prevent matching thousands of unintended tests |
|
|
||
| public MockDatabaseContainer() | ||
| { | ||
| InstanceId = Interlocked.Increment(ref _instanceCount); |
Copilot
AI
Dec 10, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Write to static field from instance method, property, or constructor.
|
|
||
| public Task InitializeAsync() | ||
| { | ||
| Interlocked.Increment(ref _initializeCount); |
Copilot
AI
Dec 10, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Write to static field from instance method, property, or constructor.
|
|
||
| public MockWebApplicationFactory() | ||
| { | ||
| InstanceId = Interlocked.Increment(ref _instanceCount); |
Copilot
AI
Dec 10, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Write to static field from instance method, property, or constructor.
|
|
||
| public Task InitializeAsync() | ||
| { | ||
| Interlocked.Increment(ref _initializeCount); |
Copilot
AI
Dec 10, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Write to static field from instance method, property, or constructor.
| public DeepNestedDependency() | ||
| { | ||
| InstanceId = Interlocked.Increment(ref _instanceCount); | ||
| } | ||
|
|
Copilot
AI
Dec 10, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Write to static field from instance method, property, or constructor.
| public DeepNestedDependency() | |
| { | |
| InstanceId = Interlocked.Increment(ref _instanceCount); | |
| } | |
| private DeepNestedDependency(int instanceId) | |
| { | |
| InstanceId = instanceId; | |
| } | |
| public static DeepNestedDependency Create() | |
| { | |
| var id = Interlocked.Increment(ref _instanceCount); | |
| return new DeepNestedDependency(id); | |
| } |
| public MiddleDependency() | ||
| { | ||
| InstanceId = Interlocked.Increment(ref _instanceCount); | ||
| } | ||
|
|
Copilot
AI
Dec 10, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Write to static field from instance method, property, or constructor.
| public MiddleDependency() | |
| { | |
| InstanceId = Interlocked.Increment(ref _instanceCount); | |
| } | |
| private MiddleDependency(int instanceId) | |
| { | |
| InstanceId = instanceId; | |
| } | |
| public static MiddleDependency Create() | |
| { | |
| var id = Interlocked.Increment(ref _instanceCount); | |
| return new MiddleDependency(id); | |
| } |
|
|
||
| public Task InitializeAsync() | ||
| { | ||
| Interlocked.Increment(ref _initializeCount); |
Copilot
AI
Dec 10, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Write to static field from instance method, property, or constructor.
|
|
||
| public TopLevelDependency() | ||
| { | ||
| InstanceId = Interlocked.Increment(ref _instanceCount); |
Copilot
AI
Dec 10, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Write to static field from instance method, property, or constructor.
|
|
||
| public Task InitializeAsync() | ||
| { | ||
| Interlocked.Increment(ref _initializeCount); |
Copilot
AI
Dec 10, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Write to static field from instance method, property, or constructor.
| if (kvp.Key.StartsWith(testClassPrefix, StringComparison.Ordinal) && kvp.Value != null) | ||
| { | ||
| if (tryAdd(kvp.Value, 0)) | ||
| { | ||
| onRootObjectAdded(kvp.Value); | ||
| } |
Copilot
AI
Dec 10, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These 'if' statements can be combined.
| if (kvp.Key.StartsWith(testClassPrefix, StringComparison.Ordinal) && kvp.Value != null) | |
| { | |
| if (tryAdd(kvp.Value, 0)) | |
| { | |
| onRootObjectAdded(kvp.Value); | |
| } | |
| if (kvp.Key.StartsWith(testClassPrefix, StringComparison.Ordinal) && kvp.Value != null && tryAdd(kvp.Value, 0)) | |
| { | |
| onRootObjectAdded(kvp.Value); |
…ect graph discovery
Code Review - PR #4036SummaryThis PR fixes a critical regression in IAsyncInitializer ordering where nested dependencies weren't being initialized before their parent objects accessed them. The fix correctly ensures that nested IAsyncInitializer dependencies (like Testcontainers) are initialized before parent objects (like WebApplicationFactory) try to use them. Strengths1. Correct Problem AnalysisThe root cause was properly identified: nested properties of injected objects were being added at depth 0 alongside direct test class properties, which caused them to initialize in the wrong order. 2. Clean Implementation
3. Excellent Test CoverageThe test file is exceptionally well-designed:
4. Documentation
Code Quality AnalysisPerformance Considerations
Memory and Allocations
Potential Issues1. Generic Type HandlingThe FullName of generic types includes type parameters. The cache key might not match this format exactly depending on how the key is constructed elsewhere. Recommendation: Add a test case with generic test classes to verify this works correctly. 2. Nested Generic TypesNested types have FullName format like Outer+Inner. If there are nested classes with properties, the LastIndexOf might not correctly separate the type from property name if the cache key format differs. 3. Null FullName Edge Case (Already Handled)Line 534 checks if currentType.FullName != null - good defensive coding for compiler-generated types. CLAUDE.md Compliance
Testing RecommendationsTests to consider adding:
Minor Suggestions
Final VerdictRecommendation: APPROVE with minor suggestions This is a high-quality fix that:
The implementation is sound and the test coverage is exemplary. The minor suggestions above are optional improvements, not blockers. Great work fixing this regression! The test infrastructure patterns (Testcontainers + WebApplicationFactory) are common in real-world testing scenarios, so this fix is important for users. Risk Level: Low |
Fixes #4032