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
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-framework-ktor-server-jvm:7.17.0'
Gradle Kotlin scriptimplementation("org.kodein.di:kodein-di-framework-ktor-server-jvm:7.17.0")
-
Declare a DI container in your application or use the
DIPlugin
Example: a Ktor Application declaration, installing theDIPlugin
(via the extension functionApplication.closestDI()
).fun main(args: Array<String>) { embeddedServer(Netty, port = 8080) { di { /* bindings */ } }.start(true) }
-
In your application, routes, etc. retrieve your DI object!
-
Retrieve your dependencies!
DIPlugin
As a Ktor Application is based on extensions and plugins 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 DIPlugin
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 plugin
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.
DIPlugin
fun main(args: Array<String>) {
embeddedServer(Netty, port = 8080) {
di { (1)
bind<Random> { singleton { SecureRandom() } } (2)
}
}.start(true)
}
1 | Install the DIPlugin (under the hood we are applying install(DIPlugin, configuration) ) |
2 | Lambda that represent a DI builder, accepting DI core features |
You cannot install multiple DIPlugin on the same Ktor Application (throws a DuplicatePluginException ).
|
Closest DI pattern
The idea behind this concept, is to be able to retrieve a DI container, from an outer class. The DIPlugin
help us with that by defining a DI container that can be retrieve from multiple places, like:
-
Application
-
ApplicationCall
-
Routing / Routes
DIPlugin
, and retrieving it from routesfun main(args: Array<String>) {
embeddedServer(Netty, port = 8080) {
di { (1)
bind<Random> { singleton { SecureRandom() } } (2)
}
routing {
get("/") {
val random by closestDI().instance<Random>() (3)
/* logic here */
}
}
}.start(true)
}
1 | Install the DIPlugin |
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 |
closestDI()
extension function receivers-
fun Application.main() { /* usage */ val di = closestDI()
/* other usage */ val random by closestDI().instance<Random>() }
-
PipelineContext<*, ApplicationCall>
get { /* usage */ val di = closestDI()
/* other usage */ val random by closestDI().instance<Random>() }
-
get("/") { /* usage */ val di = call.closestDI()
/* other usage */ val random by call.closestDI().instance<Random>() }
-
routing { /* usage */ val di = closestDI()
/* other usage */ val random by closestDI().instance<Random>() }
Because of those extension functions you can always get the DI object by using:
- closestDI() inside a Ktor class (such as Application , ApplicationCall , Route , etc.)
- di { application } inside another class, where application is the running Ktor application.
|
The closestDI() extension function will only work if your Ktor Application has the DIPlugin 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.
DIPlugin
, and retrieving it from routesfun 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 closestDI().instance<CredentialsDao>() (4)
/* logic here */
}
}
}
}.start(true)
}
1 | Install the DIPlugin |
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 DIPlugin 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 embed a DI instance.
|
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).
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.
|
Sometimes, It might be interesting to replace an existing dependency (by overriding it).
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:
// https://ktor.io/docs/routing-in-ktor.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:
-
Defining your session by implementing
DISession
Example: Defining the sessiondata 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()
-
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) } di { bind<Random> { scoped(SessionScope).singleton { SecureRandom() } } (3) /* binding */ } }.start(true) }
1 Install the Sessions
plugin2 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 closestDI().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 theDI
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-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).
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. |
get {
val random by closestDI().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 plugin. This plugin 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-framework-ktor-server-controller-jvm:7.17.0'
implementation("org.kodein.di:kodein-di-framework-ktor-server-controller-jvm:7.17.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()
.
+
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") {
val version: String by instance("version") (5)
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 | Use your DI container as in any DIAware class |
class MyController(application: Application) : AbstractDIController(application) { (1)
private val repository: DataRepository by instance("dao") (2)
override fun Routing.installRoutes() { (3)
get("/version") {
val version: String by instance("version") (4)
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 | 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.
|
-
Install your `DIController`s routes directly into the routing system
To leverage the use of
DIController
, you could use theRoute.controller
extension functions. Those functions will automatically install the routes defined in yourDIController
into the Ktor routing system.Example: Route.controller extension functionsrouting { // ... 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 aRoute
, under "/protected"Doing that the
MyFirstDIController
andMyFirstDIController
will added to the routing system but not autowired, neither bound to the DI container. Only their routes defined in theRoute.getRoutes
will be reachable on the web server (e.g.http://localhost:8080/version
).