Proxy Providers
Since
v3.0
This feature was inspired by how REQUEST-scoped providers ("beans") work in the Spring framework for Java/Kotlin.
Using this technique, NestJS does not need to re-create a whole DI-subtree on each request (which has certain implications which disallows the use of REQUEST-scoped providers in certain situations).
Rather, it injects a SINGLETON Proxy instance, which delegates access and calls to the actual instance, which is created for each request when the CLS context is set up.
There are two kinds of Proxy providers - Class and Factory.
Please note that there are some caveats to using this technique.
Class Proxy Providers
These providers look like your regular class providers, with the exception that is the @InjectableProxy()
decorator to make them easily distinguishable.
@InjectableProxy()
export class User {
id: number;
role: string;
}
To register the proxy provider, use the ClsModule.forFeature()
registration,
which exposes it an injectable provider in the parent module.
ClsModule.forFeature(User);
It can be then injected using the class name.
However, what will be actually injected is not the instance of the class, but rather the Proxy which redirects all access to an unique instance stored in the CLS context.
Populate in an enhancer
A Class provider defined in this way will be empty upon creation, so we must assign context values to it somewhere. One place to do it is an interceptor
@Injectable()
export class UserInterceptor implements NestInterceptor {
// we can inject the proxy here
constructor(private readonly user: User) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
// and assign or change values as it was a normal object
this.user.id = request.user.id;
this.user.role = 'admin';
return next.handle();
}
}
Self-populating Proxy Provider
It is also possible to inject other providers into the Proxy Provider to avoid having to do this in a separate component.
For the convenience, the CLS_REQ
and CLS_RES
are also made into Proxy Providers and are exported from the ClsModule
.
@InjectableProxy()
export class UserWithRole {
id: number;
role: string;
constructor(
@Inject(CLS_REQ) request: Request,
roleService: RoleService,
) {
this.id = request.user.id;
this.role = roleService.getForId(request.user.id);
}
}
If you need to inject a provider from an external module, use the ClsModule.forFeatureAsync()
registration to import the containing module.
ClsModule.forFeatureAsync({
// make RoleService available to the Proxy provider
import: [RoleModule],
useClass: UserWithRole,
});
Using @Inject(CLS_REQ)
, you can entirely replace @Inject(REQUEST)
in REQUEST-SCOPED providers to turn them into CLS-enabled singletons without changing the implementation.
Factory Proxy Providers
Like your normal factory providers, Proxy factory providers look familiar.
They can be only registered using the ClsModule.forFeatureAsync()
method.
Here's an example of a hypothetical factory provider that dynamically resolves to a specific tenant database connection:
ClsModule.forFeatureAsync({
provide: TENANT_CONNECTION,
import: [DatabaseConnectionModule],
inject: [CLS_REQ, DatabaseConnectionService],
useFactory: async (req: Request, dbService: DatabaseConnectionService) => {
const tenantId = req.params['tenantId'];
const connection = await dbService.getTenantConnection(tenantId);
return connection;
},
global: true, // make the TENANT_CONNECTION available for injection globally
});
Again, the factory will be called on each request and the result will be stored in the CLS context. The TENANT_CONNECTION
provider, however, will still be a singleton and will not affect the scope of whatever it is injected into.
In the service, it can be injected using the provide
token as usual:
@Injectable()
class DogsService {
constructor(
@Inject(TENANT_CONNECTION)
private readonly connection: TenantConnection,
) {}
getAll() {
return this.connection.dogs.getAll();
}
}
Caveats
No primitive values
Proxy Factory providers cannot return a primitive value. This is because the provider itself is the Proxy and it only delegates access once a property or a method is called on it (or if it itself is called in case the factory returns a function).
function
Proxies must be explicitly enabled
In order to support injecting proxies of functions, the underlying proxy target must be a function, too, in order to be able to implement the "apply" trap. However, this information cannot be extracted from the factory function itself, so if your factory returns a function, you must explicitly set the type
property to function
in the provider definition.
{
provide: SOME_FUNCTION,
useFactory: () => {
return () => {
// do something
};
},
type: 'function',
}
In versions prior to v4.0
, calling typeof
on an instance of a Proxy provider always returned function
, regardless of the value it holds. This is no longer the case. Please see Issue #82
Delayed resolution of Proxy Providers
By default, proxy providers are resolved as soon as the setup
function in an enhancer (middleware/guard/interceptor) finishes. For some use cases, it might be required that the resolution is delayed until some later point in the request lifecycle once more information is present in the CLS .
To achieve that, set resolveProxyProviders
to false
in the enhancer options and call ClsService#resolveProxyProviders()
manually at any time.
ClsModule.forRoot({
middleware: {
resolveProxyProviders: false,
},
});
Outside web request
This is also necessary outside the context of web request, otherwise all access to an injected Proxy Provider will return undefined
.
With cls.run()
If you set up the context with cls.run()
to wrap any subsequent code thar relies on Proxy Providers.
@Injectable()
export class CronController {
constructor(
private readonly someService: SomeService,
private readonly cls: ClsService,
);
@Cron('45 * * * * *')
async handleCron() {
await this.cls.run(async () => {
// prepare the context
this.cls.set('some-key', 'some-value');
// trigger Proxy Provider resolution
await this.cls.resolveProxyProviders();
await this.someService.doTheThing();
});
}
}
With @UseCls()
Since the @UseCls()
decorator wraps the function body with cls.run()
automatically, you can use the setup
function to prepare the context.
The Proxy Providers will be resolved after the setup
phase.
@Injectable()
export class CronController {
constructor(private readonly someService: SomeService);
@Cron('45 * * * * *')
@UseCls({
setup: (cls) => {
cls.set('some-key', 'some-value');
},
})
async handleCron() {
await this.someService.doTheThing();
}
}