Documentation chapters
Framework

Dependency injection

The heart of Deepkit Framework is a very powerful dependency injection container, Deepkit Injector. It's a compiling container with scopes, typesafe configuration system, automatic dependency resolving, compiler passes, tagged providers, various providers and more. It's where all class instances of your application are created and live.

Provider

A provider is used to describe how a service needs to be built. It defines what happens when a service is requested by your code, either through a dependency injection or when manually requesting the service from the container. There are several providers that allow you to register a service and define its behavior.

Class provider

A class provider is a simple class. It's automatically instantiated and dependencies are automatically resolved once that class is requested.

class Database {
    getUsers(): {username: string}[] {
        return [{username: 'Peter'}];
    }
}

class MyService {
    constructor(protected database: Database) {
    }
}

new App({
    providers: [MyService, Database],
}).run();

An verbose alternative notation is:

providers: [{provide: MyService}],

With the verbose notation you can redirect provider to an alternative class implementation.

providers: [
    {provide: MyService, useClass: OverwrittenMyService},
    OverwrittenMyService
],

Value provider

Instead of letting the dependency injection container instantiate your class, you can do so manually before the actual container is created. All dependencies need to be resolved manually as well.

const database = new Database();

new App({
    providers: [{provide: MyService, useValue: new MyService(database)}],
}).run();

Factory provider

If your service is more complex or requires complex setup calls on a newly created instance before it can be used, you can use a factory.

new App({
    providers: [
        {
            provide: MyService, 
            deps: [Database], 
            useFactory: (database: Database) => {
                return new MyService(database);
            }
        }
    ],
}).run();

You can use deps to retrieve dependencies that should be built by the container. A factory function is normally only called once, since providers are singletons by default.

Transient provider

All services are singletons unless otherwise specified, meaning a service is only instantiated once (per scope). Every other class requesting this service gets the very same instance. To disable this behavior, you can use transient providers. To enable transient you add transient: true to the provider.

//class provider with transient
{provide: MyService, transient: true}

//class provider with transient overwritten class
{provide: MyService, transient: true, useClass: OverwrittenMyService}

//class provider with transient
{provide: MyService, transient: true, useValue: new MyService(...)}

//factory provider with transient
{provide: MyService, transient: true, deps: [...], factory: ...}

Constructor/Property injection

Most of the time you use the constructor injector pattern. All dependencies are injected as constructor arguments. When you define a dependency as constructor parameter or property you essentially request an instance of that dependency from the container.

class MyService {
    constructor(protected database: Database) {
    }
}

Optional arguments should be marked as such, otherwise an error could be thrown if no instance could be resolved.

class MyService {
    constructor(protected database?: Database) {
    }
}

An alternative to constructor injection is property injection. This is usually used when you have (soft) circular dependency and don't depend on that dependency in the constructor.

import { Inject } from '@deepkit/injector';

class MyService {
    //required 
    protected database!: Inject<Database>;

    //or optional
    protected database?: Inject<Database>;
}

Configuration

In Deepkit Framework modules and your application can have configuration options. A configuration could be database urls, passwords, IPs, and so on. To retrieve that information in your services, you can use configuration injection.

#!/usr/bin/env ts-node-script
import { App } from '@deepkit/app';
import { Database } from '@deepkit/orm';
import { MongoDatabaseAdapter } from '@deepkit/mongo';

class Config {
    database: string = 'mongodb://localhost';
}

class MainDatabase extends Database {
    constructor(url: Config['database']) {
        super(new MongoDatabaseAdapter(url), []);
    }
}

new App({
    config: config,
    providers: [MainDatabase]
}).run();

The easiest way is to use the TypeScript index access operator Config['database'] as dependency in the constructor. This will automatically inject the value of 'database'.

An alternative is to use configuration partials. Those are types containing only the options you need. This is perfect when you have more than one configuration option needed in your service.

#!/usr/bin/env ts-node-script
import { App } from '@deepkit/app';
import { Database } from '@deepkit/orm';
import { MongoDatabaseAdapter } from '@deepkit/mongo';

class Config {
    databaseUrl: string = 'mongodb://localhost';
    pageTitle: string = 'My super page';
    domain: string = 'example.com';
}

class MainDatabase extends Database {
    constructor(databaseSettings: Partial<Config, 'databaseUrl'>) {
        super(new MongoDatabaseAdapter(databaseSettings), []);
    }
}

