Skip to content

Commit d5b3a53

Browse files
author
vp
committed
feat: Add bulk update and delete methods to IGenericRepository and EntityFrameworkGenericRepository
1 parent 42ae8a0 commit d5b3a53

41 files changed

Lines changed: 2445 additions & 79 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

tests/Infrastructure.IntegrationTests/EntityFramework/ActiveRecord/PMS-Design.md renamed to docs/designs/design-activeentity-pms.md

Lines changed: 45 additions & 3 deletions
Large diffs are not rendered by default.

docs/designs/design-domain-repositories-vnext.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ current issues:
1616

1717
improvements:
1818

19-
- easier to inherit/extend with new query methods. including that behaviors still work.
20-
- better support for transactions and result types.
21-
- native filtering support on repositories, no queryoptions needed then.
22-
-
19+
- easier to inherit/extend with new query methods. including that behaviors still work.
20+
- better support for transactions and result types.
21+
- native filtering support on repositories, no queryoptions needed then.
22+
- ef only repositories, no need to support in-memory or other providers, so we can use ef features like tracking, etc. and also easier to maintain.
23+
- better support for testing, no need to mock extension methods, etc.

docs/features-domain-repositories.md

Lines changed: 92 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,90 @@ var customerNames = await repository.ProjectAllAsync(
188188
cancellationToken);
189189
```
190190

191+
### Bulk Updates And Deletes
192+
193+
Use `UpdateSetAsync` and `DeleteSetAsync` for set-based operations that run directly in the repository provider without loading each entity instance first.
194+
195+
> Bulk operations are ideal for administrative tasks or background jobs that need to update or delete large numbers of entities without loading them into memory.
196+
197+
```csharp
198+
var affected = await repository.UpdateSetAsync(
199+
set => set
200+
.Set(c => c.IsActive, false)
201+
.Set(c => c.LastName, "Archived")
202+
.Set(c => c.LoginCount, c => c.LoginCount + 1),
203+
cancellationToken: cancellationToken);
204+
205+
var deleted = await repository.DeleteSetAsync(cancellationToken: cancellationToken);
206+
```
207+
208+
You can limit the affected rows with one or more specifications:
209+
210+
```csharp
211+
var affected = await repository.UpdateSetAsync(
212+
new CustomerIsInactiveSpecification(),
213+
set => set
214+
.Set(c => c.IsActive, false)
215+
.Set(c => c.LoginCount, c => c.LoginCount + 1),
216+
cancellationToken: cancellationToken);
217+
218+
var deleted = await repository.DeleteSetAsync(
219+
[
220+
new CustomerCountrySpecification("DE"),
221+
new CustomerIsInactiveSpecification()
222+
], cancellationToken: cancellationToken);
223+
```
224+
225+
Expression-based forwarding overloads are also available through `RepositoryExtensions`:
226+
227+
```csharp
228+
var affected = await repository.UpdateSetAsync(
229+
c => c.IsActive,
230+
set => set
231+
.Set(c => c.LastName, "Archived")
232+
.Set(c => c.LoginCount, c => c.LoginCount + 1),
233+
cancellationToken: cancellationToken);
234+
235+
var deleted = await repository.DeleteSetAsync(
236+
c => !c.IsActive,
237+
cancellationToken: cancellationToken);
238+
```
239+
240+
`FilterModel`-based extension overloads translate the query filters into specifications before forwarding them to the repository:
241+
242+
```csharp
243+
var filter = new FilterModel
244+
{
245+
Filters =
246+
[
247+
new FilterCriteria
248+
{
249+
Field = nameof(Customer.IsActive),
250+
Operator = FilterOperator.Equal,
251+
Value = false
252+
}
253+
]
254+
};
255+
256+
var affected = await repository.UpdateSetAsync(
257+
filter,
258+
set => set
259+
.Set(c => c.LastName, "Archived")
260+
.Set(c => c.LoginCount, c => c.LoginCount + 1),
261+
cancellationToken: cancellationToken);
262+
263+
var deleted = await repository.DeleteSetAsync(filter, cancellationToken: cancellationToken);
264+
```
265+
266+
#### Key Points For Bulk Set Operations
267+
268+
- Filtering for `UpdateSetAsync` and `DeleteSetAsync` comes from specification instances, including specifications generated from a `FilterModel`.
269+
- `FindOptions` continue to shape the query only; they do not define the `WHERE` clause.
270+
- `UpdateSetAsync` supports both constant assignments such as `.Set(c => c.IsActive, false)` and computed assignments such as `.Set(c => c.LoginCount, c => c.LoginCount + 1)`.
271+
- Only `EntityFrameworkGenericRepository<TEntity>` and `InMemoryRepository<TEntity>` currently provides a real implementation for repository bulk updates and deletes.
272+
- Other repository implementations expose the same API for consistency but currently throw `NotImplementedException`.
273+
- With Entity Framework, set-based operations execute directly in the database and do not synchronize already tracked entities in the current `DbContext`. If you need the updated database state immediately afterwards, re-query using `NoTracking` or use a fresh context/repository instance.
274+
191275
## Appendix A: Optimistic Concurrency Support
192276

193277
### Overview
@@ -202,12 +286,12 @@ sequenceDiagram
202286
203287
User1->>Repo: Get TodoItem (Version=A)
204288
User2->>Repo: Get TodoItem (Version=A)
205-
289+
206290
User1->>Repo: Update TodoItem
207291
Note over Repo: Generate new Version B
208292
Repo->>DB: Save (Version A→B)
209293
DB-->>Repo: Success
210-
294+
211295
User2->>Repo: Update TodoItem
212296
Note over Repo: Generate new Version C
213297
Repo->>DB: Save (Version A→C)
@@ -226,7 +310,7 @@ public class TodoItem : AuditableAggregateRoot<TodoItemId>, IConcurrency
226310
// Entity properties
227311
public string Title { get; set; }
228312
public TodoStatus Status { get; set; }
229-
313+
230314
// Concurrency token
231315
public Guid ConcurrencyVersion { get; set; }
232316
}
@@ -244,7 +328,7 @@ public class TodoItemEntityTypeConfiguration : IEntityTypeConfiguration<TodoItem
244328
builder.Property(e => e.ConcurrencyVersion)
245329
.IsConcurrencyToken()
246330
.ValueGeneratedOnAddOrUpdate();
247-
331+
248332
// Other configuration...
249333
}
250334
}
@@ -263,7 +347,7 @@ public class TodoItemEntityTypeConfiguration : IEntityTypeConfiguration<TodoItem
263347
```csharp
264348
public async Task UpdateTodoItemAsync(TodoItem item)
265349
{
266-
try
350+
try
267351
{
268352
await _repository.UpdateAsync(item);
269353
}
@@ -311,9 +395,9 @@ sequenceDiagram
311395
Gen->>DB: NEXT VALUE FOR OrderNumbers
312396
DB-->>Gen: 1001
313397
Gen-->>App: Result<long>.Success(1001)
314-
398+
315399
Note over App,DB: Thread-safe with internal locking
316-
400+
317401
App->>Gen: GetSequenceInfoAsync("OrderNumbers")
318402
Gen->>DB: Query metadata
319403
DB-->>Gen: {Current: 1001, Increment: 1, ...}
@@ -345,7 +429,7 @@ Register the appropriate generator for your database provider using the provided
345429
```csharp
346430
// In ConfigureServices
347431
services.AddDbContext<YourDbContext>(options => options.UseSqlServer(connectionString))
348-
.WithSequenceNumberGenerator(new SequenceNumberGeneratorOptions
432+
.WithSequenceNumberGenerator(new SequenceNumberGeneratorOptions
349433
{
350434
LockTimeout = TimeSpan.FromSeconds(60)
351435
});

src/Domain.Mediator/RepositoryDomainEventMediatorPublisherBehavior.cs

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,35 @@ public RepositoryDomainEventMediatorPublisherBehavior(
6666

6767
protected IGenericRepository<TEntity> Inner { get; }
6868

69+
/// <inheritdoc />
70+
public async Task<long> UpdateSetAsync(
71+
Action<IEntityUpdateSet<TEntity>> set,
72+
IFindOptions<TEntity> options = null,
73+
CancellationToken cancellationToken = default)
74+
{
75+
return await this.Inner.UpdateSetAsync(set, options, cancellationToken).AnyContext();
76+
}
77+
78+
/// <inheritdoc />
79+
public async Task<long> UpdateSetAsync(
80+
ISpecification<TEntity> specification,
81+
Action<IEntityUpdateSet<TEntity>> set,
82+
IFindOptions<TEntity> options = null,
83+
CancellationToken cancellationToken = default)
84+
{
85+
return await this.Inner.UpdateSetAsync(specification, set, options, cancellationToken).AnyContext();
86+
}
87+
88+
/// <inheritdoc />
89+
public async Task<long> UpdateSetAsync(
90+
IEnumerable<ISpecification<TEntity>> specifications,
91+
Action<IEntityUpdateSet<TEntity>> set,
92+
IFindOptions<TEntity> options = null,
93+
CancellationToken cancellationToken = default)
94+
{
95+
return await this.Inner.UpdateSetAsync(specifications, set, options, cancellationToken).AnyContext();
96+
}
97+
6998
public async Task<RepositoryActionResult> DeleteAsync(object id, CancellationToken cancellationToken = default)
7099
{
71100
var entity = await this.Inner
@@ -92,6 +121,32 @@ await entity.DomainEvents.GetAll()
92121
return result;
93122
}
94123

124+
/// <inheritdoc />
125+
public async Task<long> DeleteSetAsync(
126+
IFindOptions<TEntity> options = null,
127+
CancellationToken cancellationToken = default)
128+
{
129+
return await this.Inner.DeleteSetAsync(options, cancellationToken).AnyContext();
130+
}
131+
132+
/// <inheritdoc />
133+
public async Task<long> DeleteSetAsync(
134+
ISpecification<TEntity> specification,
135+
IFindOptions<TEntity> options = null,
136+
CancellationToken cancellationToken = default)
137+
{
138+
return await this.Inner.DeleteSetAsync(specification, options, cancellationToken).AnyContext();
139+
}
140+
141+
/// <inheritdoc />
142+
public async Task<long> DeleteSetAsync(
143+
IEnumerable<ISpecification<TEntity>> specifications,
144+
IFindOptions<TEntity> options = null,
145+
CancellationToken cancellationToken = default)
146+
{
147+
return await this.Inner.DeleteSetAsync(specifications, options, cancellationToken).AnyContext();
148+
}
149+
95150
public async Task<TEntity> FindOneAsync(
96151
object id,
97152
IFindOptions<TEntity> options = null,
@@ -232,4 +287,4 @@ public async Task<long> CountAsync(
232287
{
233288
return await this.Inner.CountAsync(specifications, cancellationToken).AnyContext();
234289
}
235-
}
290+
}

src/Domain/Repositories/Behaviors/RepositoryAuditStateBehavior.cs

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,35 @@ public RepositoryAuditStateBehavior(IGenericRepository<TEntity> ínner)
5151

5252
protected IGenericRepository<TEntity> Inner { get; }
5353

54+
/// <inheritdoc />
55+
public async Task<long> UpdateSetAsync(
56+
Action<IEntityUpdateSet<TEntity>> set,
57+
IFindOptions<TEntity> options = null,
58+
CancellationToken cancellationToken = default)
59+
{
60+
return await this.Inner.UpdateSetAsync(set, options, cancellationToken).AnyContext();
61+
}
62+
63+
/// <inheritdoc />
64+
public async Task<long> UpdateSetAsync(
65+
ISpecification<TEntity> specification,
66+
Action<IEntityUpdateSet<TEntity>> set,
67+
IFindOptions<TEntity> options = null,
68+
CancellationToken cancellationToken = default)
69+
{
70+
return await this.Inner.UpdateSetAsync(specification, set, options, cancellationToken).AnyContext();
71+
}
72+
73+
/// <inheritdoc />
74+
public async Task<long> UpdateSetAsync(
75+
IEnumerable<ISpecification<TEntity>> specifications,
76+
Action<IEntityUpdateSet<TEntity>> set,
77+
IFindOptions<TEntity> options = null,
78+
CancellationToken cancellationToken = default)
79+
{
80+
return await this.Inner.UpdateSetAsync(specifications, set, options, cancellationToken).AnyContext();
81+
}
82+
5483
public async Task<RepositoryActionResult> DeleteAsync(object id, CancellationToken cancellationToken = default)
5584
{
5685
if (id != default && this.options.SoftDeleteEnabled)
@@ -83,6 +112,32 @@ public async Task<RepositoryActionResult> DeleteAsync(TEntity entity, Cancellati
83112
return await this.Inner.DeleteAsync(entity, cancellationToken).AnyContext();
84113
}
85114

115+
/// <inheritdoc />
116+
public async Task<long> DeleteSetAsync(
117+
IFindOptions<TEntity> options = null,
118+
CancellationToken cancellationToken = default)
119+
{
120+
return await this.Inner.DeleteSetAsync(options, cancellationToken).AnyContext();
121+
}
122+
123+
/// <inheritdoc />
124+
public async Task<long> DeleteSetAsync(
125+
ISpecification<TEntity> specification,
126+
IFindOptions<TEntity> options = null,
127+
CancellationToken cancellationToken = default)
128+
{
129+
return await this.Inner.DeleteSetAsync(specification, options, cancellationToken).AnyContext();
130+
}
131+
132+
/// <inheritdoc />
133+
public async Task<long> DeleteSetAsync(
134+
IEnumerable<ISpecification<TEntity>> specifications,
135+
IFindOptions<TEntity> options = null,
136+
CancellationToken cancellationToken = default)
137+
{
138+
return await this.Inner.DeleteSetAsync(specifications, options, cancellationToken).AnyContext();
139+
}
140+
86141
public async Task<bool> ExistsAsync(object id, CancellationToken cancellationToken = default)
87142
{
88143
var entity = await this.Inner.FindOneAsync(id, cancellationToken: cancellationToken).AnyContext();
@@ -273,4 +328,4 @@ public enum AuditStateByType
273328
ByUserId = 0,
274329
ByUserName = 1,
275330
ByEmail = 2
276-
}
331+
}

0 commit comments

Comments
 (0)