Skip to main content

Crate rust_dix

Crate rust_dix 

Source
Expand description

rust-dix — Rust Dependency Injection Framework

A dependency injection container for Rust inspired by Microsoft.Extensions.DependencyInjection (MEDI).

§Quick start

use rust_dix::*;
use std::sync::Arc;

// Trait-based interface (recommended)
trait ILogger: Send + Sync {
    fn log(&self, msg: &str);
}

struct ConsoleLogger;
impl ILogger for ConsoleLogger {
    fn log(&self, msg: &str) { println!("[LOG] {}", msg); }
}

// Register as trait interface
let provider = ServiceCollection::new()
    .singleton::<dyn ILogger>(|_| Arc::new(ConsoleLogger))
    .build()
    .unwrap();

// Resolve as trait
let logger: Arc<dyn ILogger> = provider.get().unwrap();
logger.log("Hello");

§Keyed services: typed keys and AnyKey fallbacks

Keys can be any Any + Send + Sync + Eq + Hash + Debug value (ServiceKey::new); string keys ("fast") remain supported and are stored as String keys. Key equality includes the value type, so ServiceKey::new(1u32) is not equal to "1"; use ServiceKey::from("name") / "name".into() for string-keyed registrations.

A registration with KeyedService::any_key (or ServiceKey::any_key) acts as a fallback for keys that have no exact registration. Singleton/scoped fallback instances are cached per (registration, requested key); get_all_keyed never enumerates them.

use rust_dix::*;
use std::sync::Arc;

#[derive(Debug, PartialEq, Eq, Hash)]
enum Kind { Fast, Safe }

trait Strategy: Send + Sync { fn name(&self) -> &'static str; }
struct Fast;
impl Strategy for Fast { fn name(&self) -> &'static str { "fast" } }
struct Fallback;
impl Strategy for Fallback { fn name(&self) -> &'static str { "fallback" } }

let provider = ServiceCollection::new()
    .keyed_singleton::<dyn Strategy>(ServiceKey::new(Kind::Fast), |_| Arc::new(Fast))
    .keyed_any_singleton::<dyn Strategy>(|_, _| Arc::new(Fallback))
    .build()
    .unwrap();

let fast = provider.get_keyed::<dyn Strategy>(ServiceKey::new(Kind::Fast)).unwrap();
assert_eq!(fast.name(), "fast");
// No exact registration for "unknown": the AnyKey fallback is used.
let fallback = provider.get_keyed::<dyn Strategy>("unknown").unwrap();
assert_eq!(fallback.name(), "fallback");

§Build-time validation

rust-dix validates dependencies during build() to catch errors early:

// Circular dependency detection — factories resolve deps via `get_any`
// (the `get::<T>()` method requires `Self: Sized` and cannot be called
// on `&dyn IServiceResolver`).
let result = ServiceCollection::new()
    .singleton(|r| {
        let _b = r.get_any(std::any::type_name::<B>());  // A depends on B
        Arc::new(A)
    })
    .singleton(|r| {
        let _a = r.get_any(std::any::type_name::<A>());  // B depends on A → CIRCULAR!
        Arc::new(B)
    })
    .build();

assert!(result.is_err());
if let Err(RdiError::CircularDependency(cycle)) = result {
    println!("Circular dependency detected: {}", cycle);
}

§Async services

For services that require async initialization (e.g., database connections, remote config loading), use build_async() and the async_* registration methods. Async singletons are initialized dependency-first (registration order does not matter), so a factory may await another async singleton registered later. build_async() returns Arc<ServiceProvider>.

Async resolution is chain-based: each get_async() future captures the current resolution chain when constructed, so self/cross dependency cycles are reported as RdiError::CircularDependency instead of dead-locking. To detect cycles across detached tasks, propagate AsyncFactoryContext (see ServiceProvider::async_context) into the spawned task; without it, a fully detached task cannot inherit the chain.

use rust_dix::*;
use std::sync::Arc;

struct DbPool { url: String }

async fn connect() -> DbPool { DbPool { url: "sqlite::memory:".into() } }

let provider = futures::executor::block_on(
    ServiceCollection::new()
        .async_singleton(|_| Box::pin(async { Arc::new(connect().await) }))
        .build_async(),
)
.unwrap();

let pool: Arc<DbPool> = futures::executor::block_on(provider.get_async()).unwrap();
assert_eq!(pool.url, "sqlite::memory:");

§Features

