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

How to quickly get into kodein-di-framework-ktor-server-jvm:
  1. Add this line in your dependencies block in your application build.gradle file:

    Gradle Groovy script
    implementation '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 script
    implementation("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 or kodein-di-erased must be declared in addition to the kodein-di-framework-ktor-server-jvm package.
  2. Declare a Kodein container in your application or use the KodeinFeature

    Example: a Ktor Application declaration, installing the KodeinFeature (via the extension function Application.kodein).
    fun main(args: Array<String>) {
        embeddedServer(Netty, port = 8080) {
            kodein {
                /* bindings */
            }
       }.start(true)
    }
  3. In your application, routes, etc. retrieve your Kodein object!

  4. 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.

Example: a Ktor Application declaration, installing the 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

Example: a Ktor Application declaration, installing the KodeinFeature, and retrieving it from routes
fun 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
Available kodein() extension function receivers
  • Application

    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>()
    }
  • ApplicationCall

    get("/") {
        /* usage */
        val kodein = call.kodein()
        /* other usage */
        val random by call.kodein().instance<Random>()
    }
  • Routing

    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.

Example: a Ktor Application declaration, installing the KodeinFeature, and retrieving it from routes
fun 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.
Copying bindings

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).

Example: Copying all the bindings
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.
Overriding bindings

Sometimes, It might be interesting to replace an existing dependency (by overriding it).

Example: overriding bindings
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:

Example: extend from multiple places
- 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:

  1. Defining your session by implementing KodeinSession

    Example: Defining the session
    data 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()
  2. Defining your scoped dependencies

    Example: Defining the session scoped dependencies
    fun 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 feature
    2 Declaring a session cookie represented by UserSession
    3 Bind Random object scoped by SessionScope
  3. Retrieving your scoped dependencies

    Example: Retrieving session scoped dependencies
    embeddedServer(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 fail
    2 retrieve a Random object from the Kodein object scoped by session
  4. Clear the scope as long as the sessions are no longer used

    Example: Clear the session and scope
    get("/clear") {
        call.sessions.clearSessionScope<UserSession>() (1)
    }
    1 clear the session and remove the ScopeRegistry linked to the session
    A 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 function CurrentSession.clearSessionScope<Session>(), thus the session will be cleared and the ScopeRegistry 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).

Example: Defining call scoped dependencies
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.
Example: Retrieving call scoped dependencies
 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:

Gradle Groovy script
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'
Gradle Kotlin script
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().

+

Example: Implementing KodeinController
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
Example: Extending AbstractKodeinController
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 the Route.controller extension functions. Those functions will automatically install the routes defined in your KodeinController into the Ktor routing system.

    Example: Route.controller extension functions
    routing {
    // ...
    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 a Route, under "/protected"

    Doing that the MyFirstKodeinController and MyFirstKodeinController will added to the routing system but not autowired, neither bound to the Kodein container. Only their routes defined in the Route.getRoutes will be reachable on the web server (e.g. http://localhost:8080/version).

Route.controller extension functions and KodeinControllerFeature can be used at the same time but we recommand that you should not Declaring controllers in the Route.controller extension functions and the KodeinControllerFeature might install the same route multiple times, thus leading to exceptions.