Reputation: 787
Let's assume I have a two modules which are exporting BService
and CService
where both of those services extends AService
So code looks like this:
abstract class AService {
public run() {}
}
@Injectable()
export class BService extends AService {}
@Injectable()
export class CService extends AService {}
@Module({
providers: [BService],
exports: [BService],
})
export class BModule {}
@Module({
providers: [CService],
exports: [CService],
})
export class CModule {}
@Injectable()
class AppService {
constructor(protected readonly service: AService) {}
public run(context: string) { // let's assume context may be B or C
this.service.run();
}
}
@Module({
imports: [CModule, BModule],
providers: [{
provide: AppService,
useFactory: () => {
return new AppService(); // how to use BService/CService depending on the context?
}
}]
})
export class AppModule {}
But the key is, I cannot use REQUEST
(to inject it directly in useFactory
) from @nestjs/core
as I'm using this service in cron jobs and with the API call
I also don't think Factory
pattern is useful there, I mean it would work but I want to do it correctly
I was thinking about property based injection.
But I'm not sure how to use it in my case
Upvotes: 10
Views: 13664
Reputation: 789
In my opinion, the factory approach is exactly what you need. You described that you need a different service based on the context which is a great for for the factory approach. Let's try this:
Create an injectable factory:
import { Injectable } from '@nestjs/common';
import { AService } from './AService';
import { BService } from './BService';
import { CService } from './CService';
@Injectable()
export class ServiceFactory {
public getService(context: string) : AService {
switch(context) {
case 'a': return new BService();
case 'b': return new CService();
default: throw new Error(`No service defined for the context: "${context}"`);
}
}
}
Now import that factory into your app module:
import { ServiceFactory } from './ServiceFactory';
import { AService } from './AService';
@Module({
providers: [AppService, ServiceFactory]
})
export class AppModule {}
Now your app service will get the factory as a dependency which will create the appropriate service based on the context:
import { ServiceFactory } from './ServiceFactory';
import { AService } from './AService';
@Injectable()
class AppService {
constructor(readonly serviceFactory: ServiceFactory) { }
public run(context: string) {
const service: AService = this.serviceFactory.getService(context);
service.run();
}
}
Upvotes: 7
Reputation: 60357
If the property is static (e.g. environment variable), you can use a custom provider to choose the proper instance. However, if the property is in someway dynamic, you cannot soley rely on nest's dependency injection as it instantiates the provider on startup (with the exception of REQUEST scope, which isn't an option for you).
Create a custom provider that instantiates the needed implementation based on a static property (e.g. environment variable).
{
provide: AService,
useClass: process.ENV.useBService ? BService : CService,
}
Let's assume we have two different implementations of a service:
@Injectable()
export class BService {
public count = 0;
run() {
this.count++;
return 'B';
}
}
@Injectable()
export class CService {
public count = 0;
run() {
this.count++;
return 'C';
}
}
When the sum of the count
variables of both is even, the BService
should be used; CService
when it's odd. For this, we create a custom provider with request scope.
{
provide: 'MyService',
scope: Scope.REQUEST,
useFactory: (bService: BService, cService: CService) => {
if ((bService.count + cService.count) % 2 === 0) {
return bService;
} else {
return cService;
}
},
inject: [BService, CService],
},
If our controller now injects the MyService
token (@Inject('MyService')
) and exposes its run
method via an endpoint it will return B
C
B
...
As we want to use the default scope (Singleton!), the static instantiation of nest's dependency injection cannot be used. Instead you can use the delegate pattern to select the wanted instance in the root class (AService
in your example).
Provide all services as they are:
providers: [AService, BService, CService]
Decide dynamically in your AService
which implementation to use:
@Injectable()
export class AService {
constructor(private bService: BService, private cService: CService) {}
run(dynamicProperty) {
if (dynamicProperty === 'BService') {
return this.bService.run();
} else {
return this.cService.run();
}
}
}
Upvotes: 6