Skip to content

Conversation

@lidonis
Copy link

@lidonis lidonis commented Jul 17, 2025

Ktor DI Bridge Implementation

  • Added KoinDependencyMapExtension implementing Ktor 3.2's DependencyMapExtension interface
  • Registered via SPI in META-INF/services/io.ktor.server.plugins.di.DependencyMapExtension

Koin can resolve Ktor DI dependencies via KtorDIExtension resolution extension.

  • There is a potential problem with runBlocking usage

When dependency is not found, each library is delegating to the other in an infinite loop

Fixed a problem in CoreResolver that was not resolving extensions

Created a sample application to show usage.

Ktor DI Bridge Implementation
  - Added KoinDependencyMapExtension implementing Ktor 3.2's DependencyMapExtension interface
  - Registered via SPI in META-INF/services/io.ktor.server.plugins.di.DependencyMapExtension

Koin can resolve Ktor DI dependencies via KtorDIExtension resolution extension.
  - There is a potential problem with runBlocking usage

When dependency is not found, each library is delegating to the other in an infinite loop

Fixed a problem in CoreResolver that was not resolving extensions

Created a sample application to show usage.

coroutines_version = "1.9.0"
ktor_version = "3.1.3"//"3.2.0-eap-1310"
ktor_version = "3.2.1"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

even 3.2.2

?: resolveFromScopeSource(scope,instanceContext)
?: resolveFromScopeArchetype(scope,instanceContext)
?: if (lookupParent) resolveFromParentScopes(scope,instanceContext) else null
?: (if (lookupParent) resolveFromParentScopes(scope,instanceContext) else null)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok


override fun contains(key: DependencyKey): Boolean =
try {
resolve(key)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me check if we have a lookup check here, else we are directly resolving the key

// The blocking call is generally safe as dependency resolution is typically fast and non-blocking
// WARNING: This may cause problems for users as it can impact performance
return runBlocking {
application.dependencies.get(key)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@osipxd we can't use getDeferred here, as we don't have reified type info from this point. we only have KClass.
Not sure runBlocking is the good way. Else we can go with typeOf() or something ?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The key construction using the class reference looks right since the reified type's not available.

You can use application.dependencies.getBlocking(key) here to let Ktor handle the blocking part.

In the case of property delegates, we also have a step to indicate that the key should be checked during startup validation, which might be applicable elsewhere:

public inline operator fun <reified T> provideDelegate(
    thisRef: Any?,
    prop: KProperty<*>
): ReadOnlyProperty<Any?, T> {
    val key = DependencyKey<T>()
        .also(::require)
    return ReadOnlyProperty { _, _ ->
        getBlocking(key)
    }
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, interesting. Thanks

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bjhham one question: if Ktor DI is falling back on Koin and Koin falling back on Ktor DI, we might go with looping when none of the DIs have a given definition

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will need to either reference the unextended counterparts as the fallbacks, or introduce a flag in the qualifier to break the loop, or just allow the bridge to call in one direction.

Copy link

@bjhham bjhham left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

// The blocking call is generally safe as dependency resolution is typically fast and non-blocking
// WARNING: This may cause problems for users as it can impact performance
return runBlocking {
application.dependencies.get(key)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The key construction using the class reference looks right since the reified type's not available.

You can use application.dependencies.getBlocking(key) here to let Ktor handle the blocking part.

In the case of property delegates, we also have a step to indicate that the key should be checked during startup validation, which might be applicable elsewhere:

public inline operator fun <reified T> provideDelegate(
    thisRef: Any?,
    prop: KProperty<*>
): ReadOnlyProperty<Any?, T> {
    val key = DependencyKey<T>()
        .also(::require)
    return ReadOnlyProperty { _, _ ->
        getBlocking(key)
    }
}

@lidonis
Copy link
Author

lidonis commented Aug 8, 2025

Thanks @bjhham for the getBlocking suggestion!
That's definitely cleaner than wrapping with runBlocking.
However, there's a challenge: getBlocking is unavailable on WASM/JS targets, which would break multiplatform compatibility.

Since we need this to work across all Kotlin targets, we might need to stick with the current approach or implement a platform-specific solution:

expect fun <T> getDependencyBlocking(dependencies: DependencyMap, key: DependencyKey<T>): T

// JVM/Native implementation
actual fun <T> getDependencyBlocking(dependencies: DependencyMap, key: DependencyKey<T>): T = 
    dependencies.getBlocking(key)

// JS/WASM implementation  
actual fun <T> getDependencyBlocking(dependencies: DependencyMap, key: DependencyKey<T>): T = 
    runBlocking { dependencies.get(key) }

This way we get the benefits of getBlocking on platforms that support it, while maintaining compatibility with WASM/JS targets.

Thoughts on this approach?

@bjhham
Copy link

bjhham commented Aug 13, 2025

Hey @lidonis you can use getBlocking on other platforms, but it can only be used with deferred processing, so lazy retrieval and access after the server is started. It is this way due to the lack of runBlocking available in JS/WASM targets.

@arnaudgiuliani
Copy link
Member

arnaudgiuliani commented Sep 3, 2025

I propose to make the options to bridge explicit for now, to avoid any unwanted behavior. Also need time to get some feedback.

We may need to ask for explicit behavior for now, and let people setup things a bit like that:
image

@arnaudgiuliani
Copy link
Member

@bjhham the dependencies.getBlocking<Any?>(key) is not working, even if we pass the right class to the key:

dependency key is forged with targeted class:
image

Trying to ask Ktor DI for a given class, already declared in dependencies section:
image

@arnaudgiuliani arnaudgiuliani modified the milestones: 4.1.1, 4.2.0 Sep 3, 2025
@arnaudgiuliani
Copy link
Member

I follow it in #2294

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants