Skip to main content

Module di

Module di 

Source
Expand description

DI integration — AddDbContext on rust-dix 0.10.

Registers DbContext as Scoped and exposes the rust-ef-specific registration/erasure helpers. rust-ef keeps only this module as its DI integration surface: lifetimes, scope caching, owned resolution, disposal and MEDI registration semantics are provided by rust-dix.

§Resolution models

  • Owned (get_owned / #[inject(owned)]): caller-owned DbContext with &mut self access; allowed from the root provider and never cached.
  • Shared (scope.get()): Arc<DbContext> with &self access, one instance per scope.
ⓘ
use rust_dix::*;
use rust_ef::di::*;

let provider = ServiceCollection::new()
    .add_dbcontext(|options| {
        options.use_sqlite("data source=app.db");
    })
    .build()
    .unwrap();

// One scope per request/job (`rust-webx` creates it automatically per request).
let scope = provider.scope();
let mut ctx: DbContext = scope.get_owned().expect("DbContext");
ctx.add::<Blog>(blog);
ctx.save_changes().await?;

§Registration variants

APILifetime of the provider initializationUse when
DbContextServiceCollectionExt::add_dbcontexteager, at registration (panics on failure)simplest startup path; fail fast
DbContextServiceCollectionExt::add_dbcontext_asyncduring build_async().await (typed error)async startup; provider failures become RdiError
DbContextServiceCollectionExt::add_dbcontext_keyedeager, at registrationstatic read/write or module keys
DbContextServiceCollectionExt::add_dbcontext_keyed_asyncduring build_async().awaitasync startup with keyed contexts
DbContextServiceCollectionExt::add_dbcontext_anykeylazily, per requested key (cached)dynamic tenant/sharded contexts

§Typed context keys

DbContextKey generates IDbContextKey (the stable string identity used by #[context("...")]) and rust-dix 0.10’s official extension contract From<Key> for ServiceKey:

ⓘ
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, DbContextKey)]
enum Db { Read, Write }

let provider = ServiceCollection::new()
    .add_dbcontext_keyed(Db::Read, |o| { o.use_default_entity_model(); o.use_sqlite("read.db"); })
    .add_dbcontext_keyed(Db::Write, |o| { o.use_default_entity_model(); o.use_sqlite("write.db"); })
    .build()?;

let mut ctx = provider.get_owned_context(Db::Write)?;

// rust-dix 0.10 macro key expressions use the generated From impl:
#[inject(scoped)]
struct WriteHandler {
    #[inject(owned, key = Db::Write)]
    ctx: DbContext,
}

Typed keys are consistent across registration, DbContextResolver and handler injection. &str / String keys remain source-compatible and are registered as String keys; typed enum keys and string keys are not interchangeable. The entity model still matches IDbContextKey::name().

§Dynamic keys (AnyKey fallback)

DbContextServiceCollectionExt::add_dbcontext_anykey registers one fallback for every key without an exact registration. The configure closure receives the requested ServiceKey and builds per-key options; options and the connection pool are cached per requested key in a bounded LRU (DEFAULT_ANYKEY_CAPACITY entries, override with DbContextServiceCollectionExt::add_dbcontext_anykey_with_capacity), so repeated scopes reuse the same pool and tenant cardinality cannot grow memory without bound:

ⓘ
let provider = ServiceCollection::new()
    .add_dbcontext_anykey(|key, options| {
        let tenant = key.to_string(); // map to a *known* tenant configuration
        options.use_default_entity_model();
        options.use_postgres(tenant_connection_string(&tenant));
    })
    .build()?;

let scope = provider.scope();
let mut ctx = scope.get_keyed_owned::<DbContext>("tenant-a")?;

