rust-dix is the core crate of the rust-dix framework — a Rust dependency injection
container inspired by Microsoft.Extensions.DependencyInjection (MEDI).
[]
= "0.6"
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
let provider = new
.singleton
.singleton
.transient
.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. IServiceLocator: pass a unified DI interface 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 |
.singleton_value(v) |
Singleton | Register a plain value (wraps in Arc) |
.add(lt, f) |
any | Specify lifetime explicitly |
The factory closure f receives &dyn IServiceResolver so you can resolve
dependencies:
new
.singleton
.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 __rdi_construct_T |
#[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; use .unwrap() for required services |
.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 registered instances (keyed + unkeyed) |
.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; use .unwrap() for transient/scoped |
.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 |
.scope() |
Scope |
New scope for scoped services |
T can be a concrete type or dyn Trait:
let svc: = provider.get.unwrap;
let plugin: = provider.get.unwrap;
let all: = provider.get_all;
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 ✓ |
| Singleton | Panics (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_ownedpanics,try_get_ownedreturnsNone. - Scoped
get_ownedbypasses the scope cache (each call is fresh). This is intentional: owned&mut selfis incompatible with scope-shared semantics. - 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 services, you can still use get(). The async methods are only needed
for services registered with async_*.
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
.scoped
.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)
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│ │ .create_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.