Search code examples
angularangular-dependency-injection

Angular - choose class to inject based on URL / logic available at bootstrap


(How) is it possible to configure injectors to use a class based on some data available at bootstrap?

I have two implementations for interfacing with the backend, (for simplicity, let's say over http and over websockets).

I created two service classes, backend.http.service and backend.websocket.service. They both extend the backend.service class, so their interface is common.

I'd like my components to inject backend.service, and be ignorant of which actual interface is in use.

Hardwiring one of the services works like this:

      {
        provide: BackendService,
        useClass: WebsocketBackendService,
      },

However I'd like to do something like:

      {
        provide: BackendService,
        useClass: should_i_use_websockets() ? WebsocketBackendService : HttpBackendService,
      },

My first guess was to use useFactory instead (which means I could actually DI some services to calculate which backend to use), but then how would I instantiate the services themselves? Would the factory need to know the exact dependencies of the concrete serivces? That doesn't seem like a nice solution.

        {
        provide: BackendService,
        useFactory: (s: ServiceToDetermineWebsocketAvailability) => {
          if (s.should_i_use_websockets()) {
            return WebsocketBackendService(???);
          } else {
            return HttpBackendService(???);
          }
        }


@Injectable({ providedIn: 'root'})
export abstract class BackendService {
  connected$: Observable<boolean>;
  events$: Observable<MyEvent>;
  foo(): boolean
}

export class HttpBackendService {
  connected$ = ...;
  events$ = ...;
  foo(): boolean { return true; }

  constructor(http: HttpService) { ... }
}

export class WebsocketBackendService {
  connected$ =  ...;
  events$ = ...;
  foo(): boolean { return false; }

  constructor(websocket: WsService) { ... }
}




Solution

  • I figured it out like this. This means that the concrete services need to be annotated as well, but they do not need to be made available in the root injector, so as to avoid accidentally injecting the concrete instances instead of the abstract interface.

          {
            provide: BackendService,
            useFactory: ((rootInjector: Injector) => {
              const injector = Injector.create({
                parent: rootInjector,
                providers: [
                  { provide: HttpBackendService, useClass: HttpBackendService },
                  { provide: WebsocketBackendService, useClass: WebsocketBackendService },
                ]
              });
    
              if (should_i_use_websockets()) {
                return injector.get(WebsocketBackendService);
              } else {
                return injector.get(HttpBackendService);
              }
            }),
            deps: [Injector]
          },