@@ -216,9 +216,41 @@ hand-resolved, not at the container-built `PooledConnection`. Falls back to the
216216when the call is unknown. Pinned by ` DiCaptiveSample.cs ` (` ConnectionResolver:79 ` ,
217217` ExprBodiedResolver:123 ` , transitive ` WrapperResolver:137 ` ).
218218
219+ ## DI005 — scope-resolved scoped service cached into a field (shipped), and the OQ #3 fix recognised
220+
221+ The remedy DI001/DI002 point at is ** scope-per-operation** : inject ` IServiceScopeFactory ` , and per
222+ operation ` using var scope = factory.CreateScope(); ` then resolve the scoped dependency * inside* the
223+ scope. DI005 catches that remedy done wrong — the scope-resolved ** scoped** service ** cached into a
224+ field** . The field outlives the ` using ` scope, so the cached instance dangles after the scope (and
225+ the service) is disposed * and* is promoted to the singleton's application lifetime: the captive is
226+ back, hidden behind the API meant to fix it (a ** warning** , anchored at the field-store site).
227+
228+ The extractor (still purely syntactic) records the ** scope-creator** names with the same this-field
229+ discipline as DI004 — a ** directly-injected ` IServiceScopeFactory ` ** * and* an injected
230+ ` IServiceProvider ` (both expose ` CreateScope() ` ) — then the scope locals their ` CreateScope() `
231+ produces, and every ` scope.ServiceProvider.Get(Required)Service<T>() ` whose result is ** assigned to
232+ a field** into a ` scope_cached ` list with its store site. ` find_scope_cached_captives `
233+ (` ownlang/di.py ` ) walks each cached entry's strong transient graph like DI001; the field-store site
234+ is the finding's primary anchor, the registration the secondary.
235+
236+ ** This is the answer to P-006 open question #3 — recognising the directly-injected
237+ ` IServiceScopeFactory ` fix.** It needs no separate "approval" fact: the correct pattern (resolve
238+ inside the scope, use, ** discard** — a local, not a field store) simply ** produces no ` scope_cached `
239+ entry** , so it is silent ** by construction** . The "positive signal" is the * absence* of a captive
240+ fact. Recognising the factory injection as licence to suppress * other* captive findings would be
241+ wrong — a singleton that also injects a scoped service directly is still DI001. Pinned end-to-end by
242+ ` DiCaptiveSample.cs ` (` ScopeCachingService ` DI005 direct, ` UnitOfWorkCachingService ` DI005
243+ transitive; ` ScopeUsingService ` — the correct scope-per-operation use — and ` ClockCachingService `
244+ — a cached * singleton* , shareable — both silent) in the ` wpf-extractor ` CI job, and at the graph
245+ level by ` tests/test_ownir.py ` .
246+
219247## Next (separate slices)
220248- Per-** parameter** precision for the captive anchor (the specific injecting parameter, not just
221249 the constructor).
222- - The plural ` GetServices<T>() ` and non-generic ` GetService(typeof(T)) ` resolution forms, and a
223- directly-injected ` IServiceScopeFactory ` as the recognised fix (DI004 currently reads the
224- generic singular ` Get(Required)Service<T>() ` and the ` CreateScope() ` → scope-provider form).
250+ - The plural ` GetServices<T>() ` and non-generic ` GetService(typeof(T)) ` resolution forms (DI004
251+ currently reads the generic singular ` Get(Required)Service<T>() ` ).
252+ - ** A scope-resolved scoped service that * escapes* its scope by being returned (or passed out as a
253+ ` ref ` /` out ` /method argument)** rather than cached into a field — the same lifetime promotion as
254+ DI005, but through a data-flow edge the store-site pass does not model. Silent today
255+ (precision-safe: the extractor records only field stores, so an escaping local is no
256+ ` scope_cached ` fact). A candidate for a future flow-aware slice, not the store-site model.
0 commit comments