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.4.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] removes explicitly marked #[inject] parameters from the caller
signature and resolves them automatically:
let result = calculate.await?;
The same syntax works on methods with a receiver. Generated wrappers are async
and return Result<OriginalReturn, DiError>, because dependency resolution can
fail. Injection is explicit rather than guessed from the registry, so ordinary
caller parameters remain unambiguous.
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.4.0" }
Example
Run the complete example from this repository: