Skip to content

Allow specifying providers inside @Injectable decorator #45832

@Harpush

Description

@Harpush

Which @angular/* package(s) are relevant/related to the feature request?

core

Description

Currently providers can be provided only in component or module or root or etc.. but not inside other providers.

  • One issue is that one cannot create a provider which depends upon another provider privately.
  • Another issue is multiple instances of the same provider which are linked to the provider scope it is provided in.

An example of a case that currently suffer from those problems:

@Injectable()
class CounterService implements OnDestroy {
  ngOnDestroy(): void {}
}

const primaryCounterToken = new InjectionToken<CounterService>(
  'primaryCounterToken'
);
const primaryCounterProvider: Provider = {
  provide: primaryCounterToken,
  useClass: CounterService,
};
const secondaryCounterToken = new InjectionToken<CounterService>(
  'secondaryCounterToken'
);
const secondaryCounterProvider: Provider = {
  provide: secondaryCounterToken,
  useClass: CounterService,
};

@Injectable()
class TwoCountersService {
  constructor(
    @Inject(primaryCounterToken) primary: CounterService,
    @Inject(secondaryCounterToken) secondary: CounterService
  ) {}
}

const twoCountersProviders: Provider[] = [
  primaryCounterProvider,
  secondaryCounterProvider,
  TwoCountersService,
];

In this case CounterService has some logic but in TwoCountersService two instances of it are required. The solution here is using InjectionToken to create two instances and inject them. The second issue mentioned above is yet to be seen in this example, but the first issue is.
Lets assume I want to use this TwoCountersService in a component - I need to provide all three tokens in the component providers - hence the twoCountersProviders array. Ideally I would like to just specify TwoCountersService.

Now lets assume I want two instances of TwoCountersService just like I wanted with CounterService. At this point I am stuck due to the second issue - as I actually need 4 instances of CounterService which is not possible this way.
The only solution is using a factory provider:

const counterFactoryToken = new InjectionToken<CounterService>(
  'counterFactoryToken'
);
const counterFactoryProvider: Provider = {
  provide: secondaryCounterToken,
  useFactory: () => () => new CounterService(),
};

@Injectable()
class TwoCountersService {
  private primary = this.counterFactory();
  private secondary = this.counterFactory();

  constructor(
    @Inject(counterFactoryToken) private counterFactory: () => CounterService
  ) {}
}

// Two instances:
const firstTwoCountersToken = new InjectionToken<TwoCountersService>(
  'firstTwoCountersToken'
);
const firstTwoCountersProvider: Provider = {
  provide: firstTwoCountersToken,
  useClass: TwoCountersService,
};
const secondTwoCountersToken = new InjectionToken<TwoCountersService>(
  'secondTwoCountersToken'
);
const secondTwoCountersProvider: Provider = {
  provide: secondTwoCountersToken,
  useClass: TwoCountersService,
};

Now i can safely inject firstTwoCountersToken and secondTwoCountersToken and have two instances and get 4 new instances of CounterService.
The first issue is even more apparent here - I need to provide now: counterFactoryProvider, firstTwoCountersProvider and secondTwoCountersProvider and if there was a service injecting both - that service too.
The second issue is solved here.
But - I lose lifecycle methods (ngOnDestroy) this way which isn't what I wanted.

So instead of factory provider I need a service and manually handle lifecycle... something like:

@Injectable()
export class CounterServiceFactory {
  private registry: CounterService[] = [];

  getNew() {
    const srv = new CounterService();
    this.registry.push(srv);
    return srv;
  }

  ngOnDestroy(): void {
    for (const srv of this.registry) {
      srv.ngOnDestroy();
    }

    this.registry = [];
  }
}

And I got it working with A LOT of code and roundabout issues to solve. It still requires annoyingly providing providers carefully and a factory service for every service.

Proposed solution

If it was possible to provide providers inside @Injectable - it could be solved nicely:

@Injectable()
class CounterService implements OnDestroy {
  ngOnDestroy(): void {}
}

const primaryCounterToken = new InjectionToken<CounterService>(
  'primaryCounterToken'
);
const primaryCounterProvider: Provider = {
  provide: primaryCounterToken,
  useClass: CounterService,
};
const secondaryCounterToken = new InjectionToken<CounterService>(
  'secondaryCounterToken'
);
const secondaryCounterProvider: Provider = {
  provide: secondaryCounterToken,
  useClass: CounterService,
};

@Injectable({
  providers: [primaryCounterProvider, secondaryCounterProvider]
})
class TwoCountersService {
  constructor(
    @Inject(primaryCounterToken) primary: CounterService,
    @Inject(secondaryCounterToken) secondary: CounterService
  ) {}
}

const firstTwoCountersToken = new InjectionToken<TwoCountersService>(
  'firstTwoCountersToken'
);
const firstTwoCountersProvider: Provider = {
  provide: firstTwoCountersToken,
  useClass: TwoCountersService,
};
const secondTwoCountersToken = new InjectionToken<TwoCountersService>(
  'secondTwoCountersToken'
);
const secondTwoCountersProvider: Provider = {
  provide: secondTwoCountersToken,
  useClass: TwoCountersService,
};

Now if I need firstTwoCountersProvider and secondTwoCountersProvider - that's only what I provide and inject and all the different instances are handled automatically as providers provided in @Injectable are sharing the providing scope.
Even better if there was a service injecting both - I would just provide that service ONLY and inject it.

Alternatives considered

The long and complicated solution from above.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: coreIssues related to the framework runtimecore: di

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions