Skip to content

Commit 889c04a

Browse files
authored
Merge pull request #19 from koenbeuk/feature/before-save-changes-multiple-calls
Feature/before save changes multiple calls
2 parents 3a0a26d + 3196fd6 commit 889c04a

File tree

9 files changed

+193
-26
lines changed

9 files changed

+193
-26
lines changed

src/EntityFrameworkCore.Triggered.Abstractions/ITriggerSession.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ public interface ITriggerSession
2222
/// </summary>
2323
Task RaiseBeforeSaveTriggers(CancellationToken cancellationToken = default);
2424
/// <summary>
25+
/// Makes a snapshot of all changes in the DbContext and invokes BeforeSaveTriggers recursively based on the recursive settings until all changes have been processed
26+
/// </summary>
27+
/// <param name="skipDetectedChanges">Allows BeforeSaveTriggers not to include previously detected changes. Only new changes will be detected and fired upon. This is useful in case of multiple calls to RaiseBeforeSaveTriggers</param>
28+
Task RaiseBeforeSaveTriggers(bool skipDetectedChanges, CancellationToken cancellationToken = default);
29+
/// <summary>
2530
/// Invokes AfterSaveTriggers non-recursively. Calling this method expects that either RaiseBeforeSaveTriggers() or DiscoverChanges() has called
2631
/// </summary>
2732
/// <returns></returns>

src/EntityFrameworkCore.Triggered/Internal/RecursiveTriggerContextDiscoveryStrategy.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,12 @@ public class RecursiveTriggerContextDiscoveryStrategy : ITriggerContextDiscovery
2323
"Discovered changes: {changes} for {name}. Iteration ({iteration}/{maxRecursion})");
2424

2525
readonly string _name;
26+
readonly bool _skipDetectedChanges;
2627

27-
public RecursiveTriggerContextDiscoveryStrategy(string name)
28+
public RecursiveTriggerContextDiscoveryStrategy(string name, bool skipDetectedChanges)
2829
{
2930
_name = name ?? throw new ArgumentNullException(nameof(name));
31+
_skipDetectedChanges = skipDetectedChanges;
3032
}
3133

3234
public IEnumerable<ITriggerContextDescriptor> Discover(TriggerOptions options, TriggerContextTracker tracker, ILogger logger)
@@ -45,7 +47,7 @@ public IEnumerable<ITriggerContextDescriptor> Discover(TriggerOptions options, T
4547
IEnumerable<ITriggerContextDescriptor> changes = tracker.DiscoverChanges().ToList();
4648

4749
// In case someone made a call to TriggerSession.DetectChanges, prior to calling RaiseBeforeSaveTriggers, we want to make sure that we include that discovery result in the first iteration
48-
if (iteration == 0)
50+
if (iteration == 0 && !_skipDetectedChanges)
4951
{
5052
changes = tracker.DiscoveredChanges.ToList();
5153
}

src/EntityFrameworkCore.Triggered/TriggerSession.cs

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,16 @@ namespace EntityFrameworkCore.Triggered
1515
public class TriggerSession : ITriggerSession
1616
{
1717
static ITriggerContextDiscoveryStrategy? _beforeSaveTriggerContextDiscoveryStrategy;
18+
static ITriggerContextDiscoveryStrategy? _beforeSaveTriggerContextDiscoveryStrategyWithSkipDetectedChanges; // To satisfy RaiseBeforeSaveTrigger's overload
1819
static ITriggerContextDiscoveryStrategy? _afterSaveTriggerContextDiscoveryStrategy;
1920

2021
readonly TriggerOptions _options;
2122
readonly ITriggerDiscoveryService _triggerDiscoveryService;
2223
readonly TriggerContextTracker _tracker;
2324
readonly ILogger<TriggerSession> _logger;
2425

26+
bool _raiseBeforeSaveTriggersCalled;
27+
2528
public TriggerSession(TriggerOptions options, ITriggerDiscoveryService triggerDiscoveryService, TriggerContextTracker tracker, ILogger<TriggerSession> logger)
2629
{
2730
_options = options ?? throw new ArgumentNullException(nameof(options));
@@ -62,15 +65,36 @@ public async Task RaiseTriggers(Type openTriggerType, ITriggerContextDiscoverySt
6265
}
6366
}
6467

65-
6668
public Task RaiseBeforeSaveTriggers(CancellationToken cancellationToken)
69+
=> RaiseBeforeSaveTriggers(_raiseBeforeSaveTriggersCalled, cancellationToken);
70+
71+
public Task RaiseBeforeSaveTriggers(bool skipDetectedChanges, CancellationToken cancellationToken)
6772
{
68-
if (_beforeSaveTriggerContextDiscoveryStrategy == null)
73+
_raiseBeforeSaveTriggersCalled = true;
74+
75+
ITriggerContextDiscoveryStrategy? strategy;
76+
77+
if (skipDetectedChanges)
6978
{
70-
_beforeSaveTriggerContextDiscoveryStrategy = new RecursiveTriggerContextDiscoveryStrategy("BeforeSave");
79+
if (_beforeSaveTriggerContextDiscoveryStrategyWithSkipDetectedChanges == null)
80+
{
81+
_beforeSaveTriggerContextDiscoveryStrategyWithSkipDetectedChanges = new RecursiveTriggerContextDiscoveryStrategy("BeforeSave", true);
82+
}
83+
84+
strategy = _beforeSaveTriggerContextDiscoveryStrategyWithSkipDetectedChanges;
85+
}
86+
else
87+
{
88+
if (_beforeSaveTriggerContextDiscoveryStrategy == null)
89+
{
90+
_beforeSaveTriggerContextDiscoveryStrategy = new RecursiveTriggerContextDiscoveryStrategy("BeforeSave", false);
91+
}
92+
93+
strategy = _beforeSaveTriggerContextDiscoveryStrategy;
7194
}
7295

73-
return RaiseTriggers(typeof(IBeforeSaveTrigger<>), _beforeSaveTriggerContextDiscoveryStrategy, entityType => new BeforeSaveTriggerDescriptor(entityType), cancellationToken);
96+
_raiseBeforeSaveTriggersCalled = true;
97+
return RaiseTriggers(typeof(IBeforeSaveTrigger<>), strategy, entityType => new BeforeSaveTriggerDescriptor(entityType), cancellationToken);
7498
}
7599

76100
public Task RaiseAfterSaveTriggers(CancellationToken cancellationToken = default)

src/EntityFrameworkCore.Triggered/TriggeredDbContext.cs

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ namespace EntityFrameworkCore.Triggered
1010
{
1111
public abstract class TriggeredDbContext : DbContext
1212
{
13+
private ITriggerSession? _triggerSession;
14+
1315
protected TriggeredDbContext()
1416
: this(new DbContextOptions<DbContext>())
1517
{
@@ -32,28 +34,53 @@ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
3234

3335
public override int SaveChanges(bool acceptAllChangesOnSuccess)
3436
{
35-
var triggerService = this.GetService<ITriggerService>() ?? throw new InvalidOperationException("Triggers are not configured");
37+
if (_triggerSession == null)
38+
{
39+
var triggerService = this.GetService<ITriggerService>() ?? throw new InvalidOperationException("Triggers are not configured");
40+
_triggerSession = triggerService.CreateSession(this);
41+
}
3642

37-
var triggerSession = triggerService.CreateSession(this);
43+
try
44+
{
3845

39-
triggerSession.RaiseBeforeSaveTriggers(default).GetAwaiter().GetResult();
40-
var result = base.SaveChanges(acceptAllChangesOnSuccess);
41-
triggerSession.RaiseAfterSaveTriggers(default).GetAwaiter().GetResult();
46+
_triggerSession.RaiseBeforeSaveTriggers(default).GetAwaiter().GetResult();
47+
var result = base.SaveChanges(acceptAllChangesOnSuccess);
48+
_triggerSession.RaiseAfterSaveTriggers(default).GetAwaiter().GetResult();
4249

43-
return result;
50+
return result;
51+
}
52+
finally
53+
{
54+
_triggerSession = null;
55+
}
4456
}
4557

4658
public override async Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default)
4759
{
48-
var triggerService = this.GetService<ITriggerService>() ?? throw new InvalidOperationException("Triggers are not configured");
60+
bool createdTriggerSession = false;
4961

50-
var triggerSession = triggerService.CreateSession(this);
62+
if (_triggerSession == null)
63+
{
64+
var triggerService = this.GetService<ITriggerService>() ?? throw new InvalidOperationException("Triggers are not configured");
65+
_triggerSession = triggerService.CreateSession(this);
66+
createdTriggerSession = true;
67+
}
68+
try
69+
{
5170

52-
await triggerSession.RaiseBeforeSaveTriggers(cancellationToken).ConfigureAwait(false);
53-
var result = base.SaveChanges(acceptAllChangesOnSuccess);
54-
await triggerSession.RaiseAfterSaveTriggers(cancellationToken).ConfigureAwait(false);
71+
await _triggerSession.RaiseBeforeSaveTriggers(cancellationToken).ConfigureAwait(false);
72+
var result = base.SaveChanges(acceptAllChangesOnSuccess);
73+
await _triggerSession.RaiseAfterSaveTriggers(cancellationToken).ConfigureAwait(false);
5574

56-
return result;
75+
return result;
76+
}
77+
finally
78+
{
79+
if (createdTriggerSession)
80+
{
81+
_triggerSession = null;
82+
}
83+
}
5784
}
5885
}
5986
}

test/EntityFrameworkCore.Triggered.Tests/Internal/RecursiveTriggerContextDiscoveryStrategyTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
3939
public void DiscoverChanges_MultipleCalls_ReturnsDeltaOfChanges()
4040
{
4141
using var dbContext = new TestDbContext();
42-
var subject = new RecursiveTriggerContextDiscoveryStrategy("test");
42+
var subject = new RecursiveTriggerContextDiscoveryStrategy("test", false);
4343
var triggerContextTracker = new TriggerContextTracker(dbContext.ChangeTracker, new EntityAndTypeRecursionStrategy());
4444
triggerContextTracker.DiscoverChanges().Count();
4545
var initialContextDescriptors = subject.Discover(new TriggerOptions { }, triggerContextTracker, new NullLogger<object>()).ToList();

test/EntityFrameworkCore.Triggered.Tests/Stubs/TriggerSessionStub.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,11 @@ public Task RaiseBeforeSaveTriggers(CancellationToken cancellationToken = defaul
3030
RaiseBeforeSaveTriggersCalls += 1;
3131
return Task.CompletedTask;
3232
}
33+
34+
public Task RaiseBeforeSaveTriggers(bool skipDetectedChanges = false, CancellationToken cancellationToken = default)
35+
{
36+
RaiseBeforeSaveTriggersCalls += 1;
37+
return Task.CompletedTask;
38+
}
3339
}
3440
}

test/EntityFrameworkCore.Triggered.Tests/Stubs/TriggerStub.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,26 @@ public class TriggerStub<TEntity> : IBeforeSaveTrigger<TEntity>, IAfterSaveTrigg
1515
public ICollection<ITriggerContext<TEntity>> AfterSaveInvocations { get; } = new List<ITriggerContext<TEntity>>();
1616
public int Priority { get; set; }
1717

18+
public Func<ITriggerContext<TEntity>, CancellationToken, Task> BeforeSaveHandler { get; set; }
19+
public Func<ITriggerContext<TEntity>, CancellationToken, Task> AfterSaveHandler { get; set; }
20+
1821
public TriggerStub()
1922
{
2023

2124
}
2225

2326
public Task BeforeSave(ITriggerContext<TEntity> context, CancellationToken cancellationToken)
2427
{
28+
BeforeSaveHandler?.Invoke(context, cancellationToken);
29+
2530
BeforeSaveInvocations.Add(context);
2631
return Task.CompletedTask;
2732
}
2833

2934
public Task AfterSave(ITriggerContext<TEntity> context, CancellationToken cancellationToken)
3035
{
36+
AfterSaveHandler?.Invoke(context, cancellationToken);
37+
3138
AfterSaveInvocations.Add(context);
3239
return Task.CompletedTask;
3340
}

test/EntityFrameworkCore.Triggered.Tests/TriggerSessionTests.cs

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using Microsoft.EntityFrameworkCore;
88
using Microsoft.EntityFrameworkCore.Infrastructure;
99
using Microsoft.Extensions.DependencyInjection;
10+
using Microsoft.Extensions.Logging;
1011
using Xunit;
1112

1213
namespace EntityFrameworkCore.Triggered.Tests
@@ -29,6 +30,7 @@ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
2930
{
3031
base.OnConfiguring(optionsBuilder);
3132

33+
optionsBuilder.EnableServiceProviderCaching(false);
3234
optionsBuilder.UseInMemoryDatabase("test");
3335
optionsBuilder.UseTriggers(triggerOptions =>
3436
{
@@ -100,8 +102,7 @@ public async Task RaiseAfterSaveTriggers_RaisesOnceOnSimpleAddition()
100102
using var context = new TestDbContext();
101103
var subject = CreateSubject(context);
102104

103-
context.TestModels.Add(new TestModel
104-
{
105+
context.TestModels.Add(new TestModel {
105106
Id = Guid.NewGuid(),
106107
Name = "test1"
107108
});
@@ -146,5 +147,64 @@ public async Task RaiseAfterSaveTriggers_ThrowsOnCancelledException()
146147

147148
await Assert.ThrowsAsync<OperationCanceledException>(async () => await subject.RaiseAfterSaveTriggers(cancellationTokenSource.Token));
148149
}
150+
151+
[Fact]
152+
public async Task RaiseBeforeSaveTriggers_RecursiveCall_SkipsDiscoveredChanges()
153+
{
154+
using var context = new TestDbContext();
155+
var subject = CreateSubject(context);
156+
157+
context.TriggerStub.BeforeSaveHandler = (_1, _2) => {
158+
if (context.TriggerStub.BeforeSaveInvocations.Count > 1)
159+
{
160+
return Task.CompletedTask;
161+
}
162+
return subject.RaiseBeforeSaveTriggers(default);
163+
};
164+
165+
context.TestModels.Add(new TestModel {
166+
Id = Guid.NewGuid(),
167+
Name = "test1"
168+
});
169+
170+
subject.DiscoverChanges();
171+
await subject.RaiseBeforeSaveTriggers();
172+
173+
Assert.NotEmpty(context.TriggerStub.BeforeSaveInvocations);
174+
}
175+
176+
[Fact]
177+
public async Task RaiseBeforeSaveTriggers_SkipDetectedChangesAsTrue_ExcludesDetectedChanges()
178+
{
179+
using var context = new TestDbContext();
180+
var subject = CreateSubject(context);
181+
182+
context.TestModels.Add(new TestModel {
183+
Id = Guid.NewGuid(),
184+
Name = "test1"
185+
});
186+
187+
subject.DiscoverChanges();
188+
await subject.RaiseBeforeSaveTriggers(true);
189+
190+
Assert.Empty(context.TriggerStub.BeforeSaveInvocations);
191+
}
192+
193+
[Fact]
194+
public async Task RaiseBeforeSaveTriggers_SkipDetectedChangesAsFalse_IncludesDetectedChanges()
195+
{
196+
using var context = new TestDbContext();
197+
var subject = CreateSubject(context);
198+
199+
context.TestModels.Add(new TestModel {
200+
Id = Guid.NewGuid(),
201+
Name = "test1"
202+
});
203+
204+
subject.DiscoverChanges();
205+
await subject.RaiseBeforeSaveTriggers();
206+
207+
Assert.NotEmpty(context.TriggerStub.BeforeSaveInvocations);
208+
}
149209
}
150210
}

test/EntityFrameworkCore.Triggered.Tests/TriggeredDbContextTests.cs

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Collections.Generic;
33
using System.Linq;
44
using System.Text;
5+
using System.Threading;
56
using System.Threading.Tasks;
67
using EntityFrameworkCore.Triggered.Tests.Stubs;
78
using Microsoft.EntityFrameworkCore;
@@ -20,6 +21,13 @@ class TestModel
2021

2122
class TestDbContext : TriggeredDbContext
2223
{
24+
readonly bool _stubService;
25+
26+
public TestDbContext(bool stubService)
27+
{
28+
_stubService = stubService;
29+
}
30+
2331
public TriggerStub<TestModel> TriggerStub { get; } = new TriggerStub<TestModel>();
2432

2533
public DbSet<TestModel> TestModels { get; set; }
@@ -32,12 +40,16 @@ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
3240
optionsBuilder.UseTriggers(triggerOptions => {
3341
triggerOptions.AddTrigger(TriggerStub);
3442
});
35-
optionsBuilder.ReplaceService<ITriggerService, TriggerServiceStub>();
43+
44+
if (_stubService)
45+
{
46+
optionsBuilder.ReplaceService<ITriggerService, TriggerServiceStub>();
47+
}
3648
}
3749
}
3850

39-
TestDbContext CreateSubject()
40-
=> new TestDbContext();
51+
TestDbContext CreateSubject(bool stubService = true)
52+
=> new TestDbContext(stubService);
4153

4254
[Fact]
4355
public void SaveChanges_CreatesChangeHandlerSession()
@@ -73,10 +85,34 @@ public async Task SaveChangesAsync_CreatesChangeHandlerSession()
7385
public async Task SaveChangesAsyncWithAccept_CreatesChangeHandlerSession()
7486
{
7587
var subject = CreateSubject();
76-
var TriggersessionStub = (TriggerServiceStub)subject.GetService<ITriggerService>();
88+
var triggerSessionStub = (TriggerServiceStub)subject.GetService<ITriggerService>();
7789

7890
await subject.SaveChangesAsync(true);
79-
Assert.Equal(1, TriggersessionStub.CreateSessionCalls);
91+
Assert.Equal(1, triggerSessionStub.CreateSessionCalls);
92+
}
93+
94+
[Fact]
95+
public async Task SaveChangesAsync_RecursiveCall_ReturnsActiveTriggerSession()
96+
{
97+
var subject = CreateSubject(false);
98+
99+
subject.TriggerStub.BeforeSaveHandler = (_1, _2) => {
100+
if (subject.TriggerStub.BeforeSaveInvocations.Count > 1)
101+
{
102+
return Task.CompletedTask;
103+
}
104+
105+
return subject.SaveChangesAsync();
106+
};
107+
108+
subject.TestModels.Add(new TestModel {
109+
Id = Guid.NewGuid(),
110+
Name = "test1"
111+
});
112+
113+
await subject.SaveChangesAsync();
114+
115+
Assert.Equal(1, subject.TriggerStub.BeforeSaveInvocations.Count);
80116
}
81117
}
82118
}

0 commit comments

Comments
 (0)