Summary
The documentation describes Microsoft DI Scoped services as session-scoped in hosted Repl sessions, but Repl does not currently create an IServiceScope for a hosted session or for a command invocation.
A service registered with ReplApp.Create(services => services.AddScoped<T>()) is resolved from the app's shared root ServiceProvider. In practice, the same scoped instance is reused across commands and across distinct hosted SessionId values for the lifetime of that provider.
This is especially risky because the documentation recommends Scoped for per-user data such as authentication context and shopping carts. Following that guidance can share mutable user state across concurrent WebSocket/MCP clients.
Documentation affected
The website source currently contains several incorrect lifetime claims:
The main repository documentation also makes the broader claim:
docs/comparison.md:76: Per-session DI & state is advertised as a supported hosting feature, although IReplSessionState is session-aware but general Microsoft DI scopes are not.
Current implementation
ReplApp builds and retains one shared root provider:
The interactive loop receives one provider and reuses it for every command:
CreateSessionOverlay is an IServiceProvider fallback overlay, not a Microsoft DI scope, and does not call CreateScope:
The standard hosted adapters also do not create a scope. For example, each WebSocket connection creates a StreamedReplHost, then runs the same app/provider:
There are currently no CreateScope, CreateAsyncScope, or IServiceScopeFactory usages under src/.
Behavioral probe
Using the current main implementation with a probe service registered through AddScoped produced:
APP_PROVIDER_SAME_SESSION_TWO_COMMANDS_SAME=True
APP_PROVIDER_TWO_SESSIONS_SAME=True
EXPLICIT_SCOPE_SAME_SESSION_TWO_COMMANDS_SAME=True
EXPLICIT_SCOPE_TWO_SESSIONS_SAME=False
TRANSIENT_SAME_SESSION_TWO_COMMANDS_SAME=False
This confirms:
- two commands in one session reuse the same app-scoped instance;
- two different hosted
SessionId values also receive that same instance;
- per-session isolation only appears when the caller explicitly creates and passes different
IServiceScope.ServiceProvider instances;
Transient is new per resolution/handler invocation as expected.
In an ordinary one-shot CLI process, Scoped may appear invocation-local only because the entire app/provider/process ends after one command. Repl itself does not establish that scope boundary, and reusing the same ReplApp reuses the scoped instance.
Expected contract / decision needed
The intended lifetime contract should be made explicit and then implemented consistently. Based on the existing documentation, the likely intended behavior is:
- Hosted interactive session: create one DI scope per
IReplSessionHost / stable SessionId / transport connection; reuse it for every command in that session; dispose it when the session ends.
- One-shot CLI invocation: create one DI scope around that invocation and dispose it afterwards.
- Transient: continue resolving a new instance per handler resolution.
- External providers: document scope ownership clearly and avoid disposing caller-owned scopes unexpectedly.
If the intended contract is instead command-scoped DI, the documentation and examples should say so explicitly. If Repl intentionally delegates all scope creation to the caller, the current per-session claims should be removed and the hosting APIs should document how callers provide a scoped provider.
Suggested acceptance tests
- Two commands in the same hosted session receive the same
Scoped instance.
- Two distinct hosted sessions receive different
Scoped instances.
- A scoped disposable is disposed exactly once when its session ends.
- A one-shot CLI invocation owns and disposes its scope.
Transient remains different across handler invocations.
- Concurrent WebSocket/MCP sessions cannot observe each other's scoped per-user state.
- Website lifetime documentation and
docs/comparison.md define “session” and match the chosen behavior.
Environment
Validated against upstream yllibed/repl@d912bcbfd236a62cf696346674d761ac00fc3b96 and the current repl.yllibed.org dependency-injection page.
Summary
The documentation describes Microsoft DI
Scopedservices as session-scoped in hosted Repl sessions, but Repl does not currently create anIServiceScopefor a hosted session or for a command invocation.A service registered with
ReplApp.Create(services => services.AddScoped<T>())is resolved from the app's shared rootServiceProvider. In practice, the same scoped instance is reused across commands and across distinct hostedSessionIdvalues for the lifetime of that provider.This is especially risky because the documentation recommends
Scopedfor per-user data such as authentication context and shopping carts. Following that guidance can share mutable user state across concurrent WebSocket/MCP clients.Documentation affected
The website source currently contains several incorrect lifetime claims:
repl-website/src/content/docs/reference/dependency-injection.mdx:18:AddScoped<ICurrentUser, SessionUser>() // per-sessiondependency-injection.mdx:81-89:Scopedis described as one instance per hosted session andTransientas one per command handler invocation.dependency-injection.mdx:91-92: recommends changing per-user stores fromSingletontoScoped.The main repository documentation also makes the broader claim:
docs/comparison.md:76:Per-session DI & stateis advertised as a supported hosting feature, althoughIReplSessionStateis session-aware but general Microsoft DI scopes are not.Current implementation
ReplAppbuilds and retains one shared root provider:ReplApp.cs:19-22ReplApp.cs:472-478The interactive loop receives one provider and reuses it for every command:
InteractiveSession.cs:54-59InteractiveSession.cs:482-493CreateSessionOverlayis anIServiceProviderfallback overlay, not a Microsoft DI scope, and does not callCreateScope:ReplApp.cs:631-682The standard hosted adapters also do not create a scope. For example, each WebSocket connection creates a
StreamedReplHost, then runs the same app/provider:ReplWebSocketSession.cs:59-71StreamedReplHost.cs:181-202There are currently no
CreateScope,CreateAsyncScope, orIServiceScopeFactoryusages undersrc/.Behavioral probe
Using the current
mainimplementation with a probe service registered throughAddScopedproduced:This confirms:
SessionIdvalues also receive that same instance;IServiceScope.ServiceProviderinstances;Transientis new per resolution/handler invocation as expected.In an ordinary one-shot CLI process,
Scopedmay appear invocation-local only because the entire app/provider/process ends after one command. Repl itself does not establish that scope boundary, and reusing the sameReplAppreuses the scoped instance.Expected contract / decision needed
The intended lifetime contract should be made explicit and then implemented consistently. Based on the existing documentation, the likely intended behavior is:
IReplSessionHost/ stableSessionId/ transport connection; reuse it for every command in that session; dispose it when the session ends.If the intended contract is instead command-scoped DI, the documentation and examples should say so explicitly. If Repl intentionally delegates all scope creation to the caller, the current per-session claims should be removed and the hosting APIs should document how callers provide a scoped provider.
Suggested acceptance tests
Scopedinstance.Scopedinstances.Transientremains different across handler invocations.docs/comparison.mddefine “session” and match the chosen behavior.Environment
Validated against upstream
yllibed/repl@d912bcbfd236a62cf696346674d761ac00fc3b96and the currentrepl.yllibed.orgdependency-injection page.