Nest: Allow injection of an array of providers

Created on 12 May 2020  路  6Comments  路  Source: nestjs/nest

Feature Request

Is your feature request related to a problem? Please describe.


Eg. given the following service:

@Injectable()
export class MyService {
  constructor(
    private readonly gatherers: Gatherer[],
    private readonly notifiers: Notifier[]) {
  }

  //... do stuff
}

There's no way to handle a clean dependency injection.

Describe the solution you'd like


I'd like to be able to have two lists of providers I can inject in the module setup.

Teachability, Documentation, Adoption, Migration Strategy

Option 1: Factory

  providers: [
    Gatherer1,
    Gatherer2,
    Notifier1,
    Notifier2,
    Notifier3,
    {
      provide: MyService,
      useFactory: (gatherers: Gatherer[], notifiers: Notifier[]) => {
        return new MyService(gatherers, notifiers)
      },
      inject: [
        [Gatherer1, Gatherer2],
        [Notifier1, Notifier2, Notifier3]
      ]
    }
  ]

Option 2: Provider as an array of providers

@Injectable()
export class MyService {
  constructor(
    @Inject(GATHERER_LIST) private readonly gatherers: Gatherer[],
    @Inject(NOTIFIER_LIST) private readonly notifiers: Notifier[]) {
  }

  //... do stuff
}
  providers: [
    Gatherer1,
    Gatherer2,
    Notifier1,
    Notifier2,
    Notifier3,
    {
      provide: GATHERER_LIST,
      useProviders: [Gatherer1, Gatherer2] // maybe the existing useClass here
    },
    {
      provide: NOTIFIER_LIST,
      useProviders: [Notifier1, Notifier2, Notifier3]
    }
  ]

What is the motivation / use case for changing the behavior?

I ended up using the following workaround for now, but this is not ideal nor super flexible, and very custom made:

  providers: [
    Gatherer1,
    Gatherer2,
    Notifier1,
    Notifier2,
    Notifier3,
    {
      provide: MyService,
      useFactory: (...params: (Gatherer|Notifier)[]) => {
        const gatherers = [];
        const notifiers = [];
        for (const injection of params) {
          if (injection instanceof AbstractGatherer) gatherers.push(injection);
          if (injection instanceof AbstractNotifier) notifiers.push(injection);
        }
        return new MyService(gatherers, notifiers)
      },
      inject: [Gatherer1, Gatherer2, Notifier1, Notifier2, Notifier3]
      ]
    }
  ]
core type

Most helpful comment

@kamilmysliwiec sorry missed that issue.

In fact, my PR #2460 uses a very similar pattern as you have described in the comment above. Though it creates this "collection" provider automatically.

When adding a multi-provider from #2460 , it will add the following providers:

{
  // This `provide` token will be overwritten, since it will
  // be used by the collection provider so the token stays unique.
  provide: GATHER_LIST,
  useValue: Gatherer1,
  multi: true,
}

and it will internally create the following "collection" provider which collects all the providers with the same token automatically

{
  provide: GATHER_LIST,
  useValue: [Gatherer1, ...],
  inject: [Gatherer1, ...],
}

so when the user adds another GATHER_LIST provider, it will add it to the collection provider - without the user needing to worry about it

You'll find the implementation of creating the collection provider here and here


So to come to a conclusion, I think the solution you've provided is good for a workaround, but multi provider are simpler to use for the user, because he/she does not need to worry about this collection provider. It will be quite a burden to maintain this collection provider for the user. Can be very easily forgotten to add a new provider to that list.

All 6 comments

I think this is dupe of https://github.com/nestjs/nest/issues/770 (PR https://github.com/nestjs/nest/pull/2460)

Yes, kind of. It seems to address the same use case, but the implementation proposal is different (simpler here, imo).

{
   provide: GATHERER_LIST,
   useProviders: [Gatherer1, Gatherer2] // maybe the existing useClass here
},

Maybe instead of introducing a new property, we could use the existing one named useExisting and detect if the specified value == array. If so, construct a "multi" provider.

I think this approach is cleaner & more explicit then having the multi property as Angular (which is not that simple in Nest due to per-module providers scoping). cc @BrunnerLivio What do you think?

@kamilmysliwiec sorry missed that issue.

In fact, my PR #2460 uses a very similar pattern as you have described in the comment above. Though it creates this "collection" provider automatically.

When adding a multi-provider from #2460 , it will add the following providers:

{
  // This `provide` token will be overwritten, since it will
  // be used by the collection provider so the token stays unique.
  provide: GATHER_LIST,
  useValue: Gatherer1,
  multi: true,
}

and it will internally create the following "collection" provider which collects all the providers with the same token automatically

{
  provide: GATHER_LIST,
  useValue: [Gatherer1, ...],
  inject: [Gatherer1, ...],
}

so when the user adds another GATHER_LIST provider, it will add it to the collection provider - without the user needing to worry about it

You'll find the implementation of creating the collection provider here and here


So to come to a conclusion, I think the solution you've provided is good for a workaround, but multi provider are simpler to use for the user, because he/she does not need to worry about this collection provider. It will be quite a burden to maintain this collection provider for the user. Can be very easily forgotten to add a new provider to that list.

Hi @kamilmysliwiec

Let me add some points into the discussion:

  1. @maphe is right that injection of array of providers is useful in services (see inversify's multi injection)
  2. The prerequisite for this is that underlying DI container must allow for registering more than 1 provider for a given symbol.
  3. DI container must implement a getAll() function, returning an array of providers for a given symbol.
  4. As for registration: these two use cases should be handled:
  5. for a given symbol, register an array of providers
  6. for a given symbol, add a new provider to existing array of providers

The second case is particularly useful in plugin-module architecture that is based on SOLID principles.

For example: with each module I want to register a new implementation of my IExtension interface. This would allow me to read at runtime an array of registered IExtensions without knowing the details of all the modules (pure SOLID).

Current workaround that I know of: use cache service to cache instances of singleton providers. I believe it gets more difficult with request-scoped providers.

Another example: I would like to create an old, good SOLID decorator for an interface.
Example: classes XA and XY implement interface X. Class XA extends (decorates) functionality of class XY by injecting XY (decoratee) as constructor parameter.

export class FoobarNotificationDecorator implements IFoobarService {
  constructor(
    private readonly decoratee: IFoobarService,
    private readonly notificationService: IFolderService,
  ) {}

  getFooBarById(id: number): Promise<IFoobar | undefined> {
    this.notificationService.doSth();
    return this.decoratee.getFooBarById(id);
  }

Currently this is impossible in nestjs and I belive it could be a start of another discussion. But with multi-injection possible I would be able to easily create a factory that creates a provider and applies all decorators (that are registered for a symbol such as 'ServiceDecorator'), allowing for a nice modular architecture.

For anyone interested in the possible solutions:

This website: https://dev.to/nestjs/advanced-nestjs-dynamic-providers-1ee got me thinking.

1. In order to register multiple providers for one symbol:

Yu can use the same method as in the webiste above: basically storing an array of providers in a global variable.
This solution is not really reusable across different programming languages and it uses the fact that every exported global variable in node.js is a singleton. So all in all this should be considered as a workaround, not a good SOLID pattern.

2. In order to create an old-type interface decorator

Let's imaging web application as an operating system to which you can install any patches.
The goal is to be able to add / change / remove ANY functionality in the system without changing the source code of original module. Generally, we can extend existing modules in 4 different ways by:

  • reusing existing providers (composition by constructor injection)
  • replacing existing providers (not really doable in nest.js outside unit tests) - workaround required
  • listening to event, observables, etc (nice pattern but it leaks a little abstraction, meaning that it requires that original code is an observable or emits events that can be listened to)
  • decorating existing functionalities with SOLID decorator pattern https://en.wikipedia.org/wiki/Decorator_pattern
    This last one method is rarely used in back-end architectures although it is probably the best method to extend existing functionality. It leask zero (0!) abstraction because it does not impose any requirements on the orignal code.

To achieve sth similar in nest.js you can do:

  1. Create a typescript decorator such as this
/* eslint-disable @typescript-eslint/ban-types */
export const decoratedClasses: Record<string, Function[]> = {};

/** 
 * Marks this class as decorating another provider.
 */
export const Decorates = (
  decoratedProvider: symbol, 
): ClassDecorator => 
  <T extends Function>(target: T) => {
    const stringKey = decoratedProvider.toString();

    if (decoratedClasses[stringKey] === undefined) 
      decoratedClasses[stringKey] = [target];
    else 
      decoratedClasses[stringKey] = [...decoratedClasses[stringKey], target];
  };
  1. Create decorator provider interface
export interface IDecoratorProvider<T> {
  provideDecoratorFor(decoratee: T): T;
}

This layer is necessary to automatically inject any dependency of factory decorator.

  1. Create example provider and its decorator: you can decorate providers as well as factories of providers as in this example:
    Provider (factory): MongoDbRepositoryFactory
    Decorator: CachedMongoDbRepositoryFactoryDecorator (adds cache to repository created by the factory, not shown here as implementation is not important for this example)
    Decorator Provider: CachedMongoDbRepositoryFactoryDecoratorProvider
@Injectable()
@Decorates(MongoDbTypes.MongoDbRepositoryFactory)
export class CachedMongoDbRepositoryFactoryDecoratorProvider
implements IDecoratorProvider<IRepositoryFactory> {

  public constructor (
    @Inject(CachingTypes.CachingService)
    private readonly cachingService: CachingService,
  ) {}

  public provideDecoratorFor(decoratee: IRepositoryFactory<T, A, E>): IRepositoryFactory<T, A, E> {
    return new CachedMongoDbRepositoryFactoryDecorator(decoratee, this.cachingService);
  }
}
  1. Create an extension module which registers decorator as a provider. The module must be global for decorator to be applied dynamically to previously registered provider.
/** 
 * * This module is marked as global because it exports at least one decorator which should be applied dynamically, 
 * * without original module knowing about it.  
 */
@Global() 
@Module({
  imports: [
    CachingModule, 
  ],
  controllers: [],
  providers: [CachedMongoDbRepositoryFactoryDecoratorProvider],
  exports: [CachedMongoDbRepositoryFactoryDecoratorProvider]
})
export class MongoDbCachedModule {}
  1. Create helper to register providers that can be dynamically extended by decorators from other modules
export const createDecoratedProvider = <T>(name: symbol, baseProviderClass: new(...args: never[]) => T
): Provider<T> => ({
  provide: name,
  useFactory: <T>(
    baseProviderClass: T, 
    ...decorators: IDecoratorProvider<T>[]
  ) => {
    if (decorators.length === 0) 
      return baseProviderClass;

    let decoratedInstance = baseProviderClass;
    for (const decorator of decorators) {
      decoratedInstance = decorator.provideDecoratorFor(decoratedInstance);
    }

    return decoratedInstance;
  },
  inject: [baseProviderClass, ...decoratedClasses[name.toString()]],
  scope: Scope.REQUEST
});

Example usage:

@Module({
  imports: [...],
  controllers: [],
  providers: [ 
  MongoDbRepositoryFactory,
  createDecoratedProvider(
    MongoDbTypes.MongoDbRepositoryFactory, 
    MongoDbRepositoryFactory
  )],
  exports: [MongoDbTypes.MongoDbRepositoryFactory]
})
export class MongoDbModule {}

It works in my case and allows to register any number or extensions to original service (provider) without original service knowing about it.

If anyone finds a simpler workaround to enable this feature please let me know. Hope that helps anyone.

Was this page helpful?
0 / 5 - 0 ratings