demo(17-captive-dependency): 17 — Singleton background service captures a scoped dependency#166
demo(17-captive-dependency): 17 — Singleton background service captures a scoped dependency#166
Conversation
…es a scoped dependency See scenarios/17-captive-dependency/README.md for the expected verdict.
There was a problem hiding this comment.
GauntletCI found the following issues:
These findings reference lines outside the PR diff, so they appear here instead of inline. Expand each entry for full evidence, rationale, and suggested action.
GCI0038 — Dependency Injection Safety (`src/OrderService/Program.cs`): Potential captive dependency: singleton may capture scoped service
GCI0038 — Dependency Injection Safety
Potential captive dependency: singleton may capture scoped service
Evidence:
src/OrderService/Program.cs: mixed lifetimes detected — builder.Services.AddSingleton();
💡 Suggested action: Ensure singleton services only depend on other singletons, or use IServiceScopeFactory to create scopes explicitly.
Confidence: Medium | Severity: Warn
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: de24ab40b5
ℹ️ 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".
| builder.Services.AddSingleton<OrderReminderBackgroundService>(); | ||
| builder.Services.AddHostedService(sp => | ||
| sp.GetRequiredService<OrderReminderBackgroundService>()); | ||
| builder.Services.AddScoped<IOrderEventEmitter, OrderEventEmitter>(); |
There was a problem hiding this comment.
Avoid scoped dependency in singleton hosted service
OrderReminderBackgroundService is registered as a singleton hosted service while IOrderEventEmitter is registered as scoped, and the singleton takes that interface in its constructor. This is a captive dependency: with scope validation disabled (typical production), the scoped emitter is effectively promoted to app lifetime and can leak request-scoped state; with scope validation enabled, startup can fail when resolving the hosted service. Use IServiceScopeFactory inside the background service or make the lifetimes consistent.
Useful? React with 👍 / 👎.
17 — Singleton background service captures a scoped dependency
Expected verdict: ❌ Fails — GauntletCI should fire GCI0038 (Dependency Injection Safety — captive dependency).
What changed
A new
OrderReminderBackgroundServiceis registered as a singletonhosted service alongside a brand-new
IOrderEventEmitterregistered asscoped, all inside the same DI composition root:
The new background service constructor takes
IOrderEventEmitterdirectly:
Why this is risky
container builds it. A scoped service injected directly into a
singleton becomes a captive dependency — the singleton holds the
same scoped instance forever, across every request and every scope.
IOrderEventEmitter, this means a per-request correlation id isfrozen on first use and reused for every subsequent event — silently
attributing every emitted event to the original request.
test) and only manifests under real traffic, often weeks after deploy
when a debugging engineer notices that "every error has the same
trace id."
What GauntletCI catches
GCI0038 Dependency Injection Safety— the diff registers both anAddSingleton<…>and anAddScoped<…>lifetime in the same file(
Program.cs), the canonical shape of a captive-dependency wiringmistake.
How to fix it
IServiceScopeFactoryinto the singleton instead, and createa scope per work item to resolve
IOrderEventEmitterfresh each time.(
IServiceScopeFactory.CreateScope()insideExecuteAsync).