//or use multiple values and define as type alias
type DBConfig = Partial<Config, 'databaseUrl' | 'anotherOption'>;
class MainDatabase extends Database {
    constructor(databaseSettings: DBConfig) {
}

new App({
    config: Config,
    providers: [MainDatabase]
}).run();

Or request the whole configuration object.

class MainDatabase extends Database {
    constructor(protected database: Config) {
        super(new MongoDatabaseAdapter(database.databaseUrl), []);
    }
}

Scopes

By default, services don't live in any scope. If you have multiple scopes and put a service into that scope, that service is instantiated per scope. Your application can create custom scopes by creating a new InjectorContext. Deepkit Framework applications have already a few scopes define: For HTTP requests http, for RCP sessions rpc, and CLI commands cli. A new HTTP scope is created for each incoming request and closed when done. The same is true for an RPC session: As soon as a new client connects, an RPC scope is created and closed when the client disconnects. For CLI commands, a CLI scope is created for each executed command.

For example, if you put a service into the http scope, your service can only be requested by other services living in the same scope. Your HTTP controllers are automatically in the http scope, so they would be able to request that service. The objects HttpRequest and HttpResponse are only available in the http scope.

There are certain system services that are in a scope. For example RpcKernelConnection can be requested from RPC controllers (that are automatically in the rpc scope) and services that are in that scope as well.

import { HttpRequest } from '@deepkit/http';
import { FrameworkModule } from '@deepkit/framework';

class MyHttpSessionHandler {
    constructor(protected request: HttpRequest) {}

    getUser(): User {
        return request.user;
    }   
}

new App({
    providers: [{provide: MyHttpSessionHandler, scope: 'http'}],
    imports: [new FrameworkModule]
}).run();

Compiler passes

Compiler passes are a very powerful tool to adjust the way your services are built in the dependency injection container. Compiler passes provides a way to manipulate other service definitions that have been registered with the service container. It allows you to change properties or call methods with static arguments or with values from the container.

Compiler passes are added in the setup callback of your app object or a module.

import { App, } from '@deepkit/app';
import { Logger } from '@deepkit/logger';
import { injectorReference } from '@deepkit/injector';

class Database {
}

class DatabaseRegistry {
    databases: Database[] = [];
    logger?: Logger;

    addDatabase(database: Database) {
        this.databases.push(database);
    }

    enableAutoMigration() {
    }
}

class Config {
    migrateOnStartup: boolean = false;
}

new App({
    config: Config,
    providers: [DatabaseRegistry, Logger]
})
.setup((module, config) => {
    module.setupProvider(DatabaseRegistry).addDatabase(new Database());

    if (config.migrateOnStartup) {
        module.setupProvider(DatabaseRegistry).enableAutoMigration();
    }

    module.setupProvider(DatabaseRegistry).logger = injectorReference(Logger);
})
.run();

With module.setupProvider() you request a Proxy object from the provider interface and call each method on it. All calls will be scheduled and executed once the real service behind that provider is being built. If you change properties, these assignments will be scheduled as well, and will also be replayed for each newly created service.

Use injectorReference to reference other services. They will be replaced with the real service instance when your scheduled calls are executed.

Using the config parameter in the setup() callback allows you to configure your services depending on configuration values. This makes it possible to set up your dependency injection container very dynamically for a large variety of use cases.

Tagged providers

Dependency injection tags allow service authors to provide users a hook point to provide additional services for a specific purpose. For example, a user could provide several Transport services and tag them via an already defined tag LogTransporterTag from a Logger service. The Logger service can then request all services defined by the user for the tag LogTransporterTag.

Another use case would be to provide several Database services for a DatabaseRegistry service.

import { App } from '@deepkit/app';
import { Tag } from '@deepkit/injector';
import { cli, Command } from '@deepkit/app';

class Database {
    constructor(public name: string = 'default') {
    }
}

class DatabasesTag extends Tag<Database> {
}

@cli.controller('test')
class DatabaseRegistry {
    constructor(
        public databases: DatabasesTag
    ) {
    }
}

@cli.controller('test')
export class TestCommand implements Command {
    constructor(protected db: DatabaseRegistry) {
    }

    async execute() {
        console.log(this.db.databases);
    }
}

new App({
    controllers: [TestCommand],
    providers: [
        DatabaseRegistry,
        DatabasesTag.provide(Database),
        DatabasesTag.provide({ provide: Database, useValue: new Database('second') }),
    ]
}).run();
$ ts-node app.ts test
DatabasesTag {
  services: [ Database { name: 'default' }, Database { name: 'second' } ]
}

Tag registry

If a service needs instead of the service instances their definition (provider), it can use the TagRegistry class.

@cli.controller('test')
export class TestCommand implements Command {
    constructor(protected tagRegistry: TagRegistry) {
    }

    async execute() {
        console.log(this.tagRegistry.resolve(DatabasesTag));
    }
}
$ ts-node app.ts test
[
  TagProvider {
    provider: { provide: [class Database] },
    tag: DatabasesTag { services: [] }
  },
  TagProvider {
    provider: { provide: [class Database], useValue: [Database] },
    tag: DatabasesTag { services: [] }
  }
]
Made in Germany