What is Binding?

Binding represents items within a Context instance. A bindingconnects its value to a unique key as the address to access the entry in acontext.

Attributes of a binding

A binding typically has the following attributes:

  • key: Each binding has a key to uniquely identify itself within the context
  • scope: The scope controls how the binding value is created and cached withinthe context
  • tags: Tags are names or name/value pairs to describe or annotate a binding
  • value: Each binding must be configured with a type of value provider so thatit be resolved to a constant or calculated valueBinding

How to create a binding?

There are a few ways to create a binding:

  • Use Binding constructor:
  1. const binding = new Binding('my-key');
  • Use Binding.bind()
  1. const binding = Binding.bind('my-key');
  • Use context.bind()
  1. const context = new Context();
  2. context.bind('my-key');

How to set up a binding?

The Binding class provides a set of fluent APIs to create and configure abinding.

Supply the value or a way to resolve the value

The value can be supplied in one the following forms:

A constant

If binding is always resolved to a fixed value, we can bind it to a constant,which can be a string, a function, an object, an array, or any other types.

  1. binding.to('my-value');

Please note the constant value cannot be a Promise to avoid confusions.

A factory function

Sometimes the value needs to be dynamically calculated, such as the current timeor a value fetched from a remote service or database.

  1. binding.toDynamicValue(() => 'my-value');
  2. binding.toDynamicValue(() => new Date());
  3. binding.toDynamicValue(() => Promise.resolve('my-value'));

A class

The binding can represent an instance of a class, for example, a controller. Aclass can be used to instantiate an instance as the resolved value. Dependencyinjection is often leveraged for its members.

  1. class MyController {
  2. constructor(@inject('my-options') private options: MyOptions) {
  3. // ...
  4. }
  5. }
  6. binding.toClass(MyController);

A provider

A provider is a class with value() method to calculate the value from itsinstance. The main reason to use a provider class is to leverage dependencyinjection for the factory function.

  1. class MyValueProvider implements Provider<string> {
  2. constructor(@inject('my-options') private options: MyOptions) {
  3. // ...
  4. }
  5. value() {
  6. return this.options.defaultValue;
  7. }
  8. }
  9. binding.toProvider(MyValueProvider);

An alias

An alias is the key with optional path to resolve the value from anotherbinding. For example, if we want to get options from RestServer for the APIexplorer, we can configure the apiExplorer.options to be resolved fromservers.RestServer.options#apiExplorer.

  1. ctx.bind('servers.RestServer.options').to({apiExplorer: {path: '/explorer'}});
  2. ctx
  3. .bind('apiExplorer.options')
  4. .toAlias('servers.RestServer.options#apiExplorer');
  5. const apiExplorerOptions = await ctx.get('apiExplorer.options'); // => {path: '/explorer'}

Configure the scope

We allow a binding to be resolved within a context using one of the followingscopes:

  • BindingScope.TRANSIENT (default)
  • BindingScope.CONTEXT
  • BindingScope.SINGLETONFor a complete list of descriptions, please seeBindingScope.
  1. binding.inScope(BindingScope.SINGLETON);

The binding scope can be accessed via binding.scope.

Describe tags

Tags can be used to annotate bindings so that they can be grouped or searched.For example, we can tag a binding as a controller or repository. The tagsare often introduced by an extension point to mark its extensions contributed byother components.

There are two types of tags:

  • Simple tag - a tag string, such as 'controller'
  • Value tag - a name/value pair, such as {name: 'MyController'}Internally, we use the tag name as its value for simple tags, for example,{controller: 'controller'}.
  1. binding.tag('controller');
  2. binding.tag('controller', {name: 'MyController'});

The binding tags can be accessed via binding.tagMap or binding.tagNames.

Chain multiple steps

The Binding fluent APIs allow us to chain multiple steps as follows:

  1. context
  2. .bind('my-key')
  3. .to('my-value')
  4. .tag('my-tag');

Apply a template function

It’s common that we want to configure certain bindings with the same attributessuch as tags and scope. To allow such setting, use binding.apply():

  1. export const serverTemplate = (binding: Binding) =>
  2. binding.inScope(BindingScope.SINGLETON).tag('server');
  1. const serverBinding = new Binding<RestServer>('servers.RestServer1');
  2. serverBinding.apply(serverTemplate);