  • Three lifetimes: Singleton, Scoped, Transient
  • Keyed services: multiple implementations by key
    • string keys ("fast") and typed keys (ServiceKey::new, e.g. enums)
    • KeyedService::any_key / ServiceKey::any_key fallback registrations (keyed_any_singleton / keyed_any_scoped / keyed_any_transient)
    • per-(registration, requested key) caching for AnyKey fallbacks; exact registrations take precedence; get_all_keyed excludes fallbacks
  • Constructor injection: #[derive(Inject)] on structs
  • Attribute-based auto-registration: #[rust_dix::inject] on structs or trait impls (#[inject(key = "...")], #[inject(owned)], #[inject(owned, key = "...")] supported)
  • Compile-time module scanning: #[rust_dix::module] collects register!() declarations
  • Cross-DLL support: named service registry for cdylib plugins
  • Layered containers: child-first resolution via ServiceProviderWrapper
  • Build-time validation: sync/async dependency checks (cycles, captive dependencies, sync singleton depending on async services) during build
  • Async resolution with cycle detection: ServiceProvider::get_async / ServiceProvider::get_keyed_async; propagate AsyncFactoryContext into detached tasks so cross-task cycles also return RdiError::CircularDependency
  • Scope validation: scoped services cannot be resolved from the root provider by default (MEDI ValidateScopes parity; opt out with ServiceProviderOptions::lenient); get_owned / get_keyed_owned are the documented exception
  • Deterministic lifecycle: eager singletons are created in registration order (async singletons dependency-first); ServiceProvider::dispose / Scope::dispose release cached instances in reverse creation order
  • MEDI registration semantics: the last registration wins for a single service; get_all returns unkeyed implementations in registration order (get_all_keyed for keyed ones)
  • Typed diagnostics: RdiError exposes dedicated variants such as SyncSingletonDependsOnAsyncService and AsyncServiceRequiresGetAsync, plus RdiError::is_circular (heuristic for factory-panic-wrapped cycles)
  • Engineering baseline: MSRV 1.95, #![forbid(unsafe_code)], fmt/clippy/doc -D warnings + tests + RustSec cargo audit in CI

§Diagnostics

Prefer matching the typed RdiError variants over strings:

use rust_dix::*;
use std::sync::Arc;

struct A;
struct B;
let result = ServiceCollection::new()
    .singleton(|r| {
        let _ = r.get_any(std::any::type_name::<B>());
        Arc::new(A)
    })
    .singleton(|r| {
        let _ = r.get_any(std::any::type_name::<A>());
        Arc::new(B)
    })
    .build();

let err = result.err().expect("cycle must be rejected");
assert!(err.is_circular());
if let RdiError::CircularDependency(cycle) = err {
    assert!(cycle.contains("->"));
}

Sync resolution of an async service returns RdiError::AsyncServiceRequiresGetAsync (use get_async / get_keyed_async); registering async_* services and calling ServiceCollection::build returns RdiError::AsyncServiceRequiresBuildAsync.

§Crate layout

ModuleDescription
ServiceCollectionRegister services with a builder API
ServiceProviderThe root DI container
Scope / ServiceScopeScoped container (one per scope)
IServiceResolverResolution trait (implemented by ServiceProvider & Scope)
ServiceProviderWrapperChild-first layered container
ServiceLifetimeEnum: Singleton, Scoped, Transient
ServiceKey / KeyedServiceTyped keys and the AnyKey sentinel
AsyncFactoryContextPropagate the async resolution chain into tasks
ServiceProviderOptionsScope-validation options (lenient())
IProviderUnified trait for resolution + named registry
RdiErrorTyped errors (cycles, disposed, async diagnostics, …)

§MSRV and security

§Proc-macros

  • #[derive(Inject)] — auto-generates constructor injection code
  • #[rust_dix::inject] — auto-registration on structs or trait impls
  • #[rust_dix::module] — compile-time module scanning
  • rust_dix::register!(...) — declare services inside #[rust_dix::module]

Re-exports§

pub use collection::ServiceCollection;
pub use provider::ServiceProvider;
pub use key::KeyedService;
pub use key::ServiceKey;
pub use provider::AsyncFactoryContext;
pub use provider::ServiceProviderOptions;
pub use entry::ServiceDescriptor;
pub use entry::ServiceLifetime;
pub use entry::IServiceResolver;
pub use scope::Scope;
pub use scope::Scope as ServiceScope;
pub use scope::ScopeFactory;
pub use entry::ServiceEntry;
pub use entry::ServiceFactory;
pub use wrapper::ServiceProviderWrapper;
pub use bridge::IProvider;
pub use error::RdiError;
pub use registration::ServiceRegistration;

Modules§

bridge
Unified provider trait for service resolution and named registry access.
collection
entry
error
key
Service keys for keyed registrations.
provider
registration
scope
wrapper

Macros§

register

Attribute Macros§

inject
module

Derive Macros§

Inject