# Usages
# Adding synchronous dependencies
In the following example, we declare two services as classes: MyService
and OtherService
.
Containers are built by using a container builder.
import {createContainerBuilder, LifeCycle, SyncServiceProviderInterface} from '@random-ci/container'
class MyService {
}
class OtherService {
constructor(private myService: MyService) {
}
}
// Create a container builder
const containerBuilder = createContainerBuilder()
// Add sync factories to the container
containerBuilder.addFactory("other-service", OtherService, LifeCycle.Singleton)
containerBuilder.addFactory(
"my-service",
(provider: SyncServiceProviderInterface) => new MyService(provider.get<OtherService>('other-service')),
LifeCycle.Singleton
)
// Build the container
const container = containerBuilder.build()
// Use the container
const myService = container.get<MyService>("my-service")
const anotherService = container.get<OtherService>("other-service")
# Using constructors
Since OtherService
and MyService
are classes you can add them to the container with the method
addConstructor
.
import {createContainerBuilder, LifeCycle, SyncServiceProviderInterface} from '@random-ci/container'
class OtherService {}
class MyService {
private readonly otherService: OtherService
constructor(provider: SyncServiceProviderInterface) {
this.otherService = provider.get<OtherService>("other-service")
}
}
// Create a container builder
const containerBuilder = createContainerBuilder()
// Add a sync constructor
containerBuilder.addConstructor("other-service", OtherService, LifeCycle.Singleton)
// We inject MyService as a constructor instead of a factory.
// The constructor will get the provider as first paramter.
containerBuilder.addConstructor(
"my-service",
MyService,
LifeCycle.Singleton
)
Note: Under the hood,
addConstructor
will make a call toaddFactory
by creating a simple factory like thisprovider => new MyService(provider)
# Service keys
In the previous example we used string
to specify our service keys. You can use other types to
specify your keys.
// The ServiceKey type as defined in the @random-ci/container
type ServiceKey = string | number | Symbol
// You can use symbols
const k1: ServiceKey = Symbol("my service")
// You can use strings
const k2: ServiceKey = "my service"
// You can use numbers
const k3: ServiceKey = 2
// You can use an enum since it is represented as a number
enum ServiceKeys1 {
MyService
}
const k4: ServiceKey = ServiceKeys1.MyService
// Same with "strings" enums.
enum ServiceKeys2 {
MyService = "my service"
}
const k5: ServiceKey = ServiceKeys2.MyService
# Service lifecycle
In previous examples we add our services as singleton with LifeCycle.Singleton
.
LifeCycle
is an enum that help us specify our services' life cycles.
There are two life cycles available:
LifeCycle.Singleton
only one instance of the service will be created no matter how many times you callget
orgetAsync
.LifeCycle.Transient
gives you a new instance of the service every time you callget
orgetAsync
.
import {createContainerBuilder, LifeCycle} from '@random-ci/container'
const builder = createContainerBuilder()
builder.addConstructor('my-service', MyService, LifeCycle.Singleton)
builder.addConstructor('another-service', AnotherService, LifeCycle.Transient)
const container = builder.build()
expect(container.get('my-service')).toBe(container.get('my-service'))
expect(container.get('another-service')).not.toBe(container.get('another-service'))
# Adding asynchronous dependencies
You can also inject dependencies using async factories. In the following example we will use two services. One that fetch a configuration, and the other will connect to a database.
type Config = { dbUri: string }
class ConfigurationLoader {
load(): Promise<Config> {
// ... fetch the configuration from file
}
}
class DbConnection {
constructor(private readonly config: Config) {}
}
import {createContainerBuilder, LifeCycle, AsyncServiceProviderInterface} from '@random-ci/container'
const builder = createContainerBuilder()
// We add the service as an async factory.
builder.addAsyncFactory('config', () => new ConfigurationLoader().load(), LifeCycle.Singleton)
// We use the previously injected service.
builder.addAsyncFactory(
'connection',
async (provider: AsyncServiceProviderInterface) => {
const config = await provider.getAsync<Config>('config')
return new DbConnection(config)
},
LifeCycle.Singleton
)
// Create the container
const container = builder.build()
// Fetch the service
const connection = await container.getAsync<Connection>('connection')
When calling addAsyncFactory
you need to pass an async factory instead of a sync factory.
This async factory must return a Promise
of the service. The first parameter of the factory
is also different from the addFactory
which is sync.
You can access sync services from an async factory, in fact the
AsyncServiceProviderInterface
extends the SyncServiceProviderInterface
.
# Service providers
In the previous example we used two interfaces to retrieve services' dependencies:
AsyncServiceProviderInterface
for async dependenciesSyncServiceProviderInterface
for sync dependencies
The reason we use those interface instead of the ContainerInterface
directly is to ensure
that services added using the sync methods can't access async services, this way no sync services
can depend on async services.
Here is the declaration of those two interfaces.
export interface SyncServiceProviderInterface {
get<TService>(key: ServiceKey): TService
has(key: ServiceKey): boolean
}
// Async service provider are sync service provider
export interface AsyncServiceProviderInterface extends SyncServiceProviderInterface {
getAsync<TService>(key: ServiceKey): Promise<TService>
hasAsync(key: ServiceKey): boolean
}
This provider interface will possibly be replaced by a proxy system that allows object destructuring.
# Service loaders
When bootstrapping your container you can end up with a huge file with a lot of factory definition even if you extract factories in other files.
To resolve this issue, you can use service loaders. A service loader is just a function taking as first parameter a container builder and returning void.
import {ServiceLoaderInterface, LifeCycle, createContainerBuilder} from '@random-ci/container'
// Declare a service loader
const myLoader: ServiceLoaderInterface = builder => {
builder.addFactory('other service', () => new OtherService(), LifeCycle.Singleton)
builder.addFactory('my service', () => new MyService(), LifeCycle.Singleton)
}
// Give the loader to a container builder
const builder = createContainerBuilder({
loaders: [ myLoader ]
})
// Build the container
const container = builder.build()
// Retrieve the service
const myService = container.get<MyService>("my service")
# Decorators
You can also use decorators to build your declare your services with a loader.
import {
createContainerBuilder,
reflectServiceLoader,
Service,
Inject,
LifeCycle
} from '@random-ci/container'
@Service("my service", LifeCycle.Singleton)
class MyService {
constructor(@Inject("other service") otherService: OtherService) {}
}
@Service("other service", LifeCycle.Singleton)
class OtherService {
}
const builder = createContainerBuilder({
loaders: [ reflectServiceLoader([ OtherService, MyService ]) ]
})
const container = builder.build()
const myService = container.get<MyService>("my service")
# Scoped container
Sometimes you want your container so be used only for a given http request. You can create those kinds of container as following:
import {createContainerBuilder, createScopedContainerBuilder, LifeCycle} from '@random-ci/container'
// Create a global container
const globalContainer = createContainerBuilder()
.addFactory('foo', () => 'foo', LifeCycle.Singleton)
.addFactory('bar', () => 'bar', LifeCycle.Singleton)
.build()
// Create a container builder from the global container
const perRequestBuilder = createScopedContainerBuilder(globalContainer)
.addFactory('foobar', provider => `${provider.get('foo')}${provider.get('bar')}`, LifeCycle.Singleton)
// Create a container per request.
const perRequestContainer1 = perRequestBuilder.build()
const perRequestContainer2 = perRequestBuilder.build()
const perRequestContainer3 = perRequestBuilder.build()