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.2"
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.
Named injectable structs also receive an async builder. Supply any fields you
want manually; omitted fields retain automatic injection or their declared
#[inject(...)] fallback:
let facade = builder
.retries // manual override
.build
.await?; // database and client auto-resolve
Builder-created values are independent objects. Use resolve::<Facade>() when
the container-managed singleton instance is required.
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?;
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. Generated wrappers are async
and return Result<OriginalReturn, DiError>, because dependency resolution can
fail. 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.4.2" }
Example
Run the complete example from this repository: