Kodein DI on Ktor
You can use Kodein as-is in your Ktor project, but you can level-up your game by using the libraries kodein-di-framework-ktor-server-jvm
or kodein-di-framework-ktor-server-controller-jvm
.
Kodein does work on Ktor as-is.
The kodein-di-framework-ktor-server-jvm / kodein-di-framework-ktor-server-controller-jvm extensions add multiple ktor-specific utilities to Kodein.Using or not using this extension really depends on your needs. |
Ktor is a multiplatform project, meaning you can use it for JVM, JS and Native projects. Please note that, at the moment, Kodein utilities are only available for the JVM platform, for the server cases precisely |
Have a look at the Ktor demo project to help you going further!
Install
kodein-di-framework-ktor-server-jvm
:-
Add this line in your
dependencies
block in your applicationbuild.gradle
file:Gradle Groovy scriptimplementation 'org.kodein.di:kodein-di-generic-jvm:6.5.5' implementation 'org.kodein.di:kodein-di-framework-ktor-server-jvm:6.5.5'
Gradle Kotlin scriptimplementation("org.kodein.di:kodein-di-generic-jvm:6.5.5") implementation("org.kodein.di:kodein-di-framework-ktor-server-jvm:6.5.5")
either kodein-di-generic-jvm
orkodein-di-erased
must be declared in addition to thekodein-di-framework-ktor-server-jvm
package. -
Declare a Kodein container in your application or use the
KodeinFeature
Example: a Ktor Application declaration, installing theKodeinFeature
(via the extension functionApplication.kodein
).fun main(args: Array<String>) { embeddedServer(Netty, port = 8080) { kodein { /* bindings */ } }.start(true) }
-
In your application, routes, etc. retrieve your Kodein object!
-
Retrieve your dependencies!
KodeinFeature
As a Ktor Application is based on extensions we cannot use the KodeinAware
mechanism on it.
So, we had to find another, elegant, way to provide a global Kodein container. That’s where the KodeinFeature
stands.
It allows developers to create an instance of a Kodein container, that will be available from anywhere in their Ktor app.
To help with that, the kodein-di-framework-ktor-server-jvm
provides a custom feature
that will create and register an instance of a Kodein container in the application’s attributes.
Thus, the Kodein will be reachable from multiple places in your Ktor application.
KodeinFeature
fun main(args: Array<String>) {
embeddedServer(Netty, port = 8080) {
kodein { (1)
bind<Random>() with singleton { SecureRandom() } (2)
}
}.start(true)
}
1 | Install the KodeinFeature (under the hood we are applying install(KodeinFeature, configuration) ) |
2 | Lambda that represent a Kodein builder, accepting Kodein core features |
If you install multiple KodeinFeature on the same Ktor Application , only one will be taken into account, obviously the last one that has been declared.
|
Closest Kodein pattern
The idea behind this concept, is to be able to retrieve a Kodein container, from an outer class. The KodeinFeature
help us with that by defining a Kodein container that can be retrieve from multiple places, like:
-
Application
-
ApplicationCall
-
Routing / Routes
KodeinFeature
, and retrieving it from routesfun main(args: Array<String>) {
embeddedServer(Netty, port = 8080) {
kodein { (1)
bind<Random>() with singleton { SecureRandom() } (2)
}
routing {
get("/") {
val random by kodein().instance<Random>() (3)
/* logic here */
}
}
}.start(true)
}
1 | Install the KodeinFeature |
2 | Lambda that represent a Kodein builder, accepting Kodein core features |
3 | retrieving the Kodein container from the Application by calling PipelineContext<*, ApplicationCall>.kodein extension function |
kodein()
extension function receivers-
fun Application.main() { /* usage */ val kodein = kodein()
/* other usage */ val random by kodein().instance<Random>() }
-
PipelineContext<*, ApplicationCall>
get { /* usage */ val kodein = kodein()
/* other usage */ val random by kodein().instance<Random>() }
-
get("/") { /* usage */ val kodein = call.kodein()
/* other usage */ val random by call.kodein().instance<Random>() }
-
routing { /* usage */ val kodein = kodein()
/* other usage */ val random by kodein().instance<Random>() }
Because of those extension functions you can always get the Kodein object by using:
- kodein() inside a Ktor class (such as Application , ApplicationCall , Route , etc.)
- kodein { application } inside another class, where application is the running Ktor application.
|
The kodein() extension function will only work if your Ktor Application has the KodeinFeature installed, or if you handle the installation manually.
|
Extending the nearest Kodein container
In some cases we might want to extend our global Kodein container for local needs. For example, we could extend the Kodein container for a login Route
, by adding credentials bindings, thus they would be only available in the login Route
and its children.
We can easily achieve this goal, as we have facilities to retrieve our Kodein container with the previously defined extension functions,
To do so we have a function subKodein
available for the Routing
/ Route
classes.
KodeinFeature
, and retrieving it from routesfun main(args: Array<String>) {
embeddedServer(Netty, port = 8080) {
kodein { (1)
bind<Random>() with singleton { SecureRandom() } (2)
}
routing {
route("/login") {
subKodein {
bind<CredentialsDao> with singleton { CredentialsDao() } (3)
}
post {
val dao by kodein().instance<CredentialsDao>() (4)
/* logic here */
}
}
}
}.start(true)
}
1 | Install the KodeinFeature |
2 | Lambda that represent a Kodein builder, accepting Kodein core features |
3 | Adding new binding that will be only available for the children of the /login route |
4 | Retrieve the CredentialsDao from the nearest Kodein container |
If you define multiple routing { } features, Ktor have a specific way of joining the different routing definition, finally there is only one Routing object. Thus, if you define multiple subKodein { } in your different routing { } declaration, only one subKodein will be taking into account.
|
The subKodein mechanism will only work if your Ktor Application has the KodeinFeature installed, or if you handle the installation manually.
|
On the contrary you can define a subKodein { } object for each of your `Route`s as each of them will be able to embbed a Kodein instance.
|
With this feature we can extend our Kodein container. This extension is made by copying the none singleton / multiton, but we have the possibility to copy all the binding (including singleton / multiton).
Kodein {
bind<Foo>() with provider { Foo("rootFoo") }
bind<Bar>() with singleton { Bar(instance()) }
}
subKodein(copy = Copy.All) { (1)
/** new bindings / overrides **/
}
1 | Copying all the bindings, with the singletons / multitons |
By doing a Copy.All your original singleton / multiton won’t be available anymore, in the new Kodein container, they will exist as new instances.
|
Sometimes, It might be interesting to replace an existing dependency (by overriding it).
Kodein {
bind<Foo>() with provider { Foo("rootFoo") }
bind<Bar>() with singleton { Bar(instance()) }
}
subKodein {
bind<Foo>(overrides = true) with provider { Foo("explicitFoo") } (1)
}
subKodein(allowSilentOverrides = true) { (2)
bind<Foo> with provider { Foo("implicitFoo") }
}
1 | Overriding the Foo binding |
2 | Overriding in the subKodein will be implicit |
This feature is restricted to the Routing
/ Route
and can be used like:
- https://ktor.io/servers/features/routing.html[Routing]
routing {
/* usage */
val subKodein = subKodein { /** new bindings / overrides **/ } (1)
route("/books") {
/* usage */
subKodein { /** new bindings / overrides **/ } (2)
route("/author") {
/* usage */
subKodein { /** new bindings / overrides **/ } (3)
}
}
}
1 | extending the nearest Kodein instance, most likely the Application’s one |
2 | extending the nearest Kodein instance, the one created in <1> |
3 | extending the nearest Kodein instance, the one created in <2> |
Ktor scopes
Session scopes
With the kodein-di-framework-ktor-server-jvm
utils you can scope your dependencies upon your Ktor sessions. To do that you’ll have to follow the steps:
-
Defining your session by implementing
KodeinSession
Example: Defining the sessiondata class UserSession(val user: User) : KodeinSession { (1) override fun getSessionId() = user.id (2) }
1 Create session object that implements KtorSession
2 Implement the function getSessionId()
-
Defining your scoped dependencies
Example: Defining the session scoped dependenciesfun main(args: Array<String>) { embeddedServer(Netty, port = 8000) { install(Sessions) { (1) cookie<UserSession>("SESSION_FEATURE_SESSION_ID") (2) } kodein { bind<Random>() with scoped(SessionScope).singleton { SecureRandom() } (3) /* binding */ } }.start(true) }
1 Install the Sessions
feature2 Declaring a session cookie represented by UserSession
3 Bind Random
object scoped bySessionScope
-
Retrieving your scoped dependencies
Example: Retrieving session scoped dependenciesembeddedServer(Netty, port = 8000) { /* configurations */ routing { get("/random") { val session = call.sessions.get<UserSession>() ?: error("no session found!") (1) val random by kodein().on(session).instance<Random>() (2) call.responText("Hello ${session.user.name", your random number is ${random.nextInt()}") } } }.start(true)
1 Retrieve the session
from the request context or fail2 retrieve a Random
object from theKodein
object scoped bysession
-
Clear the scope as long as the sessions are no longer used
Example: Clear the session and scopeget("/clear") { call.sessions.clearSessionScope<UserSession>() (1) }
1 clear the session and remove the ScopeRegistry
linked to the sessionA Ktor session is cleared by calling the function CurrentSession.clear<Session>()
. To clear the session combine to the scope removal you MUST use the extension functionCurrentSession.clearSessionScope<Session>()
, thus the session will be cleared and theScopeRegistry
removed.
When working with multiple server instances you should be careful of what you are doing.
You should be aware that using the same session over multiple servers won’t give you the same instance of your scoped dependencies. In that context you might consider using a mechanism that always redirect a session request on the same server. This mechanism will not be provided by Ktor or Kodein. |
Call scope
Kodein provides a standard scope for any object (Ktor or not).
The WeakContextScope
will keep singleton and multiton instances as long as the context (= object) lives.
That’s why the CallScope
is just a wrapper upon WeakContextScope
with the target ApplicationCall
, that lives only along the Request (HTTP or Websocket).
val kodein = Kodein {
bind<Random>() with scoped(CallScope).singleton { SecureRandom() } (1)
}
1 | A Random object will be created for each Request (HTTP or Websocket) and will be retrieved as long as the Request lives. |
get {
val random by kodein().on(context).instance<Random>()
}
Kodein Controllers
To help those who want to implement a Ktor application base on a "MVC-like" architecture, we provide a custom feature. This feature is a specific module called kodein-di-framework-ktor-server-controller-jvm
. To enable it, add this line in your dependencies
block in your application build.gradle(.kts)
file:
implementation 'org.kodein.di:kodein-di-generic-jvm:6.5.5' implementation 'org.kodein.di:kodein-di-framework-ktor-server-controller-jvm:6.5.5'
implementation("org.kodein.di:kodein-di-generic-jvm:6.5.5") implementation("org.kodein.di:kodein-di-framework-ktor-server-controller-jvm:6.5.5")
either kodein-di-generic-jvm or kodein-di-erased must be declared in addition to the kodein-di-framework-ktor-server-controller-jvm package.
|
the kodein-di-framework-ktor-server-controller-jvm already have the kodein-di-framework-ktor-server-jvm as transitive dependency, so you don’t need to declare both.
|
Defining your controllers, by implementing KodeinController
, or extending AbstractKodeinController
+
To define your controllers you need, either to implement the interface KodeinController
, or to extend the class AbstractKodeinController
and implement the function Route.getRoutes()
.
+
class MyController(application: Application) : KodeinController { (1)
override val kodein by kodein { application } (2)
private val repository: DataRepository by instance("dao") (3)
override fun Route.getRoutes() { (4)
get("/version") { (5)
val version: String by instance("version") (6)
call.respondText(version)
}
}
1 | Implement KodeinController and provide a Application instance (from constructor) |
2 | Override the Kodein container, from the provided Application |
3 | Use your Kodein container as in any KodeinAware class |
4 | Override the function Route.getRoutes and define some routes |
5 | This route will be automatically register by the KodeinControllerFeature |
6 | Use your Kodein container as in any KodeinAware class |
class MyController(application: Application) : AbstractKodeinController(application) { (1)
private val repository: DataRepository by instance("dao") (2)
override fun Routing.installRoutes() { (3)
get("/version") { (4)
val version: String by instance("version") (5)
call.respondText(version)
}
}
}
1 | Extend AbstractKodeinController and provide a Application instance (from constructor) |
2 | Use your Kodein container as in any KodeinAware class |
3 | Override the function Routing.installRoutes and define some routes |
4 | This route will be automatically register by the KodeinControllerFeature |
5 | Use your Kodein container as in any KodeinAware class |
Using KodeinController or AbstractKodeinController depends on your needs.
If you don’t need to use inheritance on your controllers, then you could benefit from using AbstractKodeinController .
On the contrary, if you want to use inheritance for your controllers you should implement KodeinController and override the Kodein container by yourself.
|
Using the KodeinControllerFeature must be used in addition of the KodeinFeature
|
In your code, the KodeinControllerFeature must be declared after the KodeinFeature , as in the previous snippet 4 is declared after 1, unless you’ll see a MissingApplicationFeatureException fired
|
-
Install your `KodeinController`s routes directly into the routing system
To leverage the use of
KodeinController
, you could use theRoute.controller
extension functions. Those functions will automatically install the routes defined in yourKodeinController
into the Ktor routing system.Example: Route.controller extension functionsrouting { // ... controller { MyFirstKodeinController(instance()) } (1) controller("/protected") { `MySecondKodeinController`(instance()) } (2) // ... }
1 install the routes of MyFirstKodeinController` inside the routing system 2 install the routes of MyFirstKodeinController
inside the routing system, as child of aRoute
, under "/protected"Doing that the
MyFirstKodeinController
andMyFirstKodeinController
will added to the routing system but not autowired, neither bound to the Kodein container. Only their routes defined in theRoute.getRoutes
will be reachable on the web server (e.g.http://localhost:8080/version
).
|