9

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

Kim Kern
  • 54,283
  • 17
  • 197
  • 195
hejkerooo
  • 709
  • 2
  • 7
  • 20
  • Check my answer below to see if it fits your needs. This sounds like a good place to use the strategy pattern. https://stackoverflow.com/a/69502460/8148483 – Francisco Garcia Oct 10 '21 at 00:10

2 Answers2

7

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();
    }
}
nerdy beast
  • 779
  • 5
  • 8
6

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();
    }
  }
}
Kim Kern
  • 54,283
  • 17
  • 197
  • 195
  • None of these solutions will suit my requirements, do you have anywhere working example with https://docs.nestjs.com/providers#property-based-injection ? – hejkerooo Dec 05 '19 at 13:55
  • @KrzysztofSzostak Property based injection is just a different way of injecting a dependency. Instead of using the constructor you use an instance variable. I don't see how this would be useful in your case. Nest just doesn't offer the dynamic instantiaion that you are looking for. I'm pretty sure you only have the three options I listed. – Kim Kern Dec 05 '19 at 13:57
  • I'm looking for a similar solution to provide different service related on User Role. Is there any way to get a specific service via factory on request param. (req.user is passed by a guard using passport strategy). And is it a way you would do dealing with permission in Nestjs ? – Wax Apr 27 '22 at 20:14