Skip to content

Allow DI scopes to opt out of resolved object ownership - #7633

Merged
thomhurst merged 9 commits into
mainfrom
issue-7375-scope-ownership
Nov 5, 2026
Merged

thomhurst merged 9 commits into
mainfrom
issue-7375-scope-ownership

Conversation

@thomhurst

@thomhurst thomhurst commented Nov 5, 2026 •

Copy link
Copy Markdown
Owner

Disposable custom DI scopes can resolve services without owning their disposal. Add the protected virtual ScopeOwnsCreatedObjects hook so these scopes can return false and let TUnit dispose resolved services, while the scope itself still receives cleanup. The default remains ownership by disposable scopes.

Failed factories retain resolved services and their trackable initializer graphs through the tracker’s global reference counts, so shared services remain alive until their final owner finishes. Cleanup references stay outside session accounting and release once across row clones. A separate weak identity mark protects self-resolving scopes from double disposal without transferring ownership of their initializer properties; UntrackObjectCore shares the retirement path between normal tracking and failed-row cleanup.

Document the hook and update all public API snapshots. Add EngineTest-tagged regression tests that verify a disposable, untracking scope and its service are each disposed exactly once.

Validation:

  • Latest native AOT publish succeeded on macOS arm64; the targeted _7375 native run has 9 passes and its single intentional resolution failure.
  • Latest sibling-getter fix: both generated-metadata and reflection regressions reproduced a leaked initializer before the fix. All 16 DataRowScopeCollectionTests pass afterward in generated and reflection execution modes; all 9 ObjectGraphDiscoverer tests pass. Normal discovery still propagates getter failures.
  • Latest review fix: all 12 DataRowScopeCollectionTests pass on net10.0, including disposable/non-disposable roots, shared nested initializers, clones, and throwing initializer getters. The nested shared-owner regression failed in both cases before the fix.
  • Latest generated and reflection _7375 runs each have 7 passes and the intentional missing-registration failure; the failure observer verifies nested cleanup without initialization.
  • Targeted _7375 tests on net10.0: 2 passed in generated mode and 2 passed with --reflection.
  • Core public API snapshot test on net10.0: passed. The identical signature was added to net8.0, net9.0, and Windows .NET Framework snapshots; those legs await CI.
  • Engine.Tests was not run locally per repository instructions; CI covers its mode matrix.
  • Native AOT publish succeeded on macOS arm64; the targeted native run has 7 passes and the intentional resolution failure, including the nested cleanup observer. Registration was subsequently sorted by graph depth and revalidated with unit/generated/reflection tests.
  • Existing analyzer build warnings are tracked in Analyzer and code-fixer projects emit release-tracking and nullable build warnings #7410; native fixture/dependency warnings are tracked in Regression test fixtures emit Native AOT preservation warnings #7493.

Closes #7375

Summary by CodeRabbit

  • Bug Fixes
    • Improved cleanup of services resolved from dependency-injection scopes, including when resolution fails partway through.
    • Kept services shared across test rows available until their final owner finishes.
    • Clarified ownership and disposal behavior for disposable and non-disposable scopes; custom data sources can specify whether a scope owns resolved services.
    • Ensured partially resolved services and scopes are cleaned up while preserving the original resolution error.
  • Documentation
    • Added guidance on disposal order and service lifetime expectations during cleanup.

@coderabbitai

coderabbitai Bot commented Nov 5, 2026 •

Copy link
Copy Markdown

Explain Change →

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • Resume reviews
  • Trigger review
Walkthrough

Dependency injection data sources now expose a scope ownership override. When a scope does not own resolved objects, TUnit tracks eligible disposable objects for cleanup, including objects resolved before a resolution failure.

Changes: Scope Ownership

