Files
What you'll learn
- Retrieve a provider chosen at runtime with ModuleRef.get(), and know why get() refuses scoped providers
- Resolve a transient provider per call with resolve(), and share a sub-tree between calls with a context id
- Look a provider up across module boundaries with { strict: false }, and instantiate an unregistered class with create()
Module Reference
Constructor injection answers one question, "what does this class need", once, when the class is written. Some questions are only answered while the application runs: which of three formatters the client asked for, a fresh instance of something for this one operation, a class the container was never told about. ModuleRef is the container itself, injectable like any provider, and it lets a class ask those questions by hand. Use it when injection cannot express what you need, and not instead of injection when it can: a class that fetches everything from ModuleRef hides its dependencies, which is what injection existed to avoid.
Getting a provider
moduleRef.get(token) returns the instance registered under a token in the current module:
@Injectable()
export class PaymentsService {
constructor(private readonly moduleRef: ModuleRef) {}
charge(method: 'card' | 'transfer', amount: number) {
const gateway = this.moduleRef.get(method === 'card' ? CardGateway : TransferGateway);
return gateway.charge(amount);
}
}
The token can be a class, a string or a symbol, exactly as in @Inject(). Two limits. get() looks in the module the calling class belongs to, so a provider that lives elsewhere and is not imported is not found; pass { strict: false } as a second argument to search the whole application instead. And get() returns only singletons: for a request-scoped or transient provider it throws, with a message naming resolve(), because there is no single instance to return.
Resolving a scoped provider
moduleRef.resolve(token) is get() for scoped providers. It is asynchronous, and every call builds a new instance from its own sub-tree of the container:
const first = await this.moduleRef.resolve(TransientService);
const second = await this.moduleRef.resolve(TransientService);
console.log(first === second); // false
When several resolutions should share one sub-tree, so that they see the same request-scoped providers, make a context id and pass it to each:
const contextId = ContextIdFactory.create();
const [a, b] = await Promise.all([
this.moduleRef.resolve(TransientService, contextId),
this.moduleRef.resolve(TransientService, contextId),
]);
console.log(a === b); // true
A sub-tree created this way has no request in it; registerRequestByContextId(request, contextId) supplies one when a provider in it injects REQUEST. Inside a request, ContextIdFactory.getByRequest(request) returns the id of the sub-tree that request is already using, so a resolution joins it instead of starting another.
Creating what the container does not own
moduleRef.create(SomeClass) instantiates a class that is registered nowhere, resolving its constructor's dependencies from the container as if it were a provider:
const exporter = await this.moduleRef.create(CsvExporter);
The class needs @Injectable(), not to be registered, but because that decorator is what makes TypeScript emit the constructor's parameter types; without it the container has no idea what to pass. create() is for objects with dependencies that are made on demand and thrown away: a report, an exporter, a one-off job.
Your task
The cats API exports itself in the format the client asks for, numbers every export, and builds a report on demand.
- In
ExportService.export(), fetch the formatter for the requested format from the container, chosen by class, and take a freshTicketfor each export.Ticketis transient; read the error you get from the wrong method. - In
ExportService.report(), have the container build aReport, which no module registers, and return what it builds. StatsControllerlives in the root module andCatsModuledoes not exportCatsService. Count the cats anyway.
When it fails
Ticket is marked as a scoped provider. Request and transient-scoped providers can't be used in combination with "get()" method: Nest's own words; useresolve(), andawaitit.- Every export has the same ticket number: the ticket was resolved once and kept, or
get()was replaced by a single stored instance. Resolve inside the method, per call. Nest could not find CatsService element:get()searched only the current module. Pass{ strict: false }.Reportbuilds withtotal: 0while cats exist, or throwsCannot read properties of undefined: the class lost its@Injectable(), socreate()had no parameter types and passed nothing.
Remember
ModuleRefis the container, injectable.get(token)for singletons in this module,{ strict: false }for the whole application.resolve(token)for scoped providers, a new instance per call; share a sub-tree with a context id.create(Class)instantiates an unregistered class and resolves its dependencies; it needs@Injectable()for the metadata.- Prefer injection when the dependency is known when the class is written.
Stuck? Show a hint
export(): this.moduleRef.get(format === 'csv' ? CsvFormatter : JsonFormatter), then await this.moduleRef.resolve(Ticket). report(): await this.moduleRef.create(Report), then build(). stats(): this.moduleRef.get(CatsService, { strict: false }).
Press Run tests to start the app. Its log appears here.Graded endpoints
Two cats to export
The older one
The plain route still works next to the ones that use ModuleRef
get() fetched the CsvFormatter the container built; resolve() issued ticket 1
A different class this time, and a new ticket, because Ticket is transient and resolve() makes a new one per call
Report is registered nowhere; create() built it and injected CatsService into it
CatsModule does not export CatsService, and strict: false found it anyway