Dandi's dependency injection is heavily influenced by [Angular](https://angular.io)'s DI system.
npm install @dandi/coreDandi's dependency injection is heavily influenced by Angular's
DI system.
- Injection Token - A value that represents an injectable dependency. An Injection token can be a class
constructor, or a Symbol value. Injection tokens represent a contract or logical concept within the framework or
an application, without caring about its implementation.
- Provider - An object which describes the implementation of the contract of concept represented by an Injection
Token. The implementation can be an constant value, or can be generated using a class constructor or factory function.
- Injector - The service responsible for creating instances of injectables.
Dandi frequently uses InjectionToken objects to represent contracts or services defined only using an interface.
Rather than give the interface and token separate names (and by convention, different casing), Dandi uses identical
names, including casing, for interfaces and their corresponding injection tokens. This is possible due to TypeScript's
declaration merging feature, and provides a
more consistent feel when injecting services that use this pattern:
``
// injection token
export const SomeService = SymbolToken.for('SomeService')
// interface
export interface SomeService {
doTheWork(): void
}
// usage
class MyClass {
constructor(@Inject(SomeService) private someService: SomeService) { }
}
`
The simplest method of describing an injectable service is to add the @Injectable() decorator to it. This tells the
injector that when it encounters a dependency of the decorated class, it will instantiate a new instance of that class:
`typescript
import { Injectable } from '@dandi/core'
@Injectable()
class MyService {}
`
The @Injectable() decorator can also be used to register a service for
a different injection token, such as a token representing an interface:
`typescript
// my-interface.ts
import { InjectionToken, SymbolToken } from '@dandi/core'
export interface MyInterface {}
export const MyInterface = SymbolToken.for
// my-service.ts
import { Injectable } from '@dandi/core'
import { MyInterface } from './my-interface'
@Injectable(MyInterface)
export class MyService implements MyInterface {}
`
Providers allow you to configure the injector to map any kind of token
to any value or implementation. They are most commonly used to register
implementations of interfaces.
#### Value Providers
A value provider allows mapping an existing value to an injection token.
`typescript
import { InjectionToken, Provider, SymbolToken } from '@dandi/core'
const SomeValue = SymbolToken.for
const SomeValueProvider: Provider
provide: SomeValue,
useValue: 'any-value-you-like-here',
}
`
#### Factory Providers
A factory provider allows mapping a factory function to an injection
token. This can be helpful for making 3rd party classes injectable.
`typescript
import { InjectionToken, Provider, SymbolToken } from '@dandi/core'
import { S3 } from 'aws-sdk'
export function s3Factory(): S3 {
return new S3({ endpoint: 'http://local-dev-endpoint' })
}
export const S3Provider: Provider
provide: S3,
useFactory: s3Factory,
}
`
#### Class Providers
A class provider allows mapping a class constructor to an injection token.
`typescript
import { InjectionToken, Provider, SymbolToken } from '@dandi/core'
export interface MyInterface {}
export const MyInterface = SymbolToken.for
export class MyService implements MyInterface {}
export const MyInterfaceProvider: Provider
provide: MyInterface,
useClass: MyService,
}
`
In the above example, MyInterfaceProvider allows requests forMyInterface to be resolved as instances of MyService.
Use the @Inject() decorator to describe dependencies in a constructor:
`typescript
@Injectable()
class ServiceA {
public getSomething(): Promise
...
}
}
@Injectable()
class ServiceB {
constructor(
@Inject(ServiceA) private serviceA: ServiceA,
) {}
public async doSomething(): Promise
const something = await this.serviceA.getSomething()
console.log(something)
}
}
`
The @Inject() decorator can also be used to describe dependencies forInjector
a function or method. While Dandi does not automatically wrap function
calls, decorated functions can be invoked by an 's invoke method:
`typescript
@Injectable()
class MyService {
constructor(@Inject(Injector) private injector: Injector) {}
public async doSomething(): Promise
await this.injector.invoke(this, 'invokableMethod') // returns a Promise
}
public invokableMethod(@Inject(MyDependency) myDep: MyDependency): void {}
}
`
Normally, if the injector cannot find a provider for a dependency, it will throw an error. However, dependencies can be
marked as optional using the @Optional() decorator. Optional dependencies that cannot be resolved will be passed asundefined.
`typescript`
class MyService {
constructor(@Inject(MyDependency) @Optional() private myDep: MyDependency) {}
}
Classes and providers that are used by an application must be configured with the DandiApplication instance:
`typescript
import { DandiApplication } from '@dandi/core'
const app = new DandiApplication({
providers: [MyService, MyInterfaceProvider],
})
`
Values passed to the providers property can be class constructors, Provider instances, Module instances, or
arrays of any combination thereof. Additionally, arrays of injectables can be nested as desired - the DI system will
unpack any level of nesting.
In Dandi, a Module is a grouping dependencies, allowing many dependencies to be registered in an application with a
single line of code. Additionally, modules may expose helper methods for configuring the services or settings included
with the module.
`typescript
import { DandiApplication } from '@dandi/core'
import { ConsoleLogListener, LoggingModule } from '@dandi/core/logging'
import { PrettyColorsLogging } from '@dandi/logging'
const app = new DandiApplication({
providers: [LoggingModule.use(ConsoleLogListener, PrettyColorsLogging)],
})
`
The Dandi DI system starts with a "root" injector, where all the providers defined in the application's configuration
are registered. When executing a dependency injection request (inject or invoke), a child injector is created toInjectionScope
service the specific request. This injector is given an , which identifies the purpose for which the
injector was created. Each injector has access to the providers registered to it as well as all of its parents.
Creating a child injector can also be done manually using Injector.createChild(scope, ...providers). Creating a child
injector also provides an opportunity to add additional providers for the new injector.
Each provider can create at most one instance per injector. When an instance is created, a reference to that instance
is stored with the injector matching its scope restriction as described below, or the injector where the provider is
registered. Subsequent requests for the same injection token within the same scope will result in the same instance
being injected.
A scope restriction can be defined on either a provider or an injection token. If both a provider and an injection token
define a restriction, the restriction from the injection token is used. A scope restriction may be any value included
in the InjectionScope type - a class constructor, a function, a DependencyInjectionScope instance, or an objectCustomInjectionScope
implementing .
- Injection tokens can define a scope restriction if they are instances of OpinionatedInjectionToken, and set therestrictScope
optionrestrictScope
- Provider objects specify a scope restriction by including a keyRestrictScope
- Classes can specify a scope restriction by passing the modifier with the @Injectable decorator:@Injectable(RestrictScope(MyCustomScope))
#### Default Scoping
When neither a provider nor its injection token specifies a scope restriction, the provider will be used to generate a
single instance, which will be scoped to the injector where the provider was registered. For example, if a provider
is specified in an application's configuration, it will only ever be used to create one instance, and that instance will
be reused throughout the entire application, no matter which level of the injector hierarchy injects it.
#### Restricted Scope
When a scope restriction is defined, the injectable will be restricted to being injected only as a child of a scope
matching the defined restriction. Restricting scope is useful any time an application is processing multiple streams of
data that must remain separate - for example, handling HTTP requests. When creating instances of scope restricted
injectables, the instance is created and stored in the first injector that both has access to a provider providing
the requested injection token, and has, or is a child of a matching injection scope.
#### Scope Behaviors
A ScopeBehavior allows additional options for controlling how and where injectable instances are created.
##### perInjector
ScopeBehavior.perInjector forces a new instance of an injectable to be created for each injector that uses it. It canInjectionScope
also be invoked with an , which will add a scope restriction on top of the perInjector behavior:ScopeBehavior.perInjector(MyCustomScope)
- app.start() or app.run()InjectorContextFactory
- Pre-Init - Providers defined in container configuration are registered with the internal provider repository
- Init - is bound, scanners are runOnConfig
- Config - Provided implementations of are invokedEntryPoint
- Bootstrap - The implementation, if provided, is instantiated and invoked
The container's start() or run() method must be called to initialize the
container and start the application.
`typescript
import { DandiApplication } from '@dandi/core'
const app = new DandiApplication({
providers: [MyService, MyInterfaceProvider],
})
app.run()
`
- app.start() returns a Promise that resolves to the Injector instance created by the application.
The injector can then be used to invoke or inject objects configured in the application.
- app.run() returns a Promise that resolves to the value returned by the configured EntryPoint
implementation.
Startup logic is defined by providing an implementation of the EntryPoint interface:
`typescript
import { EntryPoint, DandiApplication, Inject, Injectable } from '@dandi/core'
@Injectable(EntryPoint)
class MyApp implements EntryPoint {
constructor(@Inject(MyService) private myService: MyService) {}
public run(): void {
// start the app
this.myService.listen()
}
}
const app = new DandiApplication({
providers: [MyApp, MyService, MyInterfaceProvider],
})
app.run()
``
- TODO
- TODO
See documentation for @dandi/core/logging