Skip to content
Buy me a coffee

Scopes

A scope is a child of the root provider with its own cache of scoped services. Singletons come from the root and are shared; scoped services are created once per scope; transients are created per resolution and owned by the scope that resolved them. It is .NET’s IServiceScope.

class
class RequestContext
RequestContext
{
readonly
RequestContext.id: `${string}-${string}-${string}-${string}-${string}`
id
=
var crypto: Crypto
crypto
.
Crypto.randomUUID(): `${string}-${string}-${string}-${string}-${string}`

The randomUUID() method of the Crypto interface is used to generate a v4 UUID using a cryptographically secure random number generator. Available only in secure contexts.

MDN Reference

randomUUID
();
}
const
const provider: ServiceProvider
provider
= new
new ServiceCollection(): ServiceCollection

Collects service registrations, then builds a

ServiceProvider

. Modeled on .NET's IServiceCollection.

@example

const services = new ServiceCollection();
services.addSingleton(Logger, ConsoleLogger);
services.addScoped(UserService, [Logger]);
const provider = services.buildServiceProvider({ validateScopes: true });

ServiceCollection
().
ServiceCollection.addScoped<RequestContext>(token: Newable<RequestContext, []>): ServiceCollection (+4 overloads)

Registers a scoped service: one instance per scope. Same forms as

addSingleton

.

addScoped
(
class RequestContext
RequestContext
).
ServiceCollection.buildServiceProvider(options?: BuildOptions): ServiceProvider

Builds the provider. Registrations are copied, so changing this collection afterwards does not affect providers already built.

buildServiceProvider
({
BuildOptions.validateScopes?: boolean | undefined

Throw a ScopeError when a scoped service is resolved from the root provider or captured by a singleton. Recommended in development.

validateScopes
: true });
await using
await using scope: ServiceProvider
scope
=
const provider: ServiceProvider
provider
.
ServiceProvider.createScope(): ServiceProvider

Creates a scope: scoped services resolve once per scope. Dispose it when done (or use await using).

createScope
();
const
const ctx: RequestContext
ctx
= await
await using scope: ServiceProvider
scope
.
ServiceProvider.getRequiredService<RequestContext>(token: Token<RequestContext>): Promise<RequestContext>

Resolves token, throwing ServiceNotFoundError if it is not registered.

getRequiredService
(
class RequestContext
RequestContext
);

A scope is a ServiceProvider too: it has the same getService, getRequiredService, getServices and keyed methods.

Create a scope where a unit of work starts and dispose it where it ends:

  • HTTP servers: one scope per request, created in a middleware. See Express.
  • Queue consumers: one scope per message.
  • CLI tools and cron jobs: one scope per command or run.
  • Tests: one scope per test, so scoped state never leaks between tests.
async function
function onMessage(message: Message): Promise<void>
onMessage
(
message: Message
message
:
interface Message
Message
) {
await using
await using scope: ServiceProvider
scope
=
const provider: ServiceProvider
provider
.
ServiceProvider.createScope(): ServiceProvider

Creates a scope: scoped services resolve once per scope. Dispose it when done (or use await using).

createScope
();
const
const handler: OrderHandler
handler
= await
await using scope: ServiceProvider
scope
.
ServiceProvider.getRequiredService<OrderHandler>(token: Token<OrderHandler>): Promise<OrderHandler>

Resolves token, throwing ServiceNotFoundError if it is not registered.

getRequiredService
(
class OrderHandler
OrderHandler
);
await
const handler: OrderHandler
handler
.
OrderHandler.handle(m: Message): Promise<void>
handle
(
message: Message
message
);
} // scoped and transient services disposed here, also when handle() throws

await scope.dispose() disposes every scoped and transient instance the scope created, in reverse creation order, by calling onDestroy(), [Symbol.asyncDispose]() or [Symbol.dispose](). Singletons are untouched: they belong to the root. Details in Lifecycle and disposal.

ServiceProvider implements Symbol.asyncDispose, so await using calls dispose() for you when the block exits, normally or by an exception. On older setups write it out:

const
const scope: ServiceProvider
scope
=
const provider: ServiceProvider
provider
.
ServiceProvider.createScope(): ServiceProvider

Creates a scope: scoped services resolve once per scope. Dispose it when done (or use await using).

createScope
();
try {
await
function work(scope: ServiceProvider): Promise<void>
work
(
const scope: ServiceProvider
scope
);
} finally {
await
const scope: ServiceProvider
scope
.
ServiceProvider.dispose(): Promise<void>

Disposes every instance this provider (or scope) created, in reverse creation order: onDestroy(), else [Symbol.asyncDispose](), else [Symbol.dispose](). Disposing the root disposes singletons; disposing a scope disposes its scoped and transient instances. Every instance is disposed even if some throw; the errors are rethrown afterwards (an AggregateError if more than one). Later calls wait for the first one and do not rethrow.

dispose
();
}

Calling dispose() again, or concurrently, waits for the first disposal to finish and never rethrows its errors. After it, every get* call and createScope() on that scope throws DisposedError, and isService returns false.

If a scope is disposed while one of its services is still being built (an async factory or onInit still running), that service is disposed as soon as it is finished, and the pending get* call rejects with DisposedError. Nothing created for a disposed scope is leaked.

  • createScope() can be called on the root or on a scope. Either way the new scope hangs off the root: scopes don’t nest, and a scope created from another scope does not share its scoped instances.
  • Disposing the root disposes its singletons (and the transients resolved from the root). After that, every scope of that root throws DisposedError too, and isService returns false on them.
  • Disposing the root does not dispose scoped services of scopes still open. Dispose scopes when their work ends, then the root at shutdown.
// Graceful shutdown: stop taking work, let open scopes finish, then dispose singletons.
const process: {
once(event: string, fn: () => void): void;
}
process
.
function once(event: string, fn: () => void): void
once
('SIGTERM', async () => {
await
const server: {
close(): Promise<void>;
}
server
.
function close(): Promise<void>
close
();
await
const provider: ServiceProvider
provider
.
ServiceProvider.dispose(): Promise<void>

Disposes every instance this provider (or scope) created, in reverse creation order: onDestroy(), else [Symbol.asyncDispose](), else [Symbol.dispose](). Disposing the root disposes singletons; disposing a scope disposes its scoped and transient instances. Every instance is disposed even if some throw; the errors are rethrown afterwards (an AggregateError if more than one). Later calls wait for the first one and do not rethrow.

dispose
();
});

buildServiceProvider() copies the registrations. Adding, removing or replacing services in the ServiceCollection afterwards has no effect on that provider or its scopes. To change wiring, change the collection and build a new provider, which is what test setups do.