Repository navigation
Bring DI lifetime handling into conformance with Brighter (#377) - #383
Conversation
Spec 015 (#377): requirements for DI lifetime conformance with Brighter, and ADR 0022 deciding that teardown releases every component in its own fault boundary, disposes the per-query lifetime last, never throws, and is awaited on the async and stream paths (#378). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ADR 0023 (#379): the handler factory creates each query's pipeline scope up front via CreatePipelineScope(), the lifetime carries it as an IAmAScope handle, and factories resolve only from that handle or fail fast. Removes the DI factory's ConditionalWeakTable and IAmALifetime.Add; amends ADR 0022's IAmALifetime contract to match. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ADR 0024 (#380): with ScopeAffinity.JoinAmbient and a Scoped handler lifetime, the DI factory adopts the ambient scope offered by an IAmAScopeProvider, after a probe, wrapped in a non-owning handle so Darker never disposes it; otherwise it falls back to an owned scope with a once-per-container Warning. Adds the Paramore.Darker.Extensions.AspNetCore package with AddDarkerRequestScope(). ADR 0014 stays Accepted with a partial-supersession note: its scope-ownership account and Scoped-QueryProcessor paragraph are superseded by 0024. ADR 0023's factory table row 4 now reads 'not a recognised scope' to match 0024's widened recognition. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Records the DI lifetime conformance change (#377) under ## Master: request-scope sharing via AddDarkerRequestScope(), robust teardown, and the seven breaking changes from ADRs 0022 and 0023. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XnmfNMt1xWhtLYLknDZx6Z
…riteria /spec:write_release_notes held a marked section only up to the next heading of any depth, which stops at "#### Breaking changes"; a literal replace duplicated the breaking-changes list. The extent now runs to the next ### or ## heading, and hand-written #### subsections are carried over verbatim on replace so a re-run never deletes them. /spec:review design did not grade Implementation Approach test plans against .agent_instructions/testing.md, so a plan to add InternalsVisibleTo for testing was scored as acceptable. Design reviews now read testing.md and score a plan that breaks one of its explicit rules as High. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XnmfNMt1xWhtLYLknDZx6Z
Issues worked across requirements, ADRs 0022-0024, the 0014 status note and the release notes, so each concept is stated one way everywhere: - Teardown guarantee on the DI path: Darker isolates its own steps and always disposes the scope; faults inside one DI scope's disposal are the container's. AC1, NFR Reliability and Constraints reworded. - Instance sharing under JoinAmbient (redesign): decorators are registered Transient unless HandlerLifetime is Singleton; a shared Scoped handler instance is accepted, documented and pinned. FR11 and NFR Concurrency reworded; Singleton decorator sharing out of scope. - Disposed borrowed scopes: no ConfigurationException wrap; a scope disposed before the build fails the probe; later disposal surfaces unwrapped. FR17 reworded, constructor and step removed. - Warning conditions: only misconfiguration fallbacks warn (FR15). - Testing plan (redesign): no InternalsVisibleTo; FR10's guard has no direct test and its AC is replaced by a public-surface criterion. - New ACs for partial-build release and release through the creating factory; release notes gain the missing items and an Added list. - Structure: 0024 invariants moved under the mechanism, unrequested API given a Scope bullet, 0014 note lists every amended decision. Includes the round 1 findings in review-design.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XnmfNMt1xWhtLYLknDZx6Z
Issues worked, each swept across ADRs 0022-0024, requirements and the release notes: - 0024 records its two decisions: the decorator clause is in the Decision sentence and the shared "Where this ADR sits" row, and "unchanged without the opt-in" is narrowed to scope behaviour (FR14). - Decorator isolation is stated for decorators Darker registers; a user's own TryAdd-preserved registration is a recorded Negative. - Step 7 tests use a recording decorator test double keyed on a shared result type, so they can be written through the public surface. - The lifetime logs through the pipeline builder's per-TResult logger, so teardown log assertions do not depend on test order. - The AdoptionDiagnostics latch is atomic, tested under concurrency. - Teardown tolerates a builder that never built (row 1). - Release notes: Simple factories' async disposal, decorator-factory migration for IAmALifetime.Add; 0022 lists its test doubles. Includes the round 2 findings in review-design.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XnmfNMt1xWhtLYLknDZx6Z
Approves the design for #377: robust pipeline teardown, the pipeline scope handle, and ambient scope adoption. ADR 0014 stays Accepted, partially superseded by a prose note in its Status. Regenerates the ADR index and adds the design approval marker. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XnmfNMt1xWhtLYLknDZx6Z
Adds tasks.md (88 tasks across five phases) and the tasks review. Issues worked, each swept across the task list and its coverage appendix: - RED mutations that could not fail: T1.3 now removes Clear() as well as the guard; T4.8 breaks the lazy stream build, and runs a query first so the provider mutation has something to remember. - ADR 0022 row 1 gets its own task (T1.17a): no release of a null handler, and teardown tolerates a builder that never built. - Async teardown isolation: T1.12 is trimmed, T1.13 adds the async per-item boundary, T1.14/T1.15 keep each awaited release inside its boundary, T1.16 becomes CHARACTERISE, and T1.16a covers the async guarded log. T1.13a covers the DI teardown fault on async paths. - Test migration (T2.10) is CHARACTERISE with a mutation per file, and deletes the multi-item tests that would duplicate T2.6. - Padding removed: T2.14 is a VERIFY with named mutations, T3.13 drops its owned fact, and T3.17 now pins the duplicate-decorator change. - Untested public behaviour: null guards (T3.8a, T4.2) and provider replacement (T3.8b); T3.3 is plumbing only. - T3.24 gates with a TaskCompletionSource and checks per-reason latching; T2.4, T3.5 and T2.12 wording fixed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XnmfNMt1xWhtLYLknDZx6Z
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XnmfNMt1xWhtLYLknDZx6Z
…te methods Structural only (ADR 0022 step 1); no behaviour change, suite green before and after. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XnmfNMt1xWhtLYLknDZx6Z
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XnmfNMt1xWhtLYLknDZx6Z
- Test: When_a_tracked_lifetime_item_throws_on_dispose_should_dispose_the_others_and_log_error - Implementation: QueryLifetimeScope takes an ILogger and wraps each item's Dispose in try/catch, logging at Error with the item type Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XnmfNMt1xWhtLYLknDZx6Z
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XnmfNMt1xWhtLYLknDZx6Z
- Test: When_factory_disposes_the_lifetime_and_the_pipeline_disposes_it_again_should_dispose_each_item_once - Characterisation — RED observed via: deleting the _disposed guard and _trackedDisposables.Clear() in QueryLifetimeScope.Dispose (DisposeCount 2, expected 1) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XnmfNMt1xWhtLYLknDZx6Z
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XnmfNMt1xWhtLYLknDZx6Z
…ped service - Test: When_execute_tears_down_a_scope_holding_an_IAsyncDisposable_only_service_should_log_error_and_return_result - Characterisation — RED observed via: removing the per-item try/catch in QueryLifetimeScope (InvalidOperationException escapes Execute) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XnmfNMt1xWhtLYLknDZx6Z
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XnmfNMt1xWhtLYLknDZx6Z
- Test: When_the_handler_dispose_throws_on_execute_should_still_release_decorators_dispose_lifetime_and_log_at_error - Implementation: PipelineBuilder.Dispose wraps each release in its own try/catch, logging at Error with the component type; the lifetime is still disposed last Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XnmfNMt1xWhtLYLknDZx6Z
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XnmfNMt1xWhtLYLknDZx6Z
- Test: When_the_handler_throws_and_its_release_also_throws_on_execute_should_surface_the_handlers_exception - Characterisation — RED observed via: rethrowing from the handler-release catch in PipelineBuilder.Dispose (caller saw the release fault, not HandlerFaultException) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XnmfNMt1xWhtLYLknDZx6Z
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XnmfNMt1xWhtLYLknDZx6Z
… disposed - Test: When_the_handler_and_both_decorators_throw_on_release_should_log_one_error_per_fault_dispose_lifetime_and_return_result - Characterisation — RED observed via: replacing the per-decorator try/catch in PipelineBuilder.Dispose with one around the whole decorator loop (Error-count assertion fails) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XnmfNMt1xWhtLYLknDZx6Z
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XnmfNMt1xWhtLYLknDZx6Z
- Test: When_a_scoped_service_throws_on_dispose_inside_the_per_query_scope_should_return_the_result_and_log_at_error - Characterisation — RED observed via: removing the per-item try/catch in QueryLifetimeScope.Dispose (Should.NotThrow fails, the dependency's exception escapes) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XnmfNMt1xWhtLYLknDZx6Z
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XnmfNMt1xWhtLYLknDZx6Z
- Test: When_the_logging_provider_throws_while_teardown_logs_a_lifetime_fault_should_still_return_the_result - Test: When_the_logging_provider_throws_while_teardown_logs_a_release_fault_should_still_return_the_result - Implementation: PipelineBuilder and QueryLifetimeScope log teardown faults through a private TryLogTeardownFault helper with its own try/catch Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XnmfNMt1xWhtLYLknDZx6Z
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XnmfNMt1xWhtLYLknDZx6Z
- Test: When_a_decorator_throws_during_initialisation_on_execute{,_async,_stream}_should_release_the_handler_and_every_created_decorator
- Test: When_a_decorator_cannot_be_created_on_execute_should_release_the_handler_and_the_decorator_created_before_it
- Implementation: GetDecorators, GetDecoratorsAsync and GetStreamDecorators assign the decorator list to its field before the loop and add each decorator straight after Create, so Dispose sees it even when Create or InitializeFromAttributeParams throws
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XnmfNMt1xWhtLYLknDZx6Z
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XnmfNMt1xWhtLYLknDZx6Z
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MiLKqEzdGHZdixwJUcenrj
- Scoped RequestCorrelation shared by the endpoint and a Scoped handler via /request-scope - Verified by running the sample: endpoint and handler ids match Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MiLKqEzdGHZdixwJUcenrj
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MiLKqEzdGHZdixwJUcenrj
…R index (T5.7) Status stays Accepted per adr_frontmatter.md; the body Status prose already records the partial supersession by ADRs 0022, 0023 and 0024. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MiLKqEzdGHZdixwJUcenrj
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MiLKqEzdGHZdixwJUcenrj
…ld, no drift) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MiLKqEzdGHZdixwJUcenrj
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MiLKqEzdGHZdixwJUcenrj
…g message - Test: When_the_logging_provider_throws_on_the_debug_message_that_starts_teardown_should_still_return_the_result (sync) and the async twin - Implementation: PipelineBuilder logs the teardown start through a guarded helper on Dispose and DisposeAsync - Code review finding 3 (specs/015-di_lifetime_conformance/review-code.md) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MiLKqEzdGHZdixwJUcenrj
…structors in the AspNetCore types Code review findings 6 and 7 (specs/015-di_lifetime_conformance/review-code.md). No behaviour change. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MiLKqEzdGHZdixwJUcenrj
…ts classes, xUnit1051) - TestDoubles: split multi-type files so each type has its own file - Test classes are named [Behavior]Tests; files are named after their happy-path method - Pass TestContext.Current.CancellationToken where xUnit1051 asked for it - No behaviour change; test counts unchanged on net8.0 and net9.0 Code review findings 1, 2 and 8 (specs/015-di_lifetime_conformance/review-code.md). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MiLKqEzdGHZdixwJUcenrj
… letting it escape - Test: When_the_offered_scope_throws_when_asked_for_its_service_provider_should_use_owned_scopes_and_log_one_warning - Implementation: the probe reads Services once inside its fault boundary - Code review finding 5 (specs/015-di_lifetime_conformance/review-code.md) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MiLKqEzdGHZdixwJUcenrj
Code review finding 9. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MiLKqEzdGHZdixwJUcenrj
The earlier tidy (9fbf92e) renamed the Extensions.Tests files but left 33 classes named When_*. Test method names and counts are unchanged (Extensions 107 on net8.0 and net9.0). Code review pass 2, finding 1. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MiLKqEzdGHZdixwJUcenrj
|
@claude Let's review this, as it is a significant change. We have two goals here: improve our handling of the lifetime of components that Brighter creates through a factory, to ensure that we don't leak, and we don't carry state between operations if the user did not ask for that. Second, to allow us to share a ServiceCollection and resolution, with an HTTPContext (and Brighter) such that we would resolve a dependency with state, such as an EF DbContext, from the same ServiceCollection scope |
|
Claude encountered an error after 3m 17s —— View job Review: DI lifetime conformance with Brighter (#377)Todo list
Goal 1 — lifetime/leak handling (ADR 0022 + 0023)This is a solid, well-contained design:
I didn't find a leak or double-dispose path in this part of the diff. Goal 2 — sharing scope with HttpContext/EF DbContext (ADR 0024)
Minor observations (not blocking)
SummaryBoth stated goals are met by the design and the code backing it: teardown is now isolated, logged, scope-last, and never-throwing (#378/#379), and |
Closes #377. Also closes #378, #379 and #380.
What this does
A Darker handler can now share the Scoped services of an ASP.NET Core request, such as an EF Core
DbContext, with the controller and Brighter handlers. Register Scoped handlers and callAddDarkerRequestScope()from the newParamore.Darker.Extensions.AspNetCorepackage. Making theQueryProcessorScoped never achieved this, and the guidance inCLAUDE.mdand the README is corrected.Spec:
specs/015-di_lifetime_conformance/. Design: ADRs 0022, 0023 and 0024. ADR 0014 stays Accepted and is noted as partially superseded.DisposeAsyncand release through the factory that created the handler. A build that fails part-way releases what it already created. A throwing logging provider cannot break teardown.IAmAScope,CreatePipelineScope()on the handler factories, andIAmALifetime.PipelineScopein place ofAdd.ScopeAffinity,IAmAScopeProvider,IAmAServiceProviderScope,DarkerOptions.DefaultScopeAffinity(defaultAlwaysNew, so existing behaviour is unchanged),AddDarkerScopeProvider<T>(), and the newParamore.Darker.Extensions.AspNetCorepackage. Adoption requiresHandlerLifetime = Scoped. An offered scope that is the root provider, from another container, or faulty is declined with one Warning. Darker never disposes an adopted scope.Lifetime validation stays in #305.
Breaking changes
Custom factory implementations must change. The full list, with migration notes, is in the spec 015 section of
release_notes.md:ReleasebecomesValueTask ReleaseAsync, andIAmALifetimeextendsIAsyncDisposable;CreatePipelineScope();IAmALifetime.Addis removed;HandlerLifetimeis Singleton.Under
JoinAmbient, queries in one scope share its Scoped services, including a Scoped handler instance. Concurrent queries in one request needAlwaysNew.Samples
SampleMinimalApigains a/request-scopeendpoint that shows the endpoint and a Scoped handler seeing the same Scoped service.Verification
dotnet build Darker.Filter.slnf -c Releasesucceeds, and the full suite passes on net8.0 and net9.0.Not in this PR
InternalsVisibleToentries, and a test that finds an internal type by name: Remove InternalsVisibleTo from the test setup #382.xUnit1051warnings in test files that predate this branch.Singleton-path benchmark (T5.9) was not run.Review notes
This is a large diff (about 190 commits). The Phase 3 and Phase 4 tests were reviewed in batches, and the Phase 4 ralph run (T4.6 to T4.9) was reviewed afterwards. T4.8's second mutation is covered by the disposed-scope probe, which was accepted.
🤖 Generated with Claude Code
https://claude.ai/code/session_01MiLKqEzdGHZdixwJUcenrj