Configure binding attributes for a class

Classes can be discovered and bound to the application context during boot. Inaddition to conventions, it’s often desirable to allow certain bindingattributes, such as scope and tags, to be specified as metadata for the class.When the class is bound, these attributes are honored to create a binding. Youcan use @bind decorator to configure how to bind a class.

  1. import {bind, BindingScope} from '@loopback/context';
  2. // @bind() accepts scope and tags
  3. @bind({
  4. scope: BindingScope.SINGLETON,
  5. tags: ['service'],
  6. })
  7. export class MyService {}
  8. // @binding.provider is a shortcut for a provider class
  9. @bind.provider({
  10. tags: {
  11. key: 'my-date-provider',
  12. },
  13. })
  14. export class MyDateProvider implements Provider<Date> {
  15. value() {
  16. return new Date();
  17. }
  18. }
  19. @bind({
  20. tags: ['controller', {name: 'my-controller'}],
  21. })
  22. export class MyController {}
  23. // @bind() can take one or more binding template functions
  24. @bind(binding => binding.tag('controller', {name: 'your-controller'})
  25. export class YourController {}

Then a binding can be created by inspecting the class,

  1. import {createBindingFromClass} from '@loopback/context';
  2. const ctx = new Context();
  3. const binding = createBindingFromClass(MyService);
  4. ctx.add(binding);

Please note createBindingFromClass also accepts an optional optionsparameter of BindingFromClassOptions type with the following settings:

  • key: Binding key, such as controllers.MyController
  • type: Artifact type, such as server, controller, repository or service
  • name: Artifact name, such as my-rest-server and my-controller, default tothe name of the bound class
  • namespace: Namespace for the binding key, such as servers and controllers.If key does not exist, its value is calculated as <namespace>.<name>.
  • typeNamespaceMapping: Mapping artifact type to binding key namespaces, suchas:
  1. {
  2. controller: 'controllers',
  3. repository: 'repositories'
  4. }
  • defaultScope: Default scope if the binding does not have an explicit scopeset. The scope from @bind of the bound class takes precedence.

Encoding value types in binding keys

String keys for bindings do not help enforce the value type. Consider theexample from the previous section:

  1. app.bind('hello').to('world');
  2. console.log(app.getSync<string>('hello'));

The code obtaining the bound value is explicitly specifying the type of thisvalue. Such solution is far from ideal:

  • Consumers have to know the exact name of the type that’s associated witheach binding key and also where to import it from.
  • Consumers must explicitly provide this type to the compiler when callingctx.get in order to benefit from compile-type checks.
  • It’s easy to accidentally provide a wrong type when retrieving the value andget a false sense of security.The third point is important because the bugs can be subtle and difficult tospot.

Consider the following REST binding key:

  1. export const HOST = 'rest.host';

The binding key does not provide any indication that undefined is a validvalue for the HOST binding. Without that knowledge, one could write thefollowing code and get it accepted by TypeScript compiler, only to learn atruntime that HOST may be also undefined and the code needs to find the server’shost name using a different way.:

  1. const resolve = promisify(dns.resolve);
  2. const host = await ctx.get<string>(RestBindings.HOST);
  3. const records = await resolve(host);
  4. // etc.

To address this problem, LoopBack provides a templated wrapper class allowingbinding keys to encode the value type too. The HOST binding described abovecan be defined as follows:

  1. export const HOST = new BindingKey<string | undefined>('rest.host');

Context methods like .get() and .getSync() understand this wrapper and usethe value type from the binding key to describe the type of the value they arereturning themselves. This allows binding consumers to omit the expected valuetype when calling .get() and .getSync().

When we rewrite the failing snippet resolving HOST names to use the new API, theTypeScript compiler immediatelly tells us about the problem:

  1. const host = await ctx.get(RestBindings.HOST);
  2. const records = await resolve(host);
  3. // Compiler complains:
  4. // - cannot convert string | undefined to string
  5. // - cannot convert undefined to string