Expand description
§Reinhardt Dependency Injection
FastAPI-inspired dependency injection system for Reinhardt.
§Features
- Type-safe: Full compile-time type checking
- Async-first: Built for async/await
- Scoped: Request-scoped and singleton dependencies
- Composable: Dependencies can depend on other dependencies
- Extensible wrappers: Custom
#[inject]wrappers can implementInjectableTypeinstead of relying on framework-defined type names - Cache: Automatic caching within request scope
- Circular Dependency Detection: Automatic runtime detection with optimized performance
§Custom #[inject] Wrappers
#[inject] resolves wrapper parameters through InjectableType. This
lets framework and application wrappers choose a concrete registry key while
exposing a domain-specific parameter type. The keyed provider API uses
FactoryOutput as the registered value and Depends as the consumer
wrapper.
§Cargo features
testing— exposes [DependencyRegistry::register_override] and [testing::OverrideGuard] for use byreinhardt-testkitand other test harnesses. When operating on a per-context registry (viaInjectionContextBuilder::with_registry),#[serial(di_registry)]is not required. Direct mutations of the global registry still require#[serial(di_registry)].
§Development Tools (dev-tools feature)
When the dev-tools feature is enabled, additional debugging and profiling tools are available:
- Visualization: Generate dependency graphs in DOT format for Graphviz
- Profiling: Track dependency resolution performance and identify bottlenecks
- Advanced Caching: LRU and TTL-based caching strategies
§Generator Support (generator feature) ✅
Generator-based dependency resolution for lazy, streaming dependency injection.
Note: Uses genawaiter crate as a workaround for unstable native async yield.
Will be migrated to native syntax when Rust stabilizes async generators.
// let gen = DependencyGenerator::new(|co| async move {
// let db = resolve_database().await;
// co.yield_(db).await;
//
// let cache = resolve_cache().await;
// co.yield_(cache).await;
// });§Example
// Define a dependency
// struct Database {
// pool: DbPool,
// }
//
// struct DatabaseKey;
// impl InjectableKey for DatabaseKey {}
//
// #[async_trait]
// impl Injectable for Database {
// async fn inject(ctx: &InjectionContext) -> Result<Self> {
// Ok(Database {
// pool: get_pool().await?,
// })
// }
// }
//
// Use in endpoint
// #[endpoint(GET "/users")]
// async fn list_users(
// db: Depends<DatabaseKey, Database>,
// ) -> Result<Vec<User>> {
// db.query("SELECT * FROM users").await
// }§InjectionContext Construction
InjectionContext is constructed using the builder pattern with a required singleton scope:
use reinhardt_di::{InjectionContext, SingletonScope};
use std::sync::Arc;
// Create singleton scope
let singleton = Arc::new(SingletonScope::new());
// Build injection context with singleton scope
let ctx = InjectionContext::builder(singleton).build();Optional request and param context can be added:
use reinhardt_di::{InjectionContext, SingletonScope};
use reinhardt_http::Request;
use std::sync::Arc;
let singleton = Arc::new(SingletonScope::new());
// Create a dummy request for demonstration
let request = Request::builder()
.method(hyper::Method::GET)
.uri("/")
.version(hyper::Version::HTTP_11)
.headers(hyper::HeaderMap::new())
.body(bytes::Bytes::new())
.build()
.unwrap();
let ctx = InjectionContext::builder(singleton)
.with_request(request)
.build();§Resolve Context
The get_di_context function provides access to the active
InjectionContext within #[injectable] function bodies, without
requiring #[inject].
This enables factories to access the DI context for purposes like passing it to downstream consumers:
use reinhardt_di::{ContextLevel, Depends, FactoryOutput, InjectableKey, get_di_context};
struct AppConfigKey;
impl InjectableKey for AppConfigKey {}
struct RouterKey;
impl InjectableKey for RouterKey {}
#[injectable(scope = "transient")]
async fn make_router(
#[inject] config: Depends<AppConfigKey, AppConfig>,
) -> FactoryOutput<RouterKey, Router> {
let di_ctx = get_di_context(ContextLevel::Current);
FactoryOutput::new(Router::new().with_di_context(di_ctx))
}ContextLevel::Root returns the application-level context, while
ContextLevel::Current returns the currently active context
(which may be a request-scoped fork).
Use try_get_di_context for a non-panicking variant that returns
None when called outside of a DI resolution context.
§Circular Dependency Detection
The DI system automatically detects circular dependencies at runtime using an optimized thread-local mechanism:
#[derive(Clone)]
struct ServiceA {
b: Arc<ServiceB>,
}
#[derive(Clone)]
struct ServiceB {
a: Arc<ServiceA>, // Circular dependency!
}
#[async_trait]
impl Injectable for ServiceA {
async fn inject(ctx: &InjectionContext) -> DiResult<Self> {
let b = ctx.resolve::<ServiceB>().await?;
Ok(ServiceA { b })
}
}
#[async_trait]
impl Injectable for ServiceB {
async fn inject(ctx: &InjectionContext) -> DiResult<Self> {
let a = ctx.resolve::<ServiceA>().await?;
Ok(ServiceB { a })
}
}
let singleton = Arc::new(SingletonScope::new());
let ctx = InjectionContext::builder(singleton).build();
// This will return Err with DiError::CircularDependency
let result = ctx.resolve::<ServiceA>().await;
assert!(result.is_err());§Performance Characteristics
- Cache Hit: < 5% overhead (cycle detection completely skipped)
- Cache Miss: 10-20% overhead (O(1) detection using HashSet)
- Deep Chains: Sampling reduces linear cost (checks every 10th at depth 50+)
- Thread Safety: Thread-local storage eliminates lock contention
§Development Tools Example
// fn visualize_dependencies() {
// let mut graph = DependencyGraph::new();
// graph.add_node("Database", "singleton");
// graph.add_node("UserService", "request");
// graph.add_dependency("UserService", "Database");
//
// println!("{}", graph.to_dot());
// }
//
// fn profile_resolution() {
// let mut profiler = DependencyProfiler::new();
// profiler.start_resolve("Database");
// // ... perform resolution ...
// profiler.end_resolve("Database");
//
// let report = profiler.generate_report();
// println!("{}", report.to_string());
// }§Auth Extractor DI Context Requirements
The reinhardt-auth crate provides injectable auth extractors that depend on
specific DI context configuration. Understanding these requirements is essential
for proper authentication integration.
§CurrentUser<U>
Loads the full user model from the database. Requires:
DatabaseConnectionregistered as a singleton inInjectionContextAuthStatepresent in request extensions (set by authentication middleware)- Feature
paramsenabled onreinhardt-auth
Returns an injection error if any requirement is missing (fail-fast behavior).
use reinhardt_auth::CurrentUser;
use reinhardt_auth::DefaultUser;
#[get("/profile/")]
pub async fn profile(
#[inject] CurrentUser(user): CurrentUser<DefaultUser>,
) -> ViewResult<Response> {
let username = user.get_username();
// ...
}§AuthInfo (lightweight alternative)
Extracts authentication metadata without a database query. Requires:
AuthStatepresent in request extensions (set by authentication middleware)- No
DatabaseConnectionneeded
§Startup Validation
Call reinhardt_auth::validate_auth_extractors() during application startup
to verify that required dependencies (e.g., DatabaseConnection) are registered
before the first request arrives.
Re-exports§
pub use context::InjectionContext;pub use context::InjectionContextBuilder;pub use context::RequestContext;pub use cycle_detection::CycleError;pub use cycle_detection::ResolutionGuard;pub use cycle_detection::begin_resolution;pub use cycle_detection::begin_scoped_resolution;pub use cycle_detection::current_dependent_scope;pub use cycle_detection::register_type_name;pub use cycle_detection::with_cycle_detection_scope;pub use factory_output::FactoryOutput;pub use function_handle::FunctionHandle;pub use injectable_key::InjectableKey;pub use override_registry::OverrideRegistry;pub use depends::Depends;pub use depends::DependsBuilder;pub use injectable::Injectable;pub use injectable_type::InjectableType;pub use injected::DependencyScope as InjectedScope;pub use injected::InjectionMetadata;pub use provider::Provider;pub use provider::ProviderFn;pub use registration::DiRegistrationList;pub use registry::DependencyRegistration;pub use registry::DependencyRegistry;pub use registry::DependencyScope;pub use registry::FactoryTrait;pub use registry::InjectableFactory;pub use registry::InjectableRegistration;pub use registry::global_registry;pub use resolve_context::ContextLevel;pub use resolve_context::get_di_context;pub use resolve_context::try_get_di_context;pub use scope::RequestScope;pub use scope::Scope;pub use scope::SingletonScope;pub use validation::RegistryValidator;pub use validation::ValidationError;pub use validation::ValidationErrorKind;pub use async_trait;pub use inventory;
Modules§
- context
- Injection context for dependency resolution
- cycle_
detection - Task-local circular dependency detection mechanism
- depends
- Depends wrapper for keyed dependency injection.
- factory_
output - Output wrapper for keyed injectable provider functions.
- function_
handle - Function handle for fluent override API
- graph
- Dependency graph analysis and visualization
- injectable
- Injectable trait for dependencies
- injectable_
key - Key marker trait for keyed dependency providers.
- injectable_
type - Trait-based wrapper resolution for
#[inject]. - injected
- Injection metadata types.
- override_
registry - Override registry for dependency injection
- provider
- Dependency providers
- registration
- Deferred DI registration list for propagating singleton registrations across module boundaries.
- registry
- Global dependency registry for FastAPI-style dependency injection
- resolve_
context - Task-local resolve context for accessing
InjectionContextwithin DI factories. - scope
- Dependency scopes
- validation
- Startup validation for the DI registry
Macros§
- submit_
registration - Helper macro for submitting registrations to inventory
Enums§
- DiError
- Errors that can occur during dependency injection resolution.
Type Aliases§
- DiResult
- A specialized
Resulttype for dependency injection operations.