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};