Layer / File(s) Summary
Ownership override and resolution
src/TUnit.Core/Attributes/TestData/DependencyInjectionDataSourceSourceAttribute.cs, docs/docs/writing-tests/dependency-injection.md, tests/TUnit.PublicAPI/*.verified.txt
The ScopeOwnsCreatedObjects hook defaults to true for disposable scopes. Resolution applies the ownership rule and retains eligible resolved objects for cleanup if resolution fails. Documentation and public API snapshots describe the hook.
Object graph and row cleanup tracking
src/TUnit.Core/Tracking/ObjectTracker.cs, src/TUnit.Core/Discovery/ObjectGraphDiscoverer.cs, src/TUnit.Engine/Services/StaticPropertyHandler.cs
The tracker can retain objects for row cleanup without session ownership. Object graph discovery can retain a partial graph after nested discovery errors. Static-property tracking uses the updated graph discovery method.
Ownership and failure regression tests
tests/TUnit.TestProject/Bugs/_7375/ScopeOwnershipTests.cs, tests/TUnit.UnitTests/DataRowScopeCollectionTests.cs, tests/TUnit.Engine.Tests/Issue7375Tests.cs
Tests cover scope ownership, self-resolution, partial failures, shared services, nested initializers, and cleanup. The engine test checks the outcome and test counts for the issue-specific run.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Feature · Severity of issue fixed: Medium

Possibly related issues

  • thomhurst/TUnit#7382 — The hook selects scope ownership per scope and supports opting out of scope ownership.
  • thomhurst/TUnit#7472 — The changes address failed-row cleanup and ownership in overlapping code, but do not address the registration race.
  • thomhurst/TUnit#7376 — The changes update external-ownership handling for nested initializer objects, but do not include the requested unit tests.
  • thomhurst/TUnit#7213 — Failed graph discovery retains partial graphs and cleanup references when an initializer getter throws.

Possibly related PRs

  • thomhurst/TUnit#7328 — Introduced disposable-scope ownership behavior that this change extends with an override.
  • thomhurst/TUnit#7384 — Implements the related scope ownership behavior; this change adds the opt-out and failed-resolution cleanup.
  • thomhurst/TUnit#7391 — Addresses cleanup after partial dependency resolution, which this change also handles for incrementally resolved members.
Merge Risk: Low · up to 79b2c

A failed getter can leave nested resources undisposed. The change is mergeable with awareness of this narrow cleanup gap.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 37.04% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documenta… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed

Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title "Allow DI scopes to opt out of resolved object ownership" matches the main change in the changeset. The PR adds a protected virtual ScopeOwnsCreatedObjects hook on DependencyInjectionDataSou…
Linked Issues check Passed The only active directly linked target is [#7375]. #7382 is a project-tracking dependency, not a coding requirement. Its coding requirements are met at the reviewed head.

1. [#7375] Overridable hook …
Out of Scope Changes check Passed The hook, its default, the regression fixtures, the documentation, and the public API snapshot entries are in scope for [#7375].

The other changes support the non-owning path of the hook and its fail…
Full details: Title check

Explanation

The title "Allow DI scopes to opt out of resolved object ownership" matches the main change in the changeset. The PR adds a protected virtual ScopeOwnsCreatedObjects hook on DependencyInjectionDataSourceAttribute<TScope>. Returning false lets TUnit track and dispose resolved services while still disposing the scope. The public API snapshots, documentation, object tracker changes, and regression tests all support this主 change. The title is specific and concise, and it is 55 characters long.

Full details: Linked Issues check

Explanation

The only active directly linked target is [#7375]. #7382 is a project-tracking dependency, not a coding requirement. Its coding requirements are met at the reviewed head.

  1. [#7375] Overridable hook with the #7328 default. Verified in src/TUnit.Core/Attributes/TestData/DependencyInjectionDataSourceSourceAttribute.cs: protected virtual bool ScopeOwnsCreatedObjects(TScope scope) =&gt; scope is IDisposable or IAsyncDisposable;. GenerateDataSources calls the hook once per scope, after cleanup.Register(scope). When the result is true, each created value is marked externally owned, as before. When it is false, TUnit keeps ownership of created values and disposes them.

  2. [#7375] Regression test. Verified in tests/TUnit.TestProject/Bugs/_7375/ScopeOwnershipTests.cs. UntrackingScopeAttribute overrides the hook to return false. ScopeOwnershipObserverTests.Tracker_Disposes_Service_And_Scope_Once asserts that the scope DisposeCount equals 1 and that the service DisposeCount equals 1. The engine wrapper Issue7375Tests expects 9 passes and 1 intentional failure. I did not run it. The author reports targeted generated, reflection, and native runs.

  3. [#7375] Public API snapshots. The DotNet10_0 verified file contains the hook entry per the change summary. The output was truncated, so I did not confirm the entry by direct read. The author reports that the net10.0 snapshot test passes locally. The net8.0, net9.0, and Net4_7 snapshots are listed in the change summary and await CI.

  4. [#7375] Documentation. The change summary for docs/docs/writing-tests/dependency-injection.md describes the hook and its default. The XML remarks on ScopeOwnsCreatedObjects and Create document the same ownership rule. I did not read the Markdown file directly.

No contradiction was found between the requirements and the code at the reviewed head. Remaining uncertainty: the snapshot file entries for non-net10 targets and the Markdown documentation text were not read directly.

Full details: Out of Scope Changes check

Explanation

The hook, its default, the regression fixtures, the documentation, and the public API snapshot entries are in scope for [#7375].

The other changes support the non-owning path of the hook and its failure cleanup:

  • DependencyInjectionDataSourceSourceAttribute.cs: On a factory failure with the hook returning false, the code discovers the initializer graph of created values and retains disposable objects for row cleanup. It skips the scope and deduplicates by reference. MarkRowOwnedScope runs only on the non-owning path.
  • ObjectTracker.cs: RetainForRowCleanup and RowCleanupReference add row-scoped global holds. IsExternallyOwned also checks row-owned scopes. UntrackObjectCore is shared between normal tracking and failed-row cleanup. The author states that regular untracking keeps its session accounting. I could not read the full body because the earlier read was truncated.
  • ObjectGraphDiscoverer.cs: retainPartialGraphOnError supports failed-row retention. Normal discovery still propagates initializer getter failures. The HasExternallyOwnedGraph change affects ownership checks on the shared traversal.
  • StaticPropertyHandler.cs: It switches to DiscoverTrackableObjectGraph. This touches default static-property tracking. The change is tied to the shared graph traversal used by the failure cleanup path. I found no concrete lack of connection. Reviewers should confirm that default static-property tracking behaves as before.
  • DataRowScopeCollectionTests.cs and the _7375 fixtures: These cover the non-owning path, failed-row cleanup, shared services, nested initializers, and self-resolving scopes.

No change was demonstrated to be unrelated to #7375.

Full details: Docstring Coverage

Explanation

Docstring coverage is 37.04% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentation-only edits and deletions within surviving functions are included; unchanged diff context and fully deleted functions are excluded. Analyzed 27 functions across 7 files.

Resolution

Write docstrings for the functions missing them to satisfy the coverage threshold.

Finishing Touches

Generate docstrings

  • Commit to this branch
  • Create a new PR
Generate unit tests (beta)

  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

A rabbit checks each scope with care,
And tracks the services left to share.
If resolving stops before the end,
The row keeps objects safe to tend.
Then cleanup comes, with counts just right,
And nested objects sleep tight.

@thomhurst

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 5, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-05T08:59:21.160421Z 79b2c2a New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions

github-actions Bot commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

Review

Small, focused change. The protected virtual bool ScopeOwnsCreatedObjects(TScope scope) hook keeps the old default (scope is IDisposable or IAsyncDisposable), so existing users see no behaviour change. The public API snapshots are updated for all four TFMs. I found no other place in src that duplicates the ownership decision, so the hook covers the one place that needed it.

Non-blocking notes:

  • The hook is evaluated once per scope, after cleanup.Register(scope). If an override throws, the scope is already registered and still gets disposed, which is the right order. Worth a line in the remarks that overrides should be cheap and side-effect free.
  • The test checks the scope and the service are each disposed once, but only through a DependsOn observer reading static state. That is fine, but UntrackingScope.Created is global. Any other test in the project that uses this attribute would break the Count == 1 assertion. It is currently isolated to this folder.
  • Design alternative: an overridable method means subclassing to change ownership. A property on the attribute would be easier to discover, but the per-scope parameter is more flexible, so keeping the method is reasonable.
  • The docs and XML remarks are clear. The new doc bullet puts the whole override in a single sentence. A short C# snippet would help, as long as it compiles under the docs-snippet CI.

LGTM, pending the CI legs for the non-net10 snapshots.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep it up!

Reviewed commit: 7a99a1723f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@greptile-apps

greptile-apps Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The PR appears safe to merge; no new actionable issues were found.

Summary

Adds ScopeOwnsCreatedObjects so disposable DI scopes can leave service disposal to TUnit.

  • DI sources can let TUnit dispose services from custom scopes.
  • Failed scope resolution still cleans up services it already created.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Create and register scope] --> B{Scope owns services?}
    B -->|Yes| C[Scope disposes services]
    B -->|No| D[TUnit tracks services]
    D --> E{Factory succeeds?}
    E -->|Yes| F[Normal tracking]
    E -->|No| G[Discover reachable cleanup objects]
    G --> H[Continue past failed getters]
    H --> I[Retain shared references]
    F --> J[Release references at cleanup]
    I --> J
    J --> K[Final owner disposes service]
Loading

Reviews (9) · Last reviewed commit: "fix: retain cleanup siblings after injec..." · Reviewed by Greptile

@thomhurst

Copy link
Copy Markdown
Owner Author

@codex review

Addressed Greptile’s partial-resolution leak in b025dd0 and resolved its thread. Validation: 8 focused unit tests pass; generated and reflection _7375 runs each report 3 passes and 1 intentional resolution failure, with the cleanup observer passing. Added Issue7375Tests for CI; Engine.Tests was not run locally per repository instructions.

Claude’s non-blocking notes: added the cheap/side-effect-free guidance to XML remarks. The new failing scenario keeps its scope records separate from the successful-row count. Retained the per-scope virtual method for flexibility. Keeping the existing short prose documentation is reasonable for this single override; a snippet is optional and does not block this change.

@github-actions

github-actions Bot commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

Review

The change looks correct and well scoped.

  • ScopeOwnsCreatedObjects(TScope) is a protected virtual hook. Its default, scope is IDisposable or IAsyncDisposable, matches the previous behaviour, so it is not a breaking change. It is evaluated once per scope, after the scope is registered for cleanup, so a throwing override still releases the scope.
  • Converting the LINQ chain to a loop makes the partial-failure path possible. If a later Create throws and the scope does not own its services, the earlier resolved services are registered for cleanup. The reference-equality HashSet stops a service that was returned twice from being registered twice. The ReferenceEquals(value, scope) and IsExternallyOwned guards prevent double disposal. The original exception is rethrown unchanged.
  • The tests cover the hook and the failure path. The unit test is parameterised over owning and non-owning scopes and checks the exactly-once disposal of both the scope and the service. The engine test uses DependsOn observers to check disposal after the run.
  • All four public API snapshots (net8, net9, net10 and net4.7) get the same signature, and the docs describe the new hook.

Optional points:

  1. In the catch block, a non-owning scope's earlier services are registered through cleanup.Register(value). A short comment saying this is the same cleanup path the success case reaches through the tracker would help future readers.
  2. The "evaluated once per scope; overrides should be cheap and side-effect free" remark is useful. If someone later calls the hook from a second place, it would be easy to violate. Keeping the single call site is worth noting.

I found no blocking issues. Approving in spirit.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b025dd0c19

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@thomhurst

Copy link
Copy Markdown
Owner Author

@codex review

Both shared-service cleanup findings are fixed in 4ef3624. Claude’s optional explanatory comment is now present at the tracker helper. Retained the single ownership-hook call site.

@github-actions

github-actions Bot commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

Review

The PR adds a protected virtual ScopeOwnsCreatedObjects(TScope) hook, with docs, public API snapshots for all TFMs, and regression tests. I only read the diff and ran nothing.

What looks good

  • The default (scope is IDisposable or IAsyncDisposable) matches the old inline behaviour, so existing users see no change.
  • Resolving members in a loop and cleaning up already-resolved services when a later Create throws is a real leak fix. The original error is rethrown unchanged.
  • Cleanup uses the same global reference counts through RetainForRowCleanup. The _released interlock keeps disposal idempotent, and UntrackObjectCore is made static so it doesn't capture instance session state.
  • Tests cover both a disposable scope that doesn't own its services and the partial-failure path. The Engine test expects 3 passed and 1 failed, which matches the deliberate failure.

Suggestions (non-blocking)

  1. The hook takes the scope as a parameter and is documented as "evaluated once per scope". That is a contract the base class can't enforce, so a subclass that returns different answers per call would behave oddly. The remarks already say overrides should be cheap and side-effect free, which is probably enough. If you want it enforced, cache the result in a local, which the code already does.
  2. In the catch block, the retained HashSet is only needed to avoid registering the same instance twice when Create returns it for several members. The ObjectTracker.IsExternallyOwned and scope checks already cover the other cases. A short comment explaining the dedupe would help future readers.
  3. RetainForRowCleanup uses GlobalContext.Current.Disposer. If the failing row has a more specific context, confirm that spans and logging attach to the intended context. This only affects diagnostics.

No blocking issues. Nice, focused fix. I'd merge once the CI snapshot legs (net8/net9/.NET Framework) pass.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4ef3624788

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@thomhurst

Copy link
Copy Markdown
Owner Author

@codex review

Fixed self-resolving scope double disposal in cb1d5de and resolved the thread. Generated and reflection net10.0 _7375 runs each have 5 passes and the expected missing-registration failure; the new observer reproduced double disposal before the fix. Engine.Tests remains for CI per repository rules.

Disposition of Claude’s optional notes: kept the single cached ownership-hook call and added the failed-row reference-deduplication comment. Failed factories never produce a TestContext, so RetainForRowCleanup intentionally uses the global disposer and null-context shared-object diagnostics (see its existing comment and RowCleanupReference); no per-test diagnostic context is available on that path.

@github-actions

github-actions Bot commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

Review: Allow DI scopes to opt out of resolved object ownership

I reviewed the diff and found no blocking issues. The change looks good.

What I checked

  • The default for ScopeOwnsCreatedObjects is scope is IDisposable or IAsyncDisposable, the same as the previous inline logic. Existing behaviour is unchanged.
  • The hook is evaluated once per scope, after the scope is registered with row cleanup. A throwing override therefore still releases the scope.
  • ReferenceEquals(created, scope) correctly keeps the tracker from disposing a scope that a factory returns as a value. Row cleanup already owns the scope.
  • The failure path handles a partial factory failure when the scope does not own its services. It retains the already-resolved disposables for row cleanup, dedupes them by reference, and rethrows the original exception. RowCleanupReference is idempotent through Interlocked.Exchange.
  • The UntrackObjectCore refactor to a static method with an explicit sessionReferences parameter and disposer parameter keeps the existing paths the same. It lets RetainForRowCleanup bypass session accounting.
  • All four public API snapshots (net8, net9, net10 and net4.7) contain the same signature. The docs and XML remarks are updated.
  • The tests cover the non-owning scope, the partial failure, and the self-resolving scope. Each checks that disposal happens exactly once.

Non-blocking suggestions

  1. RetainForRowCleanup uses GlobalContext.Current.Disposer. If the global context could be unavailable or replaced between sessions, capturing the disposer at registration time would be safer. It looks fine in the current flow.
  2. The nested if inside the catch loop can be flattened. For example, check value is IDisposable or IAsyncDisposable first in the same condition.
  3. The bool hook takes only the scope. If services are ever mixed (some scope-owned, some not), a per-object hook such as OwnsCreatedObject(scope, object) would be more flexible. The current API is simpler and fits the issue, so this is only an observation.

The Engine.Tests matrix needs to pass in CI, since it wasn't run locally. Nice work on the thorough lifecycle coverage.

🤖 Generated with Claude Code

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cb1d5de473

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@thomhurst

Copy link
Copy Markdown
Owner Author

@codex review

Both initializer-ownership findings are fixed in 9ad8fb0; details and validation are in the resolved threads. Engine.Tests awaits CI.

Disposition of Claude’s optional notes: retained the existing global-disposer capture and nested conditional; neither needs a behavior change for this fix. A per-object public hook would expand the API beyond #7375; retained the per-scope hook.

@github-actions

github-actions Bot commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

Review of #7633: ScopeOwnsCreatedObjects hook

The change is sound and does what it says. I found no blocking bugs.

What I checked

  • The default, scope is IDisposable or IAsyncDisposable, matches the old behaviour. Existing users are unaffected.
  • In created is IDisposable or IAsyncDisposable, the or binds inside the pattern, so the precedence is correct.
  • On a factory failure, each resolved service gets exactly one retained reference. That holds even when the same instance is returned for several members.
  • RowCleanupReference is idempotent through Interlocked.Exchange. It stays out of session accounting by passing sessionReference: false with a null sessionReferences, and the ! dereferences are only reached on the sessionReference: true path.
  • The public API snapshots are updated for every TFM, and the docs describe the hook.
  • The tests cover both the ownership hook and the partial-failure cleanup path.

Suggestions (non-blocking)

  1. Ownership state is spread across several places. It now lives in s_externallyOwned, s_rowOwnedScopes, HasExternallyOwnedGraph and IsExternallyOwned. The split between 'owns the graph' and 'owns only the scope itself' is subtle, and the two similarly named predicates are easy to mix up at call sites. Consider one small ownership enum (None, ExternalGraph, ExternalScopeOnly) in a single weak table, or rename them to say what they mean (IsOwnedGraph and IsOwnedScopeOnly). That would also remove the extra volatile flag and the second ConditionalWeakTable.
  2. The failure-path cleanup could be a helper. The catch block in GenerateDataSources mixes resolution, tracking and cleanup registration. Moving it into a method such as ReleasePartiallyResolved(values, resolvedCount, scope, cleanup) would keep the happy path readable and let the cleanup be tested on its own.
  3. The hook is called once per scope, per the XML docs. If a user's override throws, the scope is already registered with cleanup, so it is still disposed. A test for that case would document the behaviour.
  4. Performance: the HashSet is only allocated on the failure path. That is fine for the hot path.

Overall this is a good, well-tested change.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Chef's kiss.

Reviewed commit: 9ad8fb05f7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@thomhurst

Copy link
Copy Markdown
Owner Author

Reviewed all review surfaces for head 9ad8fb0. Claude and Codex both report no blockers; all six threads have fix dispositions and no newer replies.

Disposition of the latest optional Claude suggestions: retain the separate graph and scope identity marks, whose distinct semantics are explicit at the discovery call sites; consolidating them is optional refactoring. Keep the short failure cleanup loop inline so the retained-reference lifetime remains visible beside resolution. A throwing-hook regression is optional: cleanup.Register(scope) precedes the single hook invocation, preserving scope cleanup if it throws. The failure-only HashSet needs no change.

Promoting this reviewed head to ready. CI and the ready-for-review Claude run must complete before auto-merge can be armed.

@thomhurst
thomhurst marked this pull request as ready for review November 5, 2026 07:54
This was referenced Nov 6, 2026

This branch was previously deployed

1 inactive deployment
Pull Requests — 79b2c2a2 Deployed Nov 5, 2026 by thomhurst via modularpipeline (windows-latest) #21392
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.

Let a dependency injection data source say its disposable scope does not own what Create returns

1 participant