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
[]
= "0.6.0"
Quick start
use Arc;
use ;
;
async
async
No registration list or manual container wiring is required.
Field injection
#[injectable] generates singleton construction directly from struct fields.
Fields are automatically resolved by type; use #[inject(...)] to override a
field with a literal or transformation closure:
Arc<T>, optional, collection, trait, Provider<T>, and Lazy<T> fields are
injected without cloning. An owned T field or owned closure input requires
T: Clone; prefer Arc<T> for singleton identity and zero-copy injection.
Injected functions and methods
#[injected] automatically removes DI-shaped parameters from the caller
signature and resolves them. Arc<T>, Option<Arc<T>>, Vec<Arc<T>>,
Provider<T>, and Lazy<T> are detected without an attribute:
let result = calculate.await;
Every #[injected] function also gets a _with variant containing the full
original signature. Use the short name for automatic injection, or _with when
you want to pass every dependency manually:
calculate.await; // config auto-resolved
calculate_with; // everything supplied manually
Rust does not support function overloading, so one function name cannot accept
both arities. The generated _with variant keeps both paths explicit and
type-safe.
Use #[inject(expression)] for literal/default or transformation overrides.
Use #[argument] when an Arc<T>-shaped value must remain caller-supplied.
The same syntax works on methods with a receiver. A generated wrapper preserves the function's original return type, so it can directly satisfy Actix, Axum, Rocket, or another framework's handler contract. Resolution failures go to the process-wide handler and panic with a clear message by default. Override it before serving requests when your application has its own fatal-error policy:
set_resolution_error_handler?;
Use #[injected(fallible)] when the caller should receive
Result<OriginalReturn, DiError> and handle resolution failure itself.
Detection is based on the parameter's Rust type shape rather than the runtime
registry, keeping expansion deterministic across crates.
Providers
A provider can be a standalone sync, async, or fallible function:
async
A singleton can expose additional providers directly:
If a provider creates a dependency required by its owning singleton, make it an
associated function without &self. Otherwise the graph would be circular:
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:
// required dependency
// optional dependency
// all implementations
// selected trait implementation
// all trait implementations
// resolve later
// resolve once on first access
Provider<T> and Lazy<T> preserve the originating container, active profile,
and request context.
Multiple implementations
Duplicate names, multiple primary implementations, and unresolved ambiguity are reported as DI errors.
Scopes
The default scope is singleton:
Resolve request-scoped dependencies through a request context:
let request = global_container?.request_context;
let metadata = request..await?;
A singleton is not allowed to capture a request-scoped dependency.
Constructor options
#[singleton] and #[provider] accept:
name = "..."primaryscope = "singleton" | "prototype" | "request"eagerblockingfor expensive synchronous workprofile = "development"condition = "ENV_KEY"orcondition = "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:
A local container can select profiles explicitly:
let container = with_profiles?;
Configuration properties
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:
[]
= { = "auto-di", = "0.6.0" }
Example
Run the complete example from this repository: