lilydocs
HTTP APIWebSocketConsumer
Fundamentals3 min read

Services and dependency injection

Connect shared dependencies to controllers and message-lifetime services to the active DI scope.

Lily HTTP, WebSocket, and Consumer share the same DI infrastructure. lily_injection provides containers and scopes, lily_injectable_derive provides service registration, and lily_injection_registry provides registration infrastructure.

Dependencies#

The dependency from the first application is sufficient:

TOML
lilyrs = { version = "0.1.0", features = ["websocket"] }

Import Injectable, ServiceTrait, InjectionError, and container/scope types from lilyrs::websocket. The macros discover the facade; your application does not need separate lily_injectable_derive, lily_injection_registry, lily_error, or linkme dependencies. The registry remains internal link-time discovery infrastructure; do not write a manual service list. Include the service module in the application's compiled module tree.

For a service library independent of a framework host, select features = ["injection"] and use lilyrs::injection::{Injectable, ServiceTrait, InjectionError}. The websocket feature enabling DI internally does not expose the public lilyrs::injection module. Do not use lilyrs::di or private expansion paths. BaseGateway is a separate macro that emits direct crate paths, so its guide explicitly declares that dependency exception.

Constructor and message injection#

Save this example as src/di.rs and add mod di; to the crate root:

di.rs
use std::sync::Arc;
use lilyrs::websocket::Injectable;
use lilyrs::websocket::ServiceTrait;
use lilyrs::websocket::{Ack, Extensions, Service, WebSocketActionError, WebSocketController,
    WebSocketControllerInitError, WebSocketControllerTrait, async_trait, websocket_controller};

#[derive(Injectable)]
#[service(lifetime = "Singleton")]
struct GreetingPolicy;
impl ServiceTrait for GreetingPolicy {}

#[derive(Injectable)]
#[service(lifetime = "Scoped")]
struct GreetingService {
    #[inject]
    policy: Arc<GreetingPolicy>,
}
impl ServiceTrait for GreetingService {}

impl GreetingService {
    fn greeting(&self) -> String { "Merhaba".into() }
}

#[derive(WebSocketController)]
#[namespace("greetings")]
struct GreetingController { policy: Arc<GreetingPolicy> }

#[async_trait]
impl WebSocketControllerTrait for GreetingController {
    async fn new(extensions: Arc<Extensions>) -> Result<Self, WebSocketControllerInitError> {
        let policy = extensions.get_service::<GreetingPolicy>(None).await
            .map_err(WebSocketControllerInitError::dependency)?;
        Ok(Self { policy })
    }
}

#[websocket_controller]
impl GreetingController {
    #[message("read")]
    async fn read(&self, Service(service): Service<GreetingService>)
        -> Result<Ack<String>, WebSocketActionError>
    { Ok(Ack::new(service.greeting())) }
}

Download the complete example.

GreetingPolicy is Singleton; it is resolved in the constructor with extensions.get_service::<GreetingPolicy>(None). The controller can share this instance throughout the app. GreetingService is Scoped; the Service<GreetingService> extractor receives it from the active scope of the greetings:read message.

Sharing within a scope#

If middleware, guards, and an action resolve the same Scoped service in one message, they receive the same instance. A new scope opens for the next message. The controller constructor does not run in a message scope, so do not put a Scoped service in a controller field.

Service<T> is a parts extractor. Placing it after the payload in the action signature does not stop it from resolving first; the framework performs all parts extraction before consuming the payload.

Initialization and cleanup#

Async resource initialization belongs in ServiceTrait::initialize(&mut self), and cleanup belongs in dispose(&self). Controllers, guards, and middleware do not have a separate async dispose callback; put managed resources such as databases or connections in DI services.

Omitting the lifetime selects Transient. This guide writes lifetimes explicitly to make intent visible. Default is unnecessary when all named fields are injected; it is required to produce initial values for runtime fields that are not injected. Scopes and interfaces explains sharing boundaries in detail.

Sources for this page v0.1.0

This guide is based on the reviewed framework commit.

QUICK START
Documentation
Source code