2

In Aurelia, I have several classes which depend on the same configuration. Using IoC/DI, it seems natural that this config can be supplied as a constructor parameter. For example:

@autoinject
export class CustomerService {
    constructor(config: IRemoteServiceConfig) {
    }
}

@autoinject
export class GummyBearService {
    constructor(config: IRemoteServiceConfig) {
    }
}

In the simplest example, the IRemoteServiceConfig might look something like this (removed other stuff for brevity):

export IRemoteServiceConfig {
    endpoint: string;
    apiKey?: string;
    // etc. several other settings
}

Having the configuration injected in constructor is ideal for testing and doesn't require me to read configs and settings in each class.

The services depend on the same configuration, which I want to define once - during startup - in my application.

Reading through the Aurelia docs on dependency injection, I see several methods are available for this purpose, such as registerInstance(), registerResolver() and registerSingleton(). The docs do lack some context on how and where to define this.

I've started out with something like the following in the configure() section of my startup routine:

// register a static config; for brevity these are hardcoded settings
// but could come from anywhere
container.registerSingleton(IRemoteServiceConfig, () => {
    return <IRemoteServiceConfig> {
        endpoint: 'http://foo.com/api/v23/',
        apiKey: 'abc'
    }
});

It just doesn't seem to pick anything up (no errors). But this may also be just my ignorance on how to initialise the container.

My question: how and where can I define the IRemoteServiceConfig in Aurelia, if at all, so that once the DI kicks in for the services it automatically picks up my (hardcoded) config?

Note that in this SO question it is mentioned that "it can’t work with interfaces because TypeScript compiles those away at runtime.". The question is also +2 years old and both in Aurelia as well as in TypeScript a lot has changed. Regardless if this is still the case, the same question applies for an instance of a class rather than an interface as well.

Also please note that I'm aware of libs such as aurelia-configuration, which seem suited to store appsettings in a config file (and which work just fine). This would make more sense for the example given. But the question is purely related to how can I define an interface or specific instance of a class to be resolved by the Aurelia DI.

Juliën
  • 9,047
  • 7
  • 49
  • 80

2 Answers2

3

The answer provided by @estus is correct and I have marked it as an answer. Although, I I did encounter some additional problems to get this working properly. To point this out, I've taken the time to write an answer with additional details below, hoping it might help anyone else in the future.

1.) Use a class, not an interface
Like the accepted answer states, you cannot use interfaces - it has to be a class.

2.) Access the global container instance through aurelia.container
The docs don't mention how to get an instance of the container. But you can directly call upon the default/global container in the configure() operation through aurelia.container, like so:

aurelia.container.registerSingleton(RemoteServiceConfig, () => {
  var config = new RemoteServiceConfig();
    config.endpoint = 'http://foo.com/api/v23/',
    config.apiKey = 'abc'
});

I made the error of initialising a new instance of a Container() and also tried to inject it somehow in the configure() operation . I shouldn't have :)

3.) Split up your classes in different files
This seems silly but it is important: the classes cannot be in the same file, otherwise you'll get the following error:

key/value cannot be null or undefined. Are you trying to inject/register something that doesn't exist with DI?

For the purpose of the original question, I created all classes in the same app.ts file. And that simply doesn't seem to work.

That all being said, the full application now looks like this and works:

main.ts

import { Aurelia } from 'aurelia-framework'
import { RemoteServiceConfig } from './app';

export function configure(aurelia: Aurelia) {
  aurelia.use
    .standardConfiguration()
    .feature('resources');

  aurelia.container.registerSingleton(RemoteServiceConfig, () => {
    var config = new RemoteServiceConfig();
      config.endpoint = 'http://foo.com/api/v23/',
      config.apiKey = 'abc'
  });

  aurelia.start().then(() => aurelia.setRoot());
}

app.ts

import { autoinject } from 'aurelia-dependency-injection';

// note: classes below should be 3 different files!

@autoinject
export class App {
  constructor(private service: CustomerService) {
  }
}

@autoinject
export class CustomerService {
  constructor(private config: RemoteServiceConfig) {
  }
}

export class RemoteServiceConfig {
  public endpoint: string;
  public apiKey?: string;
}
Juliën
  • 9,047
  • 7
  • 49
  • 80
  • You could just inject the config class into App and populate it, you don't have to manually register it. What I actually do is inject another class called ConfigLoader, which itself accepts the Config and has a single "load" method. App calls ConfigLoader.load which populates the values of the injected Config (don't replace the object). Because this is all in the top-level DI container any other component that injects Config will get these values. :) – mgiesa Aug 02 '17 at 12:05
  • @mgiesa True. Although I would personally prefer using something like the aforementioned aurelia-configuration for that. Finding a different approach for configs wasn't the question, of course. – Juliën Aug 02 '17 at 12:12
  • Glad I found this post. I need to get some info from a web service which I store in an singleton which all modules can access. I have followed your solution to the letter but I cannot get it to work. It looks like a timing issue. By the time the web service responds Aurelia has started and loaded the root element. I can inject the config singleton into other elements but the data from the webservice is not there. I even just initialised some properties in as you did but these are undefined in when other classes access the singleton. Stumped... Just when I thought... Any ideas? – jcaddy Aug 15 '17 at 10:59
1

Interfaces don't exist at runtime, no matter which TypeScript version is used.

In order to be used both as an interface and as DI token, IRemoteServiceConfig should be a class:

export abstract class IRemoteServiceConfig { ... }
Estus Flask
  • 206,104
  • 70
  • 425
  • 565
  • Thanks. As mentioned in the question, I'm still puzzled as to _how_ and _where_ to define it in my Aurelia app (e.g. like how to "initialize" container in configure(), for example)? – Juliën Jul 31 '17 at 12:34
  • Does container.registerSingleton still not work for you when interface was changed to class? [The manual](http://aurelia.io/hub.html#/doc/article/aurelia/dependency-injection/latest/dependency-injection-basics/5) is clear on that, *Typically, you will want to do this configuration up-front in your application's main configure method* – Estus Flask Jul 31 '17 at 12:59
  • Indeed, that seemed to do the trick. I've accepted this as an answer. Although there were some other problems I encountered, which I wrote an additional answer to. – Juliën Jul 31 '17 at 13:54