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.
I'd like to be able to have two lists of providers I can inject in the module setup.
providers: [
Gatherer1,
Gatherer2,
Notifier1,
Notifier2,
Notifier3,
{
provide: MyService,
useFactory: (gatherers: Gatherer[], notifiers: Notifier[]) => {
return new MyService(gatherers, notifiers)
},
inject: [
[Gatherer1, Gatherer2],
[Notifier1, Notifier2, Notifier3]
]
}
]
@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]
}
]
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]
]
}
]
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:
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:
To achieve sth similar in nest.js you can do:
/* 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];
};
export interface IDecoratorProvider<T> {
provideDecoratorFor(decoratee: T): T;
}
This layer is necessary to automatically inject any dependency of factory decorator.
@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);
}
}
/**
* * 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 {}
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.
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:
and it will internally create the following "collection" provider which collects all the providers with the same token automatically
so when the user adds another
GATHER_LIST
provider, it will add it to the collection provider - without the user needing to worry about itYou'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.