Skip to content

Commit

Permalink
feat(context): tidy up context for resolving injections of a singleto…
Browse files Browse the repository at this point in the history
…n binding

- We now use owner context of the current binding to resolve injections if
the current binding scope is `SINGLETON`. This change makes sure a singleton
binding won't have dependencies outside of the owner context chain. It fixes
the root cause of #2495.
- add more tests for controller route
- add a diagram and table to explain how binding scopes impact resolution
  • Loading branch information
raymondfeng committed Mar 21, 2019
1 parent 781cd1d commit f5bf43c
Show file tree
Hide file tree
Showing 11 changed files with 506 additions and 91 deletions.
Binary file added docs/img/binding-scopes.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
105 changes: 105 additions & 0 deletions docs/site/Dependency-injection.md
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,111 @@ Error: Circular dependency detected:
lead
```

## Dependency injection for bindings with different scopes

Contexts can form a chain and bindings can be registered at different levels.
The binding scope controls not only how bound values are cached, but also how
its dependencies are resolved.

Let's take a look at the following example:

![binding-scopes](../img/binding-scopes.png)

The corresponding code is:

```ts
import {inject, Context, BindingScope} from '@loopback/context';
import {RestBindings} from '@loopback/rest';

interface Logger() {
log(message: string);
}

class PingController {
constructor(@inject('logger') private logger: Logger) {}
}

class MyService {
constructor(@inject('logger') private logger: Logger) {}
}

class ServerLogger implements Logger {
log(message: string) {
console.log('server: %s', message);
}
}

class RequestLogger implements Logger {
// Inject the http request
constructor(@inject(RestBindings.Http.REQUEST) private req: Request) {}
log(message: string) {
console.log('%s: %s', this.req.url, message);
}
}

const appCtx = new Context('application');
appCtx
.bind('controllers.PingController')
.toClass(PingController)
.inScope(BindingScope.TRANSIENT);

const serverCtx = new Context(appCtx, 'server');
serverCtx
.bind('my-service')
.toClass(MyService)
.inScope(BindingScope.SINGLETON);

serverCtx.bind('logger').toClass(ServerLogger);
```

Please note that `my-service` is a `SINGLETON` for the `server` context subtree
and it expects a `logger` to be injected.

Now we create a new context per request:

```ts
const requestCtx = new Context(serverCtx, 'request');
requestCtx.bind('logger').toClass(RequestLogger);

const myService = await requestCtx.get<MyService>('my-service');
// myService.logger should be an instance of `ServerLogger` instead of `RequestLogger`

requestCtx.close();
// myService survives as it's a singleton
```

Dependency injection for bindings in `SINGLETON` scope is resolved using the
owner context instead of the current one. This is needed to ensure that resolved
singleton bindings won't have dependencies from descendant contexts, which can
be closed before the owner context. The singleton cannot have dangling
references to values from the child context.

The story is different for `PingController` as its binding scope is `TRANSIENT`.

```ts
const requestCtx = new Context(serverCtx, 'request');
requestCtx.bind('logger').toClass(RequestLogger);

const pingController = await requestCtx.get<PingController>(
'controllers.PingController',
);
// pingController.logger should be an instance of `RequestLogger` instead of `ServerLogger`
```

A new instance of `PingController` is created for each invocation of
`await requestCtx.get<PingController>('controllers.PingController')` and its
`logger` is injected to an instance of `RequestLogger` so that it can log
information (such as `url` or `request-id`) for the `request`.

The following table illustrates how bindings and their dependencies are
resolved.

| Code | Binding Scope | Resolution Context | Owner Context | Cache Context | Dependency |
| ------------------------------------------------ | ------------- | ------------------ | ------------- | ------------- | ----------------------- |
| requestCtx.get<br>('my-service') | SINGLETON | requestCtx | serverCtx | serverCtx | logger -> ServerLogger |
| serverCtx.get<br>('my-service') | SINGLETON | serverCtx | serverCtx | serverCtx | logger -> ServerLogger |
| requestCtx.get<br>('controllers.PingController') | TRANSIENT | requestCtx | appCtx | N/A | logger -> RequestLogger |

## Additional resources

- [Dependency Injection](https://en.wikipedia.org/wiki/Dependency_injection) on
Expand Down
Loading

0 comments on commit f5bf43c

Please sign in to comment.