Dependency Injection

This optional module allows to use Koin to inject dependencies into handlers, concerns and framework classes as well as their functions.

Enabling Dependency Injection

After adding the package, you can globally enable dependency injection in your application by calling the addDependencyInjection function on the host builder, passing a pre-configured Koin instance to be used. As expected, the lifecycle of the dependencies is managed by Koin, so you can use singletons (single), factories (factory) or scoped services as needed. The implementation will create a new scope for each request handled by the server.

Kotlin
import org.genhttp.engine.internal.Host

import org.genhttp.modules.dependencyinjection.*
import org.genhttp.modules.layouting.*
import org.genhttp.modules.practices.*

import org.koin.dsl.koinApplication
import org.koin.dsl.module

class FirstDependency

class SecondDependency

suspend fun main() {
    val app = Layout.create()

    // create a Koin application and register your dependencies
    val koin = koinApplication {
        modules(module {
            single { FirstDependency() }
            single { SecondDependency() }
        })
    }.koin

    Host.create()
        .addDependencyInjection(koin)
        .handler(app)
        .defaults()
        .development()
        .run()
}

Instead of an isolated koinApplication, you can also pass the global instance of Koin (startKoin { ... }.koin).

The scope created for a request uses the qualifier RequestScope, so services that should live as long as a request are registered as scoped services of this scope. The scope is closed as soon as the handler chain returned the response, before its content is written to the client.

Kotlin
import org.genhttp.modules.dependencyinjection.RequestScope

import org.koin.dsl.koinApplication
import org.koin.dsl.module

class RequestTracker

val koin = koinApplication {
    modules(module {
        scope(RequestScope) {
            scoped { RequestTracker() }
        }
    })
}.koin

Framework Integration

The following sections show how you can add dependency injection to webservices and websockets.

Service Frameworks

The dependency injection module adds extensions to the layout builder that can be used to add DI-enabled web services, controllers, or functional handlers to your application. This requires dependency injection to be configured as described above. The following examples show how to inject the types registered above into services or controllers.

Webservices

Kotlin
class FirstDependency

class SecondDependency

val app = Layout.create()
    .addDependentService<InjectionDemo>("service")

class InjectionDemo(private val first: FirstDependency) {

    @ResourceMethod
    fun doSomething(second: SecondDependency): String = "42"

}

Functional

Kotlin
class FirstDependency

val app = DependentInline.create()
    .get { first: FirstDependency -> "42" }

Controllers

Kotlin
class FirstDependency

class SecondDependency

val app = Layout.create()
    .addDependentController<InjectionDemo>("controller")

class InjectionDemo(private val first: FirstDependency) {

    fun doSomething(second: SecondDependency): String = "42"

}

In contrast to non-injected services, the framework will create a new instance of your service on every request so that scoped dependencies can be injected. To control the lifecycle of your services, you can also register them in Koin, and they will be resolved from there.

To create an instance of a class that is not registered, genhttp-kt calls its primary constructor and resolves every parameter by its type from the scope of the request. A parameter whose type is not registered gets its default value if it has one, or null if it is nullable - otherwise, the instance cannot be created and the request fails. Parameters of functions are injected if their type is registered without a qualifier; definitions with a qualifier (named(...)) are not considered.

Websockets

The dependency injection module adds extension functions that allow dependent websocket handlers to be created with having their dependencies injected as expected.

Reactive

Kotlin
class MyDependency

val websocket = Websocket.reactive()
    .dependentHandler<MyHandler>()

class MyHandler(private val dependency: MyDependency) : ReactiveHandler {

    override suspend fun onMessage(connection: ReactiveConnection, message: WebsocketFrame) {
        // do work
    }

}

Imperative

Kotlin
class MyDependency

val websocket = Websocket.imperative()
    .dependentHandler<MyHandler>()

class MyHandler(private val dependency: MyDependency) : ImperativeHandler {

    override suspend fun handle(connection: ImperativeConnection) {
        // do work
    }

}

Handler and Concern Integration

The Dependent entry point allows you to create handlers and concerns that resolve their dependencies from Koin. As their lifecycle may differ from that of regular handlers and concerns, there are slightly different interfaces to be implemented.

Handlers

Kotlin
class FirstDependency

val app = Layout.create()
    .add("handler", Dependent.handler<InjectionHandler>())

class InjectionHandler(private val first: FirstDependency) : DependentHandler {

    override suspend fun handle(request: Request): Response? {
        // ...
        return request.respond()
            .content("Hello World")
            .build()
    }

}

Concerns

Kotlin
class FirstDependency

val app = Layout.create()
    .add(Dependent.concern<InjectionConcern>())

class InjectionConcern(private val first: FirstDependency) : DependentConcern {

    override suspend fun handle(content: Handler, request: Request): Response? {
        return content.handle(request)
    }

}

Accessing Scope and Service Provider

To obtain the Koin instance or the Scope of the current request, there are extension functions available on the Request interface:

Kotlin
fun inspect(request: Request) {
    val koin = request.getServiceProvider()
    val scope = request.getServiceScope()
}

There is also a getServiceProvider() overload directly on Server, which is what you should use from prepare(server: Server) - a handler is not yet handling a request at that point, so no Request is available:

Kotlin
class MyHandler : Handler {

    override suspend fun prepare(server: Server) {
        val koin = server.getServiceProvider()
    }

    override suspend fun handle(request: Request): Response? = null

}