hejkerooo
hejkerooo

Reputation: 787

Inject correct service based on parameter

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

Answers (2)

nerdy beast
nerdy beast

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

Kim Kern
Kim Kern

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).

Static Property

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,
}

Dynamic Property with Request-Scope

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 ...

Dynamic Property with Default-Scope

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

Related Questions