Skip to content

Commit fe22cea

Browse files
author
fabien.menager
committed
Add triggers resolution benchmarks
1 parent fc0ca32 commit fe22cea

4 files changed

Lines changed: 342 additions & 0 deletions

File tree

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
using System;
2+
using System.Reflection;
3+
using BenchmarkDotNet.Attributes;
4+
using Microsoft.EntityFrameworkCore;
5+
using Microsoft.Extensions.DependencyInjection;
6+
7+
namespace EntityFrameworkCore.Triggered.Benchmarks
8+
{
9+
/// <summary>
10+
/// Measures the setup cost (assembly scan + trigger registration) without building the
11+
/// ServiceProvider, isolating the overhead of AddAssemblyTriggers vs explicit AddTrigger&lt;T&gt;.
12+
///
13+
/// Note: TriggerTypeHelper caches results per type after the first iteration.
14+
/// BenchmarkDotNet measures steady-state (warm cache).
15+
/// </summary>
16+
[MemoryDiagnoser]
17+
public class AssemblyTriggersSetupBenchmarks
18+
{
19+
private static readonly Assembly BenchmarkAssembly = typeof(AssemblyTriggersSetupBenchmarks).Assembly;
20+
21+
/// <summary>
22+
/// Baseline: 2 triggers registered explicitly via AddTrigger&lt;T&gt;() inside UseTriggers().
23+
/// </summary>
24+
[Benchmark(Baseline = true)]
25+
public IServiceCollection Explicit_2Triggers_ViaOptions()
26+
{
27+
return new ServiceCollection()
28+
.AddDbContext<TriggeredApplicationContext>(options =>
29+
options.UseInMemoryDatabase("Setup_Explicit_Options").UseTriggers(o =>
30+
{
31+
o.AddTrigger<Triggers.SetStudentRegistrationDateTrigger>();
32+
o.AddTrigger<Triggers.SignStudentUpForMandatoryCourses>();
33+
}));
34+
}
35+
36+
/// <summary>
37+
/// Assembly scan via TriggersContextOptionsBuilder.AddAssemblyTriggers().
38+
/// Discovers 2 Student triggers + 5 StudentCourse triggers = 7 types total.
39+
/// </summary>
40+
[Benchmark]
41+
public IServiceCollection AssemblyTriggers_ViaOptions()
42+
{
43+
return new ServiceCollection()
44+
.AddDbContext<TriggeredApplicationContext>(options =>
45+
options.UseInMemoryDatabase("Setup_Assembly_Options").UseTriggers(o =>
46+
o.AddAssemblyTriggers(BenchmarkAssembly)));
47+
}
48+
49+
/// <summary>
50+
/// Baseline: 2 triggers registered explicitly via IServiceCollection.AddTrigger&lt;T&gt;().
51+
/// </summary>
52+
[Benchmark]
53+
public IServiceCollection Explicit_2Triggers_ViaServiceCollection()
54+
{
55+
return new ServiceCollection()
56+
.AddTriggeredDbContext<TriggeredApplicationContext>(options =>
57+
options.UseInMemoryDatabase("Setup_Explicit_SC"))
58+
.AddTrigger<Triggers.SetStudentRegistrationDateTrigger>()
59+
.AddTrigger<Triggers.SignStudentUpForMandatoryCourses>();
60+
}
61+
62+
/// <summary>
63+
/// Assembly scan via IServiceCollection.AddAssemblyTriggers().
64+
/// Registers all 7 triggers (2 Student + 5 StudentCourse) directly in the application DI container.
65+
/// </summary>
66+
[Benchmark]
67+
public IServiceCollection AssemblyTriggers_ViaServiceCollection()
68+
{
69+
return new ServiceCollection()
70+
.AddTriggeredDbContext<TriggeredApplicationContext>(options =>
71+
options.UseInMemoryDatabase("Setup_Assembly_SC"))
72+
.AddAssemblyTriggers(BenchmarkAssembly);
73+
}
74+
}
75+
76+
/// <summary>
77+
/// Measures the per-SaveChanges runtime cost when triggers were registered via
78+
/// AddAssemblyTriggers (7 triggers in the assembly = 2 for Student + 5 for StudentCourse)
79+
/// vs explicit registration targeting only the 2 Student triggers.
80+
///
81+
/// Answers: "Does registering extra non-applicable triggers (discovered by assembly scan)
82+
/// slow down the hot path?"
83+
///
84+
/// A new DbContext is created on every iteration to include instantiation cost.
85+
/// </summary>
86+
[MemoryDiagnoser]
87+
public class AssemblyTriggersRuntimeBenchmarks
88+
{
89+
private static readonly Assembly BenchmarkAssembly = typeof(AssemblyTriggersRuntimeBenchmarks).Assembly;
90+
91+
private IServiceProvider _explicit2OptionsProvider;
92+
private IServiceProvider _assemblyScan7OptionsProvider;
93+
private IServiceProvider _explicit2ServiceCollectionProvider;
94+
private IServiceProvider _assemblyScan7ServiceCollectionProvider;
95+
96+
[GlobalSetup]
97+
public void GlobalSetup()
98+
{
99+
_explicit2OptionsProvider = new ServiceCollection()
100+
.AddTriggeredDbContext<TriggeredApplicationContext>(options =>
101+
options.UseInMemoryDatabase("RT_Explicit_Options").UseTriggers(o =>
102+
{
103+
o.AddTrigger<Triggers.SetStudentRegistrationDateTrigger>();
104+
o.AddTrigger<Triggers.SignStudentUpForMandatoryCourses>();
105+
}))
106+
.BuildServiceProvider();
107+
108+
_assemblyScan7OptionsProvider = new ServiceCollection()
109+
.AddTriggeredDbContext<TriggeredApplicationContext>(options =>
110+
options.UseInMemoryDatabase("RT_Assembly_Options").UseTriggers(o =>
111+
o.AddAssemblyTriggers(BenchmarkAssembly)))
112+
.BuildServiceProvider();
113+
114+
_explicit2ServiceCollectionProvider = new ServiceCollection()
115+
.AddTriggeredDbContext<TriggeredApplicationContext>(options =>
116+
options.UseInMemoryDatabase("RT_Explicit_SC"))
117+
.AddTrigger<Triggers.SetStudentRegistrationDateTrigger>()
118+
.AddTrigger<Triggers.SignStudentUpForMandatoryCourses>()
119+
.BuildServiceProvider();
120+
121+
_assemblyScan7ServiceCollectionProvider = new ServiceCollection()
122+
.AddTriggeredDbContext<TriggeredApplicationContext>(options =>
123+
options.UseInMemoryDatabase("RT_Assembly_SC"))
124+
.AddAssemblyTriggers(BenchmarkAssembly)
125+
.BuildServiceProvider();
126+
127+
SeedCourse(_explicit2OptionsProvider);
128+
SeedCourse(_assemblyScan7OptionsProvider);
129+
SeedCourse(_explicit2ServiceCollectionProvider);
130+
SeedCourse(_assemblyScan7ServiceCollectionProvider);
131+
}
132+
133+
private static void SeedCourse(IServiceProvider sp)
134+
{
135+
using var scope = sp.CreateScope();
136+
using var ctx = scope.ServiceProvider.GetRequiredService<TriggeredApplicationContext>();
137+
ctx.Database.EnsureCreated();
138+
ctx.Courses.Add(new Course { Id = Guid.NewGuid(), DisplayName = "Mandatory", IsMandatory = true });
139+
ctx.SaveChanges();
140+
}
141+
142+
/// <summary>
143+
/// Baseline: 2 Student triggers registered explicitly via UseTriggers options.
144+
/// </summary>
145+
[Benchmark(Baseline = true)]
146+
public void Explicit_2Triggers_ViaOptions()
147+
{
148+
using var scope = _explicit2OptionsProvider.CreateScope();
149+
using var ctx = scope.ServiceProvider.GetRequiredService<TriggeredApplicationContext>();
150+
ctx.Students.Add(new Student { Id = Guid.NewGuid(), DisplayName = "Test" });
151+
ctx.SaveChanges();
152+
}
153+
154+
/// <summary>
155+
/// 7 triggers registered via assembly scan (2 Student + 5 StudentCourse) through UseTriggers options.
156+
/// Only the 2 Student triggers are invoked during this SaveChanges.
157+
/// </summary>
158+
[Benchmark]
159+
public void AssemblyTriggers_7Registered_2Active_ViaOptions()
160+
{
161+
using var scope = _assemblyScan7OptionsProvider.CreateScope();
162+
using var ctx = scope.ServiceProvider.GetRequiredService<TriggeredApplicationContext>();
163+
ctx.Students.Add(new Student { Id = Guid.NewGuid(), DisplayName = "Test" });
164+
ctx.SaveChanges();
165+
}
166+
167+
/// <summary>
168+
/// Baseline (application DI path): 2 Student triggers registered explicitly via IServiceCollection.
169+
/// </summary>
170+
[Benchmark]
171+
public void Explicit_2Triggers_ViaServiceCollection()
172+
{
173+
using var scope = _explicit2ServiceCollectionProvider.CreateScope();
174+
using var ctx = scope.ServiceProvider.GetRequiredService<TriggeredApplicationContext>();
175+
ctx.Students.Add(new Student { Id = Guid.NewGuid(), DisplayName = "Test" });
176+
ctx.SaveChanges();
177+
}
178+
179+
/// <summary>
180+
/// 7 triggers registered via assembly scan (2 Student + 5 StudentCourse) through IServiceCollection.
181+
/// Only the 2 Student triggers are invoked during this SaveChanges.
182+
/// </summary>
183+
[Benchmark]
184+
public void AssemblyTriggers_7Registered_2Active_ViaServiceCollection()
185+
{
186+
using var scope = _assemblyScan7ServiceCollectionProvider.CreateScope();
187+
using var ctx = scope.ServiceProvider.GetRequiredService<TriggeredApplicationContext>();
188+
ctx.Students.Add(new Student { Id = Guid.NewGuid(), DisplayName = "Test" });
189+
ctx.SaveChanges();
190+
}
191+
}
192+
}
193+

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
</ItemGroup>
1818
<ItemGroup>
1919
<ProjectReference Include="..\..\src\EntityFrameworkCore.Triggered\EntityFrameworkCore.Triggered.csproj" />
20+
<ProjectReference Include="..\..\src\EntityFrameworkCore.Triggered.Extensions\EntityFrameworkCore.Triggered.Extensions.csproj" />
2021
</ItemGroup>
2122

2223
</Project>
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
using System;
2+
using BenchmarkDotNet.Attributes;
3+
using Microsoft.EntityFrameworkCore;
4+
using Microsoft.Extensions.DependencyInjection;
5+
6+
namespace EntityFrameworkCore.Triggered.Benchmarks
7+
{
8+
/// <summary>
9+
/// Measures trigger resolution cost in isolation by creating a new DbContext on every iteration.
10+
/// This exposes the per-instantiation overhead of the triggered infrastructure compared to a
11+
/// plain DbContext, and how that overhead scales with the number of registered triggers.
12+
/// </summary>
13+
[MemoryDiagnoser]
14+
public class TriggerResolutionBenchmarks
15+
{
16+
private IServiceProvider _plainServiceProvider;
17+
private IServiceProvider _triggered0ServiceProvider; // UseTriggers() with no triggers registered
18+
private IServiceProvider _triggered1ServiceProvider; // 1 lightweight trigger (no DB query)
19+
private IServiceProvider _triggered2ServiceProvider; // 2 triggers, one of which queries the DB
20+
21+
[GlobalSetup]
22+
public void GlobalSetup()
23+
{
24+
_plainServiceProvider = new ServiceCollection()
25+
.AddDbContext<ApplicationContext>(options =>
26+
options.UseInMemoryDatabase("TriggerResolution_Plain"))
27+
.BuildServiceProvider();
28+
29+
_triggered0ServiceProvider = new ServiceCollection()
30+
.AddDbContext<TriggeredApplicationContext>(options =>
31+
options.UseInMemoryDatabase("TriggerResolution_T0").UseTriggers())
32+
.BuildServiceProvider();
33+
34+
_triggered1ServiceProvider = new ServiceCollection()
35+
.AddTriggeredDbContext<TriggeredApplicationContext>(options =>
36+
options.UseInMemoryDatabase("TriggerResolution_T1").UseTriggers(triggerOptions =>
37+
triggerOptions.AddTrigger<Triggers.SetStudentRegistrationDateTrigger>()))
38+
.BuildServiceProvider();
39+
40+
_triggered2ServiceProvider = new ServiceCollection()
41+
.AddTriggeredDbContext<TriggeredApplicationContext>(options =>
42+
options.UseInMemoryDatabase("TriggerResolution_T2").UseTriggers(triggerOptions =>
43+
{
44+
triggerOptions.AddTrigger<Triggers.SetStudentRegistrationDateTrigger>();
45+
triggerOptions.AddTrigger<Triggers.SignStudentUpForMandatoryCourses>();
46+
}))
47+
.BuildServiceProvider();
48+
49+
// Seed a mandatory course so SignStudentUpForMandatoryCourses has data to work with.
50+
SeedCourse(_triggered2ServiceProvider);
51+
}
52+
53+
private static void SeedCourse(IServiceProvider serviceProvider)
54+
{
55+
using var scope = serviceProvider.CreateScope();
56+
using var context = scope.ServiceProvider.GetRequiredService<TriggeredApplicationContext>();
57+
context.Database.EnsureCreated();
58+
context.Courses.Add(new Course { Id = Guid.NewGuid(), DisplayName = "Mandatory Course", IsMandatory = true });
59+
context.SaveChanges();
60+
}
61+
62+
/// <summary>
63+
/// Baseline: standard DbContext with no triggered infrastructure.
64+
/// A new scope and DbContext are created on each iteration.
65+
/// </summary>
66+
[Benchmark(Baseline = true)]
67+
public void PlainDbContext()
68+
{
69+
using var scope = _plainServiceProvider.CreateScope();
70+
using var context = scope.ServiceProvider.GetRequiredService<ApplicationContext>();
71+
context.Students.Add(new Student { Id = Guid.NewGuid(), DisplayName = "Test" });
72+
context.SaveChanges();
73+
}
74+
75+
/// <summary>
76+
/// Triggered infrastructure enabled (UseTriggers), but no triggers registered.
77+
/// Measures the raw overhead of the trigger resolution engine with an empty registry.
78+
/// </summary>
79+
[Benchmark]
80+
public void TriggeredDbContext_NoTriggers()
81+
{
82+
using var scope = _triggered0ServiceProvider.CreateScope();
83+
using var context = scope.ServiceProvider.GetRequiredService<TriggeredApplicationContext>();
84+
context.Students.Add(new Student { Id = Guid.NewGuid(), DisplayName = "Test" });
85+
context.SaveChanges();
86+
}
87+
88+
/// <summary>
89+
/// 1 trigger registered: SetStudentRegistrationDateTrigger (synchronous, no DB query).
90+
/// Measures resolution + execution cost for a lightweight trigger.
91+
/// </summary>
92+
[Benchmark]
93+
public void TriggeredDbContext_OneTrigger()
94+
{
95+
using var scope = _triggered1ServiceProvider.CreateScope();
96+
using var context = scope.ServiceProvider.GetRequiredService<TriggeredApplicationContext>();
97+
context.Students.Add(new Student { Id = Guid.NewGuid(), DisplayName = "Test" });
98+
context.SaveChanges();
99+
}
100+
101+
/// <summary>
102+
/// 2 triggers registered: SetStudentRegistrationDateTrigger + SignStudentUpForMandatoryCourses
103+
/// (the second performs a DB query and adds new entities).
104+
/// Measures resolution + execution cost for triggers with side effects.
105+
/// </summary>
106+
[Benchmark]
107+
public void TriggeredDbContext_TwoTriggers()
108+
{
109+
using var scope = _triggered2ServiceProvider.CreateScope();
110+
using var context = scope.ServiceProvider.GetRequiredService<TriggeredApplicationContext>();
111+
context.Students.Add(new Student { Id = Guid.NewGuid(), DisplayName = "Test" });
112+
context.SaveChanges();
113+
}
114+
}
115+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
namespace EntityFrameworkCore.Triggered.Benchmarks.Triggers
2+
{
3+
// These "noise" triggers implement IBeforeSaveTrigger<StudentCourse> (not IBeforeSaveTrigger<Student>).
4+
// They are used to simulate an assembly containing several triggers where only a subset applies
5+
// to the entity being saved — a typical real-world scenario.
6+
// AddAssemblyTriggers will discover and register them, but they will never be resolved
7+
// during a SaveChanges that only tracks Student entities.
8+
9+
public class DummyStudentCourseTrigger1 : IBeforeSaveTrigger<StudentCourse>
10+
{
11+
public void BeforeSave(ITriggerContext<StudentCourse> context) { }
12+
}
13+
14+
public class DummyStudentCourseTrigger2 : IBeforeSaveTrigger<StudentCourse>
15+
{
16+
public void BeforeSave(ITriggerContext<StudentCourse> context) { }
17+
}
18+
19+
public class DummyStudentCourseTrigger3 : IBeforeSaveTrigger<StudentCourse>
20+
{
21+
public void BeforeSave(ITriggerContext<StudentCourse> context) { }
22+
}
23+
24+
public class DummyStudentCourseTrigger4 : IBeforeSaveTrigger<StudentCourse>
25+
{
26+
public void BeforeSave(ITriggerContext<StudentCourse> context) { }
27+
}
28+
29+
public class DummyStudentCourseTrigger5 : IBeforeSaveTrigger<StudentCourse>
30+
{
31+
public void BeforeSave(ITriggerContext<StudentCourse> context) { }
32+
}
33+
}

0 commit comments

Comments
 (0)