Skip to content

Scoped DI services are shared across hosted sessions despite per-session documentation #70

Description

@autocarl

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

  1. Two commands in the same hosted session receive the same Scoped instance.
  2. Two distinct hosted sessions receive different Scoped instances.
  3. A scoped disposable is disposed exactly once when its session ends.
  4. A one-shot CLI invocation owns and disposes its scope.
  5. Transient remains different across handler invocations.
  6. Concurrent WebSocket/MCP sessions cannot observe each other's scoped per-user state.
  7. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Fields

    No fields configured for Bug.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions