@@ -84,9 +84,20 @@ public override async Task AddAsync(TEntity entity, CancellationToken token = de
8484 }
8585
8686
87- /// <inheritdoc />
87+ /// <summary>
88+ /// Deletes the entity. If <typeparamref name="TEntity"/> implements <see cref="ISoftDelete"/>,
89+ /// a soft delete is performed automatically (sets <c>IsDeleted = true</c> and issues an UPDATE).
90+ /// Otherwise a physical DELETE is executed.
91+ /// </summary>
8892 public override async Task DeleteAsync ( TEntity entity , CancellationToken token = default )
8993 {
94+ if ( SoftDeleteHelper . IsSoftDeletable < TEntity > ( ) )
95+ {
96+ SoftDeleteHelper . MarkAsDeleted ( entity ) ;
97+ await UpdateAsync ( entity , token ) ;
98+ return ;
99+ }
100+
90101 await using ( var db = DataStore . GetDbConnection ( ) )
91102 {
92103 try
@@ -115,9 +126,18 @@ public override async Task DeleteAsync(TEntity entity, CancellationToken token =
115126 }
116127 }
117128
118- /// <inheritdoc />
129+ /// <summary>
130+ /// Deletes entities matching the expression. If <typeparamref name="TEntity"/> implements
131+ /// <see cref="ISoftDelete"/>, a soft delete is performed automatically (marks each matching
132+ /// entity as deleted and issues UPDATEs). Otherwise a physical DELETE is executed.
133+ /// </summary>
119134 public async override Task < int > DeleteManyAsync ( Expression < Func < TEntity , bool > > expression , CancellationToken token = default )
120135 {
136+ if ( SoftDeleteHelper . IsSoftDeletable < TEntity > ( ) )
137+ {
138+ return await DeleteManyAsync ( expression , isSoftDelete : true , token ) ;
139+ }
140+
121141 await using ( var db = DataStore . GetDbConnection ( ) )
122142 {
123143 try
@@ -145,12 +165,157 @@ public async override Task<int> DeleteManyAsync(Expression<Func<TEntity, bool>>
145165 }
146166 }
147167
148- /// <inheritdoc />
168+ /// <summary>
169+ /// Deletes entities matching the specification. If <typeparamref name="TEntity"/> implements
170+ /// <see cref="ISoftDelete"/>, a soft delete is performed automatically.
171+ /// </summary>
149172 public async override Task < int > DeleteManyAsync ( ISpecification < TEntity > specification , CancellationToken token = default )
150173 {
151174 return await DeleteManyAsync ( specification . Predicate , token ) ;
152175 }
153176
177+ /// <summary>
178+ /// Deletes the entity using the explicitly specified delete mode. When <paramref name="isSoftDelete"/>
179+ /// is <c>true</c>, the entity must implement <see cref="ISoftDelete"/>; its <c>IsDeleted</c> property
180+ /// is set to <c>true</c> and an UPDATE is issued. When <c>false</c>, a physical DELETE is always
181+ /// performed — even if the entity implements <see cref="ISoftDelete"/>.
182+ /// </summary>
183+ /// <exception cref="InvalidOperationException">
184+ /// Thrown when <paramref name="isSoftDelete"/> is <c>true</c> but <typeparamref name="TEntity"/>
185+ /// does not implement <see cref="ISoftDelete"/>.
186+ /// </exception>
187+ public override async Task DeleteAsync ( TEntity entity , bool isSoftDelete , CancellationToken token = default )
188+ {
189+ if ( ! isSoftDelete )
190+ {
191+ // Bypass auto-detection — force a physical delete
192+ await using ( var db = DataStore . GetDbConnection ( ) )
193+ {
194+ try
195+ {
196+ if ( db . State == ConnectionState . Closed )
197+ {
198+ await db . OpenAsync ( ) ;
199+ }
200+
201+ EventTracker . AddEntity ( entity ) ;
202+ await db . DeleteAsync ( entity , cancellationToken : token ) ;
203+ }
204+ catch ( ApplicationException exception )
205+ {
206+ Logger . LogError ( exception , "Error in {0}.DeleteAsync while executing on the DbConnection." , GetType ( ) . FullName ) ;
207+ throw ;
208+ }
209+ finally
210+ {
211+ if ( db . State == ConnectionState . Open )
212+ {
213+ await db . CloseAsync ( ) ;
214+ }
215+ }
216+ }
217+ return ;
218+ }
219+
220+ SoftDeleteHelper . EnsureSoftDeletable < TEntity > ( ) ;
221+ SoftDeleteHelper . MarkAsDeleted ( entity ) ;
222+ await UpdateAsync ( entity , token ) ;
223+ }
224+
225+ /// <summary>
226+ /// Deletes entities matching the expression. When <paramref name="isSoftDelete"/> is <c>true</c>,
227+ /// each matching entity must implement <see cref="ISoftDelete"/> — its <c>IsDeleted</c> property is
228+ /// set to <c>true</c> and an UPDATE is issued instead of a DELETE.
229+ /// </summary>
230+ /// <remarks>
231+ /// The soft-delete path selects matching entities, marks each as deleted, then updates them
232+ /// one by one via Dommel's <c>UpdateAsync</c>. This is consistent with Dapper/Dommel's
233+ /// per-entity operation model (there is no bulk update-by-expression in Dommel).
234+ /// </remarks>
235+ /// <exception cref="InvalidOperationException">
236+ /// Thrown when <paramref name="isSoftDelete"/> is <c>true</c> but <typeparamref name="TEntity"/>
237+ /// does not implement <see cref="ISoftDelete"/>.
238+ /// </exception>
239+ public async override Task < int > DeleteManyAsync ( Expression < Func < TEntity , bool > > expression , bool isSoftDelete , CancellationToken token = default )
240+ {
241+ if ( ! isSoftDelete )
242+ {
243+ // Bypass auto-detection — force a physical delete
244+ await using ( var db = DataStore . GetDbConnection ( ) )
245+ {
246+ try
247+ {
248+ if ( db . State == ConnectionState . Closed )
249+ {
250+ await db . OpenAsync ( ) ;
251+ }
252+
253+ return await db . DeleteMultipleAsync ( expression , cancellationToken : token ) ;
254+ }
255+ catch ( ApplicationException exception )
256+ {
257+ Logger . LogError ( exception , "Error in {0}.DeleteManyAsync while executing on the DbConnection." , GetType ( ) . FullName ) ;
258+ throw ;
259+ }
260+ finally
261+ {
262+ if ( db . State == ConnectionState . Open )
263+ {
264+ await db . CloseAsync ( ) ;
265+ }
266+ }
267+ }
268+ }
269+
270+ SoftDeleteHelper . EnsureSoftDeletable < TEntity > ( ) ;
271+
272+ await using ( var db = DataStore . GetDbConnection ( ) )
273+ {
274+ try
275+ {
276+ if ( db . State == ConnectionState . Closed )
277+ {
278+ await db . OpenAsync ( ) ;
279+ }
280+
281+ var entities = ( await db . SelectAsync ( expression , cancellationToken : token ) ) . ToList ( ) ;
282+ int count = 0 ;
283+ foreach ( var entity in entities )
284+ {
285+ SoftDeleteHelper . MarkAsDeleted ( entity ) ;
286+ await db . UpdateAsync ( entity , cancellationToken : token ) ;
287+ count ++ ;
288+ }
289+ return count ;
290+ }
291+ catch ( ApplicationException exception )
292+ {
293+ Logger . LogError ( exception , "Error in {0}.DeleteManyAsync (soft delete) while executing on the DbConnection." , GetType ( ) . FullName ) ;
294+ throw ;
295+ }
296+ finally
297+ {
298+ if ( db . State == ConnectionState . Open )
299+ {
300+ await db . CloseAsync ( ) ;
301+ }
302+ }
303+ }
304+ }
305+
306+ /// <summary>
307+ /// Deletes entities matching the specification. When <paramref name="isSoftDelete"/> is <c>true</c>,
308+ /// each matching entity must implement <see cref="ISoftDelete"/> — its <c>IsDeleted</c> property is
309+ /// set to <c>true</c> and an UPDATE is issued instead of a DELETE.
310+ /// </summary>
311+ /// <exception cref="InvalidOperationException">
312+ /// Thrown when <paramref name="isSoftDelete"/> is <c>true</c> but <typeparamref name="TEntity"/>
313+ /// does not implement <see cref="ISoftDelete"/>.
314+ /// </exception>
315+ public async override Task < int > DeleteManyAsync ( ISpecification < TEntity > specification , bool isSoftDelete , CancellationToken token = default )
316+ {
317+ return await DeleteManyAsync ( specification . Predicate , isSoftDelete , token ) ;
318+ }
154319
155320
156321 /// <inheritdoc />
@@ -202,7 +367,8 @@ public override async Task<ICollection<TEntity>> FindAsync(Expression<Func<TEnti
202367 await db . OpenAsync ( ) ;
203368 }
204369
205- var results = await db . SelectAsync ( expression , cancellationToken : token ) ;
370+ var filteredExpression = SoftDeleteHelper . CombineWithNotDeletedFilter < TEntity > ( expression ) ;
371+ var results = await db . SelectAsync ( filteredExpression , cancellationToken : token ) ;
206372 return results . ToList ( ) ;
207373 }
208374 catch ( ApplicationException exception )
@@ -233,6 +399,13 @@ public override async Task<TEntity> FindAsync(object primaryKey, CancellationTok
233399 }
234400
235401 var result = await db . GetAsync < TEntity > ( primaryKey , cancellationToken : token ) ;
402+
403+ // Post-fetch soft-delete check: if the entity was soft-deleted, treat it as not found
404+ if ( result != null && SoftDeleteHelper . IsSoftDeletable < TEntity > ( ) && ( ( ISoftDelete ) result ) . IsDeleted )
405+ {
406+ return default ! ;
407+ }
408+
236409 return result ! ;
237410 }
238411 catch ( ApplicationException exception )
@@ -262,7 +435,8 @@ public override async Task<long> GetCountAsync(ISpecification<TEntity> selectSpe
262435 await db . OpenAsync ( ) ;
263436 }
264437
265- var results = await db . CountAsync ( selectSpec . Predicate ) ;
438+ var filteredPredicate = SoftDeleteHelper . CombineWithNotDeletedFilter < TEntity > ( selectSpec . Predicate ) ;
439+ var results = await db . CountAsync ( filteredPredicate ) ;
266440 return results ;
267441 }
268442 catch ( ApplicationException exception )
@@ -292,7 +466,8 @@ public override async Task<long> GetCountAsync(Expression<Func<TEntity, bool>> e
292466 await db . OpenAsync ( ) ;
293467 }
294468
295- var results = await db . CountAsync ( expression ) ;
469+ var filteredExpression = SoftDeleteHelper . CombineWithNotDeletedFilter < TEntity > ( expression ) ;
470+ var results = await db . CountAsync ( filteredExpression ) ;
296471 return results ;
297472 }
298473 catch ( ApplicationException exception )
@@ -350,7 +525,8 @@ public override async Task<bool> AnyAsync(Expression<Func<TEntity, bool>> expres
350525 await db . OpenAsync ( ) ;
351526 }
352527
353- var results = await db . AnyAsync ( expression ) ;
528+ var filteredExpression = SoftDeleteHelper . CombineWithNotDeletedFilter < TEntity > ( expression ) ;
529+ var results = await db . AnyAsync ( filteredExpression ) ;
354530 return results ;
355531 }
356532 catch ( ApplicationException exception )
0 commit comments