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.
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.
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() }
}
})
}.koinFramework 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
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
class FirstDependency
val app = DependentInline.create()
.get { first: FirstDependency -> "42" }Controllers
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
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
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
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
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:
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:
class MyHandler : Handler {
override suspend fun prepare(server: Server) {
val koin = server.getServiceProvider()
}
override suspend fun handle(request: Request): Response? = null
}