auto-di 0.3.2

Ergonomic async-aware automatic dependency injection for Rust
Documentation

auto-di

Async-aware automatic dependency injection for Rust. Providers register through inventory; constructor parameters form the dependency graph, and instances are initialized safely with Tokio OnceCell.

The public model is intentionally small:

  • #[singleton] registers a constructor-managed dependency.
  • #[provider] registers a factory function or factory method.

Installation

[dependencies]
auto-di = "0.3.1"

Quick start

use std::sync::Arc;
use auto_di::{application, resolve, singleton};

struct Database;

#[singleton]
async fn database() -> Database {
    Database
}

struct UserService {
    database: Arc<Database>,
}

#[singleton]
impl UserService {
    fn new(database: Arc<Database>) -> Self {
        Self { database }
    }
}

#[application]
async fn main() -> Result<(), auto_di::DiError> {
    let service = resolve::<UserService>().await?;
    Ok(())
}

No registration list or manual container wiring is required.

Providers

A provider can be a standalone sync, async, or fallible function:

#[provider]
fn config() -> AppConfig {
    AppConfig::default()
}

#[provider]
async fn database(config: Arc<AppConfig>) -> Result<Database, DatabaseError> {
    Database::connect(&config.database_url).await
}

A singleton can expose additional providers directly:

#[singleton]
impl Application {
    fn new(database: Arc<Database>) -> Self {
        Self { database }
    }

    #[provider]
    fn cache(&self) -> Cache {
        Cache::new()
    }
}

If a provider creates a dependency required by its owning singleton, make it an associated function without &self. Otherwise the graph would be circular:

#[singleton]
impl Application {
    fn new(config: Arc<AppConfig>) -> Self {
        Self { config }
    }

    #[provider]
    fn config() -> AppConfig {
        AppConfig::default()
    }
}

Direct calls such as self.cache() bypass the container and are rejected by the macro. Inject the provided type as a parameter instead.

Injection forms

Constructors and providers support:

Arc<T>                   // required dependency
Option<Arc<T>>           // optional dependency
Vec<Arc<T>>              // all implementations
Arc<dyn Trait>           // selected trait implementation
Vec<Arc<dyn Trait>>      // all trait implementations
Provider<T>              // resolve later
Lazy<T>                  // resolve once on first access

Provider<T> and Lazy<T> preserve the originating container, active profile, and request context.

Multiple implementations

trait Payments: Send + Sync {}

#[singleton(name = "stripe", primary)]
fn stripe() -> Arc<dyn Payments> {
    Arc::new(Stripe)
}

#[singleton(name = "razorpay")]
fn razorpay() -> Arc<dyn Payments> {
    Arc::new(Razorpay)
}

#[singleton]
impl Checkout {
    fn new(
        default: Arc<dyn Payments>,
        #[qualifier("razorpay")] indian: Arc<dyn Payments>,
    ) -> Self {
        Self { default, indian }
    }
}

Duplicate names, multiple primary implementations, and unresolved ambiguity are reported as DI errors.

Scopes

The default scope is singleton:

#[singleton]
fn shared_client() -> Client { Client::new() }

#[singleton(scope = "prototype")]
fn job() -> Job { Job::new() }

#[singleton(scope = "request")]
fn request_metadata() -> RequestMetadata { RequestMetadata::new() }

Resolve request-scoped dependencies through a request context:

let request = auto_di::global_container()?.request_context();
let metadata = request.resolve::<RequestMetadata>().await?;

A singleton is not allowed to capture a request-scoped dependency.

Constructor options

#[singleton] and #[provider] accept:

  • name = "..."
  • primary
  • scope = "singleton" | "prototype" | "request"
  • eager
  • blocking for expensive synchronous work
  • profile = "development"
  • condition = "ENV_KEY" or condition = "ENV_KEY=value"
  • post_construct = "async_method"
  • pre_destroy = "async_method"

Lifecycle methods may return () or Result<(), E>.

Profiles and conditions

Active profiles come from comma-separated APP_PROFILES:

#[singleton(profile = "development")]
fn development_database() -> Database { /* ... */ }

#[singleton(condition = "CACHE_ENABLED=true")]
fn cache() -> Cache { /* ... */ }

A local container can select profiles explicitly:

let container = auto_di::Container::with_profiles(["test"])?;

Configuration properties

#[configuration_properties("database")]
struct DatabaseProperties {
    url: String,       // DATABASE_URL
    pool_size: usize,  // DATABASE_POOL_SIZE
}

Validation and lifecycle

Container::validate() initializes and validates every active singleton. It surfaces missing dependencies, ambiguous providers, circular graphs, and scope violations during startup. #[application] runs validation automatically and runs shutdown hooks in dependency-safe topological order.

The resolver also detects cycles formed concurrently by different Tokio tasks, so they return an error instead of waiting on each other's OnceCell forever.

Renaming the dependency

Cargo aliases are supported:

[dependencies]
di = { package = "auto-di", version = "0.3.1" }
#[di::singleton]
fn dependency() -> Dependency { Dependency }

Example

Run the complete example from this repository:

cargo run -p auto-di --example showcase