Skip to content

Commit c91588b

Browse files
Feature/repo soft delete extras (#157)
* Added soft delete capabilities to repository pattern. * Updated gitigore --------- Co-authored-by: jasonmwebb-lv <jason.webb@leadventure.com>
1 parent 843ff0d commit c91588b

21 files changed

Lines changed: 2510 additions & 81 deletions

File tree

.claude/settings.local.json

Lines changed: 0 additions & 37 deletions
This file was deleted.

.gitignore

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,3 @@ ASALocalRun/
342342
# BeatPulse healthcheck temp database
343343
healthchecksdb
344344
/Src/RCommon.Persistence.EfCore/IEFCoreConfiguration.cs
345-
/.claude
346-
.claude/settings.local.json
347-
.claude/settings.local.json

Src/RCommon.Dapper/Crud/DapperRepository.cs

Lines changed: 183 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)