Skip to main content

reinhardt_di/
registry.rs

1//! Global dependency registry for FastAPI-style dependency injection
2//!
3//! This module provides a global registry that stores factory functions for creating
4//! dependencies. It uses the `inventory` crate to collect registrations at compile time
5//! and build a runtime registry that can be queried by type.
6
7use crate::{DiResult, Injectable, InjectionContext};
8use async_trait::async_trait;
9use dashmap::DashMap;
10use std::any::{Any, TypeId};
11use std::future::Future;
12use std::marker::PhantomData;
13use std::sync::{Arc, OnceLock};
14
15/// Scope for dependency injection
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
17pub enum DependencyScope {
18	/// Single instance shared across the entire application
19	#[default]
20	Singleton,
21	/// New instance per request, cached within the request
22	Request,
23	/// New instance every time, never cached
24	Transient,
25}
26
27impl DependencyScope {
28	/// Returns `true` if a type at scope `self` (the dependency being resolved)
29	/// lives at least as long as a type at scope `dependent` (the type
30	/// requesting resolution).
31	///
32	/// The only prohibited combination is a `Singleton`-scoped dependent
33	/// resolving a `Request`-scoped dependency, because the singleton would
34	/// capture a stale, request-bound value for the entire process lifetime.
35	pub fn outlives(&self, dependent: DependencyScope) -> bool {
36		!matches!(
37			(dependent, *self),
38			(DependencyScope::Singleton, DependencyScope::Request)
39		)
40	}
41}
42
43/// Factory trait for creating dependencies
44///
45/// Factories are async functions that can resolve dependencies from an InjectionContext
46/// and return a type-erased `Arc<dyn Any>`.
47#[async_trait]
48pub trait FactoryTrait: Send + Sync {
49	/// Create an instance of the dependency
50	async fn create(&self, ctx: &InjectionContext) -> DiResult<Arc<dyn Any + Send + Sync>>;
51}
52
53/// Wrapper for async factory functions
54pub struct AsyncFactory<F, Fut, T>
55where
56	F: Fn(Arc<InjectionContext>) -> Fut + Send + Sync,
57	Fut: Future<Output = DiResult<T>> + Send,
58	T: Any + Send + Sync + 'static,
59{
60	factory: F,
61	_phantom: std::marker::PhantomData<fn() -> (Fut, T)>,
62}
63
64impl<F, Fut, T> AsyncFactory<F, Fut, T>
65where
66	F: Fn(Arc<InjectionContext>) -> Fut + Send + Sync,
67	Fut: Future<Output = DiResult<T>> + Send,
68	T: Any + Send + Sync + 'static,
69{
70	/// Creates a new async factory from the given closure.
71	pub fn new(factory: F) -> Self {
72		Self {
73			factory,
74			_phantom: std::marker::PhantomData,
75		}
76	}
77}
78
79#[async_trait]
80impl<F, Fut, T> FactoryTrait for AsyncFactory<F, Fut, T>
81where
82	F: Fn(Arc<InjectionContext>) -> Fut + Send + Sync,
83	Fut: Future<Output = DiResult<T>> + Send + 'static,
84	T: Any + Send + Sync + 'static,
85{
86	async fn create(&self, ctx: &InjectionContext) -> DiResult<Arc<dyn Any + Send + Sync>> {
87		let ctx_arc = Arc::new(ctx.clone());
88		let instance = (self.factory)(ctx_arc).await?;
89		Ok(Arc::new(instance))
90	}
91}
92
93/// Factory that creates instances via the `Injectable` trait.
94///
95/// Bypasses `AsyncFactory`'s `Fut: Sync` bound by implementing `FactoryTrait`
96/// directly. This is necessary because `Injectable::inject` uses `async_trait`,
97/// which returns `Pin<Box<dyn Future + Send>>` (not `Sync`).
98pub struct InjectableFactory<T>(PhantomData<T>);
99
100impl<T> Default for InjectableFactory<T> {
101	fn default() -> Self {
102		Self(PhantomData)
103	}
104}
105
106impl<T> InjectableFactory<T> {
107	/// Create a new `InjectableFactory`.
108	pub fn new() -> Self {
109		Self::default()
110	}
111}
112
113#[async_trait]
114impl<T: Injectable + Any + Send + Sync + 'static> FactoryTrait for InjectableFactory<T> {
115	async fn create(&self, ctx: &InjectionContext) -> DiResult<Arc<dyn Any + Send + Sync>> {
116		// Set task-local resolve context for get_di_context() access.
117		// Since we only have &InjectionContext, clone into Arc (same pattern as AsyncFactory).
118		let ctx_arc = Arc::new(ctx.clone());
119		let resolve_ctx = crate::resolve_context::ResolveContext {
120			root: crate::resolve_context::RESOLVE_CTX
121				.try_with(|outer| Arc::clone(&outer.root))
122				.unwrap_or_else(|_| Arc::clone(&ctx_arc)),
123			current: Arc::clone(&ctx_arc),
124		};
125
126		let value = crate::resolve_context::RESOLVE_CTX
127			.scope(resolve_ctx, T::inject(ctx))
128			.await?;
129		Ok(Arc::new(value))
130	}
131}
132
133/// Type-erased factory function
134type BoxedFactory = Box<dyn FactoryTrait>;
135
136/// Global dependency registry
137///
138/// Stores factory functions for each type, along with their scope information.
139/// Uses DashMap for thread-safe concurrent access without blocking.
140pub struct DependencyRegistry {
141	factories: DashMap<TypeId, BoxedFactory>,
142	scopes: DashMap<TypeId, DependencyScope>,
143	/// Maps type ID to its direct dependencies
144	dependencies: DashMap<TypeId, Vec<TypeId>>,
145	/// Maps type ID to its type name for debugging
146	type_names: DashMap<TypeId, &'static str>,
147	/// Maps type ID to its fully-qualified type name from `std::any::type_name`.
148	/// Used for framework type detection (pseudo orphan rule).
149	qualified_type_names: DashMap<TypeId, &'static str>,
150}
151
152impl DependencyRegistry {
153	/// Create a new empty registry
154	pub fn new() -> Self {
155		Self {
156			factories: DashMap::new(),
157			scopes: DashMap::new(),
158			dependencies: DashMap::new(),
159			type_names: DashMap::new(),
160			qualified_type_names: DashMap::new(),
161		}
162	}
163
164	/// Register a factory for a type.
165	///
166	/// # Panics
167	///
168	/// Panics if a factory for the same `TypeId` is already registered.
169	/// This prevents silent overwrites that lead to non-deterministic behavior
170	/// when multiple `#[injectable_factory]` or `#[injectable]` macros produce
171	/// the same return type. See [#3457].
172	///
173	/// To check before registering (e.g. in tests), use
174	/// [`is_registered`](Self::is_registered).
175	///
176	/// [#3457]: https://github.com/kent8192/reinhardt-web/issues/3457
177	pub fn register<T: Any + Send + Sync + 'static>(
178		&self,
179		scope: DependencyScope,
180		factory: impl FactoryTrait + 'static,
181	) {
182		let type_id = TypeId::of::<T>();
183		let type_name = std::any::type_name::<T>();
184		// Check for duplicates before inserting so that no state is mutated on the
185		// error path. This avoids leaving the registry inconsistent if the panic is
186		// caught (e.g. factories pointing to the new registration while scopes still
187		// reflects the old one).
188		if self.factories.contains_key(&type_id) {
189			let short = type_name.rsplit("::").next().unwrap_or(type_name);
190			panic!(
191				"Duplicate DependencyRegistry registration for type `{type_name}`.\n\
192\n\
193Hint: reinhardt DI uses TypeId as the sole registry key. Two factories\n\
194returning the same type will conflict regardless of function name or scope.\n\
195Use a distinct newtype (e.g., `struct Primary{short}({short})`) for each."
196			);
197		}
198		self.factories.insert(type_id, Box::new(factory));
199		self.scopes.insert(type_id, scope);
200	}
201
202	/// Register a simple async factory function
203	pub fn register_async<T, F, Fut>(&self, scope: DependencyScope, factory: F)
204	where
205		T: Any + Send + Sync + 'static,
206		F: Fn(Arc<InjectionContext>) -> Fut + Send + Sync + 'static,
207		Fut: Future<Output = DiResult<T>> + Send + 'static,
208	{
209		self.register::<T>(scope, AsyncFactory::new(factory));
210	}
211
212	/// Get the scope for a type
213	pub fn get_scope<T: Any + 'static>(&self) -> Option<DependencyScope> {
214		let type_id = TypeId::of::<T>();
215		self.scopes.get(&type_id).map(|entry| *entry.value())
216	}
217
218	/// Check if a type is registered
219	pub fn is_registered<T: Any + 'static>(&self) -> bool {
220		let type_id = TypeId::of::<T>();
221		self.factories.contains_key(&type_id)
222	}
223
224	/// Get the number of registered dependencies
225	pub fn len(&self) -> usize {
226		self.factories.len()
227	}
228
229	/// Check if the registry is empty
230	pub fn is_empty(&self) -> bool {
231		self.factories.is_empty()
232	}
233
234	/// Create an instance using the registered factory
235	pub async fn create<T: Any + Send + Sync + 'static>(
236		&self,
237		ctx: &InjectionContext,
238	) -> DiResult<Arc<T>> {
239		let type_id = TypeId::of::<T>();
240
241		let factory = self.factories.get(&type_id).ok_or_else(|| {
242			crate::DiError::DependencyNotRegistered {
243				type_name: std::any::type_name::<T>().to_string(),
244			}
245		})?;
246
247		let any_arc = factory.create(ctx).await?;
248
249		any_arc
250			.downcast::<T>()
251			.map_err(|_| crate::DiError::Internal {
252				message: format!(
253					"Failed to downcast dependency: expected {}, got different type",
254					std::any::type_name::<T>()
255				),
256			})
257	}
258
259	/// Get the direct dependencies of a type
260	///
261	/// Returns a vector of TypeIds representing the types that the given type directly depends on.
262	pub fn get_dependencies(&self, type_id: TypeId) -> Vec<TypeId> {
263		self.dependencies
264			.get(&type_id)
265			.map(|deps| deps.value().clone())
266			.unwrap_or_default()
267	}
268
269	/// Get all dependencies in the registry
270	///
271	/// Returns a HashMap mapping each type to its direct dependencies.
272	pub fn get_all_dependencies(&self) -> std::collections::HashMap<TypeId, Vec<TypeId>> {
273		self.dependencies
274			.iter()
275			.map(|entry| (*entry.key(), entry.value().clone()))
276			.collect()
277	}
278
279	/// Get all type names in the registry
280	///
281	/// Returns a HashMap mapping TypeIds to their human-readable type names.
282	pub fn get_type_names(&self) -> std::collections::HashMap<TypeId, &'static str> {
283		self.type_names
284			.iter()
285			.map(|entry| (*entry.key(), *entry.value()))
286			.collect()
287	}
288
289	/// Register dependencies for a type
290	///
291	/// This is typically called automatically by the registration system.
292	/// Not intended for direct use; exposed for macro-generated code.
293	#[doc(hidden)]
294	pub fn register_dependencies(&self, type_id: TypeId, deps: impl AsRef<[TypeId]>) {
295		self.dependencies.insert(type_id, deps.as_ref().to_vec());
296	}
297
298	/// Register a type name for debugging
299	///
300	/// This is typically called automatically by the registration system.
301	/// Not intended for direct use; exposed for macro-generated code.
302	#[doc(hidden)]
303	pub fn register_type_name(&self, type_id: TypeId, type_name: &'static str) {
304		self.type_names.insert(type_id, type_name);
305	}
306
307	/// Check if a type is registered by its `TypeId`.
308	pub(crate) fn is_registered_by_id(&self, type_id: TypeId) -> bool {
309		self.factories.contains_key(&type_id)
310	}
311
312	/// Get the scope for a type by its `TypeId`.
313	pub(crate) fn get_scope_by_id(&self, type_id: TypeId) -> Option<DependencyScope> {
314		self.scopes.get(&type_id).map(|entry| *entry.value())
315	}
316
317	/// Get the type name for a `TypeId`.
318	pub(crate) fn get_type_name(&self, type_id: TypeId) -> Option<&'static str> {
319		self.type_names.get(&type_id).map(|entry| *entry.value())
320	}
321
322	/// Register the fully-qualified type name obtained from `std::any::type_name::<T>()`.
323	///
324	/// Used by the pseudo orphan rule to detect framework-managed types.
325	#[doc(hidden)]
326	pub fn register_qualified_type_name(&self, type_id: TypeId, qualified_name: &'static str) {
327		self.qualified_type_names.insert(type_id, qualified_name);
328	}
329
330	/// Get the fully-qualified type name for a given `TypeId`.
331	pub fn get_qualified_type_name(&self, type_id: &TypeId) -> Option<&'static str> {
332		self.qualified_type_names.get(type_id).map(|r| *r.value())
333	}
334
335	/// Iterate over all qualified type name mappings without allocating a new map.
336	pub fn iter_qualified_type_names(&self) -> impl Iterator<Item = (TypeId, &'static str)> + '_ {
337		self.qualified_type_names
338			.iter()
339			.map(|entry| (*entry.key(), *entry.value()))
340	}
341}
342
343#[cfg(feature = "testing")]
344impl DependencyRegistry {
345	/// Registers or overrides a factory for type `T` without panicking on
346	/// duplicate registration.
347	///
348	/// Returns an [`OverrideGuard`](crate::testing::OverrideGuard) that
349	/// restores the previous factory (or removes the entry entirely if there
350	/// was none) when dropped.
351	///
352	/// When called on a **per-context** registry (created via
353	/// `InjectionContextBuilder::with_registry`), each test operates on its
354	/// own isolated registry and `#[serial(di_registry)]` is not required.
355	///
356	/// When called on the **global** registry (`global_registry()`), tests
357	/// **must** run inside the `#[serial(di_registry)]` group because the
358	/// shared registry is mutated non-atomically.
359	///
360	/// # Safety contract
361	///
362	/// This method inserts into two separate `DashMap`s (`factories` and
363	/// `scopes`) non-atomically. When operating on a shared (global)
364	/// registry, another thread can observe a torn state where the new
365	/// factory is paired with the old scope (or vice versa). The
366	/// `#[serial(di_registry)]` requirement eliminates that window for the
367	/// global registry; per-context registries are isolated by design.
368	///
369	/// # Examples
370	///
371	/// ```rust,no_run
372	/// # use std::sync::Arc;
373	/// # use reinhardt_di::{DependencyRegistry, DependencyScope, DiResult, InjectionContext};
374	/// # fn _example(registry: Arc<DependencyRegistry>) {
375	/// let _guard = registry.register_override::<String, _, _>(
376	///     DependencyScope::Singleton,
377	///     |_ctx| async { Ok("mock".to_string()) },
378	/// );
379	/// // ... run test body ...
380	/// // `_guard` dropped here → previous factory restored.
381	/// # }
382	/// ```
383	pub fn register_override<T, F, Fut>(
384		self: &std::sync::Arc<Self>,
385		scope: crate::registry::DependencyScope,
386		factory: F,
387	) -> crate::testing::OverrideGuard
388	where
389		T: std::any::Any + Send + Sync + 'static,
390		F: Fn(std::sync::Arc<crate::InjectionContext>) -> Fut + Send + Sync + 'static,
391		Fut: std::future::Future<Output = crate::DiResult<T>> + Send + 'static,
392	{
393		let type_id = std::any::TypeId::of::<T>();
394		let async_factory = crate::registry::AsyncFactory::new(factory);
395		let boxed: Box<dyn crate::registry::FactoryTrait> = Box::new(async_factory);
396
397		// Capture previous (if any) and install the override.
398		let previous_factory = self.factories.insert(type_id, boxed);
399		let previous_scope = self.scopes.insert(type_id, scope);
400
401		// `factories` and `scopes` are always inserted/removed in lockstep, so
402		// observing one without the other indicates the `#[serial(di_registry)]`
403		// contract was violated by another writer. Assert in debug builds and
404		// fall back to "no previous" in release so the guard at least removes
405		// the entry instead of restoring a partial state.
406		debug_assert!(
407			previous_factory.is_some() == previous_scope.is_some(),
408			"torn override state: factories/scopes diverged for `{}`",
409			std::any::type_name::<T>()
410		);
411		let previous = match (previous_factory, previous_scope) {
412			(Some(f), Some(s)) => Some((f, s)),
413			_ => None,
414		};
415
416		crate::testing::OverrideGuard {
417			type_id,
418			previous,
419			registry: std::sync::Arc::downgrade(self),
420		}
421	}
422
423	/// Restores a previously-installed factory and scope. Used by
424	/// [`OverrideGuard::drop`](crate::testing::OverrideGuard).
425	///
426	/// Like `register_override`, this performs a non-atomic two-`DashMap`
427	/// mutation. For the global registry, callers MUST hold the
428	/// `#[serial(di_registry)]` lock; for per-context registries, isolation
429	/// is guaranteed by design.
430	pub(crate) fn restore_override(
431		&self,
432		type_id: std::any::TypeId,
433		factory: Box<dyn crate::registry::FactoryTrait>,
434		scope: crate::registry::DependencyScope,
435	) {
436		self.factories.insert(type_id, factory);
437		self.scopes.insert(type_id, scope);
438	}
439
440	/// Removes an override entry that had no prior registration. Used by
441	/// [`OverrideGuard::drop`](crate::testing::OverrideGuard). Same
442	/// serialization requirements as `restore_override`.
443	pub(crate) fn remove_override(&self, type_id: std::any::TypeId) {
444		self.factories.remove(&type_id);
445		self.scopes.remove(&type_id);
446	}
447}
448
449impl Default for DependencyRegistry {
450	fn default() -> Self {
451		Self::new()
452	}
453}
454
455/// Global singleton registry instance
456static GLOBAL_REGISTRY: OnceLock<Arc<DependencyRegistry>> = OnceLock::new();
457
458/// Get the global registry instance
459pub fn global_registry() -> &'static Arc<DependencyRegistry> {
460	GLOBAL_REGISTRY.get_or_init(|| {
461		let registry = Arc::new(DependencyRegistry::new());
462		initialize_registry(&registry);
463		registry
464	})
465}
466
467/// Resets the global dependency registry for test isolation.
468///
469/// This replaces the `GLOBAL_REGISTRY` `OnceLock` with a fresh instance so
470/// that the next call to `global_registry()` will re-initialize it.
471///
472/// # Safety
473///
474/// This function replaces a static `OnceLock` value using `std::ptr::write`.
475/// It is only safe to call from a single-threaded test context (e.g., with
476/// `#[serial]`) where no other thread is concurrently reading the registry.
477#[cfg(test)]
478pub fn reset_global_registry() {
479	// SAFETY: We replace the OnceLock in-place with a fresh instance.
480	// This is safe only when called from a single-threaded test context
481	// (enforced by #[serial]) where no concurrent readers exist.
482	unsafe {
483		let ptr = std::ptr::addr_of!(GLOBAL_REGISTRY) as *mut OnceLock<Arc<DependencyRegistry>>;
484		std::ptr::write(ptr, OnceLock::new());
485	}
486}
487
488/// Registration entry for inventory collection
489pub struct DependencyRegistration {
490	/// The `TypeId` of the dependency being registered.
491	pub type_id: TypeId,
492	/// The human-readable name of the type.
493	pub type_name: &'static str,
494	/// The scope (request or singleton) for this dependency.
495	pub scope: DependencyScope,
496	/// Direct dependencies of this type.
497	pub dependencies: &'static [TypeId],
498	/// A function that registers this dependency's factory with the registry.
499	pub register_fn: fn(&DependencyRegistry),
500}
501
502impl DependencyRegistration {
503	/// Create a new registration entry
504	pub const fn new<T: Send + Sync + 'static>(
505		type_name: &'static str,
506		scope: DependencyScope,
507		register_fn: fn(&DependencyRegistry),
508	) -> Self {
509		Self {
510			type_id: TypeId::of::<T>(),
511			type_name,
512			scope,
513			dependencies: &[],
514			register_fn,
515		}
516	}
517
518	/// Create a new registration entry with explicit dependencies
519	pub const fn new_with_deps<T: Send + Sync + 'static>(
520		type_name: &'static str,
521		scope: DependencyScope,
522		dependencies: &'static [TypeId],
523		register_fn: fn(&DependencyRegistry),
524	) -> Self {
525		Self {
526			type_id: TypeId::of::<T>(),
527			type_name,
528			scope,
529			dependencies,
530			register_fn,
531		}
532	}
533}
534
535// Collect all dependency registrations at compile time
536inventory::collect!(DependencyRegistration);
537
538/// Const-constructible registration entry for `#[injectable]` structs with `#[scope]`.
539///
540/// Unlike `DependencyRegistration` which uses `Box<dyn Fn>` (non-const),
541/// this struct stores a plain function pointer so it can be used in
542/// `inventory::submit!` which requires const-evaluable expressions.
543pub struct InjectableRegistration {
544	/// A function that registers this type's factory with the registry.
545	pub register_fn: fn(&DependencyRegistry),
546}
547
548impl InjectableRegistration {
549	/// Create a new `InjectableRegistration` with a function pointer.
550	pub const fn new(register_fn: fn(&DependencyRegistry)) -> Self {
551		Self { register_fn }
552	}
553}
554
555inventory::collect!(InjectableRegistration);
556
557/// Initialize the registry with all collected registrations
558fn initialize_registry(registry: &DependencyRegistry) {
559	for registration in inventory::iter::<DependencyRegistration> {
560		(registration.register_fn)(registry);
561	}
562	for registration in inventory::iter::<InjectableRegistration> {
563		(registration.register_fn)(registry);
564	}
565}
566
567/// Helper macro for submitting registrations to inventory
568///
569/// This is used internally by the `#[injectable]` and `#[injectable_factory]` macros.
570#[macro_export]
571macro_rules! submit_registration {
572	($registration:expr) => {
573		$crate::inventory::submit! {
574			$registration
575		}
576	};
577}
578
579#[cfg(test)]
580mod tests {
581	use super::*;
582	use crate::scope::SingletonScope;
583	use rstest::*;
584
585	#[derive(Clone)]
586	struct TestService {
587		value: i32,
588	}
589
590	#[rstest]
591	#[tokio::test]
592	async fn test_registry_basic() {
593		let registry = DependencyRegistry::new();
594
595		registry.register_async::<TestService, _, _>(DependencyScope::Singleton, |_ctx| async {
596			Ok(TestService { value: 42 })
597		});
598
599		assert!(registry.is_registered::<TestService>());
600		assert_eq!(
601			registry.get_scope::<TestService>(),
602			Some(DependencyScope::Singleton)
603		);
604
605		let singleton_scope = Arc::new(SingletonScope::new());
606		let ctx = InjectionContext::builder(singleton_scope).build();
607
608		let service = registry.create::<TestService>(&ctx).await.unwrap();
609		assert_eq!(service.value, 42);
610	}
611
612	#[rstest]
613	#[tokio::test]
614	async fn test_registry_not_registered() {
615		let registry = DependencyRegistry::new();
616		let singleton_scope = Arc::new(SingletonScope::new());
617		let ctx = InjectionContext::builder(singleton_scope).build();
618
619		let result = registry.create::<TestService>(&ctx).await;
620		assert!(result.is_err());
621	}
622
623	// Fixes #3457
624	#[rstest]
625	fn test_duplicate_registration_panics() {
626		let registry = DependencyRegistry::new();
627
628		registry.register_async::<TestService, _, _>(DependencyScope::Singleton, |_ctx| async {
629			Ok(TestService { value: 1 })
630		});
631
632		// Act
633		let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
634			registry.register_async::<TestService, _, _>(DependencyScope::Request, |_ctx| async {
635				Ok(TestService { value: 2 })
636			});
637		}));
638
639		// Assert
640		let err = result.expect_err("expected panic on duplicate registration");
641		let msg = err
642			.downcast_ref::<String>()
643			.map(|s| s.as_str())
644			.or_else(|| err.downcast_ref::<&str>().copied())
645			.expect("panic payload should be a string");
646		assert!(
647			msg.contains("Duplicate DependencyRegistry registration"),
648			"missing duplicate prefix: {msg}"
649		);
650		assert!(
651			msg.contains("TestService"),
652			"missing type name in panic message: {msg}"
653		);
654		assert!(
655			msg.contains("newtype"),
656			"missing newtype hint in panic message: {msg}"
657		);
658	}
659
660	// Fixes #3457 — is_registered guard prevents panic (test helper pattern)
661	#[rstest]
662	fn test_is_registered_guard_allows_skip() {
663		let registry = DependencyRegistry::new();
664
665		registry.register_async::<TestService, _, _>(DependencyScope::Singleton, |_ctx| async {
666			Ok(TestService { value: 1 })
667		});
668
669		// Second registration guarded — no panic
670		if !registry.is_registered::<TestService>() {
671			registry.register_async::<TestService, _, _>(DependencyScope::Request, |_ctx| async {
672				Ok(TestService { value: 2 })
673			});
674		}
675
676		assert!(registry.is_registered::<TestService>());
677	}
678}