rust-dix is the core crate of the rust-dix framework — a Rust dependency injection
container inspired by Microsoft.Extensions.DependencyInjection (MEDI).
[]
= "0.8"
When should you use this?
1. You want to decouple service creation from usage
Instead of MyService::new(dep1, dep2, dep3) scattered across your codebase,
you declare once how services are constructed and let the container wire them
up automatically.
// Without DI: every call site must know how to build everything
let svc = new;
// With DI: declare once, resolve anywhere.
// `MyService` is annotated with `#[rust_dix::inject(transient)]`, so it is
// registered automatically (its definition is omitted here).
let provider = from_injected
.singleton
.singleton
.build
.unwrap;
let svc: = provider.get.unwrap;
2. You need to swap implementations for testing
Change one registration line — no need to touch the code that consumes the service.
3. You have objects with different lifetimes
Some services should be shared globally (database pool), some per-request (HTTP context), some created fresh each time (value objects). rust-dix gives you three lifetimes to model this naturally.
| Lifetime | Behavior | Typical use |
|---|---|---|
| Singleton | Created once, shared everywhere | Database pool, config, event bus |
| Scoped | Created once per scope, shared within scope | HTTP request context, unit of work, transaction |
| Transient | Created every time you resolve it | Value objects, DTOs, lightweight stateless services |
let provider = new
.singleton // one pool for the app
.scoped // one per request
.transient // new each time
.build
.unwrap;
4. You want multiple instances of the same type
Keyed services let you register several implementations of the same trait, each distinguished by a string key. All three lifetimes are supported:
// Keyed with different lifetimes
let provider = new
// Singleton keyed - shared globally
.
// Scoped keyed - shared within a scope
.
// Transient keyed - new instance each time
.
.build
.unwrap;
let fast: = provider.get_keyed.unwrap;
let session: = provider.scope.get_keyed.unwrap;
let fresh1: = provider.get_keyed.unwrap;
let fresh2: = provider.get_keyed.unwrap; // different instance
5. You are building a plugin / modular system
- Layered containers: child-first, root-fallback resolution for plugin isolation. The plugin sees its own services first; host services are visible as fallback.
- Named services: register services by string name — critical for cdylib
plugins where Rust's
TypeIddiffers across compilation units. IProvider: pass a unified DI interface (resolution + named registry) to external modules that don't depend on rust-dix directly.
Quick Reference · API
Registration (ServiceCollection)
| Method | Lifetime | Use when |
|---|---|---|
.from_injected() |
mixed | Collect all #[rust_dix::inject] annotations (see below) |
.singleton(f) |
Singleton | You need exactly one instance shared globally |
.scoped(f) |
Scoped | You need one instance per scope (request/transaction) |
.transient(f) |
Transient | You need a new instance every time |
.keyed_singleton(k, f) |
Singleton | Multiple named instances (strategy pattern) |
.keyed_scoped(k, f) |
Scoped | Named instances scoped per request |
.keyed_transient(k, f) |
Transient | Named instances created fresh each time |
.instance(arc) |
Singleton | You already have a built Arc<T> |
.try_add(f) |
Singleton | Only register if not already present (MEDI TryAdd) |
.try_add_scoped(f) |
Scoped | Only register if not already present |
.try_add_transient(f) |
Transient | Only register if not already present |
.singleton_value(v) |
Singleton | Register a plain value (wraps in Arc) |
.add(lt, f) |
any | Specify lifetime explicitly |
.build() |
— | Build the provider with default (strict) scope validation |
.build_with(options) |
— | Build with explicit ServiceProviderOptions (lenient() restores 0.7.x root-scope behavior) |
.build_async() / .build_async_with(options) |
— | Async build; required when any async_* registration exists |
The factory closure f receives &dyn IServiceResolver. Generic get::<T>()
requires Self: Sized, so it cannot be called on that trait object: let the
inject macros generate the constructor (internal __rdi_construct_*,
#[doc(hidden)]), or resolve with get_any + downcast:
new
.singleton
// `Repo` is `#[derive(Inject)]` with `#[inject] pool: Arc<Pool>`; the
// generated constructor resolves its fields during registration.
.transient
.build
.unwrap;
#[rust_dix::inject] — Attribute-based auto-registration (recommended)
The preferred way to register services is to annotate structs (or trait impl blocks) directly:
use *;
// Annotate a struct → registered as its concrete type.
// Lifetime defaults to `singleton` when omitted.
// Annotate a trait impl → registered as `dyn Trait`.
// The trait type is auto-detected from `impl Trait for Type`,
// so no `as = dyn Trait` boilerplate is needed.
// For trait-oriented services, use #[derive(Inject)] on the struct (generates
// a constructor but does NOT register) and #[inject] on the impl block only.
// Putting #[inject] on BOTH struct and impl causes double registration.
// on the impl: trait registration (singleton by default)
// Then build from all annotations in one call:
let provider = from_injected.build.unwrap;
The #[rust_dix::inject] attribute automatically generates constructor code
(like #[derive(Inject)]) and registers the service via inventory
(on Linux, macOS, and Windows).
Supports both named-field structs and unit structs (zero fields):
// Unit struct — zero fields, works out of the box
;
Two placement sites, one consistent syntax:
| Placement | What it registers | Reuses constructor? |
|---|---|---|
#[inject] on struct |
Concrete type T |
Generates its own |
#[inject] on impl Trait for T |
dyn Trait |
Reuses the generated constructor (internal __rdi_construct_T, #[doc(hidden)]) |
#[derive(Inject)] on struct |
(nothing — constructor only) | — |
Choose one site, don't double-register: #[inject] generally goes on
either the struct or the impl block, not both. Putting #[inject] on both
registers the same struct twice (concrete type + dyn Trait), which is rarely
intended — consumers can then bypass the trait via get::<ConcreteType>().
For trait-oriented services (handlers, services, repositories), prefer
#[derive(Inject)] on the struct (constructor only, no registration) plus
#[inject] on the impl block. This replaces the old as = dyn Trait /
as = [dyn A, dyn B] syntax.
Resolution (ServiceProvider / Scope)
| Method | Returns | Behavior |
|---|---|---|
.get::<T>() |
Result<Arc<T>, RdiError> |
Returns Err if not registered; last registration wins |
.get_optional::<T>() |
Option<Arc<T>> |
Returns None if not registered |
.get_keyed::<T>(key) |
Result<Arc<T>, RdiError> |
Returns Err if key not found; use .unwrap() for required keys |
.try_get_keyed::<T>(key) |
Option<Arc<T>> |
Returns None if key not found |
.get_all::<T>() |
Vec<Arc<T>> |
All non-keyed registrations, in registration order (keyed excluded) |
.get_all_keyed::<T>() |
Vec<Arc<T>> |
All keyed registrations of T, in registration order (0.8.0+) |
.get_named::<T>(name) |
Option<Arc<T>> |
Named resolution (cross-DLL), returns None if missing |
.get_named_any::<T>(name) |
Option<Arc<T>> |
Named, returns None if missing |
.get_owned::<T>() |
Result<T, RdiError> |
Owned instance; Singleton returns Err; Scoped is allowed from the root (caller-owned, never cached — its Scoped dependencies still obey scope validation) |
.try_get_owned::<T>() |
Option<T> |
None if unregistered or Singleton |
.get_keyed_owned::<T>(key) |
Result<T, RdiError> |
Owned keyed instance; Singleton returns Err |
.is_registered::<T>() |
bool |
Whether T has a non-keyed registration (does not instantiate) |
.is_registered_keyed::<T>(key) |
bool |
Whether T is registered under key |
.scope() |
Scope |
New scope for scoped services |
.dispose() |
() |
Idempotent; releases instances in reverse creation order, later resolution returns Err(ProviderDisposed) (0.8.0+) |
0.8.x semantics: single-service resolution is MEDI-style "last registration wins";
get_allis unkeyed-only (useget_all_keyedfor keyed); and scope validation is strict by default — resolving a Scoped service from the root provider returnsErr(RdiError::ScopedFromRootProvider)(the exception isget_owned/try_get_owned: the owned instance is caller-owned and not cached, while its Scoped dependencies still obey validation). Build withbuild_with(ServiceProviderOptions::lenient())to keep the 0.7.x root-scope behavior. See the MEDI differences page.
T can be a concrete type or dyn Trait:
let svc: = provider.get.unwrap;
let plugin: = provider.get.unwrap;
let all: = provider.get_all;
Container injection (ServiceProvider / ScopeFactory)
The container is itself resolvable as a built-in service (no registration
required) through every resolver path — provider.get(), #[inject] fields and
build-time validation alike:
| Field | Notes |
|---|---|
#[inject(provider)] provider: Arc<ServiceProvider> |
On-demand resolution; also available in eagerly built singletons. Warning: a singleton holding this creates a strong container → singleton → container cycle; prefer ScopeFactory |
#[inject] provider: Arc<ServiceProvider> |
Same built-in, plain form (MEDI IServiceProvider parity); same strong-reference caveat |
#[inject] scope_factory: Arc<dyn ScopeFactory> |
Recommended for singleton workers: one scope per task. Weak-backed singleton, so holding it does not create an Arc cycle |
Explicit singleton::<dyn ScopeFactory>(...) registrations still take precedence
over the built-in.
Resolving services through an injected provider inside a singleton constructor is rejected at build time with a typed
FactoryPanic: it would bypass cycle/captive validation and make the result order-dependent. Store the handle and resolve lazily in methods.
Owned service resolution (&mut self without interior mutability)
get::<T>() returns Arc<T>, giving you only &T — you cannot call
&mut self methods. For services that need mutable access (e.g. a
DbContext with save_changes(&mut self)), use get_owned::<T>() to
obtain an owned T instead of resorting to RwLock + unsafe:
let provider = new
.transient
.build
.unwrap;
let mut ctx: DbContext = provider.get_owned.unwrap;
ctx.add; // ✓ &mut self — no RwLock, no unsafe
assert_eq!;
| Lifetime | get_owned behavior |
|---|---|
| Transient | New instance each call ✓ |
| Scoped | Bypasses cache, new instance each call ✓ (callable from the root too: the owned instance is not subject to root scope validation, but its Scoped dependencies are) |
| Singleton | Err(OwnedNotSupported) (shared instance cannot be owned); use try_get_owned for None |
With #[derive(Inject)]: mark bare T fields with #[inject(owned)]
and Arc<T> fields with #[inject]. Option<T> → try_get_owned,
Option<Arc<T>> → try_get:
// ...
let mut h: Handler = provider.get_owned.unwrap;
h.ctx.add; // ✓ owned field is mutable
Limitations:
- Singleton cannot be owned —
get_ownedreturnsErr,try_get_ownedreturnsNone. - Scoped
get_ownedbypasses the scope cache (each call is fresh). This is intentional: owned&mut selfis incompatible with scope-shared semantics. Owned resolution is the exception to strict scope validation:get_owned/try_get_ownedmay resolve a Scoped service from the root (the instance is caller-owned, never cached or tracked) — but Scoped dependencies resolved inside its constructor still returnErr(ScopedFromRootProvider)at the root. - Trait bound remains
T: Send + Sync + 'static(reuses the existing factory, no separate owned-factory path).
Async support (build_async)
For services that need async initialization (DB connections, network I/O, config
loading), use build_async() and the async_* registration methods.
Registration
| Method | Lifetime | Use when |
|---|---|---|
.async_singleton(f) |
Singleton | Async init, shared globally (DB pool, Redis) |
.async_scoped(f) |
Scoped | Async init, per scope (request-scoped resources) |
.async_transient(f) |
Transient | Async init, fresh each time |
.async_keyed_singleton(k, f) |
Singleton keyed | Async init, named instance |
.async_keyed_scoped(k, f) |
Scoped keyed | Async init, per-scope named instance |
.async_keyed_transient(k, f) |
Transient keyed | Async init, fresh named instance |
The async factory receives Arc<ServiceProvider> and returns a pinned future:
let provider = new
.async_singleton
.
.build_async
.await
.unwrap;
Resolution
Use get_async() / get_keyed_async() when resolving async-registered services:
let pool: = provider.get_async.await.unwrap;
let gateway: = provider.get_keyed_async.await.unwrap;
For sync-registered services you can use either get() or get_async(): get_async()
falls back to the synchronous resolution path for services registered with the sync
scoped / transient builders (0.8.0+). Async registration (async_*) is only needed
for services that require asynchronous initialization.
Async singleton initialization is dependency-first and registration-order independent
(0.8.1+): a factory may await any async singleton, and build_async() recursively
initializes dependencies first (deterministic dependency-first topological order, eager and
fail-fast). Dependency cycles return RdiError::CircularDependency instead of dead-locking.
Mixing sync and async
You can mix sync and async registrations in the same collection. Sync singletons
are validated and initialized alongside async ones during build_async():
let provider = new
.async_singleton
.singleton // sync OK
// `UserRepository` is `#[derive(Inject)]`; its constructor resolves DbPool.
.scoped // sync OK
.build_async
.await
.unwrap;
Key rule: Once you use any async_* registration, call build_async()
instead of build(). Mixing is supported — sync singletons are validated and
initialized alongside async ones.
Per-request scope with async
let provider = new
.async_singleton
.scoped
.build_async.await.unwrap;
// Each request gets its own scope
Flexible Application Patterns
🔹 Three-layered architecture (Controller → Service → Repository)
// `UserRepo` / `UserService` / `UserController` are `#[derive(Inject)]`; the
// generated constructors wire the dependencies for each layer.
let provider = new
.singleton
.transient
.transient
.transient
.build
.unwrap;
🔹 Strategy pattern with keyed services
let provider = new
.
.
.
.build
.unwrap;
🔹 Scoped per-request (web server)
🔹 Plugin isolation with ServiceProviderWrapper
let host_provider = new
.singleton
.build.unwrap;
let plugin_provider = new
.singleton
.build.unwrap;
let wrapper = new;
// PluginService resolved from plugin container
// HostService falls back to host container
🔹 Cross-DLL plugin with named services
// Host process
let provider = new
.singleton
.build.unwrap;
provider.register_named;
// Plugin loaded from cdylib (separate compilation unit)
let bus = host.;
🔹 External system integration via IProvider
let provider: = provider as ;
// Pass to third-party code or FFI boundary
Architecture
┌──────────────────────────────────────────────┐
│ ServiceCollection │
│ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │
│ │singleton │ │ scoped │ │ transient │ │
│ │.keyed_singleton│ │.keyed_*()│ │ .instance() │ │
│ └──────────┘ └──────────┘ └──────────────┘ │
│ │ .build() │
│ ▼ │
│ ServiceProvider │
│ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │
│ │ .get() │ │.get_keyed│ │ .scope() │ │
│ │ .get_all │ │.get_named│ │ │ │
│ └──────────┘ └──────────┘ └──────┬───────┘ │
│ │ │
└────────────────────────────────────┼─────────┘
│
▼
┌──────────┐
│ Scope │
│ .get() │
│ scoped │
│ cached │
└──────────┘
Relationship with rust-dix-macros
rust-dix works with or without rust-dix-macros. The macros
provide three compile-time conveniences:
#[rust_dix::inject(...)](recommended) — Attribute macro that combines constructor generation + auto-registration. One annotation on a struct (or trait impl) is all you need —ServiceCollection::from_injected()collects everything.#[derive(Inject)]— Generate the factory function automatically from struct fields. Used internally by#[rust_dix::inject].#[rust_dix::module]— Collectrust_dix::register!()declarations at compile time and generate a complete provider builder. Useful for external types, conditional compilation, and centralized management.
See macros/README.md for details.
License
MIT.