rust_ef/interceptor.rs
1//! SaveChanges interceptor �?hooks into the DbContext lifecycle.
2//!
3//! Provides an interception pipeline analogous to EFCore's
4//! `ISaveChangesInterceptor`: before/after save, and on failure.
5//! Interceptors are registered via `DbContextOptionsBuilder::add_interceptor()`.
6//!
7//! # Example (user code)
8//!
9//! ```rust,ignore
10//! use rust_ef::interceptor::{ISaveChangesInterceptor, SaveChangesContext};
11//!
12//! struct AuditInterceptor;
13//!
14//! #[async_trait::async_trait]
15//! impl ISaveChangesInterceptor for AuditInterceptor {
16//! async fn on_saving(&self, ctx: &SaveChangesContext) -> EFResult<()> {
17//! println!("Saving {} entries", ctx.entries().len());
18//! Ok(())
19//! }
20//! }
21//! ```
22
23use crate::error::{EFError, EFResult};
24use crate::tracking::EntityEntryView;
25use std::sync::Arc;
26
27// ---------------------------------------------------------------------------
28// SaveChangesContext �?context passed to interceptor hooks
29// ---------------------------------------------------------------------------
30
31/// Read-only snapshot of the save operation, passed to interceptors.
32///
33/// Built from the actual pending entries across all `DbSet`s (the real save
34/// data source), so interceptors see a snapshot consistent with what
35/// `save_changes()` will commit. This is an owned snapshot so interceptors
36/// cannot mutate the pending changes, and the borrow does not block
37/// subsequent mutating operations on `DbContext`.
38#[derive(Debug, Clone)]
39pub struct SaveChangesContext {
40 /// All pending entries before the save.
41 entries: Vec<EntityEntryView>,
42 /// Number of entries that will be added.
43 added_count: usize,
44 /// Number of entries that will be modified.
45 modified_count: usize,
46 /// Number of entries that will be deleted.
47 deleted_count: usize,
48}
49
50impl SaveChangesContext {
51 /// Creates a context from the pending entries aggregated across all
52 /// `DbSet`s. Counts are derived from each entry's `state`.
53 pub fn from_views(entries: Vec<EntityEntryView>) -> Self {
54 let added_count = entries
55 .iter()
56 .filter(|e| e.state == crate::entity::EntityState::Added)
57 .count();
58 let modified_count = entries
59 .iter()
60 .filter(|e| e.state == crate::entity::EntityState::Modified)
61 .count();
62 let deleted_count = entries
63 .iter()
64 .filter(|e| e.state == crate::entity::EntityState::Deleted)
65 .count();
66
67 Self {
68 entries,
69 added_count,
70 modified_count,
71 deleted_count,
72 }
73 }
74
75 /// Returns all pending entity entries at the time of interception.
76 pub fn entries(&self) -> &[EntityEntryView] {
77 &self.entries
78 }
79
80 /// Number of entities that will be / were added.
81 pub fn added_count(&self) -> usize {
82 self.added_count
83 }
84
85 /// Number of entities that will be / were modified.
86 pub fn modified_count(&self) -> usize {
87 self.modified_count
88 }
89
90 /// Number of entities that will be / were deleted.
91 pub fn deleted_count(&self) -> usize {
92 self.deleted_count
93 }
94
95 /// Total number of tracked entries.
96 pub fn total_count(&self) -> usize {
97 self.entries.len()
98 }
99}
100
101// ---------------------------------------------------------------------------
102// SaveChangesResultContext �?post-save context
103// ---------------------------------------------------------------------------
104
105/// Context passed to interceptors after a successful save.
106#[derive(Debug, Clone)]
107pub struct SaveChangesResultContext {
108 /// Summary of the save operation.
109 pub added: usize,
110 pub updated: usize,
111 pub deleted: usize,
112}
113
114impl SaveChangesResultContext {
115 pub fn total(&self) -> usize {
116 self.added + self.updated + self.deleted
117 }
118}
119
120// ---------------------------------------------------------------------------
121// ISaveChangesInterceptor �?the interceptor trait
122// ---------------------------------------------------------------------------
123
124/// Interface for intercepting `save_changes()` operations.
125///
126/// All methods have default no-op implementations so users only
127/// override what they need.
128///
129/// # Lifecycle
130///
131/// ```text
132/// on_saving(ctx) �?[execute SQL] �?on_saved(ctx, result)
133/// on_save_failed(ctx, error) (if error)
134/// ```
135#[async_trait::async_trait]
136pub trait ISaveChangesInterceptor: Send + Sync {
137 /// Called **before** SQL commands are executed.
138 ///
139 /// Return `Err(...)` to abort the save. The transaction will be
140 /// rolled back and `on_save_failed` will NOT be called (this is
141 /// a pre-commit rejection, not a database failure).
142 async fn on_saving(&self, _ctx: &SaveChangesContext) -> EFResult<()> {
143 Ok(())
144 }
145
146 /// Called **after** a successful save (transaction committed).
147 async fn on_saved(
148 &self,
149 _ctx: &SaveChangesContext,
150 _result: &SaveChangesResultContext,
151 ) -> EFResult<()> {
152 Ok(())
153 }
154
155 /// Called when the save **fails** with a database error.
156 ///
157 /// The transaction has already been rolled back at this point.
158 /// The error is passed by reference for inspection; the interceptor
159 /// cannot suppress it.
160 async fn on_save_failed(&self, _ctx: &SaveChangesContext, _error: &EFError) {
161 // default: no-op
162 }
163}
164
165// ---------------------------------------------------------------------------
166// InterceptorPipeline �?ordered chain of interceptors
167// ---------------------------------------------------------------------------
168
169/// An ordered, immutable chain of save-changes interceptors.
170///
171/// Created by `DbContextOptionsBuilder` and invoked by `DbContext::save_changes()`.
172pub(crate) struct InterceptorPipeline {
173 interceptors: Vec<Arc<dyn ISaveChangesInterceptor>>,
174}
175
176impl InterceptorPipeline {
177 pub fn new(interceptors: Vec<Arc<dyn ISaveChangesInterceptor>>) -> Self {
178 Self { interceptors }
179 }
180
181 /// Runs all `on_saving` hooks in registration order.
182 /// Aborts early if any interceptor returns an error.
183 pub async fn on_saving(&self, ctx: &SaveChangesContext) -> EFResult<()> {
184 for interceptor in &self.interceptors {
185 interceptor.on_saving(ctx).await?;
186 }
187 Ok(())
188 }
189
190 /// Runs all `on_saved` hooks in registration order.
191 pub async fn on_saved(
192 &self,
193 ctx: &SaveChangesContext,
194 result: &SaveChangesResultContext,
195 ) -> EFResult<()> {
196 for interceptor in &self.interceptors {
197 interceptor.on_saved(ctx, result).await?;
198 }
199 Ok(())
200 }
201
202 /// Runs all `on_save_failed` hooks. Errors from these hooks are
203 /// intentionally swallowed �?they must not mask the original error.
204 pub async fn on_save_failed(&self, ctx: &SaveChangesContext, error: &EFError) {
205 for interceptor in &self.interceptors {
206 interceptor.on_save_failed(ctx, error).await;
207 }
208 }
209}