Skip to main content

rust_ef/
di.rs

1//! DI integration — `AddDbContext` on `rust-dix` 0.10.
2//!
3//! Registers `DbContext` as **Scoped** and exposes the rust-ef-specific
4//! registration/erasure helpers. rust-ef keeps only this module as its DI
5//! integration surface: lifetimes, scope caching, owned resolution, disposal and
6//! MEDI registration semantics are provided by rust-dix.
7//!
8//! # Resolution models
9//!
10//! - **Owned** (`get_owned` / `#[inject(owned)]`): caller-owned `DbContext` with
11//!   `&mut self` access; allowed from the root provider and never cached.
12//! - **Shared** (`scope.get()`): `Arc<DbContext>` with `&self` access, one
13//!   instance per scope.
14//!
15//! ```rust,ignore
16//! use rust_dix::*;
17//! use rust_ef::di::*;
18//!
19//! let provider = ServiceCollection::new()
20//!     .add_dbcontext(|options| {
21//!         options.use_sqlite("data source=app.db");
22//!     })
23//!     .build()
24//!     .unwrap();
25//!
26//! // One scope per request/job (`rust-webx` creates it automatically per request).
27//! let scope = provider.scope();
28//! let mut ctx: DbContext = scope.get_owned().expect("DbContext");
29//! ctx.add::<Blog>(blog);
30//! ctx.save_changes().await?;
31//! ```
32//!
33//! # Registration variants
34//!
35//! | API | Lifetime of the provider initialization | Use when |
36//! |-----|------------------------------------------|----------|
37//! | [`DbContextServiceCollectionExt::add_dbcontext`] | eager, at registration (panics on failure) | simplest startup path; fail fast |
38//! | [`DbContextServiceCollectionExt::add_dbcontext_async`] | during `build_async().await` (typed error) | async startup; provider failures become `RdiError` |
39//! | [`DbContextServiceCollectionExt::add_dbcontext_keyed`] | eager, at registration | static read/write or module keys |
40//! | [`DbContextServiceCollectionExt::add_dbcontext_keyed_async`] | during `build_async().await` | async startup with keyed contexts |
41//! | [`DbContextServiceCollectionExt::add_dbcontext_anykey`] | lazily, per requested key (cached) | dynamic tenant/sharded contexts |
42//!
43//! ## Typed context keys
44//!
45//! [`DbContextKey`](crate::prelude::DbContextKey) generates [`IDbContextKey`]
46//! (the stable string identity used by `#[context("...")]`) and rust-dix 0.10's
47//! official extension contract `From<Key> for ServiceKey`:
48//!
49//! ```rust,ignore
50//! #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, DbContextKey)]
51//! enum Db { Read, Write }
52//!
53//! let provider = ServiceCollection::new()
54//!     .add_dbcontext_keyed(Db::Read, |o| { o.use_default_entity_model(); o.use_sqlite("read.db"); })
55//!     .add_dbcontext_keyed(Db::Write, |o| { o.use_default_entity_model(); o.use_sqlite("write.db"); })
56//!     .build()?;
57//!
58//! let mut ctx = provider.get_owned_context(Db::Write)?;
59//!
60//! // rust-dix 0.10 macro key expressions use the generated From impl:
61//! #[inject(scoped)]
62//! struct WriteHandler {
63//!     #[inject(owned, key = Db::Write)]
64//!     ctx: DbContext,
65//! }
66//! ```
67//!
68//! Typed keys are consistent across registration, [`DbContextResolver`] and
69//! handler injection. `&str` / `String` keys remain source-compatible and are
70//! registered as `String` keys; typed enum keys and string keys are not
71//! interchangeable. The entity model still matches `IDbContextKey::name()`.
72//!
73//! ## Dynamic keys (`AnyKey` fallback)
74//!
75//! [`DbContextServiceCollectionExt::add_dbcontext_anykey`] registers one
76//! fallback for every key without an exact registration. The configure closure
77//! receives the requested `ServiceKey` and builds per-key options; options and
78//! the connection pool are cached per requested key in a bounded LRU
79//! ([`DEFAULT_ANYKEY_CAPACITY`] entries, override with
80//! [`DbContextServiceCollectionExt::add_dbcontext_anykey_with_capacity`]), so
81//! repeated scopes reuse the same pool and tenant cardinality cannot grow memory
82//! without bound:
83//!
84//! ```rust,ignore
85//! let provider = ServiceCollection::new()
86//!     .add_dbcontext_anykey(|key, options| {
87//!         let tenant = key.to_string(); // map to a *known* tenant configuration
88//!         options.use_default_entity_model();
89//!         options.use_postgres(tenant_connection_string(&tenant));
90//!     })
91//!     .build()?;
92//!
93//! let scope = provider.scope();
94//! let mut ctx = scope.get_keyed_owned::<DbContext>("tenant-a")?;
95//! ```
96//!
97//! Security: never interpolate an untrusted requested key into SQL or a
98//! connection string directly — resolve it through an allow-list/tenant table.
99//! AnyKey contexts always use `use_default_entity_model()` (a dynamic key has no
100//! `#[context("...")]` string); use global query filters for tenant isolation.
101//!
102//! ## Async provider initialization
103//!
104//! [`DbContextServiceCollectionExt::add_dbcontext_async`] defers provider
105//! creation (connection pool) to `build_async().await`. A failure becomes a typed
106//! `RdiError` instead of a registration-time panic. The `DbContext` itself stays
107//! a synchronous Scoped service, so handlers keep using `#[inject(owned)]` /
108//! `get_owned()` unchanged:
109//!
110//! ```rust,ignore
111//! let provider = ServiceCollection::new()
112//!     .add_dbcontext_async(|options| {
113//!         options.use_postgres("postgres://app/db");
114//!         // Optional: async validation after the pool is created.
115//!         options.async_preflight(|options| {
116//!             Box::pin(async move {
117//!                 let provider = options.create_provider()?;
118//!                 let mut conn = provider.get_connection().await?;
119//!                 conn.query("SELECT 1", &[]).await?;
120//!                 Ok(())
121//!             })
122//!         });
123//!     })
124//!     .build_async()
125//!     .await?;
126//!
127//! let mut ctx: DbContext = provider.get_owned().expect("DbContext");
128//! ```
129//!
130//! Calling `build()` (sync) with async registrations returns
131//! `RdiError::AsyncServiceRequiresBuildAsync`; use `build_async().await`.
132//!
133//! # rust-dix 0.10 notes
134//!
135//! - **Macro key expressions**: `#[inject(key = <expr>)]` /
136//!   `#[inject(owned, key = <expr>)]` accept any `impl Into<ServiceKey>`.
137//!   [`DbContextKey`](crate::prelude::DbContextKey) implements the official
138//!   `From<Key> for ServiceKey` contract; do not copy rust-dix's injection macro.
139//! - **Typed vs string keys**: typed enum keys use `ServiceKey::new(key)`;
140//!   `&str`/`String` registration keys stay `String` keys. They are intentionally
141//!   not interchangeable (`ServiceKey::new("x")` ≠ `"x".into()`).
142//! - **MSRV 1.95**; rust-ef 2.1 declares `rust-version = "1.95"`.
143//! - **Async diagnostics**: a sync singleton depending on an async service reports
144//!   `SyncSingletonDependsOnAsyncService`; resolving an uninitialized async
145//!   service synchronously reports `AsyncServiceRequiresGetAsync`.
146//! - **Async cycle detection** is chain-based; if an async provider factory ever
147//!   spawns detached tasks that resolve services, capture
148//!   `provider.async_context()` and resolve through it.
149//! - **Async AnyKey is still not provided** by rust-dix 0.10; dynamic keys keep
150//!   the synchronous per-key initialization described above.
151//!
152//! # Scoped lifetime & strict scope validation
153//!
154//! rust-dix 0.8+ enables MEDI `ValidateScopes` semantics by default: resolving a
155//! Scoped service from the root provider returns
156//! `RdiError::ScopedFromRootProvider` — the root provider is **not** a scope.
157//! `get_owned` / `get_keyed_owned` are the exception (caller-owned, never cached);
158//! their constructor's Scoped dependencies still obey validation.
159//!
160//! **Migrating from rust-dix 0.7 / rust-ef ≤ 2.0.** Root resolutions that relied
161//! on root-scope caching must move to `provider.scope()` (shared) or owned
162//! resolution. A temporary escape hatch is
163//! `ServiceCollection::build_with(ServiceProviderOptions::lenient())`.
164//!
165//! # Disposal
166//!
167//! `Scope::drop` / `Scope::dispose()` release the Scoped `DbContext` instances
168//! created in that scope in reverse creation order; `ServiceProvider::dispose()`
169//! releases singletons (including async-initialized providers). After disposal,
170//! resolution returns `ScopeDisposed` / `ProviderDisposed`.
171//!
172//! # Registration semantics
173//!
174//! `add_dbcontext*` uses normal `scoped` / `keyed_scoped` registrations, so
175//! rust-dix's **last-registration-wins** rule applies: registering the same key
176//! twice keeps the last registration. `try_add_*` behaves as MEDI `TryAdd`.
177//!
178//! # Recommended: owned resolution for handlers
179//!
180//! `DbContext` methods (`set::<T>()`, `save_changes()`, `detect_changes()`)
181//! require `&mut self`; own the context via `get_owned()`, avoiding
182//! `Arc<Mutex>` and interior mutability:
183//!
184//! ```rust,ignore
185//! #[derive(Inject)]
186//! pub struct CreateBlogHandler {
187//!     #[inject(owned)]
188//!     ctx: DbContext,
189//! }
190//! ```
191//!
192//! # Shared resolution (within a scope)
193//!
194//! Resolve `Arc<DbContext>` from a scope when several consumers in the same scope
195//! must share one instance. `Arc<DbContext>` only provides `&self` access; prefer
196//! owned resolution when mutation is needed.
197//!
198//! # Multiple databases (keyed)
199//!
200//! ```rust,ignore
201//! let provider = ServiceCollection::new()
202//!     .add_dbcontext_keyed("primary", |options| {
203//!         options.use_postgres("host=primary/db");
204//!     })
205//!     .add_dbcontext_keyed("logs", |options| {
206//!         options.use_sqlite("logs.db");
207//!     })
208//!     .build()?;
209//!
210//! let mut primary: DbContext = provider.get_keyed_owned("primary")?;
211//! let scope = provider.scope();
212//! let logs: Arc<DbContext> = scope.get_keyed("logs")?;
213//! ```
214
215use crate::db_context::{DbContext, DbContextOptions, DbContextOptionsBuilder};
216use rust_dix::{IServiceResolver, RdiError, Scope};
217use std::collections::HashMap;
218use std::sync::{Arc, Mutex};
219
220/// Identity of a keyed `DbContext`.
221///
222/// `name()` must stay stable for the entity model: it is the string used by
223/// `#[context("name")]` / `#[entity(T, "name")]`.
224///
225/// Prefer `#[derive(DbContextKey)]`, which implements this trait **and**
226/// `From<Key> for rust_dix::ServiceKey` (typed key for registration, resolution
227/// and `#[inject(owned, key = Key::Read)]`). Manual implementations must also
228/// provide that `From` impl (the official rust-dix extension contract).
229/// `&str`, `&String` and `String` are implemented for source compatibility and
230/// register as string keys.
231pub trait IDbContextKey: Send + Sync {
232    /// Stable string identity of this key.
233    fn name(&self) -> &str;
234}
235
236impl IDbContextKey for &str {
237    fn name(&self) -> &str {
238        self
239    }
240}
241
242impl IDbContextKey for String {
243    fn name(&self) -> &str {
244        self.as_str()
245    }
246}
247
248impl IDbContextKey for &String {
249    fn name(&self) -> &str {
250        self.as_str()
251    }
252}
253
254/// Typed-context resolution helpers.
255///
256/// Keys are registered through rust-dix's typed `ServiceKey` path: enum key
257/// types implementing `Into<ServiceKey>` (usually via `#[derive(DbContextKey)]`)
258/// register a typed key; `&str`/`String` keys keep their `String` key. The two
259/// forms are not interchangeable, matching rust-dix 0.10+ semantics.
260/// `get_context` requires a scope (strict scope validation); use
261/// [`DbContextResolver::get_owned_context`] for root-safe owned resolution.
262pub trait DbContextResolver {
263    /// Resolve an owned `DbContext` for `key` (allowed from the root provider).
264    fn get_owned_context(
265        &self,
266        key: impl IDbContextKey + Into<ServiceKey>,
267    ) -> Result<DbContext, RdiError>;
268
269    /// Resolve a shared `Arc<DbContext>` for `key` (requires a scope).
270    fn get_context(
271        &self,
272        key: impl IDbContextKey + Into<ServiceKey>,
273    ) -> Result<Arc<DbContext>, RdiError>;
274}
275
276impl DbContextResolver for ServiceProvider {
277    fn get_owned_context(
278        &self,
279        key: impl IDbContextKey + Into<ServiceKey>,
280    ) -> Result<DbContext, RdiError> {
281        self.get_keyed_owned(key)
282    }
283
284    fn get_context(
285        &self,
286        key: impl IDbContextKey + Into<ServiceKey>,
287    ) -> Result<Arc<DbContext>, RdiError> {
288        self.get_keyed(key)
289    }
290}
291
292impl DbContextResolver for Scope {
293    fn get_owned_context(
294        &self,
295        key: impl IDbContextKey + Into<ServiceKey>,
296    ) -> Result<DbContext, RdiError> {
297        self.get_keyed_owned(key)
298    }
299
300    fn get_context(
301        &self,
302        key: impl IDbContextKey + Into<ServiceKey>,
303    ) -> Result<Arc<DbContext>, RdiError> {
304        self.get_keyed(key)
305    }
306}
307
308/// Async-initialized options wrapper registered as an async singleton by
309/// `add_dbcontext_async*`. The synchronous Scoped `DbContext` factory resolves it
310/// from the container, so handlers keep using owned resolution.
311struct AsyncDbContextOptions(Arc<DbContextOptions>);
312
313fn resolve_arc<T: Send + Sync + 'static>(resolver: &dyn IServiceResolver) -> Option<Arc<T>> {
314    let any = resolver.get_any(std::any::type_name::<T>())?;
315    any.downcast::<Arc<T>>().ok().map(|arc| Arc::clone(&*arc))
316}
317
318fn resolve_keyed_arc_by_key<T: Send + Sync + 'static>(
319    resolver: &dyn IServiceResolver,
320    key: &ServiceKey,
321) -> Option<Arc<T>> {
322    let any = resolver.get_keyed_any_key(std::any::type_name::<T>(), key)?;
323    any.downcast::<Arc<T>>().ok().map(|arc| Arc::clone(&*arc))
324}
325
326fn build_options(configure: impl FnOnce(&mut DbContextOptionsBuilder)) -> Arc<DbContextOptions> {
327    let mut builder = DbContextOptionsBuilder::new();
328    configure(&mut builder);
329    Arc::new(builder.build())
330}
331
332/// Default number of requested keys whose `AnyKey` options/provider are cached.
333pub const DEFAULT_ANYKEY_CAPACITY: usize = 256;
334
335/// Bounded LRU cache for `AnyKey` options/providers.
336///
337/// Dynamic keys have no compile-time bound, so an unbounded cache would grow
338/// with tenant/shard cardinality. Eviction only drops the cached options; live
339/// `DbContext`s keep their provider `Arc`, and a later resolution for an evicted
340/// key builds a fresh pool.
341struct AnyKeyOptionsCache {
342    capacity: usize,
343    clock: u64,
344    entries: HashMap<ServiceKey, (Arc<DbContextOptions>, u64)>,
345}
346
347impl AnyKeyOptionsCache {
348    fn new(capacity: usize) -> Self {
349        Self {
350            capacity: capacity.max(1),
351            clock: 0,
352            entries: HashMap::new(),
353        }
354    }
355
356    fn get_or_build(
357        &mut self,
358        key: &ServiceKey,
359        build: impl FnOnce() -> Arc<DbContextOptions>,
360    ) -> Arc<DbContextOptions> {
361        self.clock += 1;
362        let tick = self.clock;
363        if let Some((options, last_used)) = self.entries.get_mut(key) {
364            *last_used = tick;
365            return Arc::clone(options);
366        }
367
368        let options = build();
369        if self.entries.len() >= self.capacity {
370            if let Some(oldest) = self
371                .entries
372                .iter()
373                .min_by_key(|(_, (_, used))| *used)
374                .map(|(key, _)| key.clone())
375            {
376                self.entries.remove(&oldest);
377            }
378        }
379        self.entries
380            .insert(key.clone(), (Arc::clone(&options), tick));
381        options
382    }
383}
384
385fn async_options_factory(
386    options: Arc<DbContextOptions>,
387) -> impl Fn(
388    Arc<rust_dix::ServiceProvider>,
389)
390    -> std::pin::Pin<Box<dyn std::future::Future<Output = Arc<AsyncDbContextOptions>> + Send>>
391       + Send
392       + Sync
393       + 'static {
394    move |_provider| {
395        let options = Arc::clone(&options);
396        Box::pin(async move {
397            options
398                .create_provider()
399                .expect("DbContext provider initialization failed during build_async");
400            if let Some(preflight) = options.async_preflight.clone() {
401                if let Err(err) = preflight(Arc::clone(&options)).await {
402                    panic!("DbContext async preflight failed: {err}");
403                }
404            }
405            Arc::new(AsyncDbContextOptions(options))
406        })
407    }
408}
409
410fn context_from_async_options(runtime: Arc<AsyncDbContextOptions>) -> Arc<DbContext> {
411    let ctx = DbContext::from_options(&runtime.0).expect("Failed to create DbContext");
412    Arc::new(ctx)
413}
414
415/// Adds the `DbContext` registration helpers to `rust_dix::ServiceCollection`.
416pub trait DbContextServiceCollectionExt {
417    /// Registers a `DbContext` as **Scoped** with the default key.
418    ///
419    /// The provider (connection pool) is created eagerly at registration time and
420    /// a failure panics, fail-fast at startup. A later registration for the same
421    /// key replaces this one (MEDI last-registration-wins).
422    fn add_dbcontext(
423        self,
424        configure: impl FnOnce(&mut DbContextOptionsBuilder) + Send + Sync + 'static,
425    ) -> Self;
426
427    /// Registers a typed/keyed `DbContext` as **Scoped**.
428    ///
429    /// `key` can be an enum deriving [`DbContextKey`](crate::prelude::DbContextKey)
430    /// (which implements [`IDbContextKey`] and `Into<ServiceKey>`) or a
431    /// `&str`/`String` (source-compatible; registered as a `String` key). The
432    /// key's `name()` is used for the entity model.
433    fn add_dbcontext_keyed(
434        self,
435        key: impl IDbContextKey + Into<ServiceKey>,
436        configure: impl FnOnce(&mut DbContextOptionsBuilder) + Send + Sync + 'static,
437    ) -> Self;
438
439    /// Registers **one fallback** `DbContext` for every requested key without an
440    /// exact registration (rust-dix `AnyKey`).
441    ///
442    /// The configure closure receives the requested [`ServiceKey`] and builds
443    /// per-key options. Built options/providers are cached per requested key
444    /// (bounded LRU, [`DEFAULT_ANYKEY_CAPACITY`] entries), so connection pools
445    /// are reused across scopes. The context always uses
446    /// `use_default_entity_model()`; isolate tenants with query filters.
447    ///
448    /// Security: map the requested key through an allow-list/tenant table; never
449    /// interpolate it directly into SQL or connection strings.
450    fn add_dbcontext_anykey(
451        self,
452        configure: impl Fn(&ServiceKey, &mut DbContextOptionsBuilder) + Send + Sync + 'static,
453    ) -> Self;
454
455    /// [`Self::add_dbcontext_anykey`] with an explicit LRU capacity.
456    ///
457    /// `capacity` bounds how many requested keys keep a cached options/provider
458    /// (and connection pool). Evicting an entry does not invalidate live
459    /// `DbContext`s; a later resolution for that key builds a fresh pool.
460    fn add_dbcontext_anykey_with_capacity(
461        self,
462        capacity: usize,
463        configure: impl Fn(&ServiceKey, &mut DbContextOptionsBuilder) + Send + Sync + 'static,
464    ) -> Self;
465
466    /// Registers the default `DbContext` with **async-initialized** provider
467    /// options.
468    ///
469    /// Provider creation runs during `build_async().await`; failures surface as
470    /// `RdiError` instead of a registration-time panic. An
471    /// [`DbContextOptionsBuilder::async_preflight`](crate::db_context::DbContextOptionsBuilder::async_preflight)
472    /// hook, when configured, runs after provider creation. The `DbContext`
473    /// remains a synchronous Scoped service, so owned resolution keeps working.
474    /// A sync `build()` returns `RdiError::AsyncServiceRequiresBuildAsync`.
475    fn add_dbcontext_async(
476        self,
477        configure: impl FnOnce(&mut DbContextOptionsBuilder) + Send + Sync + 'static,
478    ) -> Self;
479
480    /// Keyed variant of [`Self::add_dbcontext_async`].
481    fn add_dbcontext_keyed_async(
482        self,
483        key: impl IDbContextKey + Into<ServiceKey>,
484        configure: impl FnOnce(&mut DbContextOptionsBuilder) + Send + Sync + 'static,
485    ) -> Self;
486}
487
488impl DbContextServiceCollectionExt for ::rust_dix::ServiceCollection {
489    fn add_dbcontext(
490        self,
491        configure: impl FnOnce(&mut DbContextOptionsBuilder) + Send + Sync + 'static,
492    ) -> Self {
493        let options = build_options(configure);
494        // Eagerly build the provider (and its connection pool) at registration
495        // time so a misconfigured connection string / TLS setup fails fast at
496        // startup. The built provider is cached in `DbContextOptions`, so the
497        // per-request scoped factory below reuses it via `Arc::clone`.
498        options
499            .create_provider()
500            .expect("DbContext provider initialization failed at startup");
501        self.scoped(move |_| {
502            let ctx = DbContext::from_options(&options).expect("Failed to create DbContext");
503            Arc::new(ctx) as Arc<DbContext>
504        })
505    }
506
507    fn add_dbcontext_keyed(
508        self,
509        key: impl IDbContextKey + Into<ServiceKey>,
510        configure: impl FnOnce(&mut DbContextOptionsBuilder) + Send + Sync + 'static,
511    ) -> Self {
512        let key_name = key.name().to_string();
513        let service_key: ServiceKey = key.into();
514        let mut builder = DbContextOptionsBuilder::new();
515        configure(&mut builder);
516        if !builder.uses_default_entity_model() {
517            builder.context_key(key_name);
518        }
519        let options = Arc::new(builder.build());
520        options
521            .create_provider()
522            .expect("DbContext provider initialization failed at startup");
523        self.keyed_scoped(service_key, move |_| {
524            let ctx = DbContext::from_options(&options).expect("Failed to create DbContext");
525            Arc::new(ctx) as Arc<DbContext>
526        })
527    }
528
529    fn add_dbcontext_anykey(
530        self,
531        configure: impl Fn(&ServiceKey, &mut DbContextOptionsBuilder) + Send + Sync + 'static,
532    ) -> Self {
533        self.add_dbcontext_anykey_with_capacity(DEFAULT_ANYKEY_CAPACITY, configure)
534    }
535
536    fn add_dbcontext_anykey_with_capacity(
537        self,
538        capacity: usize,
539        configure: impl Fn(&ServiceKey, &mut DbContextOptionsBuilder) + Send + Sync + 'static,
540    ) -> Self {
541        let options_cache = Arc::new(Mutex::new(AnyKeyOptionsCache::new(capacity)));
542        self.keyed_any_scoped::<DbContext>(move |_resolver, key| {
543            let options = {
544                let mut cache = options_cache
545                    .lock()
546                    .unwrap_or_else(|poisoned| poisoned.into_inner());
547                cache.get_or_build(key, || {
548                    let mut builder = DbContextOptionsBuilder::new();
549                    configure(key, &mut builder);
550                    // A dynamic requested key has no compile-time `#[context]`
551                    // name, so the shared default entity model is the only model
552                    // that can be resolved. Isolation belongs in query filters.
553                    builder.use_default_entity_model();
554                    let options = Arc::new(builder.build());
555                    options
556                        .create_provider()
557                        .expect("AnyKey DbContext provider initialization failed");
558                    options
559                })
560            };
561            let ctx = DbContext::from_options(&options).expect("Failed to create DbContext");
562            Arc::new(ctx) as Arc<DbContext>
563        })
564    }
565
566    fn add_dbcontext_async(
567        self,
568        configure: impl FnOnce(&mut DbContextOptionsBuilder) + Send + Sync + 'static,
569    ) -> Self {
570        let options = build_options(configure);
571        self.async_singleton::<AsyncDbContextOptions>(async_options_factory(options))
572            .scoped(move |resolver| {
573                let runtime = resolve_arc::<AsyncDbContextOptions>(resolver).expect(
574                    "async DbContext options are not initialized; call ServiceCollection::build_async()",
575                );
576                context_from_async_options(runtime)
577            })
578    }
579
580    fn add_dbcontext_keyed_async(
581        self,
582        key: impl IDbContextKey + Into<ServiceKey>,
583        configure: impl FnOnce(&mut DbContextOptionsBuilder) + Send + Sync + 'static,
584    ) -> Self {
585        let key_name = key.name().to_string();
586        let service_key: ServiceKey = key.into();
587        let mut builder = DbContextOptionsBuilder::new();
588        configure(&mut builder);
589        if !builder.uses_default_entity_model() {
590            builder.context_key(key_name);
591        }
592        let options = Arc::new(builder.build());
593        let scoped_key = service_key.clone();
594        self.async_keyed_singleton::<AsyncDbContextOptions>(
595            service_key,
596            async_options_factory(options),
597        )
598        .keyed_scoped(scoped_key.clone(), move |resolver| {
599            let runtime = resolve_keyed_arc_by_key::<AsyncDbContextOptions>(resolver, &scoped_key)
600                .expect("async keyed DbContext options are not initialized; call build_async()");
601            context_from_async_options(runtime)
602        })
603    }
604}
605
606// Re-export the rust-dix 0.10 types users need for registration/build/scope
607// control so a Rust project can integrate rust-ef without also depending on
608// rust-dix directly. `ServiceProviderOptions` powers the strict/lenient
609// scope-validation choice in `ServiceCollection::build_with`.
610pub use rust_dix::{ServiceCollection, ServiceKey, ServiceProvider, ServiceProviderOptions};