Inversion of Control with Aurelia - Part 2

We've covered how Aurelia uses the IoC pattern at the macro level- via lifecycle hooks and conventions, now let's take a look at how a sub-pattern of IoC, "dependency injection" works in Aurelia applications.

aurelia inversion of control

Dependency Injection

If you haven't heard of the dependency injection pattern (DI), here's what Wikipedia has to say about it:

Dependency injection is a software design pattern that implements inversion of control for resolving dependencies.

A dependency is an object that can be used (a service). An injection is the passing of a dependency to a dependent object (a client) that would use it. The service is made part of the client's state. Passing the service to the client, rather than allowing a client to build or find the service, is the fundamental requirement of the pattern.

Dependency injection allows a program design to follow the dependency inversion principle. The client delegates to external code (the injector) the responsibility of providing its dependencies. The client is not allowed to call the injector code. It is the injecting code that constructs the services and calls the client to inject them. This means the client code does not need to know about the injecting code. The client does not need to know how to construct the services. The client does not need to know which actual services it is using. The client only needs to know about the intrinsic interfaces of the services because these define how the client may use the services. This separates the responsibilities of use and construction.
Wikipedia

Two modules are the key enablers for the DI pattern's application in Aurelia:

  • dependency-injection: Contains a lightweight, extensible dependency injection container for JavaScript.
  • metadata: Contains utilities for reading and writing the metadata of JavaScript functions. It provides a consistent way of accessing type, annotation and origin metadata across a number of languages and formats.

Example

To illustrate how the DI and metadata modules work together, let's define a typical view-model class that we can refer back to. The github users view-model in Aurelia's skeleton-navigation sample app is as good a candidate as any:

JavaScript:

import {inject} from 'aurelia-framework';  
import {HttpClient} from 'aurelia-fetch-client';

@inject(HttpClient)
export class Users {  
  http;
  constructor(http) {
    this.http = http;
    ...
  }
  ...
}

TypeScript:

import {autoInject} from 'aurelia-framework';  
import {HttpClient} from 'aurelia-fetch-client';

@autoInject()
export class Users {  
  constructor(private http: HttpClient) {
    ...
  }
  ...
}

How is a view-model created at runtime?

We know from the previous post that Aurelia takes care of the boilerplate work of locating and instantiating view-models, but how does this work exactly?

Aurelia uses the DI container to instantiate all view-models.

View-models following the dependency inversion principle such as the one above, do not instantiate or locate their own dependencies. They rely on Aurelia to supply the dependencies as constructor arguments.

To instantiate a class, Aurelia's DI container needs to know the class's dependencies.

In strongly-typed languages that support reflection, DI container implementations can determine a class's dependencies by reflecting on the constructor function's argument list. There are some caveats to this, but at the simplest level, this is how dependencies are discovered.

In JavaScript, type information can be stored as metadata.

In JavaScript, there is no reliable way to determine a constructor function's argument types. To overcome this, we must embed this information on the class itself, as "metadata". An ES7 proposal for a standard way of adding metadata to classes, called the Metadata Reflection API can be polyfilled and leveraged for this purpose.

By combining the Metadata Reflection API with the ES7 decorator proposal we can use decorators to add constructor signature information to our classes, to be consumed by Aurelia's DI container. That's what's happening on the @inject(HttpClient) line in the JavaScript version of the view-model.

TypeScript can automate the creation of type metadata.

If you're using TypeScript with the experimental emitDecoratorMetadata flag, it's even easier to add constructor signature information to your classes. Simply add the @autoInject() decorator to your class- no need to list the constructor's parameter types!

How does @autoInject() work? To understand this you need to understand what TypeScript's emitDecoratorMetadata flag does. The flag causes the TypeScript compiler to polyfill the Metadata Reflection API and add a special decorator definition to the transpiled TypeScript code. The decorator is then applied to your class's constructor, methods and properties in the transpiled code. This has the effect of adding type metadata for constructor parameters, method parameters and property-accessor return types.

Aurelia's @autoInject() decorator consumes the type metadata created by TypeScript's decorator and applies it to the class in the same way that the @inject(...) decorator does. In other words, @autoInject() translates TypeScript metadata to the metadata representation used by Aurelia.

Dependency resolution is a recursive process

In our example, the Users view-model has a dependency on the Aurelia HttpClient. When the DI container instantiates the Users class it first needs to retrieve the HttpClient instance or instantiate one if it doesn't already exist in the container. The HttpClient may have dependencies of it's own, which the DI container will recursively resolve until the full dependency chain has been identified.

Using the container

The majority of this post was spent describing how Aurelia's DI container works with respect to one use case: instantiating a viewmodel. In practice there are many ways to leverage the container to decouple app logic. In the next post we'll look at common use cases for the container in application code as well it's use in the Aurelia code base.