Skip to main content

rust_ef/
di.rs

1//! DI integration — `AddDbContext` on `rust-dix`.
2//!
3//! Supports single-context (default) and multi-context (keyed) registration.
4//! `DbContext` is registered as **Scoped** and can be resolved either as
5//! `Arc<DbContext>` (shared within a scope) or as owned `DbContext` (fresh
6//! instance, idiomatic `&mut self` access).
7//!
8//! # Recommended: owned resolution for handlers
9//!
10//! `DbContext` methods (`set::<T>()`, `save_changes()`, `detect_changes()`)
11//! require `&mut self`. The idiomatic pattern is to **own** the context via
12//! `get_owned()`, avoiding `Arc<Mutex>` and interior mutability entirely.
13//!
14//! ```rust,ignore
15//! use rust_dix::*;
16//! use rust_ef::di::*;
17//! use rust_ef::db_context::DbContext;
18//! use rust_ef_sqlite::DbContextOptionsBuilderExt as _;
19//!
20//! // rust-dix 0.6+: `build()` returns `Arc<ServiceProvider>` directly,
21//! // and `get_owned()` returns `Result<T, RdiError>`.
22//! let provider = ServiceCollection::new()
23//!     .add_dbcontext(|options| {
24//!         options.use_sqlite("data source=app.db");
25//!     })
26//!     .build()
27//!     .unwrap();
28//!
29//! // Owned: fresh instance, direct &mut self access — no locks needed.
30//! let mut ctx: DbContext = provider.get_owned().expect("DbContext");
31//! ctx.set::<Blog>().add(blog);
32//! ctx.save_changes().await?;
33//! ```
34//!
35//! Handlers declare a bare `ctx: DbContext` field marked with
36//! `#[inject(owned)]` — `#[derive(Inject)]` resolves it via `get_owned()`.
37//! Unmarked fields fall back to `Default::default()`. Use `#[inject(scoped)]`
38//! (not bare `#[inject]`, which defaults to Singleton) to avoid captive
39//! dependency errors with the Scoped `DbContext`:
40//! ```rust,ignore
41//! #[derive(Inject)]
42//! pub struct CreateBlogHandler {
43//!     #[inject(owned)]
44//!     ctx: DbContext,            // bare T + #[inject(owned)] → get_owned()
45//! }
46//!
47//! #[inject(scoped)]
48//! #[async_trait]
49//! impl IRequestHandler<CreateBlogRequest, BlogModel> for CreateBlogHandler {
50//!     async fn handle(&mut self, req: CreateBlogRequest) -> Result<BlogModel> {
51//!         self.ctx.set::<Blog>().add(blog);
52//!         self.ctx.save_changes().await?;
53//!         // ...
54//!     }
55//! }
56//! ```
57//!
58//! # Shared resolution (within a scope)
59//!
60//! When multiple consumers in the same scope must share a single instance
61//! (e.g. an `IHostedService` that seeds data before handlers run), resolve
62//! as `Arc<DbContext>`:
63//! ```rust,ignore
64//! use rust_dix::scope::ScopeFactory;  // for create_scope()
65//!
66//! let scope = provider.create_scope();
67//! let ctx: Arc<DbContext> = scope.get().expect("DbContext");
68//! // Additional get() calls within this scope return the same instance.
69//! ```
70//!
71//! > **Note**: `Arc<DbContext>` only provides `&self` access. Mutation
72//! > requires `Arc::get_mut` (refcount == 1) or restructuring to owned
73//! > resolution. Prefer `get_owned()` for any scope that needs `&mut self`.
74//!
75//! # Multiple databases (keyed)
76//!
77//! ```rust,ignore
78//! let provider = ServiceCollection::new()
79//!     .add_dbcontext_keyed("primary", |options| {
80//!         options.use_postgres("host=primary/db");
81//!     })
82//!     .add_dbcontext_keyed("logs", |options| {
83//!         options
84//!             .use_sqlite("logs.db")
85//!             .add_interceptor(AuditInterceptor);
86//!     })
87//!     .build()
88//!     .unwrap();
89//!
90//! // Owned keyed resolution (recommended for handlers):
91//! let mut primary: DbContext = provider.get_keyed_owned("primary").expect("primary ctx");
92//! let mut logs: DbContext = provider.get_keyed_owned("logs").expect("logs ctx");
93//!
94//! // Shared keyed resolution (within a scope):
95//! // let primary: Arc<DbContext> = scope.get_keyed("primary");
96//! ```
97//!
98//! ## Scoped Lifetime
99//!
100//! `add_dbcontext` registers the context as **Scoped**. Resolving via
101//! `get()` shares the instance within a scope; resolving via `get_owned()`
102//! bypasses the cache and returns a fresh instance each call (both are
103//! isolated across scopes). Resolving directly from the root
104//! `ServiceProvider` (without creating a scope) degrades to a fresh
105//! instance per call (transient).
106//!
107//! > **rust-webapp**: the HTTP pipeline automatically creates a scope per
108//! > request. Handlers are resolved via `get_owned::<Handler>()`, which
109//! > owns a fresh `DbContext` — no manual scope management needed.
110
111use crate::db_context::{DbContext, DbContextOptionsBuilder};
112use std::sync::Arc;
113
114/// Adds `add_dbcontext` and `add_dbcontext_keyed` to `rust_dix::ServiceCollection`.
115pub trait DbContextServiceCollectionExt {
116    /// Registers a `DbContext` as **scoped** with default key.
117    ///
118    /// The closure receives a `DbContextOptionsBuilder` for provider
119    /// configuration. Resolve via `get_owned::<DbContext>()` for idiomatic
120    /// `&mut self` access, or `get::<DbContext>()` for shared `Arc` access
121    /// within a scope.
122    fn add_dbcontext(
123        self,
124        configure: impl FnOnce(&mut DbContextOptionsBuilder) + Send + Sync + 'static,
125    ) -> Self;
126
127    /// Registers a keyed `DbContext` as **scoped**.
128    ///
129    /// Use this when you need multiple database connections in the same
130    /// application. Each key identifies a distinct `DbContext` instance
131    /// with its own provider and interceptors. Resolve via
132    /// `get_keyed_owned::<DbContext>("key")` (owned) or
133    /// `get_keyed::<DbContext>("key")` (shared `Arc`).
134    ///
135    /// # Example
136    ///
137    /// ```rust,ignore
138    /// .add_dbcontext_keyed("logs", |options| {
139    ///     options.use_sqlite("logs.db");
140    /// })
141    /// ```
142    fn add_dbcontext_keyed(
143        self,
144        key: &str,
145        configure: impl FnOnce(&mut DbContextOptionsBuilder) + Send + Sync + 'static,
146    ) -> Self;
147}
148
149impl DbContextServiceCollectionExt for ::rust_dix::ServiceCollection {
150    fn add_dbcontext(
151        self,
152        configure: impl FnOnce(&mut DbContextOptionsBuilder) + Send + Sync + 'static,
153    ) -> Self {
154        let mut builder = DbContextOptionsBuilder::new();
155        configure(&mut builder);
156        let options = Arc::new(builder.build());
157        // Eagerly build the provider (and its connection pool) at registration
158        // time so a misconfigured connection string / TLS setup fails fast at
159        // startup instead of panicking on the first request. The built provider
160        // is cached in `DbContextOptions::provider_cache`, so the per-request
161        // scoped factory below reuses it via `Arc::clone`.
162        options
163            .create_provider()
164            .expect("DbContext provider initialization failed at startup");
165        self.scoped(move |_| {
166            let ctx = DbContext::from_options(&options).expect("Failed to create DbContext");
167            Arc::new(ctx) as Arc<DbContext>
168        })
169    }
170
171    fn add_dbcontext_keyed(
172        self,
173        key: &str,
174        configure: impl FnOnce(&mut DbContextOptionsBuilder) + Send + Sync + 'static,
175    ) -> Self {
176        let mut builder = DbContextOptionsBuilder::new();
177        configure(&mut builder);
178        builder.context_key(key);
179        let options = Arc::new(builder.build());
180        options
181            .create_provider()
182            .expect("DbContext provider initialization failed at startup");
183        self.keyed_scoped(key, move |_| {
184            let ctx = DbContext::from_options(&options).expect("Failed to create DbContext");
185            Arc::new(ctx) as Arc<DbContext>
186        })
187    }
188}
189
190pub use rust_dix::{ServiceCollection, ServiceProvider};