A lightweight, async-friendly, scoped dependency injection container for Rust
- β Singleton / Scoped / Transient lifetimes
- β Named service instances
- β Async factory support
- β Circular dependency detection
- β Procedural macro for registration
- β Task-local scope isolation
- β
Thread-safe with
Arc
+RwLock
[dependencies]
di = { package = "rust_di", version = "" }
ctor = "0.4" # Required for automatic handler & pipeline registration
Why ctor
?
di
uses the ctor
crate to automatically register services at startup. Without it, nothing will be
wired up.
#[derive(Default)]
pub struct Logger;
#[di::registry(
Singleton,
Singleton(name = "file_logger", factory = FileLoggerFactory),
Transient(factory),
Scoped
)]
impl Logger { }
#[tokio::main]
async fn main() {
di::DIScope::run_with_scope(|| async {
let scope = di::DIScope::current().unwrap();
let logger = scope.get::<Logger>().await.unwrap();
logger.read().await.log("Hello from DI!");
let file_logger = scope.get_by_name::<Logger>("file_logger").await.unwrap();
file_logger.read().await.log("To file!");
}).await;
}
The #[di::with_di_scope] macro wraps an async fn in DIScope::run_with_scope(...), automatically initializing the task-local context required for resolving dependencies.
You can replace the entire DIScope::run_with_scope
block in your main function with a simple macro
:
use di::{with_di_scope, DIScope};
#[di::registry(Singleton)]
impl Logger {}
#[di::with_di_scope]
async fn main() {
let scope = DIScope::current().unwrap();
let logger = scope.get::<Logger>().await.unwrap();
logger.read().await.log("Hello from DI!");
}
use di::{with_di_scope, DIScope};
use tokio::sync::mpsc::{self, Receiver};
#[derive(Default)]
struct QueueConsumer {
queue: Receiver<String>,
}
#[di::registry(Singleton(factory = QueueConsumerFactory))]
impl QueueConsumer {}
async fn QueueConsumerFactory(_: std::sync::Arc<DIScope>) -> Result<QueueConsumer, di::DiError> {
let (tx, rx) = mpsc::channel(100);
tokio::spawn(async move {
let _ = tx.send("Hello from queue".into()).await;
});
Ok(QueueConsumer { queue: rx })
}
async fn run_consumer_loop() {
let scope = DIScope::current().unwrap();
let consumer = scope.get::<QueueConsumer>().await.unwrap();
while let Some(msg) = consumer.read().await.queue.recv().await {
handle_message(msg).await;
}
}
#[with_di_scope]
async fn handle_message(msg: String) {
let scope = DIScope::current().unwrap();
let logger = scope.get::<Logger>().await.unwrap();
logger.read().await.log(&format!("Received: {msg}"));
}
This pattern is ideal for long-running background tasks, workers, or event handlers that need access to scoped services.
- Eliminates boilerplate around
DIScope::run_with_scope
- Ensures
task-local
variables are properly initialized - Works seamlessly in
main
,background loops
, or anyasync entrypoint
- Encourages
clean
, scoped service resolution
Lifetime | Behavior |
---|---|
Singleton | One instance per app |
Scoped | One instance per DIScope::run_with_scope |
Transient | New instance every time |
Use #[di::registry(...)]
to register services declaratively:
#[di::registry(
Singleton,
Scoped(factory),
Transient(name = "custom")
)]
impl MyService {}
Supports:
- Singleton, Scoped, Transient
- factory β use
DiFactory
orcustom factory
- name = "..." β register named instance
cargo test-default
Covers:
- Singleton caching
- Scoped reuse
- Transient instantiation
- Named resolution
- Circular dependency detection
- All services are stored as
Arc<RwLock<T>>
- Internally uses
DashMap
,ArcSwap
, andOnceCell
- Task-local isolation via
tokio::task_local!
Because DIScope
relies on task-local
variables (tokio::task_local!
), spawning a new task with tokio::spawn
will lose the current DI scope context.
tokio::spawn(async {
// β This will panic: no DI scope found
let scope = DIScope::current().unwrap();
});
If you need to spawn a task that uses DI, wrap the task in a new scope:
tokio::spawn(async {
di::DIScope::run_with_scope(|| async {
let scope = di::DIScope::current().unwrap();
let logger = scope.get::<Logger>().await.unwrap();
logger.read().await.log("Inside spawned task");
}).await;
});
Alternatively, pass the resolved dependencies into the task before spawning.
This project aims to show support for Ukraine and its people amidst a war that has been ongoing since 2014. This war has a genocidal nature and has led to the deaths of thousands, injuries to millions, and significant property damage. We believe that the international community should focus on supporting Ukraine and ensuring security and freedom for its people.
Join us and show your support using the hashtag #StandForUkraine. Together, we can help bring attention to the issues faced by Ukraine and provide aid.