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.0 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.
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.