Kodein-DI on Ktor

You can use Kodein-DI 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-DI 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 multi-platform project, meaning you can use it for JVM, JS and Native projects. Please note that, at the moment, Kodein-DI utilities are only available for the JVM platform, for the server cases precisely.

Have a look at the Ktor demo project to help you to go 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-framework-ktor-server-jvm:7.7.0'
    Gradle Kotlin script
    implementation("org.kodein.di:kodein-di-framework-ktor-server-jvm:7.7.0")
  2. Declare a DI container in your application or use the DIFeature

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

  4. Retrieve your dependencies!

DIFeature

As a Ktor Application is based on extensions we cannot use the DIAware mechanism on it. So, we had to find another, elegant, way to provide a global DI container. That’s where the DIFeature stands. It allows developers to create an instance of a DI 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 DI container in the application’s attributes. Thus, the DI will be reachable from multiple places in your Ktor application.

Example: a Ktor Application declaration, installing the DIFeature
fun main(args: Array<String>) {
    embeddedServer(Netty, port = 8080) {
        di { (1)
            bind<Random> { singleton { SecureRandom() } } (2)
        }
   }.start(true)
}
1 Install the DIFeature (under the hood we are applying install(DIFeature, configuration))
2 Lambda that represent a DI builder, accepting DI core features
You cannot install multiple DIFeature on the same Ktor Application (throws a DuplicateApplicationFeatureException).

Closest DI pattern

The idea behind this concept, is to be able to retrieve a DI container, from an outer class. The DIFeature help us with that by defining a DI container that can be retrieve from multiple places, like:

  • Application

  • ApplicationCall

  • Routing / Routes

Example: a Ktor Application declaration, installing the DIFeature, and retrieving it from routes
fun main(args: Array<String>) {
    embeddedServer(Netty, port = 8080) {
        di { (1)
            bind<Random> { singleton { SecureRandom() } } (2)
        }

        routing {
            get("/") {
                val random by di().instance<Random>() (3)
                /* logic here */
            }
        }
   }.start(true)
}
1 Install the DIFeature
2 Lambda that represent a DI builder, accepting DI core features
3 retrieving the DI container from the Application by calling PipelineContext<*, ApplicationCall>.di extension function
Available di() extension function receivers
  • Application

    fun Application.main() {
        /* usage */
        val di = di()
        /* other usage */
        val random by di().instance<Random>()
    }
  • PipelineContext<*, ApplicationCall>

    get {
        /* usage */
        val di = di()
        /* other usage */
        val random by di().instance<Random>()
    }
  • ApplicationCall

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

    routing {
        /* usage */
        val di = di()
        /* other usage */
        val random by di().instance<Random>()
    }
Because of those extension functions you can always get the DI object by using: - di() inside a Ktor class (such as Application, ApplicationCall, Route, etc.) - di { application } inside another class, where application is the running Ktor application.
The di() extension function will only work if your Ktor Application has the DIFeature installed, or if you handle the installation manually.

Extending the nearest DI container

In some cases we might want to extend our global DI container for local needs. For example, we could extend the DI 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 DI container with the previously defined extension functions, To do so we have a function subDI available for the Routing / Route classes.

Example: a Ktor Application declaration, installing the DIFeature, and retrieving it from routes
fun main(args: Array<String>) {
    embeddedServer(Netty, port = 8080) {
        di { (1)
            bind<Random>() { singleton { SecureRandom() } } (2)
        }

        routing {
            route("/login") {
                subDI {
                    bind<CredentialsDao> { singleton { CredentialsDao() } } (3)
                }

                post {
                    val dao by di().instance<CredentialsDao>() (4)
                    /* logic here */
                }
            }
        }
   }.start(true)
}
1 Install the DIFeature
2 Lambda that represent a DI builder, accepting DI 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 DI 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 subDI { } in your different routing { } declaration, only one subDI will be taking into account.
The subDI mechanism will only work if your Ktor Application has the DIFeature installed, or if you handle the installation manually.
On the contrary you can define a subDI { } object for each of your `Route`s as each of them will be able to embbed a DI instance.
Copying bindings

With this feature we can extend our DI 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
DI {
    bind<Foo> { provider { Foo("rootFoo") } }
    bind<Bar> { singleton { Bar(instance()) } }
}

subDI(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 DI 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
DI {
    bind<Foo>() { provider { Foo("rootFoo") } }
    bind<Bar>() { singleton { Bar(instance()) } }
}

subDI {
    bind<Foo>(overrides = true) { provider { Foo("explicitFoo") } } (1)
}
subDI(allowSilentOverrides = true) { (2)
    bind<Foo> { provider { Foo("implicitFoo") } }
}
1 Overriding the Foo binding
2 Overriding in the subDI 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 subDI = subDI { /** new bindings / overrides **/ } (1)

        route("/books") {
            /* usage */
            subDI { /** new bindings / overrides **/ } (2)

            route("/author") {
                /* usage */
                subDI { /** new bindings / overrides **/ } (3)
            }
        }
    }
1 extending the nearest DI instance, most likely the Application’s one
2 extending the nearest DI instance, the one created in <1>
3 extending the nearest DI 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 DISession

    Example: Defining the session
    data class UserSession(val user: User) : DISession { (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)
            }
            di {
                bind<Random> { 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 di().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 DI 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-DI.

Call scope

Kodein-DI 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 di = DI {
    bind<Random> { 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 di().on(context).instance<Random>()
}

DI 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-framework-ktor-server-controller-jvm:7.7.0'
Gradle Kotlin script
implementation("org.kodein.di:kodein-di-framework-ktor-server-controller-jvm:7.7.0")
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 DIController, or extending AbstractDIController

+ To define your controllers you need, either to implement the interface DIController, or to extend the class AbstractDIController and implement the function Route.getRoutes().

+

Example: Implementing DIController
class MyController(application: Application) : DIController { (1)
    override val di by di { 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 DIController and provide a Application instance (from constructor)
2 Override the DI container, from the provided Application
3 Use your DI container as in any DIAware class
4 Override the function Route.getRoutes and define some routes
5 This route will be automatically register by the DIControllerFeature
6 Use your DI container as in any DIAware class
Example: Extending AbstractDIController
class MyController(application: Application) : AbstractDIController(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 AbstractDIController and provide a Application instance (from constructor)
2 Use your DI container as in any DIAware class
3 Override the function Routing.installRoutes and define some routes
4 This route will be automatically register by the DIControllerFeature
5 Use your DI container as in any DIAware class
Using DIController or AbstractDIController depends on your needs.
If you don’t need to use inheritance on your controllers, then you could benefit from using AbstractDIController.
On the contrary, if you want to use inheritance for your controllers you should implement DIController and override the DI container by yourself.
Using the DIControllerFeature must be used in addition of the DIFeature
In your code, the DIControllerFeature must be declared after the DIFeature, as in the previous snippet 4 is declared after 1, unless you’ll see a MissingApplicationFeatureException fired
  • Install your `DIController`s routes directly into the routing system

    To leverage the use of DIController, you could use the Route.controller extension functions. Those functions will automatically install the routes defined in your DIController into the Ktor routing system.

    Example: Route.controller extension functions
    routing {
    // ...
    controller { MyFirstDIController(instance()) } (1)
    controller("/protected") { MySecondDIController(instance()) } (2)
    // ...
    }
    1 install the routes of MyFirstDIController` inside the routing system
    2 install the routes of MyFirstDIController inside the routing system, as child of a Route, under "/protected"

    Doing that the MyFirstDIController and MyFirstDIController will added to the routing system but not autowired, neither bound to the DI 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 DIControllerFeature can be used at the same time but we recommand that you should not Declaring controllers in the Route.controller extension functions and the DIControllerFeature might install the same route multiple times, thus leading to exceptions.