Skip to content

A lightweight, thread-safe Dependency Injection (DI) library for asynchronous Rust applications. It supports Singleton, Scoped, and Transient service lifetimes, designed for clean architecture and improved testability.

Notifications You must be signed in to change notification settings

bordunosp/rust_di

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

38 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Crates.io Build Status Docs.rs License Downloads

🧩 di β€” Dependency Injection for Rust

A lightweight, async-friendly, scoped dependency injection container for Rust


✨ Features

  • βœ… 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

πŸš€ Quick Start

1. Add to Cargo.toml

[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.


2. Register services

#[derive(Default)]
pub struct Logger;

#[di::registry(
    Singleton,
    Singleton(name = "file_logger", factory = FileLoggerFactory),
    Transient(factory),
    Scoped
)]
impl Logger { }

3. Resolve services

#[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;
}

πŸŒ€ Automatic DI Scope Initialization - #[di::with_di_scope]

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.


βœ… Example: Replacing main

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!");
}

🧠 This macro fully replaces the manual block shown in section 3. Resolve services.


πŸ” Example: Background queue consumer loop

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.


βœ… Why use #[with_di_scope]?

  • Eliminates boilerplate around DIScope::run_with_scope
  • Ensures task-local variables are properly initialized
  • Works seamlessly in main, background loops, or any async entrypoint
  • Encourages clean, scoped service resolution

🧠 Lifetimes

Lifetime Behavior
Singleton One instance per app
Scoped One instance per DIScope::run_with_scope
Transient New instance every time

🧰 Procedural Macro

Use #[di::registry(...)] to register services declaratively:

#[di::registry(
    Singleton,
    Scoped(factory),
    Transient(name = "custom")
)]
impl MyService {}

Supports:

  • Singleton, Scoped, Transient
  • factory β€” use DiFactory or custom factory
  • name = "..." β€” register named instance

πŸ§ͺ Testing

cargo test-default

Covers:

  • Singleton caching
  • Scoped reuse
  • Transient instantiation
  • Named resolution
  • Circular dependency detection

πŸ”’ Safety

  • All services are stored as Arc<RwLock<T>>
  • Internally uses DashMap, ArcSwap, and OnceCell
  • Task-local isolation via tokio::task_local!

⚠️ Limitation: tokio::spawn drops DI context

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();
});

βœ… Workaround

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.


#StandForUkraine πŸ‡ΊπŸ‡¦

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.

About

A lightweight, thread-safe Dependency Injection (DI) library for asynchronous Rust applications. It supports Singleton, Scoped, and Transient service lifetimes, designed for clean architecture and improved testability.

Topics

Resources

Stars

Watchers

Forks

Packages

No packages published

Languages