diff --git a/codeSnippets/settings.gradle.kts b/codeSnippets/settings.gradle.kts index a0a7d3dd8..4d0686edb 100644 --- a/codeSnippets/settings.gradle.kts +++ b/codeSnippets/settings.gradle.kts @@ -163,6 +163,7 @@ module("snippets", "tutorial-server-docker-compose") module("snippets", "htmx-integration") module("snippets", "server-http-request-lifecycle") module("snippets", "openapi-spec-gen") +module("snippets", "server-di") if(!System.getProperty("os.name").startsWith("Windows")) { module("snippets", "embedded-server-native") diff --git a/codeSnippets/snippets/server-di/README.md b/codeSnippets/snippets/server-di/README.md new file mode 100644 index 000000000..007c5f2ed --- /dev/null +++ b/codeSnippets/snippets/server-di/README.md @@ -0,0 +1,12 @@ +# Ktor Dependency Injection sample + +This project demonstrates the usage of Ktor’s built-in Dependency Injection (DI) plugin. +> This sample is a part of the [codeSnippets](../../README.md) Gradle project. + +## Running the Project + +```bash +./gradlew :server-di:run +``` + +Then, navigate to [http://localhost:8080/greet/world](http://localhost:8080/greet/world). \ No newline at end of file diff --git a/codeSnippets/snippets/server-di/build.gradle.kts b/codeSnippets/snippets/server-di/build.gradle.kts new file mode 100644 index 000000000..465ebb488 --- /dev/null +++ b/codeSnippets/snippets/server-di/build.gradle.kts @@ -0,0 +1,29 @@ +val ktor_version: String by project +val kotlin_version: String by project +val logback_version: String by project + +plugins { + application + kotlin("jvm") +} + +application { + mainClass.set("io.ktor.server.netty.EngineMain") +} + +repositories { + mavenCentral() + maven { url = uri("https://maven.pkg.jetbrains.space/public/p/ktor/eap") } +} + +dependencies { + implementation("io.ktor:ktor-server-core-jvm:$ktor_version") + implementation("io.ktor:ktor-server-di:$ktor_version") + implementation("io.ktor:ktor-server-netty-jvm:$ktor_version") + implementation("io.ktor:ktor-server-config-yaml:${ktor_version}") + implementation("io.ktor:ktor-server-content-negotiation-jvm:$ktor_version") + implementation("io.ktor:ktor-serialization-kotlinx-json-jvm:$ktor_version") + implementation("ch.qos.logback:logback-classic:${logback_version}") + testImplementation("io.ktor:ktor-server-test-host-jvm:$ktor_version") + testImplementation(kotlin("test")) +} diff --git a/codeSnippets/snippets/server-di/requests.http b/codeSnippets/snippets/server-di/requests.http new file mode 100644 index 000000000..ea4fdd0de --- /dev/null +++ b/codeSnippets/snippets/server-di/requests.http @@ -0,0 +1,22 @@ +@host = http://localhost:8080 + +### Successful checkout using cookies and amount +POST http://localhost:8080/checkout?amount=1500 +Content-Type: application/x-www-form-urlencoded +Cookie: userId=alice; cartId=cart123 + +### + +### Missing userId cookie +POST http://localhost:8080/checkout?amount=1500 +Content-Type: application/x-www-form-urlencoded +Cookie: cartId=cart-123 + +### + +### Missing amount query parameter +POST http://localhost:8080/checkout +Content-Type: application/x-www-form-urlencoded +Cookie: userId=alice; cartId=cart-123 + +### diff --git a/codeSnippets/snippets/server-di/src/main/kotlin/com.example/Application.kt b/codeSnippets/snippets/server-di/src/main/kotlin/com.example/Application.kt new file mode 100644 index 000000000..e03f477b7 --- /dev/null +++ b/codeSnippets/snippets/server-di/src/main/kotlin/com.example/Application.kt @@ -0,0 +1,31 @@ +package com.example + +import io.ktor.server.application.Application +import io.ktor.server.plugins.di.dependencies +import io.ktor.server.response.respondText +import io.ktor.server.routing.get +import io.ktor.server.routing.routing + +fun main(args: Array): Unit = io.ktor.server.netty.EngineMain.main(args) + +fun Application.module( + greetingService: GreetingService, + userRepository: UserRepository, +) { + routing { + val optional: OptionalConfig? by dependencies + + get("/greet/{name}") { + val name = call.parameters["name"] ?: "World" + call.respondText(greetingService.greet(name)) + } + + get("/db") { + call.respondText("DB = ${userRepository.db}") + } + + get("/optional") { + call.respondText("Optional = $optional") + } + } +} diff --git a/codeSnippets/snippets/server-di/src/main/kotlin/com.example/AsyncDependencies.kt b/codeSnippets/snippets/server-di/src/main/kotlin/com.example/AsyncDependencies.kt new file mode 100644 index 000000000..d2fc3cfb7 --- /dev/null +++ b/codeSnippets/snippets/server-di/src/main/kotlin/com.example/AsyncDependencies.kt @@ -0,0 +1,20 @@ +package com.example + +import io.ktor.server.application.Application +import io.ktor.server.application.log +import io.ktor.server.plugins.di.dependencies +import kotlinx.coroutines.delay + +data class EventsConnection(val connected: Boolean) + +suspend fun Application.installEvents() { + val conn: EventsConnection = dependencies.resolve() + log.info("Events connection ready: $conn") +} + +suspend fun Application.loadEventsConnection() { + dependencies.provide { + delay(200) // simulate async work + EventsConnection(true) + } +} \ No newline at end of file diff --git a/codeSnippets/snippets/server-di/src/main/kotlin/com.example/Database.kt b/codeSnippets/snippets/server-di/src/main/kotlin/com.example/Database.kt new file mode 100644 index 000000000..7c3507077 --- /dev/null +++ b/codeSnippets/snippets/server-di/src/main/kotlin/com.example/Database.kt @@ -0,0 +1,5 @@ +package com.example + +interface Database + +class PostgresDatabase(val url: String) : Database \ No newline at end of file diff --git a/codeSnippets/snippets/server-di/src/main/kotlin/com.example/GreetingService.kt b/codeSnippets/snippets/server-di/src/main/kotlin/com.example/GreetingService.kt new file mode 100644 index 000000000..c1d27490b --- /dev/null +++ b/codeSnippets/snippets/server-di/src/main/kotlin/com.example/GreetingService.kt @@ -0,0 +1,9 @@ +package com.example + +interface GreetingService { + fun greet(name: String): String +} + +class GreetingServiceImpl : GreetingService { + override fun greet(name: String): String = "Hello, $name!" +} \ No newline at end of file diff --git a/codeSnippets/snippets/server-di/src/main/kotlin/com.example/Logging.kt b/codeSnippets/snippets/server-di/src/main/kotlin/com.example/Logging.kt new file mode 100644 index 000000000..cd4acca94 --- /dev/null +++ b/codeSnippets/snippets/server-di/src/main/kotlin/com.example/Logging.kt @@ -0,0 +1,19 @@ +package com.example + +import io.ktor.server.application.* +import io.ktor.server.plugins.di.dependencies +import java.io.PrintStream + +class Logger(private val out: PrintStream) { + fun log(message: String) { + out.println("[LOG] $message") + } +} + +fun Application.logging(printStreamProvider: () -> PrintStream) { + dependencies { + provide { + Logger(printStreamProvider()) + } + } +} diff --git a/codeSnippets/snippets/server-di/src/main/kotlin/com.example/OptionalExample.kt b/codeSnippets/snippets/server-di/src/main/kotlin/com.example/OptionalExample.kt new file mode 100644 index 000000000..163e64b61 --- /dev/null +++ b/codeSnippets/snippets/server-di/src/main/kotlin/com.example/OptionalExample.kt @@ -0,0 +1,3 @@ +package com.example + +data class OptionalConfig(val value: String) diff --git a/codeSnippets/snippets/server-di/src/main/kotlin/com.example/PaymentService.kt b/codeSnippets/snippets/server-di/src/main/kotlin/com.example/PaymentService.kt new file mode 100644 index 000000000..b78998b91 --- /dev/null +++ b/codeSnippets/snippets/server-di/src/main/kotlin/com.example/PaymentService.kt @@ -0,0 +1,62 @@ +package com.example + +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.plugins.di.annotations.* +import io.ktor.server.plugins.di.dependencies +import io.ktor.server.response.* +import io.ktor.server.routing.* + +interface PaymentProcessor { + suspend fun handlePayment(call: ApplicationCall, userId: String, cartId: String, amount: Long) +} +class CreditCardPaymentProvider( + val baseUrl: String, + val clientKey: String, + val hashEncoding: (String) -> String +) : PaymentProcessor { + override suspend fun handlePayment(call: ApplicationCall, userId: String, cartId: String, amount: Long) { + call.response.header("X-Transaction-Id", "$userId:$cartId") + call.response.header("X-Digest", hashEncoding("$clientKey:$userId:$cartId:$amount")) + call.respondRedirect("$baseUrl/payment/$amount") + } +} + +class PointsBalancePaymentProvider( + val updatePoints: suspend (String, Long) -> Long +) : PaymentProcessor { + override suspend fun handlePayment(call: ApplicationCall, userId: String, cartId: String, amount: Long) { + updatePoints(userId, amount) + call.respondRedirect("/paymentComplete?userId=$userId&cartId=$cartId&amount=$amount") + } +} + +fun Application.configureExternalPaymentProvider( + @Property("payments.url") baseUrl: String, + @Property("payments.clientKey") clientKey: String, +) { + dependencies { + provide("external") { CreditCardPaymentProvider(baseUrl, clientKey) { it.reversed() } } + } +} + + +fun Application.paymentsHandling( + @Named("external") payments: PaymentProcessor +) { + log.info("Using payment processor: $payments") + routing { + post("/checkout") { + val userId = call.request.cookies["userId"] + ?: return@post call.respondText("Login required", status = HttpStatusCode.Forbidden) + val cartId = call.request.cookies["cartId"] + ?: return@post call.respondText("Cart ID missing", status = HttpStatusCode.Forbidden) + val amount = call.request.queryParameters["amount"]?.toLongOrNull() ?: return@post call.respondText("Amount missing", status = HttpStatusCode.BadRequest) + + payments.handlePayment(call, userId, cartId, amount) + } + get("/paymment/{amount}") { + call.respondText("Payment for ${call.parameters["amount"]} is pending...") + } + } +} \ No newline at end of file diff --git a/codeSnippets/snippets/server-di/src/main/kotlin/com.example/PrintStreamProvider.kt b/codeSnippets/snippets/server-di/src/main/kotlin/com.example/PrintStreamProvider.kt new file mode 100644 index 000000000..796a0d414 --- /dev/null +++ b/codeSnippets/snippets/server-di/src/main/kotlin/com.example/PrintStreamProvider.kt @@ -0,0 +1,5 @@ +package com.example + +import java.io.PrintStream + +fun stdout(): () -> PrintStream = { System.out } \ No newline at end of file diff --git a/codeSnippets/snippets/server-di/src/main/kotlin/com.example/Repositories.kt b/codeSnippets/snippets/server-di/src/main/kotlin/com.example/Repositories.kt new file mode 100644 index 000000000..1718ffb87 --- /dev/null +++ b/codeSnippets/snippets/server-di/src/main/kotlin/com.example/Repositories.kt @@ -0,0 +1,9 @@ +package com.example + +import io.ktor.server.plugins.di.annotations.Property + +fun provideDatabase( + @Property("database.connectionUrl") connectionUrl: String +): Database = PostgresDatabase(connectionUrl) + +open class UserRepository(val db: Database) diff --git a/codeSnippets/snippets/server-di/src/main/resources/application.yaml b/codeSnippets/snippets/server-di/src/main/resources/application.yaml new file mode 100644 index 000000000..4b8a92dae --- /dev/null +++ b/codeSnippets/snippets/server-di/src/main/resources/application.yaml @@ -0,0 +1,25 @@ +ktor: + deployment: + port: 8080 + application: + dependencies: + - com.example.RepositoriesKt.provideDatabase + - com.example.UserRepository + - com.example.GreetingServiceImpl + - com.example.PrintStreamProviderKt.stdout + modules: + - com.example.ApplicationKt.module + - com.example.LoggingKt.logging + - com.example.PaymentServiceKt.configureExternalPaymentProvider + - com.example.PaymentServiceKt.paymentsHandling +database: + connectionUrl: postgres://localhost:3037/admin + +connection: + domain: api.example.com + path: /v1 + protocol: https + +payments: + url: "http://localhost:8080" + clientKey: "super-secret-client-key" \ No newline at end of file diff --git a/codeSnippets/snippets/server-di/src/test/kotlin/GreetingTest.kt b/codeSnippets/snippets/server-di/src/test/kotlin/GreetingTest.kt new file mode 100644 index 000000000..cfde054b8 --- /dev/null +++ b/codeSnippets/snippets/server-di/src/test/kotlin/GreetingTest.kt @@ -0,0 +1,29 @@ +package com.example + +import io.ktor.client.request.get +import io.ktor.client.statement.bodyAsText +import io.ktor.server.testing.* +import kotlin.test.* + +class GreetingTest { + @Test + fun testGreeting() = testApplication { + application { + module( + greetingService = FakeGreetingService(), + userRepository = FakeUserRepository(), + ) + } + + val response = client.get("/greet/Test") + assertEquals("Fake greeting", response.bodyAsText()) + } +} + +class FakeGreetingService : GreetingService { + override fun greet(name: String) = "Fake greeting" +} + +class FakeUserRepository : UserRepository(FakeDatabase()) + +class FakeDatabase : Database \ No newline at end of file diff --git a/ktor.tree b/ktor.tree index 790027b68..a0ae555c0 100644 --- a/ktor.tree +++ b/ktor.tree @@ -54,7 +54,14 @@ accepts-web-file-names="configuration.html,configurations.html,environments.html,configuration-file.html"/> - + + + + + + + + diff --git a/topics/server-dependency-injection.md b/topics/server-dependency-injection.md index 32f799065..7fcfd430b 100644 --- a/topics/server-dependency-injection.md +++ b/topics/server-dependency-injection.md @@ -1,4 +1,4 @@ -[//]: # (title: Dependency Injection) +[//]: # (title: Dependency injection) @@ -8,12 +8,21 @@

Required dependencies: io.ktor:ktor-server-di

+ + -The Dependency Injection (DI) plugin allows you to register services and configuration objects once and inject them -into your application modules, plugins, routes, and other components throughout your project. Ktor's DI is designed to -integrate naturally with its existing application lifecycle, supporting scoping and structured configuration -out of the box. +[Dependency injection (DI)](https://en.wikipedia.org/wiki/Dependency_injection) is a design pattern that helps you +supply components with the dependencies they require. Instead of creating concrete implementations directly, modules +depend on abstractions, and a DI container is responsible for constructing and providing the appropriate instances at +runtime. This separation reduces coupling, improves testability, and makes it easier to replace or reconfigure +implementations without modifying existing code. + +Ktor provides a built‑in DI plugin that lets you register services and configuration objects once and access them +throughout your application. You can [inject these dependencies into modules](server-di-dependency-resolution.md#inject-into-modules), +plugins, routes, and other Ktor components in a consistent, type‑safe way. The plugin integrates with the Ktor +application lifecycle and supports scoping, structured configuration, and [automatic resource management](server-di-resource-lifecycle-management.md), +making it easier to organize and maintain application‑level services. ## Add dependencies @@ -21,276 +30,40 @@ To use DI, include the `%artifact_name%` artifact in your build script: -## Basic dependency registration - -You can register dependencies using lambdas, function references, or constructor references: - -```kotlin -dependencies { - // Lambda-based - provide { GreetingServiceImpl() } - - // Function references - provide(::GreetingServiceImpl) - provide(BankServiceImpl::class) - provide(::createBankTeller) - - // Registering a lambda as a dependency - provide<() -> GreetingService> { { GreetingServiceImpl() } } -} -``` - -## Configuration-based dependency registration - -You can configure dependencies declaratively using classpath references in your configuration file. This supports -both function and class references: - -```yaml -# application.yaml -ktor: - application: - dependencies: - - com.example.RepositoriesKt.provideDatabase - - com.example.UserRepository -database: - connectionUrl: postgres://localhost:3037/admin -``` - -```kotlin -// Repositories.kt -fun provideDatabase(@Property("database.connectionUrl") connectionUrl: String): Database = - PostgresDatabase(connectionUrl) - -class UserRepository(val db: Database) { - // implementation -} -``` - -Ktor resolves constructor and function parameters automatically using the DI container. You can use annotations like -`@Property` or `@Named` to override or explicitly bind parameters in special cases, such as when the type alone is not -enough to distinguish a value. If omitted, Ktor will attempt to resolve parameters by type using the DI container. - -## Dependency resolution and injection - -### Resolving dependencies - -To resolve dependencies, you can use property delegation or direct resolution: - -```kotlin -// Using property delegation -val service: GreetingService by dependencies - -// Direct resolution -val service = dependencies.resolve() -``` - -### Asynchronous dependency resolution - -To support asynchronous loading, you can use suspending functions: - -```kotlin -suspend fun Application.installEvents() { - val kubernetesConnection: EventsConnection = dependencies.resolve() // suspends until provided -} - -suspend fun Application.loadEventsConnection() { - dependencies.provide { - connect(property("app.events")) - } -} -``` - -The DI plugin will automatically suspend `resolve()` calls until all dependencies are ready. - -### Injecting into application modules - -You can inject dependencies directly into application modules by specifying parameters in the module function. Ktor -will resolve these dependencies from the DI container based on type matching. - -First, register your dependency providers in the `dependencies` section of the config: - -```yaml -ktor: - application: - dependencies: - - com.example.PrintStreamProviderKt.stdout - modules: - - com.example.LoggingKt.logging -``` - -Here’s what the dependency provider and module function look like: - -```kotlin -// com.example.PrintStreamProvider.kt -fun stdout(): () -> PrintStream = { System.out } -``` - -```kotlin -// com.example.Logging.kt -fun Application.logging(printStreamProvider: () -> PrintStream) { - dependencies { - provide { SimpleLogger(printStreamProvider()) } - } -} -``` - -Use `@Named` for injecting specifically keyed dependencies: +## How dependency injection works in Ktor -```kotlin -fun Application.userRepository(@Named("mongo") database: Database) { - // Uses the dependency named "mongo" -} -``` +In Ktor, dependency injection is a single, integrated process that consists of two closely related steps: -### Property and configuration injection +* [Registering dependencies](server-di-dependency-registration.md) — declaring how instances are created. +* [Resolving dependencies](server-di-dependency-resolution.md) — accessing and injecting those instances at runtime. -Use `@Property` to inject configuration values directly: +These steps are handled by a single DI container. -```yaml -connection: - domain: api.example.com - path: /v1 - protocol: https -``` +To begin using dependency injection in your application, start with [registering dependencies](server-di-dependency-registration.md). +Once dependencies are declared, continue with [resolving dependencies](server-di-dependency-resolution.md). -```kotlin -val connection: Connection = application.property("connection") -``` +## Supported features -This simplifies working with structured configuration and supports automatic parsing of primitive types. +The DI plugin supports a range of features intended to cover common application needs: -## Advanced dependency features +* [Type-safe dependency resolution](server-di-dependency-resolution.md). +* [Optional and nullable dependencies](server-di-dependency-resolution.md#optional-dependencies). +* [Covariant generic resolution](server-di-dependency-resolution.md#covariant-generics). +* [Asynchronous dependency resolution](server-di-dependency-resolution.md#async-dependency-resolution). +* [Automatic and custom resource lifecycle management](server-di-resource-lifecycle-management.md). -### Optional and nullable dependencies +## Configuration and lifecycle behavior -Use nullable types to handle optional dependencies gracefully: +The behavior of the DI container can be customized using configuration options. These options control how dependency +keys are matched, how conflicts are handled, and how resolution behaves in advanced scenarios. -```kotlin -// Using property delegation -val config: Config? by dependencies +For configuration details, see [](server-di-configuration.md). -// Or direct resolution -val config = dependencies.resolve() -``` - -### Covariant generics - -Ktor's DI system supports type covariance, which allows injecting a value as one of its supertypes when the type -parameter is covariant. This is especially useful for collections and interfaces that work with subtypes. - -```kotlin -dependencies { - provide> { listOf("one", "two") } -} - -// This will work due to type parameter covariance support -val stringList: List by dependencies -// This will also work -val stringCollection: Collection by dependencies -``` - -Covariance also works with non-generic supertypes: - -```kotlin -dependencies { - provide { BufferedOutputStream(System.out) } -} - -// This works because BufferedOutputStream is a subtype of OutputStream -val outputStream: OutputStream by dependencies -``` - -#### Limitations - -While the DI system supports covariance for generic types, it currently does not support resolving parameterized types -across type argument subtypes. That means you cannot retrieve a dependency using a type that is more specific or more -general than what was registered. - -For example, the following code will not resolve: - -```kotlin -dependencies { - provide> { CsqSink() } -} - -// Will not resolve -val charSequenceSink: Sink by dependencies -``` - -## Resource lifecycle management - -The DI plugin handles lifecycle and cleanup automatically when the application shuts down. - -### AutoCloseable support - -By default, any dependency that implements `AutoCloseable` is automatically closed when your application stops: - -```kotlin -class DatabaseConnection : AutoCloseable { - override fun close() { - // Close connections, release resources - } -} - -dependencies { - provide { DatabaseConnection() } -} -``` - -### Custom cleanup logic - -You can define custom cleanup logic by specifying a `cleanup` function: - -```kotlin -dependencies { - provide { ResourceManagerImpl() } cleanup { manager -> - manager.releaseResources() - } -} -``` - -### Scoped cleanup with key - -Use `key` to manage named resources and their cleanup: - -```kotlin -dependencies { - key("second") { - provide { CustomCloser() } - cleanup { it.closeMe() } - } -} -``` - -Dependencies are cleaned up in reverse order of declaration to ensure proper teardown. +For resource cleanup and shutdown behavior, see [](server-di-resource-lifecycle-management.md). ## Testing with dependency injection -The DI plugin provides tooling to simplify testing. You can override dependencies before loading your application -modules: - -```kotlin -fun test() = testApplication { - application { - dependencies.provide { - MockService() - } - loadServices() - } -} -``` - -### Loading configuration in tests - -Use `configure()` to load configuration files easily in your tests: - -```kotlin -fun test() = testApplication { - // Load properties from the default config file path - configure() - // Load multiple files with overrides - configure("root-config.yaml", "test-overrides.yaml") -} -``` +The DI plugin integrates with Ktor’s testing utilities and supports overriding dependencies, loading configuration, and +controlling conflict behavior in test environments. -Conflicting declarations are ignored by the test engine to let you override freely. +For more information and examples, see [](server-di-testing.md). diff --git a/topics/server-di-configuration.md b/topics/server-di-configuration.md new file mode 100644 index 000000000..dec7a29f0 --- /dev/null +++ b/topics/server-di-configuration.md @@ -0,0 +1,101 @@ +[//]: # (title: Configure the DI plugin) + + + + +

+Required dependencies: io.ktor:ktor-server-di +

+ + +
+ +You can configure the [dependency injection (DI) plugin](server-dependency-injection.md) in your application configuration file. These settings affect +the behavior of dependency resolution globally and apply to all registered dependencies. + +### Dependency key mapping + +The `ktor.di.keyMapping` property defines how dependency keys are generalized and matched during resolution. This +determines which registered dependencies are considered compatible when resolving a requested type. + +```yaml +ktor: + di: + keyMapping: Supertypes * Nullables * OutTypeArgumentsSupertypes * RawTypes +``` + +The above example matches the default key mapping used by the DI plugin. + +#### Available key mapping options + + + +<code>Default</code> +Uses the default combination: +Supertypes * Nullables * OutTypeArgumentsSupertypes * RawTypes + + +<code>Supertypes</code> +Allows resolving a dependency using any of its supertypes. + + +<code>Nullables</code> +Allows matching nullable and non-nullable variants of a type. + + +<code>OutTypeArgumentsSupertypes</code> +Allows covariance on out type parameters. + + +<code>RawTypes</code> +Allows resolving generic types without considering type arguments. + + +<code>Unnamed</code> +Ignores dependency names (@Named) when matching. + + + +#### Combine key mapping options + +You can combine key mapping options using set operators `*` (intersection), `+` (union) and `()` (grouping). + +In the following example, a dependency registered as `List` can be resolved as `Collection` (`Supertypes`), +`List` or `List?` (`RawTypes` and `Nullables`): + +```yaml +ktor: + di: + keyMapping: Supertypes + (Nullables * RawTypes) +``` + +It will not resolve as `Collection?`, because that combination is not included in the expression. + +### Conflict resolution policy + +The `ktor.di.conflictPolicy` property controls how the DI container behaves when multiple providers are registered for +the same dependency key: + +```yaml +ktor: + di: + conflictPolicy: Default +``` + +#### Available policies + + + +<code>Default</code> +Throws an exception when a conflicting dependency is declared + + +<code>OverridePrevious</code> +Overrides the previous dependency with the newly provided one. + + +<code>IgnoreConflicts</code> +In test environments, the DI plugin uses IgnoreConflicts by default. This allows test code to override +production dependencies without triggering errors. + + \ No newline at end of file diff --git a/topics/server-di-dependency-registration.md b/topics/server-di-dependency-registration.md new file mode 100644 index 000000000..590f21cbd --- /dev/null +++ b/topics/server-di-dependency-registration.md @@ -0,0 +1,121 @@ +[//]: # (title: Dependency registration) + + + + +

+Required dependencies: io.ktor:ktor-server-di +

+ + +
+ +Ktor’s [dependency injection (DI)](server-dependency-injection.md) container needs to know how to create objects that your application depends on. This +process is called dependency registration. + +### Basic dependency registration + +Basic dependency registration is done in code, typically within an `Application` module using the `dependencies {}` +block. + +You can register dependencies by providing [lambdas](#lambda-registration), [function references](#function-reference), +[class references](#class-reference), or [constructor references](#constructor-reference): + +#### Use a lambda {id="lambda-registration"} + +Use a lambda when you want full control over how an instance is created: + +```kotlin +dependencies { + provide { GreetingServiceImpl() } +} +``` +This registers a provider for `GreetingService`. Whenever `GreetingService` is requested, the lambda is executed to +create an instance. + +#### Use a constructor reference {id="constructor-reference"} + +If a class can be created using its constructor and all constructor parameters are already registered in the DI +container, you can use a constructor reference. + +```kotlin +dependencies { + provide(::GreetingServiceImpl) +} +``` +This tells your application to use the constructor of `GreetingServiceImpl`, and let DI resolve its parameters. + +#### Use a class reference {id="class-reference"} + +You can register a concrete class without binding it to an interface: + +```kotlin +dependencies { + provide(BankServiceImpl::class) +} +``` +In this case, the dependency is resolved by its `BankServiceImpl` type. +This is useful when the implementation type is injected directly and no abstraction is required. + +#### Use a function reference {id="function-reference"} + +You can register a function that creates and returns an instance: + +```kotlin +dependencies { + provide(::createBankTeller) +} +``` + +The DI container resolves the function parameters and uses the return value as the dependency instance. + +#### Use a factory lambda {id="factory-lambda-registration"} + +You can register a function itself as a dependency: + +```kotlin +dependencies { + provide<() -> GreetingService> { + { GreetingServiceImpl() } + } +} +``` + +This registers a function that can be injected and called manually to create new instances. + +### Named dependency registration {id="named-registration"} + +You can assign a name to a dependency at registration time to distinguish multiple providers of the same type. + +This is useful when you need to register more than one implementation or instance for a single type and select between +them explicitly during resolution. + +To assign a name to a dependency, pass the name as the first argument to the `provide()` function: + +```kotlin +dependencies { + provide("default") { GreetingServiceImpl() } + provide("alternative") { AlternativeGreetingServiceImpl() } +} +``` + +Named dependencies must be [resolved explicitly using the `@Named` annotation](server-di-dependency-resolution.md#resolve-named). + +### Configuration-based dependency registration + +You can configure dependencies declaratively using classpath references in your configuration file. You can list a +function that returns an object, or a class with a resolvable constructor. + +List the dependencies under the `ktor.application.dependencies` group in your configuration file: + + + + +```yaml +``` +{src="snippets/server-di/src/main/resources/application.yaml" include-lines="1,4-7"} + + + + +Ktor resolves function and constructor parameters automatically using the DI container. diff --git a/topics/server-di-dependency-resolution.md b/topics/server-di-dependency-resolution.md new file mode 100644 index 000000000..8818a4972 --- /dev/null +++ b/topics/server-di-dependency-resolution.md @@ -0,0 +1,177 @@ +[//]: # (title: Dependency resolution) + + + + +

+Required dependencies: io.ktor:ktor-server-di +

+ + +
+ +After you [register dependencies](server-di-dependency-registration.md), you can resolve them from the dependency +injection (DI) container and inject them into application code. + +You can resolve dependencies explicitly from the DI container using either [property delegation](#property-delegation) +or [direct resolution](#direct-resolution). + +### Use property delegation {id="property-delegation"} + +When using property delegation, the dependency is resolved lazily when the property is first accessed: + +```kotlin +val service: GreetingService by dependencies +``` + +### Use direct resolution {id="direct-resolution"} + +Direct resolution returns the dependency immediately or suspends until it becomes available: + +```kotlin +val service = dependencies.resolve() +``` + +### Parameter resolution + +When resolving constructors or functions, Ktor resolves parameters using the DI container. Parameters are resolved by +type by default. + +If type-based resolution is insufficient, you can use annotations to explicitly bind parameters. + +#### Use named dependencies {id="resolve-named"} + +Use the `@Named` annotation to resolve a dependency [registered with a specified name](server-di-dependency-registration.md#named-registration): + +```kotlin +fun Application.userRepository(@Named("mongo") database: Database) { + // Uses the dependency named "mongo" +} +``` + +#### Use configuration properties + +Use the `@Property` annotation to inject a value from the application configuration: + +```kotlin +``` +{src="snippets/server-di/src/main/kotlin/com.example/Repositories.kt" include-symbol="provideDatabase"} + + +In the above example, the `database.connectionUrl` property is resolved from the application configuration: + + + + +```yaml +``` +{src="snippets/server-di/src/main/resources/application.yaml" include-lines="1,4-6,13-14"} + + + + +### Asynchronous dependency resolution {id="async-dependency-resolution"} + +To support asynchronous loading, you can use suspending functions: + +```kotlin +``` +{src="snippets/server-di/src/main/kotlin/com.example/AsyncDependencies.kt" include-lines="8-20"} + +The DI plugin will automatically suspend `resolve()` calls until all dependencies are ready. + +### Inject dependencies into application modules {id="inject-into-modules"} + +You can inject dependencies directly into application modules by specifying parameters in the module function. Ktor +will resolve these dependencies from the DI container based on type matching. + +First, register your dependency providers in the `ktor.application.dependencies` group in your configuration file: + + + + +```yaml +``` +{src="snippets/server-di/src/main/resources/application.yaml" include-lines="1,4-5,9-10,12"} + + + + +Define the dependency provider and module function with parameters for the dependencies you want injected. You can then +use the injected dependencies directly within the module function: + + + + +```kotlin +``` +{src="snippets/server-di/src/main/kotlin/com.example/PrintStreamProvider.kt"} + + + + +```kotlin +``` +{src="snippets/server-di/src/main/kotlin/com.example/Logging.kt"} + + + + + +## Advanced dependency resolution + +### Optional and nullable dependencies {id="optional-dependencies"} + +Use nullable types to handle optional dependencies gracefully: + +```kotlin +// Uses property delegation +val config: Config? by dependencies + +// Uses direct resolution +val config = dependencies.resolve() +``` + +### Covariant generics {id="covariant-generics"} + +Ktor's DI system supports type covariance, which allows injecting a value as one of its supertypes when the type +parameter is covariant. This is especially useful for collections and interfaces that work with subtypes. + +```kotlin +dependencies { + provide> { listOf("one", "two") } +} + +// This will work due to type parameter covariance support +val stringList: List by dependencies +// This will also work +val stringCollection: Collection by dependencies +``` + +Covariance also works with non-generic supertypes: + +```kotlin +dependencies { + provide { BufferedOutputStream(System.out) } +} + +// This works because BufferedOutputStream is a subtype of OutputStream +val outputStream: OutputStream by dependencies +``` + +#### Limitations + +While the DI system supports covariance for generic types, it currently does not support resolving parameterized types +across type argument subtypes. That means you cannot retrieve a dependency using a type that is more specific or more +general than what was registered. + +For example, the following code will not resolve: + +```kotlin +dependencies { + provide> { CsqSink() } +} + +// Will not resolve +val charSequenceSink: Sink by dependencies +``` diff --git a/topics/server-di-resource-lifecycle-management.md b/topics/server-di-resource-lifecycle-management.md new file mode 100644 index 000000000..25661b379 --- /dev/null +++ b/topics/server-di-resource-lifecycle-management.md @@ -0,0 +1,56 @@ +[//]: # (title: Resource lifecycle management) + + + + +

+Required dependencies: io.ktor:ktor-server-di +

+ + +
+ +The [dependency injection (DI) plugin](server-dependency-injection.md) handles lifecycle and cleanup automatically when the application shuts down. + +### AutoCloseable support + +By default, any dependency that implements `AutoCloseable` is automatically closed when your application stops: + +```kotlin +class DatabaseConnection : AutoCloseable { + override fun close() { + // Close connections, release resources + } +} + +dependencies { + provide { DatabaseConnection() } +} +``` + +### Custom cleanup logic + +You can define custom cleanup logic by specifying a `cleanup` function: + +```kotlin +dependencies { + provide { ResourceManagerImpl() } cleanup { manager -> + manager.releaseResources() + } +} +``` + +### Scoped cleanup with key + +Use `key` to manage named resources and their cleanup: + +```kotlin +dependencies { + key("second") { + provide { CustomCloser() } + cleanup { it.closeMe() } + } +} +``` + +Dependencies are cleaned up in reverse order of declaration to ensure proper teardown. diff --git a/topics/server-di-testing.md b/topics/server-di-testing.md new file mode 100644 index 000000000..6a5ecdaa7 --- /dev/null +++ b/topics/server-di-testing.md @@ -0,0 +1,41 @@ +[//]: # (title: Testing with dependency injection) + + + + +

+Required dependencies: io.ktor:ktor-server-di +

+ + +
+ +The [dependency injection (DI) plugin](server-dependency-injection.md) provides tooling to simplify testing. + +You can override dependencies before loading your application modules: + +```kotlin +fun test() = testApplication { + application { + dependencies.provide { + MockService() + } + loadServices() + } +} +``` + +### Loading configuration in tests + +Use `configure()` to load configuration files easily in your tests: + +```kotlin +fun test() = testApplication { + // Load properties from the default config file path + configure() + // Load multiple files with overrides + configure("root-config.yaml", "test-overrides.yaml") +} +``` + +Conflicting declarations are ignored by the test engine to let you override freely.