Controlling service injection per configuration in Angular

In the previous post I mentioned we could use a factory method and the useFactory attribute of the Injectable decorator to read which service to inject from some sort of configuration file.

To do this we will configure our factories per build configuration. Controlling dependency injection per configuration allows us to to things like injecting different services for different clients/versions or injecting different services for dev and prod.

We will start by adding a services.factories.ts file to our environments folder (if you created your app with Angular Cli, otherwise just add it wherever you please) and adding the following service factory in it:

import { FirstService } from 'src/services/first.service';

export let mainServiceFactory = () => {
  return new FirstService();
};

// include factory methods for the other services that need customization

We will need to add one factory method per service we want to customize.
Then we add a similar file called services.factories.alt.ts that uses our alternate services:

import { SecondService } from 'src/services/second.service';

export let mainServiceFactory = () => {
  return new SecondService();
};

In our services themselves, here I’m reusing FirstService from the previous posts, we specify the useFactory attribute in our decorator:

import { Injectable } from '@angular/core';
import { mainServiceFactory } from 'src/environments/service.factories';

@Injectable({
  providedIn: 'root',
  useFactory: mainServiceFactory
})
export class FirstService {

  constructor() { }

  output() {
    return 'first';
  }
}

For the SecondService mentioned in the factories, I’m again reusing the service from the previous posts.

Then in the angular.json file we will add some new build configurations or modify existing ones to customize the services per configuration.

Since the angular.json file can get quite lengthy, I’ll only include the important bits.

// ... under architect / build, after options
"configurations": {
            "altServices": {
              "fileReplacements": [
                {
                  "replace": "src/environments/service.factories.ts",
                  "with": "src/environments/service.factories.alt.ts"
                }
              ]
            },
            "production": {
// ... normaly found after build
"serve": {
          "builder": "@angular-devkit/build-angular:dev-server",
          "options": {
            "browserTarget": "dependency-injection:build"
          },
          "configurations": {
            "altServices": {
              "browserTarget": "dependency-injection:build:altServices"
            },
            "production": {
              "browserTarget": "dependency-injection:build:production"
            }
          }
        },
// ... if you're using e2e tests
"e2e": {
          "builder": "@angular-devkit/build-angular:protractor",
          "options": {
            "protractorConfig": "e2e/protractor.conf.js",
            "devServerTarget": "dependency-injection:serve"
          },
          "configurations": {
            "altServices": {
              "devServerTarget": "dependency-injection:serve:altServices"
            },
            "production": {
              "devServerTarget": "dependency-injection:serve:production"
            }
          }
        },

By using ng serve or ng build, the regular services will be used. To switch to the other services we need to use the -c switch like such:

ng serve -c altServices
ng build -c altServices

Note that the unused services are tree shaken, meaning they won’t appear in the final bundle. To check this for yourself do a ng build, ng serve won’t do, and inspect and the main.js file in your dist folder.

One downside of this approach is that this creates a circular reference between the file containing the factory method and the service itself. To fix this you can add a third class, a service registry which contains the services and has no dependency. The service factories will register themselves with this registry and then the services will only depend on the registry and not the factories themselves. See this SO question.

Instead of using factories, we could also use providers instead. This would allow us to only create one provider and set it up in one place, in our @NgModule providers attribute rather than in all services we want to customize. The downside to this is that it prevents tree shaking of any sort.

Let’s show how we would do this.

We would create a service.providers.ts and service.providers.alt.ts file instead of the service.factories.ts. Here’s an example of such a file:

export let servicesProvider = [
  { provide: FirstService, useClass: FirstService },
  { provide: GraphicsService, useClass: GraphicsService },
];

Remove the useFactory attribute in our service and then configure our AppModule to use this provider:

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule
  ],
  providers: [servicesProvider],
  bootstrap: [AppComponent]
})
export class AppModule { }

 

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s