Skip to content

Commit ca4e916

Browse files
committed
Merged with master
2 parents dd2e2ce + c48fc4f commit ca4e916

33 files changed

+270
-183
lines changed

README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -90,15 +90,15 @@ public class Startup
9090
}
9191
```
9292

93-
### Recursion (Cascading changes)
94-
`BeforeSaveTrigger<TEntity>` supports recursion. This is useful since it allows your triggers to subsequently modify the same DbContext entity graph and have it raise additional triggers. By default this behavior is turned on and protected from infinite loops by limiting the number of recursion runs. If you don't like this behavior or want to change it, you can do so by:
93+
### Cascading changes (previously called Recursion)
94+
`BeforeSaveTrigger<TEntity>` supports cascading triggers. This is useful since it allows your triggers to subsequently modify the same DbContext entity graph and have it raise additional triggers. By default this behavior is turned on and protected from infinite loops by limiting the number of cascading cycles. If you don't like this behavior or want to change it, you can do so by:
9595
```csharp
9696
optionsBuilder.UseTriggers(triggerOptions => {
97-
triggerOptions.RecursionMode(RecursionMode.EntityAndType).MaxRecusion(20)
97+
triggerOptions.CascadeBehavior(CascadeBehavior.EntityAndType).MaxRecusion(20)
9898
})
9999
```
100100

101-
Currently there are 2 types of recursion strategies out of the box, with the support to providing your own: `NoRecursion` and `EntityAndTypeRecursion` (default). The former simply disables recursion whereas the latter recursively detects changes and raises triggers for those changes for as long as the combination of the Entity and the change type is unique. `EntityAndTypeRecursion` is the recommended and default recursion strategy.
101+
Currently there are 2 types of cascading strategies out of the box, with the support to providing your own: `NoCascade` and `EntityAndType` (default). The former simply disables cascading whereas the latter cascades triggers for as long as the combination of the Entity and the change type is unique. `EntityAndType` is the recommended and default cascading strategy.
102102

103103
### Inheritance
104104
Triggers support inheritance and sort execution of these triggers based on least concrete to most concrete. Given the following example:
@@ -124,7 +124,7 @@ Triggers shine in combination with DI. When configured, triggers are resolved fr
124124
- DEPRECATED: Use one of our integration packages. Currently we only support direct integration with ASP.NET Core through the [EntityFrameworkCore.Triggered.AspNetCore](https://www.nuget.org/packages/EntityFrameworkCore.Triggered.AspNetCore/) nuget package. This will use the IHttpContextAccessor to access the scoped ServiceProvider of the current request. To use this, please make sure to either call: `services.AddAspNetCoreTriggeredDbContext<MyTriggeredDbContext>()` or `triggerOptions.UseAspNetCoreIntegration()`.
125125

126126
### Error handling
127-
In some cases, you want to be triggered when a DbUpdateException occurs. For this purpose we have `IAfterSaveFailedTrigger<TEntity>`. This gets triggered for all entities as part of the change set when DbContext.SaveChanges raises a DbUpdateException. The handling method: `AfterSaveFailed` in turn gets called with the trigger context containing the entity as well as the exception. You may attempt to call `DbContext.SaveChanges` again from within this trigger. This will not raise triggers that are already raised and only raise triggers that have since become relevant (based on recursive configuration).
127+
In some cases, you want to be triggered when a DbUpdateException occurs. For this purpose we have `IAfterSaveFailedTrigger<TEntity>`. This gets triggered for all entities as part of the change set when DbContext.SaveChanges raises a DbUpdateException. The handling method: `AfterSaveFailed` in turn gets called with the trigger context containing the entity as well as the exception. You may attempt to call `DbContext.SaveChanges` again from within this trigger. This will not raise triggers that are already raised and only raise triggers that have since become relevant (based on the cascading configuration).
128128

129129
### Transactions
130130
Many database providers support the concept of a Transaction. By default when using SqlServer with EntityFrameworkCore, any call to SaveChanges will be wrapped in a transaction. Any changes made in `IBeforeSaveTrigger<TEntity>` will be included within the transaction and changes made in `IAfterSaveTrigger<TEntity>` will not. However, it is possible for the user to [explicitly control transactions](https://docs.microsoft.com/en-us/ef/core/saving/transactions). Triggers are extensible and one such extension are [Transactional Triggers](https://www.nuget.org/packages/EntityFrameworkCore.Triggered.Transactions/). In order to use this plugin you will have to implement a few steps:
@@ -174,4 +174,4 @@ public class ApplicationDbContext : DbContext {
174174

175175
### Similar products
176176
- [Ramses](https://github.com/JValck/Ramses): Lifecycle hooks for EFCore. A simple yet effective way of reacting to changes. Great for situations where you simply want to make sure that a property is set before saving to the database. Limited though in features as there is no dependency injection, no async support, no extensibility model and lifecycle hooks need to be implemented on the entity type itself.
177-
- [EntityFramework.Triggers](https://github.com/NickStrupat/EntityFramework.Triggers). Add triggers to your entities with insert, update, and delete events. There are three events for each: before, after, and upon failure. A fine alternative to EntityFrameworkCore.Triggered. It has been around for some time and has support for EF6 and boast a decent community. There are plenty of trigger types to opt into including the option to cancel SaveChanges from within a trigger. A big drawback however is that it does not support recursion so that triggers can never be relied on to enforce a domain constraint.
177+
- [EntityFramework.Triggers](https://github.com/NickStrupat/EntityFramework.Triggers). Add triggers to your entities with insert, update, and delete events. There are three events for each: before, after, and upon failure. A fine alternative to EntityFrameworkCore.Triggered. It has been around for some time and has support for EF6 and boast a decent community. There are plenty of trigger types to opt into including the option to cancel SaveChanges from within a trigger. A big drawback however is that it does not support cascading triggers so that triggers can never be relied on to enforce a domain constraint.

samples/v2/2 - PrimarySchool/README.MD

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Hello world
2-
A sample application showing triggers using dependency injection and causing recursion.
2+
A sample application showing triggers using dependency injection and cascading triggers.
33

44
## Triggers
55

src/EntityFrameworkCore.Triggered.Abstractions/ITriggerSession.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,11 @@ public interface ITriggerSession : IDisposable
1717
Task RaiseAfterSaveStartingTriggers(CancellationToken cancellationToken = default);
1818
Task RaiseAfterSaveCompletedTriggers(CancellationToken cancellationToken = default);
1919
/// <summary>
20-
/// Makes a snapshot of all changes in the DbContext and invokes BeforeSaveTriggers recursively based on the recursive settings until all changes have been processed
20+
/// Makes a snapshot of all changes in the DbContext and invokes BeforeSaveTriggers while detecting and cascading based on the cascade settings until all changes have been processed
2121
/// </summary>
2222
Task RaiseBeforeSaveTriggers(CancellationToken cancellationToken = default);
2323
/// <summary>
24-
/// Makes a snapshot of all changes in the DbContext and invokes BeforeSaveTriggers recursively based on the recursive settings until all changes have been processed
24+
/// Makes a snapshot of all changes in the DbContext and invokes BeforeSaveTriggers while detecting and cascading based on the cascade settings until all changes have been processed
2525
/// </summary>
2626
/// <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>
2727
Task RaiseBeforeSaveTriggers(bool skipDetectedChanges, CancellationToken cancellationToken = default);
@@ -30,11 +30,11 @@ public interface ITriggerSession : IDisposable
3030
/// </summary>
3131
void CaptureDiscoveredChanges();
3232
/// <summary>
33-
/// Invokes AfterSaveTriggers non-recursively. Calling this method expects that either RaiseBeforeSaveTriggers() or DiscoverChanges() has been called
33+
/// Invokes AfterSaveTriggers. Calling this method expects that either RaiseBeforeSaveTriggers() or DiscoverChanges() has been called
3434
/// </summary>
3535
Task RaiseAfterSaveTriggers(CancellationToken cancellationToken = default);
3636
/// <summary>
37-
/// Invokes AfterSaveFailedTriggers non-recursively. Calling this method expects that either RaiseBeforeSaveTriggers() or DiscoverChanges() has been called
37+
/// Invokes AfterSaveFailedTriggers. Calling this method expects that either RaiseBeforeSaveTriggers() or DiscoverChanges() has been called
3838
/// </summary>
3939
Task RaiseAfterSaveFailedTriggers(Exception exception, CancellationToken cancellationToken = default);
4040
}

src/EntityFrameworkCore.Triggered.Transactions/TriggeredSessionExtensions.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public static Task RaiseBeforeCommitTriggers(this ITriggerSession triggerSession
2626

2727
if (_beforeCommitTriggerContextDiscoveryStrategy == null)
2828
{
29-
_beforeCommitTriggerContextDiscoveryStrategy = new NonRecursiveTriggerContextDiscoveryStrategy("BeforeCommit");
29+
_beforeCommitTriggerContextDiscoveryStrategy = new NonCascadingTriggerContextDiscoveryStrategy("BeforeCommit");
3030
}
3131

3232

@@ -42,7 +42,7 @@ public static Task RaiseAfterCommitTriggers(this ITriggerSession triggerSession,
4242

4343
if (_afterCommitTriggerContextDiscoveryStrategy == null)
4444
{
45-
_afterCommitTriggerContextDiscoveryStrategy = new NonRecursiveTriggerContextDiscoveryStrategy("AfterCommit");
45+
_afterCommitTriggerContextDiscoveryStrategy = new NonCascadingTriggerContextDiscoveryStrategy("AfterCommit");
4646
}
4747

4848
return ((TriggerSession)triggerSession).RaiseTriggers(typeof(IAfterCommitTrigger<>), null, _afterCommitTriggerContextDiscoveryStrategy, entityType => new AfterCommitTriggerDescriptor(entityType), cancellationToken);
@@ -56,7 +56,7 @@ public static Task RaiseBeforeRollbackTriggers(this ITriggerSession triggerSessi
5656

5757
if (_beforeRollbackTriggerContextDiscoveryStrategy == null)
5858
{
59-
_beforeRollbackTriggerContextDiscoveryStrategy = new NonRecursiveTriggerContextDiscoveryStrategy("BeforeRollback");
59+
_beforeRollbackTriggerContextDiscoveryStrategy = new NonCascadingTriggerContextDiscoveryStrategy("BeforeRollback");
6060
}
6161

6262
return ((TriggerSession)triggerSession).RaiseTriggers(typeof(IBeforeRollbackTrigger<>), null, _beforeRollbackTriggerContextDiscoveryStrategy, entityType => new BeforeRollbackTriggerDescriptor(entityType), cancellationToken);
@@ -72,7 +72,7 @@ public static Task RaiseAfterRollbackTriggers(this ITriggerSession triggerSessio
7272

7373
if (_afterRollbackTriggerContextDiscoveryStrategy == null)
7474
{
75-
_afterRollbackTriggerContextDiscoveryStrategy = new NonRecursiveTriggerContextDiscoveryStrategy("AfterRollback");
75+
_afterRollbackTriggerContextDiscoveryStrategy = new NonCascadingTriggerContextDiscoveryStrategy("AfterRollback");
7676
}
7777

7878

src/EntityFrameworkCore.Triggered/Infrastructure/RecursionMode.cs renamed to src/EntityFrameworkCore.Triggered/Infrastructure/CascadeBehavior.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
namespace EntityFrameworkCore.Triggered.Infrastructure
22
{
3-
public enum RecursionMode
3+
public enum CascadeBehavior
44
{
55
/// <summary>
6-
/// Disables recursion. Any changes made in <see cref="EntityFrameworkCore.Triggered.IBeforeSaveTrigger{TEntity}"/> will not raise additional triggers
6+
/// Disables cascading. Any changes made in <see cref="EntityFrameworkCore.Triggered.IBeforeSaveTrigger{TEntity}"/> will not raise additional triggers
77
/// </summary>
88
/// <remarks>
9-
/// No recursion is often not desired since it puts a soft restriction on <see cref="EntityFrameworkCore.Triggered.IBeforeSaveTrigger{TEntity}"/>.
9+
/// No cascading is often not desired since it puts a soft restriction on <see cref="EntityFrameworkCore.Triggered.IBeforeSaveTrigger{TEntity}"/>.
1010
/// </remarks>
1111
None,
1212
/// <summary>

src/EntityFrameworkCore.Triggered/Infrastructure/Internal/TriggersOptionExtension.cs

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
using System.Collections.Generic;
33
using System.Linq;
44
using EntityFrameworkCore.Triggered.Internal;
5-
using EntityFrameworkCore.Triggered.Internal.RecursionStrategy;
5+
using EntityFrameworkCore.Triggered.Internal.CascadeStrategies;
66
using EntityFrameworkCore.Triggered.Lyfecycles;
77
using Microsoft.EntityFrameworkCore.Diagnostics;
88
using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -61,8 +61,8 @@ public override long GetServiceProviderHashCode()
6161
}
6262
}
6363

64-
hashCode ^= extension._maxRecursion.GetHashCode();
65-
hashCode ^= extension._recursionMode.GetHashCode();
64+
hashCode ^= extension._maxCascadeCycles.GetHashCode();
65+
hashCode ^= extension._cascadeBehavior.GetHashCode();
6666

6767
if (extension._serviceProviderTransform != null)
6868
{
@@ -84,16 +84,16 @@ public override void PopulateDebugInfo(IDictionary<string, string> debugInfo)
8484

8585
debugInfo["Triggers:TriggersCount"] = (((TriggersOptionExtension)Extension)._triggers?.Count() ?? 0).ToString();
8686
debugInfo["Triggers:TriggerTypesCount"] = (((TriggersOptionExtension)Extension)._triggerTypes?.Count() ?? 0).ToString();
87-
debugInfo["Triggers:MaxRecursion"] = ((TriggersOptionExtension)Extension)._maxRecursion.ToString();
88-
debugInfo["Triggers:RecursionMode"] = ((TriggersOptionExtension)Extension)._recursionMode.ToString();
87+
debugInfo["Triggers:MaxCascadeCycles"] = ((TriggersOptionExtension)Extension)._maxCascadeCycles.ToString();
88+
debugInfo["Triggers:CascadeBehavior"] = ((TriggersOptionExtension)Extension)._cascadeBehavior.ToString();
8989
}
9090
}
9191

9292
private ExtensionInfo? _info;
9393
private IEnumerable<(object typeOrInstance, ServiceLifetime lifetime)>? _triggers;
9494
private IEnumerable<Type> _triggerTypes;
95-
private int _maxRecursion = 100;
96-
private RecursionMode _recursionMode = RecursionMode.EntityAndType;
95+
private int _maxCascadeCycles = 100;
96+
private CascadeBehavior _cascadeBehavior = CascadeBehavior.EntityAndType;
9797
private Func<IServiceProvider, IServiceProvider>? _serviceProviderTransform;
9898

9999
public TriggersOptionExtension()
@@ -119,16 +119,16 @@ public TriggersOptionExtension(TriggersOptionExtension copyFrom)
119119
}
120120

121121
_triggerTypes = copyFrom._triggerTypes;
122-
_maxRecursion = copyFrom._maxRecursion;
123-
_recursionMode = copyFrom._recursionMode;
122+
_maxCascadeCycles = copyFrom._maxCascadeCycles;
123+
_cascadeBehavior = copyFrom._cascadeBehavior;
124124
_serviceProviderTransform = copyFrom._serviceProviderTransform;
125125
}
126126

127127
public DbContextOptionsExtensionInfo Info
128128
=> _info ??= new ExtensionInfo(this);
129129

130-
public int MaxRecursion => _maxRecursion;
131-
public RecursionMode RecursionMode => _recursionMode;
130+
public int MaxCascadeCycles => _maxCascadeCycles;
131+
public CascadeBehavior CascadeBehavior => _cascadeBehavior;
132132
public IEnumerable<(object typeOrInstance, ServiceLifetime lifetime)> Triggers => _triggers ?? Enumerable.Empty<(object typeOrInstance, ServiceLifetime lifetime)>();
133133

134134
public void ApplyServices(IServiceCollection services)
@@ -153,17 +153,17 @@ public void ApplyServices(IServiceCollection services)
153153

154154

155155
services.Configure<TriggerOptions>(triggerServiceOptions => {
156-
triggerServiceOptions.MaxRecursion = _maxRecursion;
156+
triggerServiceOptions.MaxCascadeCycles = _maxCascadeCycles;
157157
});
158158

159-
var recursionStrategyType = _recursionMode switch
159+
var cascadeStrategyType = _cascadeBehavior switch
160160
{
161-
RecursionMode.None => typeof(NoRecursionStrategy),
162-
RecursionMode.EntityAndType => typeof(EntityAndTypeRecursionStrategy),
163-
_ => throw new InvalidOperationException("Unsupported recursion mode")
161+
CascadeBehavior.None => typeof(NoCascadeStrategy),
162+
CascadeBehavior.EntityAndType => typeof(EntityAndTypeCascadeStrategy),
163+
_ => throw new InvalidOperationException("Unsupported cascading mode")
164164
};
165165

166-
services.TryAddTransient(typeof(IRecursionStrategy), recursionStrategyType);
166+
services.TryAddTransient(typeof(ICascadeStrategy), cascadeStrategyType);
167167

168168
if (_triggers != null)
169169
{
@@ -221,20 +221,20 @@ private bool TypeIsValidTrigger(Type type)
221221
}
222222
}
223223

224-
public TriggersOptionExtension WithRecursionMode(RecursionMode recursionMode)
224+
public TriggersOptionExtension WithCascadeBehavior(CascadeBehavior cascadeBehavior)
225225
{
226226
var clone = Clone();
227227

228-
clone._recursionMode = recursionMode;
228+
clone._cascadeBehavior = cascadeBehavior;
229229

230230
return clone;
231231
}
232232

233-
public TriggersOptionExtension WithMaxRecursion(int maxRecursion)
233+
public TriggersOptionExtension WithMaxCascadeCycles(int maxCascadeCycles)
234234
{
235235
var clone = Clone();
236236

237-
clone._maxRecursion = maxRecursion;
237+
clone._maxCascadeCycles = maxCascadeCycles;
238238

239239
return clone;
240240
}

src/EntityFrameworkCore.Triggered/Infrastructure/TriggersContextOptionsBuilder.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,11 @@ public TriggersContextOptionsBuilder AddTrigger(Type triggerType, ServiceLifetim
2727
public TriggersContextOptionsBuilder AddTrigger(object trigger)
2828
=> WithOption(e => e.WithAdditionalTrigger(trigger));
2929

30-
public TriggersContextOptionsBuilder RecursionMode(RecursionMode recursionMode = Infrastructure.RecursionMode.EntityAndType)
31-
=> WithOption(e => e.WithRecursionMode(recursionMode));
30+
public TriggersContextOptionsBuilder CascadeBehavior(CascadeBehavior cascadeBehavior = Infrastructure.CascadeBehavior.EntityAndType)
31+
=> WithOption(e => e.WithCascadeBehavior(cascadeBehavior));
3232

33-
public TriggersContextOptionsBuilder MaxRecusion(int maxRecursion = 100)
34-
=> WithOption(e => e.WithMaxRecursion(maxRecursion));
33+
public TriggersContextOptionsBuilder MaxCascadeCycles(int maxCascadingCycles = 100)
34+
=> WithOption(e => e.WithMaxCascadeCycles(maxCascadingCycles));
3535

3636
public TriggersContextOptionsBuilder AddTriggerType(Type triggerType)
3737
=> WithOption(e => e.WithAdditionalTriggerType(triggerType));

src/EntityFrameworkCore.Triggered/Internal/RecursionStrategies/EntityAndTypeRecursionStrategy.cs renamed to src/EntityFrameworkCore.Triggered/Internal/CascadeStrategies/EntityAndTypeCascadeStrategy.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
using Microsoft.EntityFrameworkCore.ChangeTracking;
22

3-
namespace EntityFrameworkCore.Triggered.Internal.RecursionStrategy
3+
namespace EntityFrameworkCore.Triggered.Internal.CascadeStrategies
44
{
5-
public class EntityAndTypeRecursionStrategy : IRecursionStrategy
5+
public class EntityAndTypeCascadeStrategy : ICascadeStrategy
66
{
7-
public bool CanRecurse(EntityEntry entry, ChangeType changeType, TriggerContextDescriptor previousTriggerContextDescriptor)
7+
public bool CanCascade(EntityEntry entry, ChangeType changeType, TriggerContextDescriptor previousTriggerContextDescriptor)
88
=> changeType != previousTriggerContextDescriptor.ChangeType;
99
}
1010
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
using Microsoft.EntityFrameworkCore.ChangeTracking;
2+
3+
namespace EntityFrameworkCore.Triggered.Internal.CascadeStrategies
4+
{
5+
public interface ICascadeStrategy
6+
{
7+
bool CanCascade(EntityEntry entry, ChangeType changeType, TriggerContextDescriptor previousTriggerContextDescriptor);
8+
}
9+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
using Microsoft.EntityFrameworkCore.ChangeTracking;
2+
3+
namespace EntityFrameworkCore.Triggered.Internal.CascadeStrategies
4+
{
5+
public class NoCascadeStrategy : ICascadeStrategy
6+
{
7+
public bool CanCascade(EntityEntry entry, ChangeType changeType, TriggerContextDescriptor previousTriggerContextDescriptor)
8+
=> false;
9+
}
10+
}

0 commit comments

Comments
 (0)