Security: never interpolate an untrusted requested key into SQL or a connection string directly — resolve it through an allow-list/tenant table. AnyKey contexts always use use_default_entity_model() (a dynamic key has no #[context("...")] string); use global query filters for tenant isolation.

§Async provider initialization

DbContextServiceCollectionExt::add_dbcontext_async defers provider creation (connection pool) to build_async().await. A failure becomes a typed RdiError instead of a registration-time panic. The DbContext itself stays a synchronous Scoped service, so handlers keep using #[inject(owned)] / get_owned() unchanged:

ⓘ
let provider = ServiceCollection::new()
    .add_dbcontext_async(|options| {
        options.use_postgres("postgres://app/db");
        // Optional: async validation after the pool is created.
        options.async_preflight(|options| {
            Box::pin(async move {
                let provider = options.create_provider()?;
                let mut conn = provider.get_connection().await?;
                conn.query("SELECT 1", &[]).await?;
                Ok(())
            })
        });
    })
    .build_async()
    .await?;

let mut ctx: DbContext = provider.get_owned().expect("DbContext");

Calling build() (sync) with async registrations returns RdiError::AsyncServiceRequiresBuildAsync; use build_async().await.

§rust-dix 0.10 notes

  • Macro key expressions: #[inject(key = <expr>)] / #[inject(owned, key = <expr>)] accept any impl Into<ServiceKey>. DbContextKey implements the official From<Key> for ServiceKey contract; do not copy rust-dix’s injection macro.
  • Typed vs string keys: typed enum keys use ServiceKey::new(key); &str/String registration keys stay String keys. They are intentionally not interchangeable (ServiceKey::new("x") ≠ "x".into()).
  • MSRV 1.95; rust-ef 2.1 declares rust-version = "1.95".
  • Async diagnostics: a sync singleton depending on an async service reports SyncSingletonDependsOnAsyncService; resolving an uninitialized async service synchronously reports AsyncServiceRequiresGetAsync.
  • Async cycle detection is chain-based; if an async provider factory ever spawns detached tasks that resolve services, capture provider.async_context() and resolve through it.
  • Async AnyKey is still not provided by rust-dix 0.10; dynamic keys keep the synchronous per-key initialization described above.

§Scoped lifetime & strict scope validation

rust-dix 0.8+ enables MEDI ValidateScopes semantics by default: resolving a Scoped service from the root provider returns RdiError::ScopedFromRootProvider — the root provider is not a scope. get_owned / get_keyed_owned are the exception (caller-owned, never cached); their constructor’s Scoped dependencies still obey validation.

Migrating from rust-dix 0.7 / rust-ef ≤ 2.0. Root resolutions that relied on root-scope caching must move to provider.scope() (shared) or owned resolution. A temporary escape hatch is ServiceCollection::build_with(ServiceProviderOptions::lenient()).

§Disposal

Scope::drop / Scope::dispose() release the Scoped DbContext instances created in that scope in reverse creation order; ServiceProvider::dispose() releases singletons (including async-initialized providers). After disposal, resolution returns ScopeDisposed / ProviderDisposed.

§Registration semantics

add_dbcontext* uses normal scoped / keyed_scoped registrations, so rust-dix’s last-registration-wins rule applies: registering the same key twice keeps the last registration. try_add_* behaves as MEDI TryAdd.

DbContext methods (set::<T>(), save_changes(), detect_changes()) require &mut self; own the context via get_owned(), avoiding Arc<Mutex> and interior mutability:

ⓘ
#[derive(Inject)]
pub struct CreateBlogHandler {
    #[inject(owned)]
    ctx: DbContext,
}

§Shared resolution (within a scope)

Resolve Arc<DbContext> from a scope when several consumers in the same scope must share one instance. Arc<DbContext> only provides &self access; prefer owned resolution when mutation is needed.

§Multiple databases (keyed)

ⓘ
let provider = ServiceCollection::new()
    .add_dbcontext_keyed("primary", |options| {
        options.use_postgres("host=primary/db");
    })
    .add_dbcontext_keyed("logs", |options| {
        options.use_sqlite("logs.db");
    })
    .build()?;

let mut primary: DbContext = provider.get_keyed_owned("primary")?;
let scope = provider.scope();
let logs: Arc<DbContext> = scope.get_keyed("logs")?;

Structs§

ServiceCollection
Service collection — register services, then call .build().
ServiceKey
Key used by keyed registrations, including the AnyKey fallback sentinel. Key of a keyed service registration / resolution.
ServiceProvider
Built service provider — the root DI container (read-only after build).
ServiceProviderOptions
Options for ServiceCollection::build_with / ServiceCollection::build_async_with.

Constants§

DEFAULT_ANYKEY_CAPACITY
Default number of requested keys whose AnyKey options/provider are cached.

Traits§

DbContextResolver
Typed-context resolution helpers.
DbContextServiceCollectionExt
Adds the DbContext registration helpers to rust_dix::ServiceCollection.
IDbContextKey
Identity of a keyed DbContext.