Skip to content

Commit bfa1085

Browse files
committed
2 parents 7d70ff9 + 1eb97ff commit bfa1085

File tree

9 files changed

+36
-211
lines changed

9 files changed

+36
-211
lines changed

README.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ Triggers for EF Core. Respond to changes in your DbContext before and after they
1515
1. Install the package from [NuGet](https://www.nuget.org/packages/EntityFrameworkCore.Triggered)
1616
2. Implement Triggers by implementing `IBeforeSaveTrigger<TEntity>` and `IAfterSaveTrigger<TEntity>`
1717
3. Register your triggers with your DbContext
18-
4. View our [samples](https://github.com/koenbeuk/EntityFrameworkCore.Triggered/tree/master/samples)
18+
4. View our [samples](https://github.com/koenbeuk/EntityFrameworkCore.Triggered/tree/master/samples) and [more samples](https://github.com/koenbeuk/EntityFrameworkCore.Triggered.Samples) and [a sample application](https://github.com/koenbeuk/EntityFrameworkCore.BookStoreSampleApp)
1919
5. Check out our [wiki](https://github.com/koenbeuk/EntityFrameworkCore.Triggered/wiki) for tips and tricks on getting started and being succesfull.
2020

2121
> Since EntityFrameworkCore.Triggered 2.0, triggers will be invoked automatically, however this requires EFCore 5.0. If you're stuck with EFCore 3.1 then you can use [EntityFrameworkCore.Triggered V1](https://www.nuget.org/packages/EntityFrameworkCore.Triggered/1.1.0). This requires you to inherit from `TriggeredDbContext` or manual management of trigger sessions.
@@ -96,6 +96,9 @@ public class Startup
9696
}
9797
```
9898

99+
### Related articles
100+
[Triggers for Entity Framework Core](https://onthedrift.com/posts/efcore-triggered-part1/) - Introduces the idea of using EFCore triggers in your codebase
101+
99102
### Trigger discovery
100103
In the given example, we register triggers directly with our DbContext. This is the recommended approach starting from version 2.3 and 1.4 respectively. If you're on an older version then its recommend to register triggers with your applications DI container instead:
101104

@@ -115,6 +118,13 @@ services.AddDbContext<ApplicationContext>(options => options.UseTriggers(trigger
115118
services.AddAssemblyTriggers();
116119
```
117120

121+
### DB Context Pooling
122+
When using DbContextPooling, Triggers need additional help in discovering the IServiceProvider that was used to obtain a Lease on the current DbContext. This library exposes an easy-to-use plugin to enable this additional complexity which requires a call to `AddTriggeredDbContextPool`.
123+
124+
```csharp
125+
services.AddDbContextPool<ApplicationDbContext>(...); // Before
126+
services.AddTriggeredDbContextPool<ApplicationDbContext>(...); // After
127+
```
118128

119129
### Cascading changes (previously called Recursion)
120130
`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:

benchmarks/EntityFrameworkCore.Triggered.Benchmarks/ApplicationContext.cs

Lines changed: 1 addition & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,18 @@
22
using System.Collections.Generic;
33
using System.Security.Cryptography.X509Certificates;
44
using System.Text;
5-
using Com.Setarit.Ramses;
6-
using Com.Setarit.Ramses.LifecycleListener;
7-
using EntityFrameworkCore.Triggers;
85
using Microsoft.EntityFrameworkCore;
96
using Microsoft.EntityFrameworkCore.Infrastructure;
107

118
namespace EntityFrameworkCore.Triggered.Benchmarks
129
{
13-
public class Student : Com.Setarit.Ramses.LifecycleListener.IBeforeAddingListener
10+
public class Student
1411
{
1512
public Guid Id { get; set; }
1613

1714
public string DisplayName { get; set; }
1815

1916
public DateTimeOffset RegistrationDate { get; set; }
20-
21-
void IBeforeAddingListener.BeforeAdding()
22-
{
23-
this.RegistrationDate = DateTimeOffset.Now;
24-
}
2517
}
2618

2719
public class Course
@@ -76,25 +68,6 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
7668
public DbSet<StudentCourse> StudentCourses { get; set; }
7769
}
7870

79-
public class ApplicationContextWithTriggers : DbContextWithTriggers, IApplicationContextContract
80-
{
81-
public ApplicationContextWithTriggers(DbContextOptions<ApplicationContextWithTriggers> options, IServiceProvider serviceProvider) : base(serviceProvider, options)
82-
{
83-
}
84-
85-
86-
protected override void OnModelCreating(ModelBuilder modelBuilder)
87-
{
88-
modelBuilder.Entity<StudentCourse>().HasKey(x => new { x.StudentId, x.CourseId });
89-
}
90-
91-
public DbSet<Student> Students { get; set; }
92-
93-
public DbSet<Course> Courses { get; set; }
94-
95-
public DbSet<StudentCourse> StudentCourses { get; set; }
96-
}
97-
9871
public class TriggeredApplicationContext : DbContext, IApplicationContextContract
9972
{
10073
public TriggeredApplicationContext(DbContextOptions<TriggeredApplicationContext> options) : base(options)
@@ -112,27 +85,4 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
11285

11386
public DbSet<StudentCourse> StudentCourses { get; set; }
11487
}
115-
116-
public class RamsesApplicationContext : LifecycleDbContext, IApplicationContextContract
117-
{
118-
public RamsesApplicationContext(DbContextOptions<RamsesApplicationContext> options) : base(options)
119-
{
120-
}
121-
122-
protected override void OnModelCreating(ModelBuilder modelBuilder)
123-
{
124-
modelBuilder.Entity<StudentCourse>().HasKey(x => new { x.StudentId, x.CourseId });
125-
}
126-
127-
public override int SaveChanges()
128-
{
129-
return base.SaveWithLifecycles();
130-
}
131-
132-
public DbSet<Student> Students { get; set; }
133-
134-
public DbSet<Course> Courses { get; set; }
135-
136-
public DbSet<StudentCourse> StudentCourses { get; set; }
137-
}
13888
}

benchmarks/EntityFrameworkCore.Triggered.Benchmarks/EmbracingFeaturesBenchmarks.cs

Lines changed: 0 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
using System.Linq;
33
using BenchmarkDotNet;
44
using BenchmarkDotNet.Attributes;
5-
using EntityFrameworkCore.Triggers;
65
using Microsoft.Diagnostics.Runtime.Interop;
76
using Microsoft.Diagnostics.Tracing.Analysis.GC;
87
using Microsoft.EntityFrameworkCore;
@@ -31,35 +30,8 @@ public void GlobalSetup()
3130
triggerOptions.AddTrigger<Triggers.SignStudentUpForMandatoryCourses>();
3231
});
3332
})
34-
.AddDbContext<ApplicationContextWithTriggers>(options => {
35-
options
36-
.UseInMemoryDatabase(nameof(WithDbContextWithTriggers));
37-
})
38-
.AddDbContext<RamsesApplicationContext>(options => {
39-
options
40-
.UseInMemoryDatabase(nameof(RamsesApplicationContext));
41-
})
42-
.AddTriggers()
43-
.AddSingleton(typeof(ITriggers<,>), typeof(Triggers<,>))
44-
.AddSingleton(typeof(ITriggers<>), typeof(Triggers<>))
45-
.AddSingleton(typeof(ITriggers), typeof(EntityFrameworkCore.Triggers.Triggers))
4633
.BuildServiceProvider();
4734

48-
Triggers<Student, ApplicationContextWithTriggers>.GlobalInserting.Add(entry => {
49-
entry.Entity.RegistrationDate = DateTimeOffset.Now;
50-
});
51-
52-
Triggers<Student, ApplicationContextWithTriggers>.GlobalInserting.Add(entry => {
53-
var mandatoryCourses = entry.Context.Courses.Where(x => x.IsMandatory).ToList();
54-
55-
foreach (var mandatoryCourse in mandatoryCourses)
56-
{
57-
entry.Context.StudentCourses.Add(new StudentCourse {
58-
CourseId = mandatoryCourse.Id,
59-
StudentId = entry.Entity.Id
60-
});
61-
}
62-
});
6335
}
6436

6537
[Params(50)]
@@ -129,44 +101,6 @@ public void WithDbContext()
129101
}
130102
}
131103

132-
[Benchmark]
133-
public void WithDbContextWithTriggers()
134-
{
135-
// setup
136-
{
137-
using var scope = _serviceProvider.CreateScope();
138-
using var context = scope.ServiceProvider.GetRequiredService<ApplicationContextWithTriggers>();
139-
140-
context.Database.EnsureDeleted();
141-
142-
context.Courses.Add(new Course { Id = Guid.NewGuid(), DisplayName = "Test", IsMandatory = true });
143-
context.SaveChanges();
144-
}
145-
146-
// execute
147-
for (var outerBatch = 0; outerBatch < OuterBatches; outerBatch++)
148-
{
149-
using var scope = _serviceProvider.CreateScope();
150-
using var context = scope.ServiceProvider.GetRequiredService<ApplicationContextWithTriggers>();
151-
152-
for (var innerBatch = 0; innerBatch < InnerBatches; innerBatch++)
153-
{
154-
var student = new Student { Id = Guid.NewGuid(), DisplayName = "Test" };
155-
context.Add(student);
156-
}
157-
158-
context.SaveChanges();
159-
}
160-
161-
// validate
162-
{
163-
using var scope = _serviceProvider.CreateScope();
164-
using var context = scope.ServiceProvider.GetRequiredService<ApplicationContextWithTriggers>();
165-
166-
Validate(context);
167-
}
168-
}
169-
170104
[Benchmark]
171105
public void WithTriggeredDbContext()
172106
{
@@ -205,55 +139,5 @@ public void WithTriggeredDbContext()
205139
Validate(context);
206140
}
207141
}
208-
209-
210-
[Benchmark]
211-
public void WithRamsesDbContext()
212-
{
213-
// setup
214-
{
215-
using var scope = _serviceProvider.CreateScope();
216-
using var context = scope.ServiceProvider.GetRequiredService<RamsesApplicationContext>();
217-
218-
context.Database.EnsureDeleted();
219-
220-
context.Courses.Add(new Course { Id = Guid.NewGuid(), DisplayName = "Test", IsMandatory = true });
221-
context.SaveChanges();
222-
}
223-
224-
// execute
225-
for (var outerBatch = 0; outerBatch < OuterBatches; outerBatch++)
226-
{
227-
using var scope = _serviceProvider.CreateScope();
228-
using var context = scope.ServiceProvider.GetRequiredService<RamsesApplicationContext>();
229-
230-
for (var innerBatch = 0; innerBatch < InnerBatches; innerBatch++)
231-
{
232-
var student = new Student { Id = Guid.NewGuid(), DisplayName = "Test" };
233-
234-
context.Add(student);
235-
236-
var mandatoryCourses = context.Courses.Where(x => x.IsMandatory).ToList();
237-
238-
foreach (var mandatoryCourse in mandatoryCourses)
239-
{
240-
context.StudentCourses.Add(new StudentCourse {
241-
CourseId = mandatoryCourse.Id,
242-
StudentId = student.Id
243-
});
244-
}
245-
}
246-
247-
context.SaveChanges();
248-
}
249-
250-
// validate
251-
{
252-
using var scope = _serviceProvider.CreateScope();
253-
using var context = scope.ServiceProvider.GetRequiredService<RamsesApplicationContext>();
254-
255-
Validate(context);
256-
}
257-
}
258142
}
259143
}

benchmarks/EntityFrameworkCore.Triggered.Benchmarks/EntityFrameworkCore.Triggered.Benchmarks.csproj

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,8 @@
1313
</PropertyGroup>
1414
<ItemGroup>
1515
<PackageReference Include="BenchmarkDotNet" Version="0.12.1" />
16-
<PackageReference Include="EntityFrameworkCore.Triggers" Version="1.2.2" />
1716
<PackageReference Include="EntityFrameworkCore.Triggered" Version="2.2.0" />
1817
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="5.0.0" />
19-
<PackageReference Include="Ramses" Version="3.0.1" />
2018
</ItemGroup>
2119

2220
</Project>

benchmarks/EntityFrameworkCore.Triggered.Benchmarks/PlainOverheadBenchmarks.cs

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
using System.Linq;
33
using BenchmarkDotNet;
44
using BenchmarkDotNet.Attributes;
5-
using EntityFrameworkCore.Triggers;
65
using Microsoft.Diagnostics.Runtime.Interop;
76
using Microsoft.Diagnostics.Tracing.Analysis.GC;
87
using Microsoft.EntityFrameworkCore;
@@ -28,15 +27,6 @@ public void GlobalSetup()
2827
.UseInMemoryDatabase(nameof(WithTriggeredDbContext))
2928
.UseTriggers();
3029
})
31-
.AddDbContext<ApplicationContextWithTriggers>(options => {
32-
options
33-
.UseInMemoryDatabase(nameof(WithDbContextWithTriggers));
34-
})
35-
.AddDbContext<RamsesApplicationContext>(options => {
36-
options
37-
.UseInMemoryDatabase(nameof(RamsesApplicationContext));
38-
})
39-
.AddTriggers()
4030
.BuildServiceProvider();
4131
}
4232

@@ -107,23 +97,10 @@ public void WithDbContext()
10797
Execute<ApplicationContext>();
10898
}
10999

110-
[Benchmark]
111-
public void WithDbContextWithTriggers()
112-
{
113-
Execute<ApplicationContextWithTriggers>();
114-
}
115-
116100
[Benchmark]
117101
public void WithTriggeredDbContext()
118102
{
119103
Execute<TriggeredApplicationContext>();
120104
}
121-
122-
123-
[Benchmark]
124-
public void WithRamsesDbContext()
125-
{
126-
Execute<RamsesApplicationContext>();
127-
}
128105
}
129106
}

src/EntityFrameworkCore.Triggered/Internal/TriggerContextDescriptor.cs

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
using System;
2+
using System.Buffers;
3+
using System.Collections.Concurrent;
24
using System.Collections.Generic;
35
using Microsoft.EntityFrameworkCore.ChangeTracking;
46

57
namespace EntityFrameworkCore.Triggered.Internal
68
{
79
public readonly struct TriggerContextDescriptor
810
{
9-
[ThreadStatic]
10-
static Dictionary<Type, Func<object, PropertyValues?, ChangeType, object>>? _cachedTriggerContextFactories;
11+
static readonly ConcurrentDictionary<Type, Func<object, PropertyValues?, ChangeType, object>> _cachedTriggerContextFactories = new();
1112

1213
readonly EntityEntry _entityEntry;
1314
readonly ChangeType _changeType;
@@ -37,19 +38,10 @@ public object GetTriggerContext()
3738

3839
var entityType = entityEntry.Entity.GetType();
3940

40-
if (_cachedTriggerContextFactories == null)
41-
{
42-
_cachedTriggerContextFactories = new Dictionary<Type, Func<object, PropertyValues?, ChangeType, object>>();
43-
}
44-
45-
if (!_cachedTriggerContextFactories.TryGetValue(entityType, out var triggerContextFactory))
46-
{
47-
triggerContextFactory = (Func<object, PropertyValues?, ChangeType, object>)typeof(TriggerContextFactory<>).MakeGenericType(entityType)
41+
var triggerContextFactory = _cachedTriggerContextFactories.GetOrAdd(entityType, entityType =>
42+
(Func<object, PropertyValues?, ChangeType, object>)typeof(TriggerContextFactory<>).MakeGenericType(entityType)
4843
.GetMethod(nameof(TriggerContextFactory<object>.Activate))
49-
.CreateDelegate(typeof(Func<object, PropertyValues?, ChangeType, object>));
50-
51-
_cachedTriggerContextFactories.Add(entityType, triggerContextFactory);
52-
}
44+
.CreateDelegate(typeof(Func<object, PropertyValues?, ChangeType, object>)));
5345

5446
return triggerContextFactory(entityEntry.Entity, originalValues, changeType);
5547
}

src/EntityFrameworkCore.Triggered/Internal/TriggerFactory.cs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Collections.Concurrent;
23
using System.Collections.Generic;
34
using System.Diagnostics;
45
using System.Linq;
@@ -10,6 +11,7 @@ namespace EntityFrameworkCore.Triggered.Internal
1011
{
1112
public sealed class TriggerFactory
1213
{
14+
static readonly ConcurrentDictionary<Type, Type> _instanceFactoryTypeCache = new();
1315
readonly IServiceProvider _internalServiceProvider;
1416

1517
public TriggerFactory(IServiceProvider internalServiceProvider)
@@ -30,12 +32,19 @@ public IEnumerable<object> Resolve(IServiceProvider serviceProvider, Type trigge
3032
}
3133

3234
// Alternatively, triggers may be registered with the extension configuration
33-
var triggerServiceFactories = _internalServiceProvider.GetServices(typeof(ITriggerInstanceFactory<>).MakeGenericType(triggerType)).Cast<ITriggerInstanceFactory>();
35+
var instanceFactoryType = _instanceFactoryTypeCache.GetOrAdd(triggerType,
36+
triggerType => typeof(ITriggerInstanceFactory<>).MakeGenericType(triggerType)
37+
);
38+
39+
var triggerServiceFactories = _internalServiceProvider.GetServices(instanceFactoryType);
3440
if (triggerServiceFactories.Any())
3541
{
3642
foreach (var triggerServiceFactory in triggerServiceFactories)
3743
{
38-
yield return triggerServiceFactory.Create(serviceProvider);
44+
if (triggerServiceFactory is not null)
45+
{
46+
yield return ((ITriggerInstanceFactory)triggerServiceFactory).Create(serviceProvider);
47+
}
3948
}
4049
}
4150
}

0 commit comments

Comments
 (0)