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