├── .gitignore ├── EntityFramework.Triggers.sln ├── LICENSE ├── README.md ├── src └── EntityFrameworkCore.Triggers │ ├── ArrayEqualityComparer.cs │ ├── DbContextExtensions.cs │ ├── DbContextWithTriggers.cs │ ├── DelegateSynchronyUnion.cs │ ├── EntityEntryComparer.cs │ ├── EntityFrameworkCore.Triggers.csproj │ ├── Entry.cs │ ├── GenericServiceCache.cs │ ├── IEntry.cs │ ├── ITriggerEntityInvoker.cs │ ├── ITriggerEvent.cs │ ├── ITriggerInvoker.cs │ ├── ITriggerInvokerAsync.cs │ ├── ITriggers.cs │ ├── ITriggersBuilder.cs │ ├── ServiceCollectionExtensions.cs │ ├── ServiceProviderExtensions.cs │ ├── ServiceRetrieval.cs │ ├── ServiceRetrieval_1.cs │ ├── TriggerEntityInvoker.cs │ ├── TriggerEvent.Generated.cs │ ├── TriggerEvent.Generated.tt │ ├── TriggerEvent.cs │ ├── TriggerEventExtensions.cs │ ├── TriggerInvoker.cs │ ├── TriggerInvokerAsync.cs │ ├── TriggerInvokerAsyncCache.cs │ ├── Triggers.cs │ ├── TriggersBuilder.cs │ ├── TriggersEqualityComparer.cs │ ├── Triggers_1.cs │ ├── Triggers_2.cs │ └── TypeExtensions.cs └── test ├── Testing ├── Program.cs └── Testing.csproj ├── TestingAsync ├── Program.cs └── TestingAsync.csproj └── TestsCore ├── AddingEntitiesWithinABeforeTrigger.cs ├── Context.cs ├── Entity.cs ├── GenericServiceCache_2.cs ├── Person.cs ├── TestBase.cs ├── TestsCore.csproj ├── Thing.cs ├── ThingTestBase.cs └── UnitTests.cs /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.sln.docstates 8 | 9 | # Build results 10 | [Dd]ebug/ 11 | [Dd]ebugPublic/ 12 | [Rr]elease/ 13 | [Rr]eleases/ 14 | x64/ 15 | x86/ 16 | build/ 17 | bld/ 18 | [Bb]in/ 19 | [Oo]bj/ 20 | 21 | # Roslyn cache directories 22 | *.ide/ 23 | 24 | # MSTest test Results 25 | [Tt]est[Rr]esult*/ 26 | [Bb]uild[Ll]og.* 27 | 28 | #NUNIT 29 | *.VisualState.xml 30 | TestResult.xml 31 | 32 | # Build Results of an ATL Project 33 | [Dd]ebugPS/ 34 | [Rr]eleasePS/ 35 | dlldata.c 36 | 37 | *_i.c 38 | *_p.c 39 | *_i.h 40 | *.ilk 41 | *.meta 42 | *.obj 43 | *.pch 44 | *.pdb 45 | *.pgc 46 | *.pgd 47 | *.rsp 48 | *.sbr 49 | *.tlb 50 | *.tli 51 | *.tlh 52 | *.tmp 53 | *.tmp_proj 54 | *.log 55 | *.vspscc 56 | *.vssscc 57 | .builds 58 | *.pidb 59 | *.svclog 60 | *.scc 61 | 62 | # Chutzpah Test files 63 | _Chutzpah* 64 | 65 | # Visual C++ cache files 66 | ipch/ 67 | *.aps 68 | *.ncb 69 | *.opensdf 70 | *.sdf 71 | *.cachefile 72 | 73 | # Visual Studio profiler 74 | *.psess 75 | *.vsp 76 | *.vspx 77 | 78 | # TFS 2012 Local Workspace 79 | $tf/ 80 | 81 | # Guidance Automation Toolkit 82 | *.gpState 83 | 84 | # ReSharper is a .NET coding add-in 85 | _ReSharper*/ 86 | *.[Rr]e[Ss]harper 87 | *.DotSettings.user 88 | 89 | # JustCode is a .NET coding addin-in 90 | .JustCode 91 | 92 | # TeamCity is a build add-in 93 | _TeamCity* 94 | 95 | # DotCover is a Code Coverage Tool 96 | *.dotCover 97 | 98 | # NCrunch 99 | _NCrunch_* 100 | .*crunch*.local.xml 101 | 102 | # MightyMoose 103 | *.mm.* 104 | AutoTest.Net/ 105 | 106 | # Web workbench (sass) 107 | .sass-cache/ 108 | 109 | # Installshield output folder 110 | [Ee]xpress/ 111 | 112 | # DocProject is a documentation generator add-in 113 | DocProject/buildhelp/ 114 | DocProject/Help/*.HxT 115 | DocProject/Help/*.HxC 116 | DocProject/Help/*.hhc 117 | DocProject/Help/*.hhk 118 | DocProject/Help/*.hhp 119 | DocProject/Help/Html2 120 | DocProject/Help/html 121 | 122 | # Click-Once directory 123 | publish/ 124 | 125 | # Publish Web Output 126 | *.[Pp]ublish.xml 127 | *.azurePubxml 128 | # TODO: Comment the next line if you want to checkin your web deploy settings 129 | # but database connection strings (with potential passwords) will be unencrypted 130 | *.pubxml 131 | *.publishproj 132 | 133 | # NuGet Packages 134 | *.nupkg 135 | # The packages folder can be ignored because of Package Restore 136 | **/packages/* 137 | # except build/, which is used as an MSBuild target. 138 | !**/packages/build/ 139 | # If using the old MSBuild-Integrated Package Restore, uncomment this: 140 | #!**/packages/repositories.config 141 | 142 | # Windows Azure Build Output 143 | csx/ 144 | *.build.csdef 145 | 146 | # Windows Store app package directory 147 | AppPackages/ 148 | 149 | # Others 150 | sql/ 151 | *.Cache 152 | ClientBin/ 153 | [Ss]tyle[Cc]op.* 154 | ~$* 155 | *~ 156 | *.dbmdl 157 | *.dbproj.schemaview 158 | *.pfx 159 | *.publishsettings 160 | node_modules/ 161 | 162 | # RIA/Silverlight projects 163 | Generated_Code/ 164 | 165 | # Backup & report files from converting an old project file 166 | # to a newer Visual Studio version. Backup files are not needed, 167 | # because we have git ;-) 168 | _UpgradeReport_Files/ 169 | Backup*/ 170 | UpgradeLog*.XML 171 | UpgradeLog*.htm 172 | 173 | # SQL Server files 174 | *.mdf 175 | *.ldf 176 | 177 | # Business Intelligence projects 178 | *.rdl.data 179 | *.bim.layout 180 | *.bim_*.settings 181 | 182 | # Microsoft Fakes 183 | FakesAssemblies/ 184 | /.vs 185 | /.vscode 186 | -------------------------------------------------------------------------------- /EntityFramework.Triggers.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29319.158 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{C641D16F-6B2F-42C2-81AE-ABBF7C1D4A85}" 7 | ProjectSection(SolutionItems) = preProject 8 | README.md = README.md 9 | EndProjectSection 10 | EndProject 11 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EntityFrameworkCore.Triggers", "src\EntityFrameworkCore.Triggers\EntityFrameworkCore.Triggers.csproj", "{8A37C5E0-C635-4324-ABD8-572E3C68438A}" 12 | EndProject 13 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Testing", "test\Testing\Testing.csproj", "{FF33E298-5F3C-41B3-99FE-B434B317B58C}" 14 | EndProject 15 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestsCore", "test\TestsCore\TestsCore.csproj", "{6FD4046C-8BC8-42EB-9023-71188F5422EC}" 16 | EndProject 17 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{D7D8E993-0048-470A-9AAC-57AB11898D76}" 18 | EndProject 19 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{574C1572-AEB6-49BD-88F0-448848A6DBA1}" 20 | EndProject 21 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestingAsync", "test\TestingAsync\TestingAsync.csproj", "{6894C968-E451-4A62-A4EF-ACD5814BDA92}" 22 | EndProject 23 | Global 24 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 25 | Debug|Any CPU = Debug|Any CPU 26 | Release|Any CPU = Release|Any CPU 27 | EndGlobalSection 28 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 29 | {8A37C5E0-C635-4324-ABD8-572E3C68438A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 30 | {8A37C5E0-C635-4324-ABD8-572E3C68438A}.Debug|Any CPU.Build.0 = Debug|Any CPU 31 | {8A37C5E0-C635-4324-ABD8-572E3C68438A}.Release|Any CPU.ActiveCfg = Release|Any CPU 32 | {8A37C5E0-C635-4324-ABD8-572E3C68438A}.Release|Any CPU.Build.0 = Release|Any CPU 33 | {FF33E298-5F3C-41B3-99FE-B434B317B58C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 34 | {FF33E298-5F3C-41B3-99FE-B434B317B58C}.Debug|Any CPU.Build.0 = Debug|Any CPU 35 | {FF33E298-5F3C-41B3-99FE-B434B317B58C}.Release|Any CPU.ActiveCfg = Release|Any CPU 36 | {FF33E298-5F3C-41B3-99FE-B434B317B58C}.Release|Any CPU.Build.0 = Release|Any CPU 37 | {6FD4046C-8BC8-42EB-9023-71188F5422EC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 38 | {6FD4046C-8BC8-42EB-9023-71188F5422EC}.Debug|Any CPU.Build.0 = Debug|Any CPU 39 | {6FD4046C-8BC8-42EB-9023-71188F5422EC}.Release|Any CPU.ActiveCfg = Release|Any CPU 40 | {6FD4046C-8BC8-42EB-9023-71188F5422EC}.Release|Any CPU.Build.0 = Release|Any CPU 41 | {6894C968-E451-4A62-A4EF-ACD5814BDA92}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 42 | {6894C968-E451-4A62-A4EF-ACD5814BDA92}.Debug|Any CPU.Build.0 = Debug|Any CPU 43 | {6894C968-E451-4A62-A4EF-ACD5814BDA92}.Release|Any CPU.ActiveCfg = Release|Any CPU 44 | {6894C968-E451-4A62-A4EF-ACD5814BDA92}.Release|Any CPU.Build.0 = Release|Any CPU 45 | EndGlobalSection 46 | GlobalSection(SolutionProperties) = preSolution 47 | HideSolutionNode = FALSE 48 | EndGlobalSection 49 | GlobalSection(NestedProjects) = preSolution 50 | {8A37C5E0-C635-4324-ABD8-572E3C68438A} = {D7D8E993-0048-470A-9AAC-57AB11898D76} 51 | {FF33E298-5F3C-41B3-99FE-B434B317B58C} = {574C1572-AEB6-49BD-88F0-448848A6DBA1} 52 | {6FD4046C-8BC8-42EB-9023-71188F5422EC} = {574C1572-AEB6-49BD-88F0-448848A6DBA1} 53 | {6894C968-E451-4A62-A4EF-ACD5814BDA92} = {574C1572-AEB6-49BD-88F0-448848A6DBA1} 54 | EndGlobalSection 55 | GlobalSection(ExtensibilityGlobals) = postSolution 56 | SolutionGuid = {F8B27114-EEC8-4C90-A8B9-336951F10840} 57 | EndGlobalSection 58 | EndGlobal 59 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Nick Strupat 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | EntityFramework.Triggers 2 | ======================== 3 | 4 | Add triggers to your entities with insert, update, and delete events. There are three events for each: before, after, and upon failure. 5 | 6 | This repo contains the code for both the `EntityFramework` and `EntityFrameworkCore` projects, as well as the ASP.NET Core support projects. 7 | 8 | ### Nuget packages for triggers 9 | 10 | | EF version | .NET support | NuGet package | 11 | |:------------|:------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------| 12 | | >= 6.1.3 | >= Framework 4.6.1 | [![NuGet Status](http://img.shields.io/nuget/v/EntityFramework.Triggers.svg?style=flat)](https://www.nuget.org/packages/EntityFramework.Triggers/) | 13 | | >= Core 2.0 | >= Framework 4.6.1 || >= Standard 2.0 | [![NuGet Status](http://img.shields.io/nuget/v/EntityFrameworkCore.Triggers.svg?style=flat)](https://www.nuget.org/packages/EntityFrameworkCore.Triggers/) | 14 | 15 | ### Nuget packages for ASP.NET Core dependency injection methods 16 | 17 | | EF version | .NET support | NuGet package | 18 | |:------------|:------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 19 | | >= 6.1.3 | >= Framework 4.6.1 | [![NuGet Status](http://img.shields.io/nuget/v/NickStrupat.EntityFramework.Triggers.AspNetCore.svg?style=flat)](https://www.nuget.org/packages/NickStrupat.EntityFramework.Triggers.AspNetCore/)| 20 | | >= Core 2.0 | >= Framework 4.6.1 || >= Standard 2.0 | [![NuGet Status](http://img.shields.io/nuget/v/NickStrupat.EntityFrameworkCore.Triggers.AspNetCore.svg?style=flat)](https://www.nuget.org/packages/NickStrupat.EntityFrameworkCore.Triggers.AspNetCore/) | 21 | 22 | ## Basic usage with a global singleton 23 | 24 | To use triggers on your entities, simply have your DbContext inherit from `DbContextWithTriggers`. If you can't change your DbContext inheritance chain, you simply need to override your `SaveChanges...` as demonstrated [below](#manual-overriding-to-enable-triggers) 25 | 26 | ```csharp 27 | public abstract class Trackable { 28 | public DateTime Inserted { get; private set; } 29 | public DateTime Updated { get; private set; } 30 | 31 | static Trackable() { 32 | Triggers.Inserting += entry => entry.Entity.Inserted = entry.Entity.Updated = DateTime.UtcNow; 33 | Triggers.Updating += entry => entry.Entity.Updated = DateTime.UtcNow; 34 | } 35 | } 36 | 37 | public class Person : Trackable { 38 | public Int64 Id { get; private set; } 39 | public String Name { get; set; } 40 | } 41 | 42 | public class Context : DbContextWithTriggers { 43 | public DbSet People { get; set; } 44 | } 45 | ``` 46 | 47 | As you may have guessed, what we're doing above is enabling automatic insert and update stamps for any entity that inherits `Trackable`. Events are raised from the base class/interfaces, up to the events specified on the entity class being used. It's just as easy to set up soft deletes (the Deleting, Updating, and Inserting events are cancellable from within a handler, logging, auditing, and more!). 48 | 49 | ## Usage with dependency injection 50 | 51 | This library fully supports dependency injection. The two features are: 52 | 53 | 1) Injecting the triggers and handler registrations to avoid the global singleton in previous versions 54 | 55 | ```csharp 56 | serviceCollection 57 | .AddSingleton(typeof(ITriggers<,>), typeof(Triggers<,>)) 58 | .AddSingleton(typeof(ITriggers<>), typeof(Triggers<>)) 59 | .AddSingleton(typeof(ITriggers), typeof(Triggers)); 60 | ``` 61 | 62 | 2) Using injected services right inside your global handlers 63 | 64 | ```csharp 65 | Triggers().GlobalInserted.Add( 66 | entry => entry.Service.Broadcast("Inserted", entry.Entity) 67 | ); 68 | 69 | Triggers().GlobalInserted.Add<(IServiceBus Bus, IServiceX X)>( 70 | entry => { 71 | entry.Service.Bus.Broadcast("Inserted", entry.Entity); 72 | entry.Service.X.DoSomething(); 73 | } 74 | ); 75 | ``` 76 | 77 | 3) Using injected services right inside your injected handlers 78 | 79 | ```csharp 80 | public class Startup 81 | { 82 | public void ConfigureServices(IServiceCollection services) 83 | { 84 | ... 85 | services.AddDbContext(); 86 | services.AddTriggers(); 87 | } 88 | 89 | public void Configure(IApplicationBuilder app, IHostingEnvironment env) 90 | { 91 | ... 92 | app.UseTriggers(builder => 93 | { 94 | builder.Triggers().Inserted.Add( 95 | entry => Debug.WriteLine(entry.Entity.ToString()) 96 | ); 97 | builder.Triggers().Inserted.Add( 98 | entry => Debug.WriteLine(entry.Entity.FirstName) 99 | ); 100 | 101 | // receive injected services inside your handler, either with just a single service type or with a value tuple of services 102 | builder.Triggers().GlobalInserted.Add( 103 | entry => entry.Service.Broadcast("Inserted", entry.Entity) 104 | ); 105 | builder.Triggers().GlobalInserted.Add<(IServiceBus Bus, IServiceX X)>( 106 | entry => { 107 | entry.Service.Bus.Broadcast("Inserted", entry.Entity); 108 | entry.Service.X.DoSomething(); 109 | } 110 | ); 111 | }); 112 | } 113 | } 114 | ``` 115 | 116 | ## How to enable triggers if you can't derive from `DbContextWithTriggers` 117 | 118 | If you can't easily change what your `DbContext` class inherits from (ASP.NET Identity users, for example), you can override your `SaveChanges...` methods to call the `SaveChangesWithTriggers...` extension methods. Alternatively, you can call `SaveChangesWithTriggers...` directly instead of `SaveChanges...` if, for example, you want to control which changes cause triggers to be fired. 119 | 120 | ```csharp 121 | class YourContext : DbContext { 122 | // Your usual DbSet<> properties 123 | 124 | #region If you're targeting EF 6 125 | public override Int32 SaveChanges() { 126 | return this.SaveChangesWithTriggers(base.SaveChanges); 127 | } 128 | public override Task SaveChangesAsync(CancellationToken cancellationToken) { 129 | return this.SaveChangesWithTriggersAsync(base.SaveChangesAsync, cancellationToken); 130 | } 131 | #endregion 132 | 133 | #region If you're targeting EF Core 134 | public override Int32 SaveChanges() { 135 | return this.SaveChangesWithTriggers(base.SaveChanges, acceptAllChangesOnSuccess: true); 136 | } 137 | public override Int32 SaveChanges(Boolean acceptAllChangesOnSuccess) { 138 | return this.SaveChangesWithTriggers(base.SaveChanges, acceptAllChangesOnSuccess); 139 | } 140 | public override Task SaveChangesAsync(CancellationToken cancellationToken = default(CancellationToken)) { 141 | return this.SaveChangesWithTriggersAsync(base.SaveChangesAsync, acceptAllChangesOnSuccess: true, cancellationToken: cancellationToken); 142 | } 143 | public override Task SaveChangesAsync(Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken = default(CancellationToken)) { 144 | return this.SaveChangesWithTriggersAsync(base.SaveChangesAsync, acceptAllChangesOnSuccess, cancellationToken); 145 | } 146 | #endregion 147 | } 148 | 149 | #region If you didn't/can't override `SaveChanges...`, you can (not recommended) call 150 | dbContext.SaveChangesWithTriggers(dbContext.SaveChanges); 151 | dbContext.SaveChangesWithTriggersAsync(dbContext.SaveChangesAsync); 152 | #endregion 153 | ``` 154 | 155 | ## Longer example (targeting EF6 for now) 156 | 157 | ```csharp 158 | using System; 159 | using System.Data.Entity; 160 | using System.Data.Entity.Infrastructure; 161 | using System.Data.Entity.Migrations; 162 | using System.Linq; 163 | using System.Threading; 164 | using System.Threading.Tasks; 165 | using EntityFramework.Triggers; 166 | 167 | namespace Example { 168 | public class Program { 169 | public abstract class Trackable { 170 | public virtual DateTime Inserted { get; private set; } 171 | public virtual DateTime Updated { get; private set; } 172 | 173 | static Trackable() { 174 | Triggers.Inserting += entry => entry.Entity.Inserted = entry.Entity.Updated = DateTime.UtcNow; 175 | Triggers.Updating += entry => entry.Entity.Updated = DateTime.UtcNow; 176 | } 177 | } 178 | 179 | public abstract class SoftDeletable : Trackable { 180 | public virtual DateTime? Deleted { get; private set; } 181 | 182 | public Boolean IsSoftDeleted => Deleted != null; 183 | public void SoftDelete() => Deleted = DateTime.UtcNow; 184 | public void SoftRestore() => Deleted = null; 185 | 186 | static SoftDeletable() { 187 | Triggers.Deleting += entry => { 188 | entry.Entity.SoftDelete(); 189 | entry.Cancel = true; // Cancels the deletion, but will persist changes with the same effects as EntityState.Modified 190 | }; 191 | } 192 | } 193 | 194 | public class Person : SoftDeletable { 195 | public virtual Int64 Id { get; private set; } 196 | public virtual String FirstName { get; set; } 197 | public virtual String LastName { get; set; } 198 | } 199 | 200 | public class LogEntry { 201 | public virtual Int64 Id { get; private set; } 202 | public virtual String Message { get; set; } 203 | } 204 | 205 | public class Context : DbContextWithTriggers { 206 | public virtual DbSet People { get; set; } 207 | public virtual DbSet Log { get; set; } 208 | } 209 | internal sealed class Configuration : DbMigrationsConfiguration { 210 | public Configuration() { 211 | AutomaticMigrationsEnabled = true; 212 | } 213 | } 214 | 215 | static Program() { 216 | Triggers.Inserting += e => { 217 | e.Context.Log.Add(new LogEntry { Message = "Insert trigger fired for " + e.Entity.FirstName }); 218 | Console.WriteLine("Inserting " + e.Entity.FirstName); 219 | }; 220 | Triggers.Updating += e => Console.WriteLine($"Updating {e.Original.FirstName} to {e.Entity.FirstName}"); 221 | Triggers.Deleting += e => Console.WriteLine("Deleting " + e.Entity.FirstName); 222 | Triggers.Inserted += e => Console.WriteLine("Inserted " + e.Entity.FirstName); 223 | Triggers.Updated += e => Console.WriteLine("Updated " + e.Entity.FirstName); 224 | Triggers.Deleted += e => Console.WriteLine("Deleted " + e.Entity.FirstName); 225 | } 226 | 227 | private static void Main(String[] args) => Task.WaitAll(MainAsync(args)); 228 | 229 | private static async Task MainAsync(String[] args) { 230 | using (var context = new Context()) { 231 | context.Database.Delete(); 232 | context.Database.Create(); 233 | 234 | var log = context.Log.ToList(); 235 | var nickStrupat = new Person { 236 | FirstName = "Nick", 237 | LastName = "Strupat" 238 | }; 239 | 240 | context.People.Add(nickStrupat); 241 | await context.SaveChangesAsync(); 242 | 243 | nickStrupat.FirstName = "Nicholas"; 244 | context.SaveChanges(); 245 | context.People.Remove(nickStrupat); 246 | await context.SaveChangesAsync(); 247 | } 248 | } 249 | } 250 | } 251 | ``` 252 | 253 | ## See also 254 | 255 | - [https://github.com/NickStrupat/EntityFramework.Rx](https://github.com/NickStrupat/EntityFramework.Rx) for **hot** observables of your EF operations 256 | - [https://github.com/NickStrupat/EntityFramework.PrimaryKey](https://github.com/NickStrupat/EntityFramework.PrimaryKey) to easily get the primary key of any entity (including composite keys) 257 | - [https://github.com/NickStrupat/EntityFramework.TypedOriginalValues](https://github.com/NickStrupat/EntityFramework.TypedOriginalValues) to get a proxy object of the orginal values of your entity (typed access to Property("...").OriginalValue) 258 | - [https://github.com/NickStrupat/EntityFramework.SoftDeletable](https://github.com/NickStrupat/EntityFramework.SoftDeletable) for base classes which encapsulate the soft-delete pattern (including keeping a history with user id, etc.) 259 | - [https://github.com/NickStrupat/EntityFramework.VersionedProperties](https://github.com/NickStrupat/EntityFramework.VersionedProperties) for a library of classes which auto-magically keep an audit history of the changes to the specified property 260 | 261 | ## Contributing 262 | 263 | 1. [Create an issue](https://github.com/NickStrupat/EntityFramework.Triggers/issues/new) 264 | 2. Let's find some point of agreement on your suggestion. 265 | 3. Fork it! 266 | 4. Create your feature branch: `git checkout -b my-new-feature` 267 | 5. Commit your changes: `git commit -am 'Add some feature'` 268 | 6. Push to the branch: `git push origin my-new-feature` 269 | 7. Submit a pull request :D 270 | 271 | ## History 272 | 273 | [Commit history](https://github.com/NickStrupat/EntityFramework.Triggers/commits/master) 274 | 275 | ## License 276 | 277 | [MIT License](https://github.com/NickStrupat/EntityFramework.Triggers/blob/master/README.md) 278 | -------------------------------------------------------------------------------- /src/EntityFrameworkCore.Triggers/ArrayEqualityComparer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace EntityFrameworkCore.Triggers; 5 | 6 | public sealed class ArrayEqualityComparer : IEqualityComparer 7 | { 8 | public static readonly ArrayEqualityComparer Default = new(EqualityComparer.Default); 9 | 10 | private readonly IEqualityComparer elementEqualityComparer; 11 | 12 | public ArrayEqualityComparer(IEqualityComparer elementEqualityComparer) 13 | { 14 | ArgumentNullException.ThrowIfNull(elementEqualityComparer); 15 | this.elementEqualityComparer = elementEqualityComparer; 16 | } 17 | 18 | public Boolean Equals(TElement[]? x, TElement[]? y) 19 | { 20 | if (ReferenceEquals(x, y)) // both null or both referencing the same array object 21 | return true; 22 | if (x is null || y is null) 23 | return false; 24 | if (x.Length != y.Length) 25 | return false; 26 | for (var i = 0; i != x.Length; i++) 27 | if (!elementEqualityComparer.Equals(x[i], y[i])) 28 | return false; 29 | return true; 30 | } 31 | 32 | public Int32 GetHashCode(TElement[] types) 33 | { 34 | ArgumentNullException.ThrowIfNull(types); 35 | return HashCode.Combine(types.Length); 36 | } 37 | } -------------------------------------------------------------------------------- /src/EntityFrameworkCore.Triggers/DbContextExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | #if EF_CORE 7 | using Microsoft.EntityFrameworkCore; 8 | namespace EntityFrameworkCore.Triggers { 9 | #else 10 | using System.Data.Entity; 11 | using System.Data.Entity.Infrastructure; 12 | using System.Data.Entity.Validation; 13 | namespace EntityFramework.Triggers { 14 | #endif 15 | public static class DbContextExtensions { 16 | /// 17 | /// Saves all changes made in this context to the underlying database, firing trigger events accordingly. Only call this within your DbContext class. 18 | /// 19 | /// 20 | /// Always pass base.SaveChanges 21 | #if EF_CORE 22 | /// 23 | /// Indicates whether is called after the changes have 24 | /// been sent successfully to the database. 25 | /// 26 | /// this.SaveChangesWithTriggers(base.SaveChanges, true); 27 | #else 28 | /// this.SaveChangesWithTriggers(base.SaveChanges); 29 | #endif 30 | /// The number of objects written to the underlying database. 31 | #if EF_CORE 32 | public static Int32 SaveChangesWithTriggers(this DbContext dbContext, Func baseSaveChanges, Boolean acceptAllChangesOnSuccess = true) { 33 | return dbContext.SaveChangesWithTriggers(baseSaveChanges, null, acceptAllChangesOnSuccess); 34 | #else 35 | public static Int32 SaveChangesWithTriggers(this DbContext dbContext, Func baseSaveChanges) { 36 | return dbContext.SaveChangesWithTriggers(baseSaveChanges, null); 37 | #endif 38 | } 39 | /// 40 | /// Saves all changes made in this context to the underlying database, firing trigger events accordingly. Only call this within your DbContext class. 41 | /// 42 | /// 43 | /// Always pass base.SaveChanges 44 | #if EF_CORE 45 | /// 46 | /// Indicates whether is called after the changes have 47 | /// been sent successfully to the database. 48 | /// 49 | /// this.SaveChangesWithTriggers(base.SaveChanges, true); 50 | #else 51 | /// this.SaveChangesWithTriggers(base.SaveChanges); 52 | #endif 53 | /// The number of objects written to the underlying database. 54 | #if EF_CORE 55 | public static Int32 SaveChangesWithTriggers(this DbContext dbContext, Func baseSaveChanges, IServiceProvider serviceProvider, Boolean acceptAllChangesOnSuccess = true) { 56 | #else 57 | public static Int32 SaveChangesWithTriggers(this DbContext dbContext, Func baseSaveChanges, IServiceProvider serviceProvider) { 58 | #endif 59 | if (dbContext == null) 60 | throw new ArgumentNullException(nameof(dbContext)); 61 | var invoker = GenericServiceCache>.GetOrAdd(dbContext.GetType()); 62 | var swallow = false; 63 | try { 64 | var afterActions = invoker.RaiseChangingEvents(dbContext, serviceProvider); 65 | #if EF_CORE 66 | var result = baseSaveChanges(acceptAllChangesOnSuccess); 67 | #else 68 | var result = baseSaveChanges(); 69 | #endif 70 | invoker.RaiseChangedEvents(dbContext, serviceProvider, afterActions); 71 | return result; 72 | } 73 | catch (DbUpdateException ex) when (invoker.RaiseFailedEvents(dbContext, serviceProvider, ex, ref swallow)) { 74 | } 75 | #if !EF_CORE 76 | catch (DbEntityValidationException ex) when (invoker.RaiseFailedEvents(dbContext, serviceProvider, ex, ref swallow)) { 77 | } 78 | #endif 79 | catch (Exception ex) when(invoker.RaiseFailedEvents(dbContext, serviceProvider, ex, ref swallow)) { 80 | } 81 | return 0; 82 | } 83 | 84 | /// 85 | /// Asynchronously saves all changes made in this context to the underlying database, firing trigger events accordingly. 86 | /// 87 | /// 88 | /// Always pass base.SaveChangesAsync 89 | #if EF_CORE 90 | /// 91 | /// Indicates whether is called after the changes have 92 | /// been sent successfully to the database. 93 | /// 94 | #endif 95 | /// A to observe while waiting for the task to complete. 96 | /// this.SaveChangesWithTriggersAsync(); 97 | /// A task that represents the asynchronous save operation. The task result contains the number of objects written to the underlying database. 98 | #if EF_CORE 99 | public static Task SaveChangesWithTriggersAsync(this DbContext dbContext, Func> baseSaveChangesAsync, Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken = default) { 100 | return dbContext.SaveChangesWithTriggersAsync(baseSaveChangesAsync, null, acceptAllChangesOnSuccess, cancellationToken); 101 | #else 102 | public static Task SaveChangesWithTriggersAsync(this DbContext dbContext, Func> baseSaveChangesAsync, CancellationToken cancellationToken = default) { 103 | return dbContext.SaveChangesWithTriggersAsync(baseSaveChangesAsync, null, cancellationToken); 104 | #endif 105 | } 106 | 107 | /// 108 | /// Asynchronously saves all changes made in this context to the underlying database, firing trigger events accordingly. 109 | /// 110 | /// 111 | /// Always pass base.SaveChangesAsync 112 | #if EF_CORE 113 | /// 114 | /// Indicates whether is called after the changes have 115 | /// been sent successfully to the database. 116 | /// 117 | #endif 118 | /// A to observe while waiting for the task to complete. 119 | /// this.SaveChangesWithTriggersAsync(); 120 | /// A task that represents the asynchronous save operation. The task result contains the number of objects written to the underlying database. 121 | #if EF_CORE 122 | public static async Task SaveChangesWithTriggersAsync(this DbContext dbContext, Func> baseSaveChangesAsync, IServiceProvider serviceProvider, Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken = default) { 123 | #else 124 | public static async Task SaveChangesWithTriggersAsync(this DbContext dbContext, Func> baseSaveChangesAsync, IServiceProvider serviceProvider, CancellationToken cancellationToken = default) { 125 | #endif 126 | if (dbContext == null) 127 | throw new ArgumentNullException(nameof(dbContext)); 128 | var invoker = GenericServiceCache>.GetOrAdd(dbContext.GetType()); 129 | try { 130 | var afterActions = await invoker.RaiseChangingEventsAsync(dbContext, serviceProvider); 131 | #if EF_CORE 132 | var result = await baseSaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken).ConfigureAwait(false); 133 | #else 134 | var result = await baseSaveChangesAsync(cancellationToken).ConfigureAwait(false); 135 | #endif 136 | await invoker.RaiseChangedEventsAsync(dbContext, serviceProvider, afterActions); 137 | return result; 138 | } 139 | catch (DbUpdateException ex) { 140 | var swallow = await invoker.RaiseFailedEventsAsync(dbContext, serviceProvider, ex); 141 | if (!swallow) 142 | throw; 143 | } 144 | #if !EF_CORE 145 | catch (DbEntityValidationException ex) { 146 | var swallow = await invoker.RaiseFailedEventsAsync(dbContext, serviceProvider, ex); 147 | if (!swallow) 148 | throw; 149 | } 150 | #endif 151 | catch (Exception ex) { 152 | var swallow = await invoker.RaiseFailedEventsAsync(dbContext, serviceProvider, ex); 153 | if (!swallow) 154 | throw; 155 | } 156 | return 0; 157 | } 158 | 159 | #if EF_CORE 160 | public static Task SaveChangesWithTriggersAsync(this DbContext dbContext, Func> baseSaveChangesAsync, IServiceProvider serviceProvider, CancellationToken cancellationToken = default) { 161 | return dbContext.SaveChangesWithTriggersAsync(baseSaveChangesAsync, serviceProvider, true, cancellationToken); 162 | } 163 | public static Task SaveChangesWithTriggersAsync(this DbContext dbContext, Func> baseSaveChangesAsync, CancellationToken cancellationToken = default) { 164 | return dbContext.SaveChangesWithTriggersAsync(baseSaveChangesAsync, null, true, cancellationToken); 165 | } 166 | #endif 167 | } 168 | } -------------------------------------------------------------------------------- /src/EntityFrameworkCore.Triggers/DbContextWithTriggers.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | #if EF_CORE 6 | using Microsoft.EntityFrameworkCore; 7 | namespace EntityFrameworkCore.Triggers 8 | #else 9 | using System.Data.Entity; 10 | using System.Data.Common; 11 | using System.Data.Entity.Core.Objects; 12 | using System.Data.Entity.Infrastructure; 13 | namespace EntityFramework.Triggers 14 | #endif 15 | { 16 | /// 17 | /// A -derived class with trigger functionality called automatically 18 | /// 19 | public class DbContextWithTriggers : DbContext 20 | { 21 | public Boolean TriggersEnabled { get; set; } = true; 22 | 23 | private readonly IServiceProvider serviceProvider; 24 | 25 | public DbContextWithTriggers() : base() { } 26 | public DbContextWithTriggers(IServiceProvider serviceProvider) : base() => this.serviceProvider = serviceProvider; 27 | #if EF_CORE 28 | public DbContextWithTriggers(DbContextOptions options) : base(options) {} 29 | 30 | public DbContextWithTriggers(IServiceProvider serviceProvider, DbContextOptions options) : base(options) => this.serviceProvider = serviceProvider; 31 | 32 | public override Int32 SaveChanges() { 33 | return TriggersEnabled ? this.SaveChangesWithTriggers(base.SaveChanges, serviceProvider) : base.SaveChanges(); 34 | } 35 | 36 | public override Int32 SaveChanges(Boolean acceptAllChangesOnSuccess) { 37 | return TriggersEnabled ? this.SaveChangesWithTriggers(base.SaveChanges, serviceProvider, acceptAllChangesOnSuccess) : base.SaveChanges(acceptAllChangesOnSuccess); 38 | } 39 | 40 | public override Task SaveChangesAsync(CancellationToken cancellationToken = default) { 41 | return TriggersEnabled ? this.SaveChangesWithTriggersAsync(base.SaveChangesAsync, serviceProvider, cancellationToken) : base.SaveChangesAsync(cancellationToken); 42 | } 43 | 44 | public override Task SaveChangesAsync(Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken = default) { 45 | return TriggersEnabled ? this.SaveChangesWithTriggersAsync(base.SaveChangesAsync, serviceProvider, acceptAllChangesOnSuccess, cancellationToken) : base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken); 46 | } 47 | #else 48 | public DbContextWithTriggers(DbCompiledModel model) : base(model) {} 49 | public DbContextWithTriggers(String nameOrConnectionString) : base(nameOrConnectionString) {} 50 | public DbContextWithTriggers(DbConnection existingConnection, Boolean contextOwnsConnection) : base(existingConnection, contextOwnsConnection) {} 51 | public DbContextWithTriggers(ObjectContext objectContext, Boolean dbContextOwnsObjectContext) : base(objectContext, dbContextOwnsObjectContext) {} 52 | public DbContextWithTriggers(String nameOrConnectionString, DbCompiledModel model) : base(nameOrConnectionString, model) {} 53 | public DbContextWithTriggers(DbConnection existingConnection, DbCompiledModel model, Boolean contextOwnsConnection) : base(existingConnection, model, contextOwnsConnection) {} 54 | 55 | public DbContextWithTriggers(IServiceProvider serviceProvider, DbCompiledModel model) : base(model) => this.serviceProvider = serviceProvider; 56 | public DbContextWithTriggers(IServiceProvider serviceProvider, String nameOrConnectionString) : base(nameOrConnectionString) => this.serviceProvider = serviceProvider; 57 | public DbContextWithTriggers(IServiceProvider serviceProvider, DbConnection existingConnection, Boolean contextOwnsConnection) : base(existingConnection, contextOwnsConnection) => this.serviceProvider = serviceProvider; 58 | public DbContextWithTriggers(IServiceProvider serviceProvider, ObjectContext objectContext, Boolean dbContextOwnsObjectContext) : base(objectContext, dbContextOwnsObjectContext) => this.serviceProvider = serviceProvider; 59 | public DbContextWithTriggers(IServiceProvider serviceProvider, String nameOrConnectionString, DbCompiledModel model) : base(nameOrConnectionString, model) => this.serviceProvider = serviceProvider; 60 | public DbContextWithTriggers(IServiceProvider serviceProvider, DbConnection existingConnection, DbCompiledModel model, Boolean contextOwnsConnection) : base(existingConnection, model, contextOwnsConnection) => this.serviceProvider = serviceProvider; 61 | 62 | public override Int32 SaveChanges() { 63 | return TriggersEnabled ? this.SaveChangesWithTriggers(base.SaveChanges, serviceProvider) : base.SaveChanges(); 64 | } 65 | public override Task SaveChangesAsync(CancellationToken cancellationToken) { 66 | return TriggersEnabled ? this.SaveChangesWithTriggersAsync(base.SaveChangesAsync, serviceProvider, cancellationToken) : base.SaveChangesAsync(cancellationToken); 67 | } 68 | #endif 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/EntityFrameworkCore.Triggers/DelegateSynchronyUnion.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace EntityFrameworkCore.Triggers; 5 | 6 | internal readonly struct DelegateSynchronyUnion : IEquatable> 7 | { 8 | private readonly Delegate @delegate; 9 | 10 | public DelegateSynchronyUnion(Action action) => @delegate = action ?? throw new ArgumentNullException(nameof(action)); 11 | public DelegateSynchronyUnion(Func func) => @delegate = func ?? throw new ArgumentNullException(nameof(func)); 12 | 13 | public void Invoke(T value) 14 | { 15 | switch (@delegate) 16 | { 17 | case Action action: 18 | action(value); 19 | break; 20 | case Func func: 21 | func(value).Wait(); 22 | break; 23 | } 24 | } 25 | 26 | public async Task InvokeAsync(T value) 27 | { 28 | switch (@delegate) 29 | { 30 | case Action action: 31 | action(value); 32 | break; 33 | case Func func: 34 | await func(value); 35 | break; 36 | } 37 | } 38 | 39 | public override Int32 GetHashCode() => @delegate.GetHashCode(); 40 | public override String? ToString() => @delegate.ToString(); 41 | public override Boolean Equals(Object? o) => o is DelegateSynchronyUnion dsu && Equals(dsu); 42 | public Boolean Equals(DelegateSynchronyUnion dsu) => @delegate == dsu.@delegate; 43 | } -------------------------------------------------------------------------------- /src/EntityFrameworkCore.Triggers/EntityEntryComparer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Microsoft.EntityFrameworkCore.ChangeTracking; 4 | 5 | namespace EntityFrameworkCore.Triggers; 6 | 7 | internal class EntityEntryComparer : IEqualityComparer 8 | { 9 | public static readonly EntityEntryComparer Default = new(); 10 | 11 | private EntityEntryComparer() { } 12 | 13 | public Boolean Equals(EntityEntry? x, EntityEntry? y) 14 | { 15 | if (ReferenceEquals(x, y)) 16 | return true; 17 | if (x is null || y is null) 18 | return false; 19 | return ReferenceEquals(x.Entity, y.Entity); 20 | } 21 | 22 | public Int32 GetHashCode(EntityEntry? obj) => obj?.Entity.GetHashCode() ?? 0; 23 | } -------------------------------------------------------------------------------- /src/EntityFrameworkCore.Triggers/EntityFrameworkCore.Triggers.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | enable 6 | nullable 7 | 618 8 | Supports events for entity inserting, inserted, updating, updated, deleting, and deleted. Also events on insert failure, update failure, and delete failure. 9 | EntityFrameworkCore.Triggers 10 | EntityFrameworkCore.Triggers 11 | Nick Strupat 12 | EntityFrameworkCore.Triggers 13 | entity-framework-core;entityframeworkcore;triggers 14 | https://github.com/NickStrupat/EntityFramework.Triggers 15 | https://raw.githubusercontent.com/NickStrupat/EntityFramework.Triggers/master/LICENSE 16 | 1.2.3.0 17 | 1.2.3.0 18 | 1.2.3 19 | true 20 | Fix bug in async change events: https://github.com/NickStrupat/EntityFramework.Triggers/issues/61 21 | EF_CORE 22 | 23 | 24 | 25 | 26 | True 27 | True 28 | TriggerEvent.Generated.tt 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | TextTemplatingFileGenerator 39 | TriggerEvent.Generated.cs 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | True 50 | True 51 | TriggerEvent.Generated.tt 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/EntityFrameworkCore.Triggers/Entry.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using Microsoft.EntityFrameworkCore; 4 | 5 | namespace EntityFrameworkCore.Triggers; 6 | 7 | #region Base classes 8 | 9 | #region Non-generic service 10 | 11 | internal abstract class Entry( 12 | TEntity entity, 13 | TDbContext context, 14 | IServiceProvider service) 15 | : IEntry 16 | where TEntity : class 17 | where TDbContext : DbContext 18 | { 19 | public TEntity Entity { get; } = entity; 20 | Object IEntry.Entity => Entity; 21 | public TDbContext Context { get; } = context; 22 | public IServiceProvider Service { get; } = service; 23 | DbContext IEntry.Context => Context; 24 | } 25 | 26 | internal abstract class BeforeEntry( 27 | TEntity entity, 28 | TDbContext context, 29 | IServiceProvider service, 30 | Boolean cancel) 31 | : Entry(entity, context, service), IBeforeEntry 32 | where TEntity : class 33 | where TDbContext : DbContext 34 | { 35 | public Boolean Cancel { get; set; } = cancel; 36 | } 37 | 38 | internal abstract class BeforeChangeEntry( 39 | TEntity entity, 40 | TDbContext context, 41 | IServiceProvider service, 42 | Boolean cancel) 43 | : BeforeEntry(entity, context, service, cancel), IBeforeChangeEntry 44 | where TEntity : class 45 | where TDbContext : DbContext 46 | { 47 | private TEntity? original; 48 | public TEntity Original => original ??= (TEntity)Context.Entry(Entity).OriginalValues.ToObject(); 49 | } 50 | 51 | internal abstract class FailedEntry( 52 | TEntity entity, 53 | TDbContext context, 54 | IServiceProvider service, 55 | Exception exception, 56 | Boolean swallow) 57 | : Entry(entity, context, service), IFailedEntry 58 | where TEntity : class 59 | where TDbContext : DbContext 60 | { 61 | public Exception Exception { get; } = exception; 62 | public Boolean Swallow { get; set; } = swallow; 63 | } 64 | 65 | internal abstract class ChangeFailedEntry( 66 | TEntity entity, 67 | TDbContext context, 68 | IServiceProvider service, 69 | Exception exception, 70 | Boolean swallow) 71 | : FailedEntry(entity, context, service, exception, swallow) 72 | , IChangeFailedEntry 73 | where TEntity : class 74 | where TDbContext : DbContext; 75 | 76 | #endregion 77 | 78 | #region Generic service 79 | 80 | internal abstract class Entry( 81 | TEntity entity, 82 | TDbContext context, 83 | IServiceProvider service) 84 | : Entry(entity, context, service), IEntry 85 | where TEntity : class 86 | where TDbContext : DbContext 87 | { 88 | public new TService Service { get; } = ServiceRetrieval.GetService(service); 89 | } 90 | 91 | internal abstract class BeforeEntry( 92 | TEntity entity, 93 | TDbContext context, 94 | IServiceProvider service, 95 | Boolean cancel) 96 | : Entry(entity, context, service), IBeforeEntry 97 | where TEntity : class 98 | where TDbContext : DbContext 99 | { 100 | public Boolean Cancel { get; set; } = cancel; 101 | } 102 | 103 | internal abstract class BeforeChangeEntry( 104 | TEntity entity, 105 | TDbContext context, 106 | IServiceProvider service, 107 | Boolean cancel) 108 | : BeforeEntry(entity, context, service, cancel), 109 | IBeforeChangeEntry 110 | where TEntity : class 111 | where TDbContext : DbContext 112 | { 113 | private TEntity? original; 114 | public TEntity Original => original ??= (TEntity)Context.Entry(Entity).OriginalValues.ToObject(); 115 | } 116 | 117 | internal abstract class FailedEntry( 118 | TEntity entity, 119 | TDbContext context, 120 | IServiceProvider service, 121 | Exception exception, 122 | Boolean swallow) 123 | : Entry(entity, context, service), IFailedEntry 124 | where TEntity : class 125 | where TDbContext : DbContext 126 | { 127 | public Exception Exception { get; } = exception; 128 | public Boolean Swallow { get; set; } = swallow; 129 | } 130 | 131 | internal abstract class ChangeFailedEntry( 132 | TEntity entity, 133 | TDbContext context, 134 | IServiceProvider service, 135 | Exception exception, 136 | Boolean swallow) 137 | : FailedEntry(entity, context, service, exception, swallow), 138 | IChangeFailedEntry 139 | where TEntity : class 140 | where TDbContext : DbContext; 141 | 142 | #endregion 143 | 144 | #region Wrapped with generic service 145 | 146 | internal abstract class WrappedEntry(TEntry entry) 147 | : IEntry 148 | where TEntry : class, IEntry 149 | where TEntity : class 150 | where TDbContext : DbContext 151 | { 152 | protected readonly TEntry Entry = entry; 153 | 154 | Object IEntry.Entity => Entity; 155 | public TDbContext Context => Entry.Context; 156 | public TEntity Entity => Entry.Entity; 157 | DbContext IEntry.Context => Context; 158 | IServiceProvider IEntry.Service => Entry.Service; 159 | public TService Service { get; } = ServiceRetrieval.GetService(entry.Service); 160 | } 161 | 162 | internal abstract class WrappedBeforeEntry(TEntry entry) 163 | : WrappedEntry(entry), IBeforeEntry 164 | where TEntry : class, IBeforeEntry 165 | where TEntity : class 166 | where TDbContext : DbContext 167 | { 168 | public Boolean Cancel 169 | { 170 | get => Entry.Cancel; 171 | set => Entry.Cancel = value; 172 | } 173 | } 174 | 175 | internal abstract class WrappedBeforeChangeEntry(TEntry entry) 176 | : WrappedBeforeEntry(entry), 177 | IBeforeChangeEntry 178 | where TEntry : class, IBeforeChangeEntry 179 | where TEntity : class 180 | where TDbContext : DbContext 181 | { 182 | public TEntity Original => Entry.Original; 183 | } 184 | 185 | internal abstract class WrappedFailedEntry(TEntry entry) 186 | : WrappedEntry(entry), IFailedEntry 187 | where TEntry : class, IFailedEntry 188 | where TEntity : class 189 | where TDbContext : DbContext 190 | { 191 | public Exception Exception => Entry.Exception; 192 | 193 | public Boolean Swallow 194 | { 195 | get => Entry.Swallow; 196 | set => Entry.Swallow = value; 197 | } 198 | } 199 | 200 | internal abstract class WrappedChangeFailedEntry(TEntry entry) 201 | : WrappedFailedEntry(entry), 202 | IChangeFailedEntry 203 | where TEntry : class, IChangeFailedEntry 204 | where TEntity : class 205 | where TDbContext : DbContext; 206 | 207 | #endregion 208 | 209 | #endregion 210 | 211 | #region Final classes 212 | 213 | #region Non-generic service 214 | 215 | internal class InsertingEntry( 216 | TEntity entity, 217 | TDbContext context, 218 | IServiceProvider service, 219 | Boolean cancel) 220 | : BeforeEntry(entity, context, service, cancel), IInsertingEntry 221 | where TEntity : class 222 | where TDbContext : DbContext; 223 | 224 | internal class UpdatingEntry( 225 | TEntity entity, 226 | TDbContext context, 227 | IServiceProvider service, 228 | Boolean cancel) 229 | : BeforeChangeEntry(entity, context, service, cancel), IUpdatingEntry 230 | where TEntity : class 231 | where TDbContext : DbContext; 232 | 233 | internal class DeletingEntry( 234 | TEntity entity, 235 | TDbContext context, 236 | IServiceProvider service, 237 | Boolean cancel) 238 | : BeforeChangeEntry(entity, context, service, cancel), IDeletingEntry 239 | where TEntity : class 240 | where TDbContext : DbContext; 241 | 242 | internal class InsertFailedEntry( 243 | TEntity entity, 244 | TDbContext context, 245 | IServiceProvider service, 246 | Exception exception, 247 | Boolean swallow) 248 | : FailedEntry(entity, context, service, exception, swallow), 249 | IInsertFailedEntry 250 | where TEntity : class 251 | where TDbContext : DbContext; 252 | 253 | internal class UpdateFailedEntry( 254 | TEntity entity, 255 | TDbContext context, 256 | IServiceProvider service, 257 | Exception exception, 258 | Boolean swallow) 259 | : ChangeFailedEntry(entity, context, service, exception, swallow), 260 | IUpdateFailedEntry 261 | where TEntity : class 262 | where TDbContext : DbContext; 263 | 264 | internal class DeleteFailedEntry( 265 | TEntity entity, 266 | TDbContext context, 267 | IServiceProvider service, 268 | Exception exception, 269 | Boolean swallow) 270 | : ChangeFailedEntry(entity, context, service, exception, swallow), 271 | IDeleteFailedEntry 272 | where TEntity : class 273 | where TDbContext : DbContext; 274 | 275 | internal class InsertedEntry(TEntity entity, TDbContext context, IServiceProvider service) 276 | : Entry(entity, context, 277 | service), IInsertedEntry 278 | where TEntity : class 279 | where TDbContext : DbContext; 280 | 281 | internal class UpdatedEntry(TEntity entity, TDbContext context, IServiceProvider service) 282 | : Entry(entity, context, 283 | service), IUpdatedEntry 284 | where TEntity : class 285 | where TDbContext : DbContext; 286 | 287 | internal class DeletedEntry(TEntity entity, TDbContext context, IServiceProvider service) 288 | : Entry(entity, context, 289 | service), IDeletedEntry 290 | where TEntity : class 291 | where TDbContext : DbContext; 292 | 293 | #endregion 294 | 295 | #region Generic service 296 | 297 | internal class InsertingEntry( 298 | TEntity entity, 299 | TDbContext context, 300 | IServiceProvider service, 301 | Boolean cancel) 302 | : BeforeEntry(entity, context, service, cancel), 303 | IInsertingEntry 304 | where TEntity : class 305 | where TDbContext : DbContext; 306 | 307 | internal class UpdatingEntry( 308 | TEntity entity, 309 | TDbContext context, 310 | IServiceProvider service, 311 | Boolean cancel) 312 | : BeforeChangeEntry(entity, context, service, cancel), 313 | IUpdatingEntry 314 | where TEntity : class 315 | where TDbContext : DbContext; 316 | 317 | internal class DeletingEntry( 318 | TEntity entity, 319 | TDbContext context, 320 | IServiceProvider service, 321 | Boolean cancel) 322 | : BeforeChangeEntry(entity, context, service, cancel), 323 | IDeletingEntry 324 | where TEntity : class 325 | where TDbContext : DbContext; 326 | 327 | internal class InsertFailedEntry( 328 | TEntity entity, 329 | TDbContext context, 330 | IServiceProvider service, 331 | Exception exception, 332 | Boolean swallow) 333 | : FailedEntry(entity, context, service, exception, swallow), 334 | IInsertFailedEntry 335 | where TEntity : class 336 | where TDbContext : DbContext; 337 | 338 | internal class UpdateFailedEntry( 339 | TEntity entity, 340 | TDbContext context, 341 | IServiceProvider service, 342 | Exception exception, 343 | Boolean swallow) 344 | : ChangeFailedEntry(entity, context, service, exception, swallow), 345 | IUpdateFailedEntry 346 | where TEntity : class 347 | where TDbContext : DbContext; 348 | 349 | internal class DeleteFailedEntry( 350 | TEntity entity, 351 | TDbContext context, 352 | IServiceProvider service, 353 | Exception exception, 354 | Boolean swallow) 355 | : ChangeFailedEntry(entity, context, service, exception, swallow), 356 | IDeleteFailedEntry 357 | where TEntity : class 358 | where TDbContext : DbContext; 359 | 360 | internal class InsertedEntry( 361 | TEntity entity, 362 | TDbContext context, 363 | IServiceProvider service) 364 | : Entry(entity, context, 365 | service), IInsertedEntry 366 | where TEntity : class 367 | where TDbContext : DbContext; 368 | 369 | internal class UpdatedEntry( 370 | TEntity entity, 371 | TDbContext context, 372 | IServiceProvider service) 373 | : Entry(entity, context, 374 | service), IUpdatedEntry 375 | where TEntity : class 376 | where TDbContext : DbContext; 377 | 378 | internal class DeletedEntry( 379 | TEntity entity, 380 | TDbContext context, 381 | IServiceProvider service) 382 | : Entry(entity, context, 383 | service), IDeletedEntry 384 | where TEntity : class 385 | where TDbContext : DbContext; 386 | 387 | #endregion 388 | 389 | #region Wrapped with generic service 390 | 391 | internal class WrappedInsertingEntry(IInsertingEntry entry) 392 | : WrappedBeforeEntry, TEntity, TDbContext, TService>(entry), 393 | IInsertingEntry 394 | where TEntity : class 395 | where TDbContext : DbContext; 396 | 397 | internal class WrappedUpdatingEntry(IUpdatingEntry entry) 398 | : WrappedBeforeChangeEntry, TEntity, TDbContext, TService>(entry), 399 | IUpdatingEntry 400 | where TEntity : class 401 | where TDbContext : DbContext; 402 | 403 | internal class WrappedDeletingEntry(IDeletingEntry entry) 404 | : WrappedBeforeChangeEntry, TEntity, TDbContext, TService>(entry), 405 | IDeletingEntry 406 | where TEntity : class 407 | where TDbContext : DbContext; 408 | 409 | internal class 410 | WrappedInsertFailedEntry(IInsertFailedEntry entry) 411 | : WrappedFailedEntry, TEntity, TDbContext, TService>(entry), 412 | IInsertFailedEntry 413 | where TEntity : class 414 | where TDbContext : DbContext; 415 | 416 | internal class 417 | WrappedUpdateFailedEntry(IUpdateFailedEntry entry) 418 | : WrappedChangeFailedEntry, TEntity, TDbContext, TService>(entry), 419 | IUpdateFailedEntry 420 | where TEntity : class 421 | where TDbContext : DbContext; 422 | 423 | internal class 424 | WrappedDeleteFailedEntry(IDeleteFailedEntry entry) 425 | : WrappedChangeFailedEntry, TEntity, TDbContext, TService>(entry), 426 | IDeleteFailedEntry 427 | where TEntity : class 428 | where TDbContext : DbContext; 429 | 430 | internal class WrappedInsertedEntry(IInsertedEntry entry) 431 | : WrappedEntry, TEntity, TDbContext, TService>(entry), 432 | IInsertedEntry 433 | where TEntity : class 434 | where TDbContext : DbContext; 435 | 436 | internal class WrappedUpdatedEntry(IUpdatedEntry entry) 437 | : WrappedEntry, TEntity, TDbContext, TService>(entry), 438 | IUpdatedEntry 439 | where TEntity : class 440 | where TDbContext : DbContext; 441 | 442 | internal class WrappedDeletedEntry(IDeletedEntry entry) 443 | : WrappedEntry, TEntity, TDbContext, TService>(entry), 444 | IDeletedEntry 445 | where TEntity : class 446 | where TDbContext : DbContext; 447 | 448 | #endregion 449 | 450 | #endregion -------------------------------------------------------------------------------- /src/EntityFrameworkCore.Triggers/GenericServiceCache.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | 4 | namespace EntityFrameworkCore.Triggers; 5 | 6 | public static class GenericServiceCache 7 | where TConstructedGenericImplementation : TInterface 8 | { 9 | private static readonly Type GenericTypeDefinition = 10 | typeof(TConstructedGenericImplementation).GetGenericTypeDefinition(); 11 | 12 | private static readonly ConcurrentDictionary cache = new(ArrayEqualityComparer.Default); 13 | 14 | private static TInterface Factory(Type[] types) => 15 | (TInterface)Activator.CreateInstance(GenericTypeDefinition.MakeGenericType(types))!; 16 | 17 | public static TInterface GetOrAdd(params Type[] genericArguments) => cache.GetOrAdd(genericArguments, Factory); 18 | } -------------------------------------------------------------------------------- /src/EntityFrameworkCore.Triggers/IEntry.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore; 3 | 4 | namespace EntityFrameworkCore.Triggers; 5 | 6 | #region Non-generic 7 | public interface IEntry 8 | { 9 | /// The entity 10 | Object Entity { get; } 11 | 12 | /// The DbContext instance 13 | DbContext Context { get; } 14 | 15 | /// The service provider, if one is provided 16 | IServiceProvider Service { get; } 17 | } 18 | 19 | public interface IBeforeEntry : IEntry { 20 | /// Get or set a value that marks the change to be cancelled if true 21 | Boolean Cancel { get; set; } 22 | } 23 | 24 | public interface IFailedEntry : IEntry { 25 | /// The exception raised by the attempted change 26 | Exception Exception { get; } 27 | 28 | /// Get or set a value that marks the exception to be caught and swallowed if true, or to be allowed to propagate if false 29 | Boolean Swallow { get; set; } 30 | } 31 | 32 | public interface IAfterEntry : IEntry {} 33 | public interface IChangeEntry : IEntry {} 34 | public interface IBeforeChangeEntry : IChangeEntry, IBeforeEntry {} 35 | public interface IChangeFailedEntry : IChangeEntry, IFailedEntry {} 36 | public interface IAfterChangeEntry : IChangeEntry, IAfterEntry {} 37 | 38 | #region Specific to events 39 | public interface IInsertingEntry : IBeforeEntry { 40 | /// Get or set a value that marks the insertion to be cancelled if true (the entity will not be persisted) 41 | new Boolean Cancel { get; set; } 42 | } 43 | public interface IUpdatingEntry : IBeforeChangeEntry { 44 | /// Get or set a value that marks the update to be cancelled if true (changes to the entity properties will not be persisted) 45 | new Boolean Cancel { get; set; } 46 | } 47 | public interface IDeletingEntry : IBeforeChangeEntry { 48 | /// Get or set a value that marks the deletion to be cancelled if true (the entity will not be deleted, but changes to the entity properties will be persisted) 49 | new Boolean Cancel { get; set; } 50 | } 51 | 52 | public interface IInsertFailedEntry : IFailedEntry {} 53 | public interface IUpdateFailedEntry : IChangeFailedEntry {} 54 | public interface IDeleteFailedEntry : IChangeFailedEntry {} 55 | 56 | public interface IInsertedEntry : IAfterEntry {} 57 | public interface IUpdatedEntry : IAfterChangeEntry {} 58 | public interface IDeletedEntry : IAfterChangeEntry {} 59 | #endregion 60 | #endregion 61 | 62 | #region Without specific DbContext type 63 | /// Contains the context and the instance of the changed entity 64 | public interface IEntry : IEntry where TEntity : class { 65 | /// The entity 66 | new TEntity Entity { get; } 67 | } 68 | 69 | public interface IBeforeEntry : IBeforeEntry, IEntry where TEntity : class {} 70 | public interface IFailedEntry : IFailedEntry, IEntry where TEntity : class {} 71 | public interface IAfterEntry : IAfterEntry, IEntry where TEntity : class {} 72 | public interface IChangeEntry : IChangeEntry, IEntry where TEntity : class {} 73 | public interface IBeforeChangeEntry : IBeforeChangeEntry, IChangeEntry, IBeforeEntry where TEntity : class { 74 | /// An object representing the state of the entity before the changes 75 | TEntity Original { get; } 76 | } 77 | public interface IChangeFailedEntry : IChangeFailedEntry, IChangeEntry, IFailedEntry where TEntity : class {} 78 | public interface IAfterChangeEntry : IAfterChangeEntry, IChangeEntry, IAfterEntry where TEntity : class {} 79 | 80 | #region Specific to events 81 | public interface IInsertingEntry : IInsertingEntry, IBeforeEntry where TEntity : class {} 82 | public interface IUpdatingEntry : IUpdatingEntry, IBeforeChangeEntry where TEntity : class {} 83 | public interface IDeletingEntry : IDeletingEntry, IBeforeChangeEntry where TEntity : class {} 84 | 85 | public interface IInsertFailedEntry : IInsertFailedEntry, IFailedEntry where TEntity : class {} 86 | public interface IUpdateFailedEntry : IUpdateFailedEntry, IChangeFailedEntry where TEntity : class {} 87 | public interface IDeleteFailedEntry : IDeleteFailedEntry, IChangeFailedEntry where TEntity : class {} 88 | 89 | public interface IInsertedEntry : IInsertedEntry, IAfterEntry where TEntity : class {} 90 | public interface IUpdatedEntry : IUpdatedEntry, IAfterChangeEntry where TEntity : class {} 91 | public interface IDeletedEntry : IDeletedEntry, IAfterChangeEntry where TEntity : class {} 92 | #endregion 93 | #endregion 94 | 95 | #region With specific DbContext type 96 | /// 97 | /// Contains the context and the instance of the changed entity 98 | public interface IEntry : IEntry where TEntity : class where TDbContext : DbContext { 99 | /// The TDbContext instance 100 | new TDbContext Context { get; } 101 | } 102 | 103 | public interface IBeforeEntry : IBeforeEntry , IEntry where TEntity : class where TDbContext : DbContext {} 104 | public interface IFailedEntry : IFailedEntry , IEntry where TEntity : class where TDbContext : DbContext {} 105 | public interface IAfterEntry : IAfterEntry , IEntry where TEntity : class where TDbContext : DbContext {} 106 | public interface IChangeEntry : IChangeEntry , IEntry where TEntity : class where TDbContext : DbContext {} 107 | public interface IBeforeChangeEntry : IBeforeChangeEntry, IChangeEntry, IBeforeEntry where TEntity : class where TDbContext : DbContext {} 108 | public interface IChangeFailedEntry : IChangeFailedEntry, IChangeEntry, IFailedEntry where TEntity : class where TDbContext : DbContext {} 109 | public interface IAfterChangeEntry : IAfterChangeEntry , IChangeEntry, IAfterEntry where TEntity : class where TDbContext : DbContext {} 110 | 111 | #region Specific to events 112 | public interface IInsertingEntry : IInsertingEntry, IBeforeEntry where TEntity : class where TDbContext : DbContext {} 113 | public interface IUpdatingEntry : IUpdatingEntry , IBeforeChangeEntry where TEntity : class where TDbContext : DbContext {} 114 | public interface IDeletingEntry : IDeletingEntry , IBeforeChangeEntry where TEntity : class where TDbContext : DbContext {} 115 | 116 | public interface IInsertFailedEntry : IInsertFailedEntry, IFailedEntry where TEntity : class where TDbContext : DbContext {} 117 | public interface IUpdateFailedEntry : IUpdateFailedEntry, IChangeFailedEntry where TEntity : class where TDbContext : DbContext {} 118 | public interface IDeleteFailedEntry : IDeleteFailedEntry, IChangeFailedEntry where TEntity : class where TDbContext : DbContext {} 119 | 120 | public interface IInsertedEntry : IInsertedEntry, IAfterEntry where TEntity : class where TDbContext : DbContext {} 121 | public interface IUpdatedEntry : IUpdatedEntry , IAfterChangeEntry where TEntity : class where TDbContext : DbContext {} 122 | public interface IDeletedEntry : IDeletedEntry , IAfterChangeEntry where TEntity : class where TDbContext : DbContext {} 123 | #endregion 124 | #endregion 125 | 126 | #region With generic service 127 | public interface IEntry : IEntry where TEntity : class where TDbContext : DbContext { 128 | /// The service object injected by the dependency injection container, if one is provided 129 | new TService Service { get; } 130 | } 131 | 132 | public interface IBeforeEntry : IBeforeEntry , IEntry where TEntity : class where TDbContext : DbContext {} 133 | public interface IFailedEntry : IFailedEntry , IEntry where TEntity : class where TDbContext : DbContext {} 134 | public interface IAfterEntry : IAfterEntry , IEntry where TEntity : class where TDbContext : DbContext {} 135 | public interface IChangeEntry : IChangeEntry , IEntry where TEntity : class where TDbContext : DbContext {} 136 | public interface IBeforeChangeEntry : IBeforeChangeEntry, IChangeEntry, IBeforeEntry where TEntity : class where TDbContext : DbContext {} 137 | public interface IChangeFailedEntry : IChangeFailedEntry, IChangeEntry, IFailedEntry where TEntity : class where TDbContext : DbContext {} 138 | public interface IAfterChangeEntry : IAfterChangeEntry , IChangeEntry, IAfterEntry where TEntity : class where TDbContext : DbContext {} 139 | 140 | #region Specific to events 141 | public interface IInsertingEntry : IInsertingEntry, IBeforeEntry where TEntity : class where TDbContext : DbContext {} 142 | public interface IUpdatingEntry : IUpdatingEntry , IBeforeChangeEntry where TEntity : class where TDbContext : DbContext {} 143 | public interface IDeletingEntry : IDeletingEntry , IBeforeChangeEntry where TEntity : class where TDbContext : DbContext {} 144 | 145 | public interface IInsertFailedEntry : IInsertFailedEntry, IFailedEntry where TEntity : class where TDbContext : DbContext {} 146 | public interface IUpdateFailedEntry : IUpdateFailedEntry, IChangeFailedEntry where TEntity : class where TDbContext : DbContext {} 147 | public interface IDeleteFailedEntry : IDeleteFailedEntry, IChangeFailedEntry where TEntity : class where TDbContext : DbContext {} 148 | 149 | public interface IInsertedEntry : IInsertedEntry, IAfterEntry where TEntity : class where TDbContext : DbContext {} 150 | public interface IUpdatedEntry : IUpdatedEntry , IAfterChangeEntry where TEntity : class where TDbContext : DbContext {} 151 | public interface IDeletedEntry : IDeletedEntry , IAfterChangeEntry where TEntity : class where TDbContext : DbContext {} 152 | #endregion 153 | #endregion -------------------------------------------------------------------------------- /src/EntityFrameworkCore.Triggers/ITriggerEntityInvoker.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Microsoft.EntityFrameworkCore; 4 | 5 | namespace EntityFrameworkCore.Triggers; 6 | 7 | internal interface ITriggerEntityInvoker where TDbContext : DbContext { 8 | void RaiseInserting (IServiceProvider serviceProvider, Object entity, TDbContext dbc, ref Boolean cancel); 9 | void RaiseUpdating (IServiceProvider serviceProvider, Object entity, TDbContext dbc, ref Boolean cancel); 10 | void RaiseDeleting (IServiceProvider serviceProvider, Object entity, TDbContext dbc, ref Boolean cancel); 11 | void RaiseInsertFailed(IServiceProvider serviceProvider, Object entity, TDbContext dbc, Exception ex, ref Boolean swallow); 12 | void RaiseUpdateFailed(IServiceProvider serviceProvider, Object entity, TDbContext dbc, Exception ex, ref Boolean swallow); 13 | void RaiseDeleteFailed(IServiceProvider serviceProvider, Object entity, TDbContext dbc, Exception ex, ref Boolean swallow); 14 | void RaiseInserted (IServiceProvider serviceProvider, Object entity, TDbContext dbc); 15 | void RaiseUpdated (IServiceProvider serviceProvider, Object entity, TDbContext dbc); 16 | void RaiseDeleted (IServiceProvider serviceProvider, Object entity, TDbContext dbc); 17 | 18 | Task RaiseInsertingAsync (IServiceProvider serviceProvider, Object entity, TDbContext dbc, Boolean cancel); 19 | Task RaiseUpdatingAsync (IServiceProvider serviceProvider, Object entity, TDbContext dbc, Boolean cancel); 20 | Task RaiseDeletingAsync (IServiceProvider serviceProvider, Object entity, TDbContext dbc, Boolean cancel); 21 | Task RaiseInsertFailedAsync(IServiceProvider serviceProvider, Object entity, TDbContext dbc, Exception ex, Boolean swallow); 22 | Task RaiseUpdateFailedAsync(IServiceProvider serviceProvider, Object entity, TDbContext dbc, Exception ex, Boolean swallow); 23 | Task RaiseDeleteFailedAsync(IServiceProvider serviceProvider, Object entity, TDbContext dbc, Exception ex, Boolean swallow); 24 | Task RaiseInsertedAsync (IServiceProvider serviceProvider, Object entity, TDbContext dbc); 25 | Task RaiseUpdatedAsync (IServiceProvider serviceProvider, Object entity, TDbContext dbc); 26 | Task RaiseDeletedAsync (IServiceProvider serviceProvider, Object entity, TDbContext dbc); 27 | } -------------------------------------------------------------------------------- /src/EntityFrameworkCore.Triggers/ITriggerEvent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Microsoft.EntityFrameworkCore; 4 | 5 | namespace EntityFrameworkCore.Triggers; 6 | 7 | internal interface ITriggerEventInternal 8 | { 9 | void Raise(Object entry); 10 | Task RaiseAsync(Object entry); 11 | } 12 | 13 | public interface ITriggerEvent {} 14 | 15 | public interface ITriggerEvent 16 | : ITriggerEvent 17 | where TEntry : IEntry 18 | { 19 | void Add(Action handler); 20 | void Remove(Action handler); 21 | void Add(Func handler); 22 | void Remove(Func handler); 23 | } 24 | 25 | public interface ITriggerEvent 26 | : ITriggerEvent 27 | where TEntry : IEntry 28 | where TEntity : class 29 | { 30 | new void Add(Action handler); 31 | new void Remove(Action handler); 32 | new void Add(Func handler); 33 | new void Remove(Func handler); 34 | } 35 | 36 | public interface ITriggerEvent 37 | : ITriggerEvent 38 | where TEntry : IEntry 39 | where TEntity : class 40 | where TDbContext : DbContext 41 | { 42 | new void Add(Action handler); 43 | new void Remove(Action handler); 44 | new void Add(Func handler); 45 | new void Remove(Func handler); 46 | } -------------------------------------------------------------------------------- /src/EntityFrameworkCore.Triggers/ITriggerInvoker.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Microsoft.EntityFrameworkCore; 4 | 5 | namespace EntityFrameworkCore.Triggers; 6 | 7 | internal interface ITriggerInvoker { 8 | List> RaiseChangingEvents(DbContext dbContext, IServiceProvider serviceProvider); 9 | 10 | void RaiseChangedEvents(DbContext dbContext, IServiceProvider serviceProvider, IEnumerable> afterEvents); 11 | 12 | Boolean RaiseFailedEvents(DbContext dbContext, IServiceProvider serviceProvider, DbUpdateException dbUpdateException, ref Boolean swallow); 13 | 14 | Boolean RaiseFailedEvents(DbContext dbContext, IServiceProvider serviceProvider, Exception exception, ref Boolean swallow); 15 | } -------------------------------------------------------------------------------- /src/EntityFrameworkCore.Triggers/ITriggerInvokerAsync.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using Microsoft.EntityFrameworkCore; 5 | 6 | namespace EntityFrameworkCore.Triggers; 7 | 8 | internal interface ITriggerInvokerAsync 9 | { 10 | Task>> RaiseChangingEventsAsync(DbContext dbContext, IServiceProvider serviceProvider); 11 | 12 | Task RaiseChangedEventsAsync(DbContext dbContext, IServiceProvider serviceProvider, IEnumerable> afterEvents); 13 | 14 | Task RaiseFailedEventsAsync(DbContext dbContext, IServiceProvider serviceProvider, DbUpdateException dbUpdateException); 15 | 16 | Task RaiseFailedEventsAsync(DbContext dbContext, IServiceProvider serviceProvider, Exception exception); 17 | } -------------------------------------------------------------------------------- /src/EntityFrameworkCore.Triggers/ITriggers.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore; 3 | 4 | namespace EntityFrameworkCore.Triggers; 5 | 6 | public interface ITriggers 7 | : ITriggers 8 | where TEntity : class 9 | where TDbContext : DbContext 10 | { 11 | new IInsertingTriggerEvent Inserting { get; } 12 | new IInsertFailedTriggerEvent InsertFailed { get; } 13 | new IInsertedTriggerEvent Inserted { get; } 14 | new IDeletingTriggerEvent Deleting { get; } 15 | new IDeleteFailedTriggerEvent DeleteFailed { get; } 16 | new IDeletedTriggerEvent Deleted { get; } 17 | new IUpdatingTriggerEvent Updating { get; } 18 | new IUpdateFailedTriggerEvent UpdateFailed { get; } 19 | new IUpdatedTriggerEvent Updated { get; } 20 | } 21 | 22 | public interface ITriggers 23 | : ITriggers 24 | where TEntity : class 25 | { 26 | new IInsertingTriggerEvent Inserting { get; } 27 | new IInsertFailedTriggerEvent InsertFailed { get; } 28 | new IInsertedTriggerEvent Inserted { get; } 29 | new IDeletingTriggerEvent Deleting { get; } 30 | new IDeleteFailedTriggerEvent DeleteFailed { get; } 31 | new IDeletedTriggerEvent Deleted { get; } 32 | new IUpdatingTriggerEvent Updating { get; } 33 | new IUpdateFailedTriggerEvent UpdateFailed { get; } 34 | new IUpdatedTriggerEvent Updated { get; } 35 | } 36 | 37 | public interface ITriggers 38 | { 39 | IInsertingTriggerEvent Inserting { get; } 40 | IInsertFailedTriggerEvent InsertFailed { get; } 41 | IInsertedTriggerEvent Inserted { get; } 42 | IDeletingTriggerEvent Deleting { get; } 43 | IDeleteFailedTriggerEvent DeleteFailed { get; } 44 | IDeletedTriggerEvent Deleted { get; } 45 | IUpdatingTriggerEvent Updating { get; } 46 | IUpdateFailedTriggerEvent UpdateFailed { get; } 47 | IUpdatedTriggerEvent Updated { get; } 48 | } -------------------------------------------------------------------------------- /src/EntityFrameworkCore.Triggers/ITriggersBuilder.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | 3 | namespace EntityFrameworkCore.Triggers; 4 | 5 | public interface ITriggersBuilder 6 | { 7 | ITriggers Triggers() 8 | where TEntity : class 9 | where TDbContext : DbContext; 10 | 11 | ITriggers Triggers() 12 | where TEntity : class; 13 | 14 | ITriggers Triggers(); 15 | } -------------------------------------------------------------------------------- /src/EntityFrameworkCore.Triggers/ServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | 3 | namespace EntityFrameworkCore.Triggers; 4 | 5 | public static class ServiceCollectionExtensions 6 | { 7 | public static IServiceCollection AddTriggers( 8 | this IServiceCollection serviceCollection, 9 | ServiceLifetime lifetime = ServiceLifetime.Singleton) 10 | { 11 | serviceCollection.Add(new ServiceDescriptor(typeof(Triggers<,>), typeof(Triggers<,>), lifetime)); 12 | serviceCollection.Add(new ServiceDescriptor(typeof(Triggers<>), typeof(Triggers<>), lifetime)); 13 | serviceCollection.Add(new ServiceDescriptor(typeof(Triggers), typeof(Triggers), lifetime)); 14 | serviceCollection.Add(new ServiceDescriptor(typeof(ITriggers<,>), sp => sp.GetRequiredService(typeof(Triggers<,>)), lifetime)); 15 | serviceCollection.Add(new ServiceDescriptor(typeof(ITriggers<>), sp => sp.GetRequiredService(typeof(Triggers<>)), lifetime)); 16 | serviceCollection.Add(new ServiceDescriptor(typeof(ITriggers), sp => sp.GetRequiredService(typeof(Triggers)), lifetime)); 17 | return serviceCollection; 18 | } 19 | } -------------------------------------------------------------------------------- /src/EntityFrameworkCore.Triggers/ServiceProviderExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace EntityFrameworkCore.Triggers; 4 | 5 | public static class ServiceProviderExtensions 6 | { 7 | public static IServiceProvider UseTriggers(this IServiceProvider services, Action configureTriggers) 8 | { 9 | if (services == null) 10 | throw new ArgumentNullException(nameof(services)); 11 | if (configureTriggers == null) 12 | throw new ArgumentNullException(nameof(configureTriggers)); 13 | configureTriggers.Invoke(new TriggersBuilder(services)); 14 | return services; 15 | } 16 | } -------------------------------------------------------------------------------- /src/EntityFrameworkCore.Triggers/ServiceRetrieval.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace EntityFrameworkCore.Triggers; 4 | 5 | internal static class ServiceRetrieval 6 | { 7 | public static readonly Type[] ValueTupleTypes = 8 | { 9 | typeof(ValueTuple<>), 10 | typeof(ValueTuple<,>), 11 | typeof(ValueTuple<,,>), 12 | typeof(ValueTuple<,,,>), 13 | typeof(ValueTuple<,,,,>), 14 | typeof(ValueTuple<,,,,,>), 15 | typeof(ValueTuple<,,,,,,>), 16 | typeof(ValueTuple<,,,,,,,>) 17 | }; 18 | } -------------------------------------------------------------------------------- /src/EntityFrameworkCore.Triggers/ServiceRetrieval_1.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.CodeAnalysis; 3 | using System.Linq; 4 | using System.Linq.Expressions; 5 | using System.Reflection; 6 | using Microsoft.Extensions.DependencyInjection; 7 | 8 | namespace EntityFrameworkCore.Triggers; 9 | 10 | internal static class ServiceRetrieval 11 | { 12 | public static TService GetService(IServiceProvider serviceProvider) => func(serviceProvider); 13 | 14 | private static readonly Func func = 15 | typeof(TService).IsGenericType && IsAValueTupleType(typeof(TService), out var valueTupleFactory) 16 | ? valueTupleFactory 17 | : sp => (TService)sp.GetRequiredService(typeof(TService)); 18 | 19 | private static Boolean IsAValueTupleType(Type type, [NotNullWhen(true)] out Func? valueTupleFactory) 20 | { 21 | var genericTypeDefinition = type.GetGenericTypeDefinition(); 22 | if (ServiceRetrieval.ValueTupleTypes.Contains(genericTypeDefinition)) 23 | return GetValueTupleCreationDelegate(type.GetGenericArguments(), out valueTupleFactory); 24 | valueTupleFactory = null; 25 | return false; 26 | } 27 | 28 | private static Boolean GetValueTupleCreationDelegate(Type[] genericTypes, out Func valueTupleFactory) 29 | { 30 | var create = typeof(ValueTuple).GetMethods().Single(IsValueTupleCreateMethod).MakeGenericMethod(genericTypes); 31 | var parameter = Expression.Parameter(typeof(IServiceProvider)); 32 | var arguments = genericTypes.Select(genericType => Expression.Convert(GetCall(sp => sp.GetRequiredService(genericType)), genericType)); 33 | var call = Expression.Call(create, arguments); 34 | valueTupleFactory = Expression.Lambda>(call, parameter).Compile(); 35 | return true; 36 | 37 | Boolean IsValueTupleCreateMethod(MethodInfo x) => x.Name == nameof(ValueTuple.Create) && x.IsGenericMethod && x.GetGenericArguments().Length == genericTypes.Length; 38 | MethodCallExpression GetCall(Func serviceGetter) => Expression.Call(Expression.Constant(serviceGetter.Target), serviceGetter.Method, parameter); 39 | } 40 | } -------------------------------------------------------------------------------- /src/EntityFrameworkCore.Triggers/TriggerEntityInvoker.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.EntityFrameworkCore; 6 | 7 | namespace EntityFrameworkCore.Triggers; 8 | 9 | internal sealed class TriggerEntityInvoker : ITriggerEntityInvoker where TDbContext : DbContext where TEntity : class 10 | { 11 | private static readonly Action, IServiceProvider> RaiseInsertingActions; 12 | private static readonly Action, IServiceProvider> RaiseUpdatingActions; 13 | private static readonly Action, IServiceProvider> RaiseDeletingActions; 14 | private static readonly Action, IServiceProvider> RaiseInsertFailedActions; 15 | private static readonly Action, IServiceProvider> RaiseUpdateFailedActions; 16 | private static readonly Action, IServiceProvider> RaiseDeleteFailedActions; 17 | private static readonly Action, IServiceProvider> RaiseInsertedActions; 18 | private static readonly Action, IServiceProvider> RaiseUpdatedActions; 19 | private static readonly Action, IServiceProvider> RaiseDeletedActions; 20 | 21 | private static readonly Func, IServiceProvider, Task> RaiseInsertingActionsAsync; 22 | private static readonly Func, IServiceProvider, Task> RaiseUpdatingActionsAsync; 23 | private static readonly Func, IServiceProvider, Task> RaiseDeletingActionsAsync; 24 | private static readonly Func, IServiceProvider, Task> RaiseInsertFailedActionsAsync; 25 | private static readonly Func, IServiceProvider, Task> RaiseUpdateFailedActionsAsync; 26 | private static readonly Func, IServiceProvider, Task> RaiseDeleteFailedActionsAsync; 27 | private static readonly Func, IServiceProvider, Task> RaiseInsertedActionsAsync; 28 | private static readonly Func, IServiceProvider, Task> RaiseUpdatedActionsAsync; 29 | private static readonly Func, IServiceProvider, Task> RaiseDeletedActionsAsync; 30 | 31 | static TriggerEntityInvoker() 32 | { 33 | (RaiseInsertingActions , RaiseInsertingActionsAsync ) = GetRaiseActions>(nameof(Triggers.GlobalInserting ), nameof(Triggers.Inserting )); 34 | (RaiseUpdatingActions , RaiseUpdatingActionsAsync ) = GetRaiseActions>(nameof(Triggers.GlobalUpdating ), nameof(Triggers.Updating )); 35 | (RaiseDeletingActions , RaiseDeletingActionsAsync ) = GetRaiseActions>(nameof(Triggers.GlobalDeleting ), nameof(Triggers.Deleting )); 36 | (RaiseInsertFailedActions, RaiseInsertFailedActionsAsync) = GetRaiseActions>(nameof(Triggers.GlobalInsertFailed), nameof(Triggers.InsertFailed)); 37 | (RaiseUpdateFailedActions, RaiseUpdateFailedActionsAsync) = GetRaiseActions>(nameof(Triggers.GlobalUpdateFailed), nameof(Triggers.UpdateFailed)); 38 | (RaiseDeleteFailedActions, RaiseDeleteFailedActionsAsync) = GetRaiseActions>(nameof(Triggers.GlobalDeleteFailed), nameof(Triggers.DeleteFailed)); 39 | (RaiseInsertedActions , RaiseInsertedActionsAsync ) = GetRaiseActions>(nameof(Triggers.GlobalInserted ), nameof(Triggers.Inserted )); 40 | (RaiseUpdatedActions , RaiseUpdatedActionsAsync ) = GetRaiseActions>(nameof(Triggers.GlobalUpdated ), nameof(Triggers.Updated )); 41 | (RaiseDeletedActions , RaiseDeletedActionsAsync ) = GetRaiseActions>(nameof(Triggers.GlobalDeleted ), nameof(Triggers.Deleted )); 42 | } 43 | 44 | private static (Action action, Func func) GetRaiseActions(String globalTriggersEventName, String triggersEventName) 45 | where TEntry : IEntry 46 | { 47 | var pairs = GetTypePairs().ToArray(); 48 | var raiseActions = new List>(pairs.Length); 49 | var raiseFuncs = new List>(pairs.Length); 50 | foreach (var (dbContextType, entityType) in pairs) 51 | { 52 | var triggersType = typeof(Triggers<,>).MakeGenericType(entityType, dbContextType); 53 | var itriggersType = typeof(ITriggers<,>).MakeGenericType(entityType, dbContextType); 54 | 55 | var globalTriggerEventGetter = 56 | triggersType 57 | .GetProperty(globalTriggersEventName)! 58 | .GetGetMethod()! 59 | .CreateDelegate>(); 60 | 61 | var instanceTriggerEventGetter = 62 | typeof(ITriggers) 63 | .GetProperty(triggersEventName)! 64 | .GetGetMethod()! 65 | .CreateDelegate>(); 66 | 67 | 68 | void RaiseGlobalThenInstance(TEntry entry, IServiceProvider sp) 69 | { 70 | globalTriggerEventGetter().Raise(entry); 71 | if (sp?.GetService(itriggersType) is ITriggers triggers) 72 | instanceTriggerEventGetter(triggers).Raise(entry); 73 | } 74 | 75 | async Task RaiseGlobalThenInstanceAsync(TEntry entry, IServiceProvider sp) 76 | { 77 | await globalTriggerEventGetter().RaiseAsync(entry); 78 | if (sp?.GetService(itriggersType) is ITriggers triggers) 79 | await instanceTriggerEventGetter(triggers).RaiseAsync(entry); 80 | } 81 | 82 | raiseActions.Add(RaiseGlobalThenInstance); 83 | raiseFuncs.Add(RaiseGlobalThenInstanceAsync); 84 | } 85 | return (RaiseActions, RaiseFuncsAsync); 86 | 87 | void RaiseActions(TEntry entry, IServiceProvider sp) 88 | { 89 | foreach (var raiseAction in raiseActions) 90 | raiseAction(entry, sp); 91 | } 92 | 93 | async Task RaiseFuncsAsync(TEntry entry, IServiceProvider sp) 94 | { 95 | foreach (var raiseFunc in raiseFuncs) 96 | await raiseFunc(entry, sp); 97 | } 98 | 99 | IEnumerable<(Type dbContextType, Type entityType)> GetTypePairs() 100 | { 101 | var dbContextTypes = GetInheritanceChain(includeInterfaces:false, typeof(DbContext)); 102 | foreach (var entityType in GetInheritanceChain().Distinct()) 103 | foreach (var dbContextType in dbContextTypes) 104 | yield return (dbContextType, entityType); 105 | } 106 | } 107 | 108 | private static List GetInheritanceChain(Boolean includeInterfaces = true, Type? terminator = null) where T : class 109 | { 110 | terminator ??= typeof(Object); 111 | var types = new List(); 112 | for (var type = typeof(T);; type = type!.BaseType) 113 | { 114 | types.Add(type!); 115 | if (type == terminator) 116 | break; 117 | if (includeInterfaces) 118 | types.AddRange(type!.GetDeclaredInterfaces().Reverse()); 119 | } 120 | types.Reverse(); 121 | return types; 122 | } 123 | 124 | public void RaiseInserting (IServiceProvider sp, Object entity, TDbContext dbc, ref Boolean cancel) { var entry = new InsertingEntry ((TEntity) entity, dbc, sp, cancel) ; RaiseInsertingActions (entry, sp); cancel = entry.Cancel; } 125 | public void RaiseUpdating (IServiceProvider sp, Object entity, TDbContext dbc, ref Boolean cancel) { var entry = new UpdatingEntry ((TEntity) entity, dbc, sp, cancel) ; RaiseUpdatingActions (entry, sp); cancel = entry.Cancel; } 126 | public void RaiseDeleting (IServiceProvider sp, Object entity, TDbContext dbc, ref Boolean cancel) { var entry = new DeletingEntry ((TEntity) entity, dbc, sp, cancel) ; RaiseDeletingActions (entry, sp); cancel = entry.Cancel; } 127 | public void RaiseInsertFailed(IServiceProvider sp, Object entity, TDbContext dbc, Exception ex, ref Boolean swallow) { var entry = new InsertFailedEntry((TEntity) entity, dbc, sp, ex, swallow); RaiseInsertFailedActions(entry, sp); swallow = entry.Swallow; } 128 | public void RaiseUpdateFailed(IServiceProvider sp, Object entity, TDbContext dbc, Exception ex, ref Boolean swallow) { var entry = new UpdateFailedEntry((TEntity) entity, dbc, sp, ex, swallow); RaiseUpdateFailedActions(entry, sp); swallow = entry.Swallow; } 129 | public void RaiseDeleteFailed(IServiceProvider sp, Object entity, TDbContext dbc, Exception ex, ref Boolean swallow) { var entry = new DeleteFailedEntry((TEntity) entity, dbc, sp, ex, swallow); RaiseDeleteFailedActions(entry, sp); swallow = entry.Swallow; } 130 | public void RaiseInserted (IServiceProvider sp, Object entity, TDbContext dbc) { var entry = new InsertedEntry ((TEntity) entity, dbc, sp) ; RaiseInsertedActions (entry, sp); } 131 | public void RaiseUpdated (IServiceProvider sp, Object entity, TDbContext dbc) { var entry = new UpdatedEntry ((TEntity) entity, dbc, sp) ; RaiseUpdatedActions (entry, sp); } 132 | public void RaiseDeleted (IServiceProvider sp, Object entity, TDbContext dbc) { var entry = new DeletedEntry ((TEntity) entity, dbc, sp) ; RaiseDeletedActions (entry, sp); } 133 | 134 | public async Task RaiseInsertingAsync (IServiceProvider sp, Object entity, TDbContext dbc, Boolean cancel) { var entry = new InsertingEntry ((TEntity) entity, dbc, sp, cancel) ; await RaiseInsertingActionsAsync (entry, sp); return entry.Cancel; } 135 | public async Task RaiseUpdatingAsync (IServiceProvider sp, Object entity, TDbContext dbc, Boolean cancel) { var entry = new UpdatingEntry ((TEntity) entity, dbc, sp, cancel) ; await RaiseUpdatingActionsAsync (entry, sp); return entry.Cancel; } 136 | public async Task RaiseDeletingAsync (IServiceProvider sp, Object entity, TDbContext dbc, Boolean cancel) { var entry = new DeletingEntry ((TEntity) entity, dbc, sp, cancel) ; await RaiseDeletingActionsAsync (entry, sp); return entry.Cancel; } 137 | public async Task RaiseInsertFailedAsync(IServiceProvider sp, Object entity, TDbContext dbc, Exception ex, Boolean swallow) { var entry = new InsertFailedEntry((TEntity) entity, dbc, sp, ex, swallow); await RaiseInsertFailedActionsAsync(entry, sp); return entry.Swallow; } 138 | public async Task RaiseUpdateFailedAsync(IServiceProvider sp, Object entity, TDbContext dbc, Exception ex, Boolean swallow) { var entry = new UpdateFailedEntry((TEntity) entity, dbc, sp, ex, swallow); await RaiseUpdateFailedActionsAsync(entry, sp); return entry.Swallow; } 139 | public async Task RaiseDeleteFailedAsync(IServiceProvider sp, Object entity, TDbContext dbc, Exception ex, Boolean swallow) { var entry = new DeleteFailedEntry((TEntity) entity, dbc, sp, ex, swallow); await RaiseDeleteFailedActionsAsync(entry, sp); return entry.Swallow; } 140 | public async Task RaiseInsertedAsync (IServiceProvider sp, Object entity, TDbContext dbc) { var entry = new InsertedEntry ((TEntity) entity, dbc, sp) ; await RaiseInsertedActionsAsync (entry, sp); } 141 | public async Task RaiseUpdatedAsync (IServiceProvider sp, Object entity, TDbContext dbc) { var entry = new UpdatedEntry ((TEntity) entity, dbc, sp) ; await RaiseUpdatedActionsAsync (entry, sp); } 142 | public async Task RaiseDeletedAsync (IServiceProvider sp, Object entity, TDbContext dbc) { var entry = new DeletedEntry ((TEntity) entity, dbc, sp) ; await RaiseDeletedActionsAsync (entry, sp); } 143 | } -------------------------------------------------------------------------------- /src/EntityFrameworkCore.Triggers/TriggerEvent.Generated.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Microsoft.EntityFrameworkCore; 4 | 5 | namespace EntityFrameworkCore.Triggers; 6 | 7 | public interface IInsertingTriggerEvent 8 | : ITriggerEvent, TEntity, TDbContext> 9 | where TEntity : class 10 | where TDbContext : DbContext 11 | { 12 | void Add(Action> handler); 13 | void Remove(Action> handler); 14 | void Add(Func, Task> handler); 15 | void Remove(Func, Task> handler); 16 | } 17 | 18 | internal class InsertingTriggerEvent 19 | : TriggerEvent, TEntity, TDbContext> 20 | , IInsertingTriggerEvent 21 | where TEntity : class 22 | where TDbContext : DbContext 23 | { 24 | public void Add(Action> handler) => 25 | Add(ref wrappedHandlers, handler, new DelegateSynchronyUnion>(entry => handler.Invoke(new WrappedInsertingEntry(entry)))); 26 | 27 | public void Remove(Action> handler) => 28 | Remove(ref wrappedHandlers, handler); 29 | 30 | public void Add(Func, Task> handler) => 31 | Add(ref wrappedHandlers, handler, new DelegateSynchronyUnion>(entry => handler.Invoke(new WrappedInsertingEntry(entry)))); 32 | 33 | public void Remove(Func, Task> handler) => 34 | Remove(ref wrappedHandlers, handler); 35 | } 36 | 37 | public interface IInsertFailedTriggerEvent 38 | : ITriggerEvent, TEntity, TDbContext> 39 | where TEntity : class 40 | where TDbContext : DbContext 41 | { 42 | void Add(Action> handler); 43 | void Remove(Action> handler); 44 | void Add(Func, Task> handler); 45 | void Remove(Func, Task> handler); 46 | } 47 | 48 | internal class InsertFailedTriggerEvent 49 | : TriggerEvent, TEntity, TDbContext> 50 | , IInsertFailedTriggerEvent 51 | where TEntity : class 52 | where TDbContext : DbContext 53 | { 54 | public void Add(Action> handler) => 55 | Add(ref wrappedHandlers, handler, new DelegateSynchronyUnion>(entry => handler.Invoke(new WrappedInsertFailedEntry(entry)))); 56 | 57 | public void Remove(Action> handler) => 58 | Remove(ref wrappedHandlers, handler); 59 | 60 | public void Add(Func, Task> handler) => 61 | Add(ref wrappedHandlers, handler, new DelegateSynchronyUnion>(entry => handler.Invoke(new WrappedInsertFailedEntry(entry)))); 62 | 63 | public void Remove(Func, Task> handler) => 64 | Remove(ref wrappedHandlers, handler); 65 | } 66 | 67 | public interface IInsertedTriggerEvent 68 | : ITriggerEvent, TEntity, TDbContext> 69 | where TEntity : class 70 | where TDbContext : DbContext 71 | { 72 | void Add(Action> handler); 73 | void Remove(Action> handler); 74 | void Add(Func, Task> handler); 75 | void Remove(Func, Task> handler); 76 | } 77 | 78 | internal class InsertedTriggerEvent 79 | : TriggerEvent, TEntity, TDbContext> 80 | , IInsertedTriggerEvent 81 | where TEntity : class 82 | where TDbContext : DbContext 83 | { 84 | public void Add(Action> handler) => 85 | Add(ref wrappedHandlers, handler, new DelegateSynchronyUnion>(entry => handler.Invoke(new WrappedInsertedEntry(entry)))); 86 | 87 | public void Remove(Action> handler) => 88 | Remove(ref wrappedHandlers, handler); 89 | 90 | public void Add(Func, Task> handler) => 91 | Add(ref wrappedHandlers, handler, new DelegateSynchronyUnion>(entry => handler.Invoke(new WrappedInsertedEntry(entry)))); 92 | 93 | public void Remove(Func, Task> handler) => 94 | Remove(ref wrappedHandlers, handler); 95 | } 96 | 97 | public interface IDeletingTriggerEvent 98 | : ITriggerEvent, TEntity, TDbContext> 99 | where TEntity : class 100 | where TDbContext : DbContext 101 | { 102 | void Add(Action> handler); 103 | void Remove(Action> handler); 104 | void Add(Func, Task> handler); 105 | void Remove(Func, Task> handler); 106 | } 107 | 108 | internal class DeletingTriggerEvent 109 | : TriggerEvent, TEntity, TDbContext> 110 | , IDeletingTriggerEvent 111 | where TEntity : class 112 | where TDbContext : DbContext 113 | { 114 | public void Add(Action> handler) => 115 | Add(ref wrappedHandlers, handler, new DelegateSynchronyUnion>(entry => handler.Invoke(new WrappedDeletingEntry(entry)))); 116 | 117 | public void Remove(Action> handler) => 118 | Remove(ref wrappedHandlers, handler); 119 | 120 | public void Add(Func, Task> handler) => 121 | Add(ref wrappedHandlers, handler, new DelegateSynchronyUnion>(entry => handler.Invoke(new WrappedDeletingEntry(entry)))); 122 | 123 | public void Remove(Func, Task> handler) => 124 | Remove(ref wrappedHandlers, handler); 125 | } 126 | 127 | public interface IDeleteFailedTriggerEvent 128 | : ITriggerEvent, TEntity, TDbContext> 129 | where TEntity : class 130 | where TDbContext : DbContext 131 | { 132 | void Add(Action> handler); 133 | void Remove(Action> handler); 134 | void Add(Func, Task> handler); 135 | void Remove(Func, Task> handler); 136 | } 137 | 138 | internal class DeleteFailedTriggerEvent 139 | : TriggerEvent, TEntity, TDbContext> 140 | , IDeleteFailedTriggerEvent 141 | where TEntity : class 142 | where TDbContext : DbContext 143 | { 144 | public void Add(Action> handler) => 145 | Add(ref wrappedHandlers, handler, new DelegateSynchronyUnion>(entry => handler.Invoke(new WrappedDeleteFailedEntry(entry)))); 146 | 147 | public void Remove(Action> handler) => 148 | Remove(ref wrappedHandlers, handler); 149 | 150 | public void Add(Func, Task> handler) => 151 | Add(ref wrappedHandlers, handler, new DelegateSynchronyUnion>(entry => handler.Invoke(new WrappedDeleteFailedEntry(entry)))); 152 | 153 | public void Remove(Func, Task> handler) => 154 | Remove(ref wrappedHandlers, handler); 155 | } 156 | 157 | public interface IDeletedTriggerEvent 158 | : ITriggerEvent, TEntity, TDbContext> 159 | where TEntity : class 160 | where TDbContext : DbContext 161 | { 162 | void Add(Action> handler); 163 | void Remove(Action> handler); 164 | void Add(Func, Task> handler); 165 | void Remove(Func, Task> handler); 166 | } 167 | 168 | internal class DeletedTriggerEvent 169 | : TriggerEvent, TEntity, TDbContext> 170 | , IDeletedTriggerEvent 171 | where TEntity : class 172 | where TDbContext : DbContext 173 | { 174 | public void Add(Action> handler) => 175 | Add(ref wrappedHandlers, handler, new DelegateSynchronyUnion>(entry => handler.Invoke(new WrappedDeletedEntry(entry)))); 176 | 177 | public void Remove(Action> handler) => 178 | Remove(ref wrappedHandlers, handler); 179 | 180 | public void Add(Func, Task> handler) => 181 | Add(ref wrappedHandlers, handler, new DelegateSynchronyUnion>(entry => handler.Invoke(new WrappedDeletedEntry(entry)))); 182 | 183 | public void Remove(Func, Task> handler) => 184 | Remove(ref wrappedHandlers, handler); 185 | } 186 | 187 | public interface IUpdatingTriggerEvent 188 | : ITriggerEvent, TEntity, TDbContext> 189 | where TEntity : class 190 | where TDbContext : DbContext 191 | { 192 | void Add(Action> handler); 193 | void Remove(Action> handler); 194 | void Add(Func, Task> handler); 195 | void Remove(Func, Task> handler); 196 | } 197 | 198 | internal class UpdatingTriggerEvent 199 | : TriggerEvent, TEntity, TDbContext> 200 | , IUpdatingTriggerEvent 201 | where TEntity : class 202 | where TDbContext : DbContext 203 | { 204 | public void Add(Action> handler) => 205 | Add(ref wrappedHandlers, handler, new DelegateSynchronyUnion>(entry => handler.Invoke(new WrappedUpdatingEntry(entry)))); 206 | 207 | public void Remove(Action> handler) => 208 | Remove(ref wrappedHandlers, handler); 209 | 210 | public void Add(Func, Task> handler) => 211 | Add(ref wrappedHandlers, handler, new DelegateSynchronyUnion>(entry => handler.Invoke(new WrappedUpdatingEntry(entry)))); 212 | 213 | public void Remove(Func, Task> handler) => 214 | Remove(ref wrappedHandlers, handler); 215 | } 216 | 217 | public interface IUpdateFailedTriggerEvent 218 | : ITriggerEvent, TEntity, TDbContext> 219 | where TEntity : class 220 | where TDbContext : DbContext 221 | { 222 | void Add(Action> handler); 223 | void Remove(Action> handler); 224 | void Add(Func, Task> handler); 225 | void Remove(Func, Task> handler); 226 | } 227 | 228 | internal class UpdateFailedTriggerEvent 229 | : TriggerEvent, TEntity, TDbContext> 230 | , IUpdateFailedTriggerEvent 231 | where TEntity : class 232 | where TDbContext : DbContext 233 | { 234 | public void Add(Action> handler) => 235 | Add(ref wrappedHandlers, handler, new DelegateSynchronyUnion>(entry => handler.Invoke(new WrappedUpdateFailedEntry(entry)))); 236 | 237 | public void Remove(Action> handler) => 238 | Remove(ref wrappedHandlers, handler); 239 | 240 | public void Add(Func, Task> handler) => 241 | Add(ref wrappedHandlers, handler, new DelegateSynchronyUnion>(entry => handler.Invoke(new WrappedUpdateFailedEntry(entry)))); 242 | 243 | public void Remove(Func, Task> handler) => 244 | Remove(ref wrappedHandlers, handler); 245 | } 246 | 247 | public interface IUpdatedTriggerEvent 248 | : ITriggerEvent, TEntity, TDbContext> 249 | where TEntity : class 250 | where TDbContext : DbContext 251 | { 252 | void Add(Action> handler); 253 | void Remove(Action> handler); 254 | void Add(Func, Task> handler); 255 | void Remove(Func, Task> handler); 256 | } 257 | 258 | internal class UpdatedTriggerEvent 259 | : TriggerEvent, TEntity, TDbContext> 260 | , IUpdatedTriggerEvent 261 | where TEntity : class 262 | where TDbContext : DbContext 263 | { 264 | public void Add(Action> handler) => 265 | Add(ref wrappedHandlers, handler, new DelegateSynchronyUnion>(entry => handler.Invoke(new WrappedUpdatedEntry(entry)))); 266 | 267 | public void Remove(Action> handler) => 268 | Remove(ref wrappedHandlers, handler); 269 | 270 | public void Add(Func, Task> handler) => 271 | Add(ref wrappedHandlers, handler, new DelegateSynchronyUnion>(entry => handler.Invoke(new WrappedUpdatedEntry(entry)))); 272 | 273 | public void Remove(Func, Task> handler) => 274 | Remove(ref wrappedHandlers, handler); 275 | } 276 | 277 | -------------------------------------------------------------------------------- /src/EntityFrameworkCore.Triggers/TriggerEvent.Generated.tt: -------------------------------------------------------------------------------- 1 | <#@ template debug="false" hostspecific="false" language="C#" #> 2 | <#@ assembly name="System.Core" #> 3 | <#@ import namespace="System.Linq" #> 4 | <#@ import namespace="System.Text" #> 5 | <#@ import namespace="System.Collections.Generic" #> 6 | <#@ output extension=".cs" #> 7 | using System; 8 | using System.Threading.Tasks; 9 | using Microsoft.EntityFrameworkCore; 10 | 11 | namespace EntityFrameworkCore.Triggers; 12 | 13 | <# foreach (var triggerName in new [] { "Inserting", "InsertFailed", "Inserted", "Deleting", "DeleteFailed", "Deleted", "Updating", "UpdateFailed", "Updated" }) 14 | { 15 | var entryTypeName = $"I{triggerName}Entry"; 16 | var entryWithServiceTypeName = $"I{triggerName}Entry"; 17 | #> 18 | public interface I<#= triggerName #>TriggerEvent 19 | : ITriggerEventEntry, TEntity, TDbContext> 20 | where TEntity : class 21 | where TDbContext : DbContext 22 | { 23 | void Add(ActionEntry> handler); 24 | void Remove(ActionEntry> handler); 25 | void Add(FuncEntry, Task> handler); 26 | void Remove(FuncEntry, Task> handler); 27 | } 28 | 29 | internal class <#= triggerName #>TriggerEvent 30 | : TriggerEventEntry, TEntity, TDbContext> 31 | , I<#= triggerName #>TriggerEvent 32 | where TEntity : class 33 | where TDbContext : DbContext 34 | { 35 | public void Add(ActionEntry> handler) => 36 | Add(ref wrappedHandlers, handler, new DelegateSynchronyUnion<<#= entryTypeName #>>(entry => handler.Invoke(new Wrapped<#= triggerName #>Entry(entry)))); 37 | 38 | public void Remove(ActionEntry> handler) => 39 | Remove(ref wrappedHandlers, handler); 40 | 41 | public void Add(FuncEntry, Task> handler) => 42 | Add(ref wrappedHandlers, handler, new DelegateSynchronyUnion<<#= entryTypeName #>>(entry => handler.Invoke(new Wrapped<#= triggerName #>Entry(entry)))); 43 | 44 | public void Remove(FuncEntry, Task> handler) => 45 | Remove(ref wrappedHandlers, handler); 46 | } 47 | 48 | <# } #> 49 | -------------------------------------------------------------------------------- /src/EntityFrameworkCore.Triggers/TriggerEvent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Immutable; 3 | using System.Threading.Tasks; 4 | using Microsoft.EntityFrameworkCore; 5 | 6 | namespace EntityFrameworkCore.Triggers; 7 | 8 | internal abstract class TriggerEvent : ITriggerEvent, ITriggerEventInternal 9 | { 10 | public void Raise(Object entry) => RaiseInternal(entry); 11 | public Task RaiseAsync(Object entry) => RaiseInternalAsync(entry); 12 | 13 | protected abstract void RaiseInternal(Object entry); 14 | protected abstract Task RaiseInternalAsync(Object entry); 15 | } 16 | 17 | internal class TriggerEvent 18 | : TriggerEvent 19 | , ITriggerEvent 20 | , IEquatable> 21 | where TEntry : IEntry 22 | where TEntity : class 23 | where TDbContext : DbContext 24 | { 25 | internal TriggerEvent() {} 26 | 27 | private protected ImmutableArray wrappedHandlers = ImmutableArray.Empty; 28 | 29 | protected sealed override void RaiseInternal(Object entry) 30 | { 31 | var x = (TEntry)entry; 32 | var latestWrappedHandlers = ImmutableInterlockedRead(ref wrappedHandlers); 33 | foreach (var wrappedHandler in latestWrappedHandlers) 34 | wrappedHandler.Invoke(x); 35 | } 36 | 37 | protected sealed override async Task RaiseInternalAsync(Object entry) 38 | { 39 | var x = (TEntry)entry; 40 | var latestWrappedHandlers = ImmutableInterlockedRead(ref wrappedHandlers); 41 | foreach (var wrappedHandler in latestWrappedHandlers) 42 | await wrappedHandler.InvokeAsync(x); 43 | } 44 | 45 | protected struct WrappedHandler : IEquatable 46 | { 47 | private readonly Delegate source; 48 | private readonly DelegateSynchronyUnion wrapper; 49 | 50 | public WrappedHandler(Delegate source, DelegateSynchronyUnion wrapper) 51 | { 52 | this.source = source ?? throw new ArgumentNullException(nameof(source)); 53 | this.wrapper = wrapper; 54 | } 55 | 56 | public Boolean Equals(WrappedHandler other) => ReferenceEquals(source, other.source) || source == other.source; 57 | public override Boolean Equals(Object? o) => o is WrappedHandler wrappedHandler && Equals(wrappedHandler); 58 | public override Int32 GetHashCode() => source.GetHashCode() ^ wrapper.GetHashCode(); 59 | public void Invoke(TEntry entry) => wrapper.Invoke(entry); 60 | public Task InvokeAsync(TEntry entry) => wrapper.InvokeAsync(entry); 61 | } 62 | 63 | protected static void Add(ref ImmutableArray wrappedHandlers, Delegate source, DelegateSynchronyUnion wrapper) 64 | { 65 | ImmutableArray initial, computed; 66 | do 67 | { 68 | initial = ImmutableInterlockedRead(ref wrappedHandlers); 69 | computed = initial.Add(new WrappedHandler(source, wrapper)); 70 | } 71 | while (initial != ImmutableInterlocked.InterlockedCompareExchange(ref wrappedHandlers, computed, initial)); 72 | } 73 | 74 | protected static void Remove(ref ImmutableArray wrappedHandlers, Delegate source) 75 | { 76 | ImmutableArray initial, computed; 77 | do 78 | { 79 | initial = ImmutableInterlockedRead(ref wrappedHandlers); 80 | var index = initial.LastIndexOf(new WrappedHandler(source, default)); 81 | if (index == -1) 82 | return; 83 | computed = initial.RemoveAt(index); 84 | } 85 | while (initial != ImmutableInterlocked.InterlockedCompareExchange(ref wrappedHandlers, computed, initial)); 86 | } 87 | 88 | private static ImmutableArray ImmutableInterlockedRead(ref ImmutableArray array) => 89 | ImmutableInterlocked.InterlockedCompareExchange(ref array, ImmutableArray.Empty, ImmutableArray.Empty); 90 | 91 | public override Boolean Equals(Object? obj) => obj is TriggerEvent triggerEvent && Equals(triggerEvent); 92 | public Boolean Equals(TriggerEvent? other) => other != null && ImmutableInterlockedRead(ref wrappedHandlers).Equals(ImmutableInterlockedRead(ref other.wrappedHandlers)); 93 | public override Int32 GetHashCode() => ImmutableInterlockedRead(ref wrappedHandlers).GetHashCode(); 94 | 95 | public void Add(Action handler) 96 | { 97 | if (handler == null) 98 | throw new ArgumentNullException(nameof(handler)); 99 | Add(ref wrappedHandlers, handler, new DelegateSynchronyUnion(handler)); 100 | } 101 | 102 | public void Remove(Action handler) 103 | { 104 | if (handler == null) 105 | throw new ArgumentNullException(nameof(handler)); 106 | Remove(ref wrappedHandlers, handler); 107 | } 108 | 109 | public void Add(Func handler) 110 | { 111 | if (handler == null) 112 | throw new ArgumentNullException(nameof(handler)); 113 | Add(ref wrappedHandlers, handler, new DelegateSynchronyUnion(handler)); 114 | } 115 | 116 | public void Remove(Func handler) 117 | { 118 | if (handler == null) 119 | throw new ArgumentNullException(nameof(handler)); 120 | Remove(ref wrappedHandlers, handler); 121 | } 122 | } -------------------------------------------------------------------------------- /src/EntityFrameworkCore.Triggers/TriggerEventExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace EntityFrameworkCore.Triggers; 5 | 6 | internal static class TriggerEventExtensions 7 | { 8 | public static void Raise(this ITriggerEvent triggersEvent, Object entry) => ((ITriggerEventInternal)triggersEvent).Raise(entry); 9 | public static Task RaiseAsync(this ITriggerEvent triggersEvent, Object entry) => ((ITriggerEventInternal)triggersEvent).RaiseAsync(entry); 10 | } -------------------------------------------------------------------------------- /src/EntityFrameworkCore.Triggers/TriggerInvoker.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.ChangeTracking; 6 | 7 | namespace EntityFrameworkCore.Triggers; 8 | 9 | internal class TriggerInvoker : ITriggerInvoker where TDbContext : DbContext { 10 | 11 | public List> RaiseChangingEvents(DbContext dbContext, IServiceProvider serviceProvider) { 12 | var entries = dbContext.ChangeTracker.Entries().ToList(); 13 | var triggeredEntries = new List(entries.Count); 14 | var afterEvents = new List>(entries.Count); 15 | while (entries.Any()) { 16 | foreach (var entry in entries) { 17 | var cancel = false; 18 | 19 | var after = RaiseChangingEvent(entry, dbContext, serviceProvider, ref cancel); 20 | if (after != null && !cancel) 21 | afterEvents.Add(after.Value); 22 | 23 | triggeredEntries.Add(entry); 24 | if (cancel) 25 | entry.State = GetCanceledEntityState(entry.State); 26 | } 27 | var newEntries = dbContext.ChangeTracker.Entries().Except(triggeredEntries, EntityEntryComparer.Default); 28 | entries.Clear(); 29 | entries.AddRange(newEntries); 30 | } 31 | return afterEvents; 32 | } 33 | 34 | private static EntityState GetCanceledEntityState(EntityState entityState) { 35 | switch (entityState) { 36 | case EntityState.Added: 37 | return EntityState.Detached; 38 | case EntityState.Deleted: 39 | return EntityState.Modified; 40 | case EntityState.Modified: 41 | return EntityState.Unchanged; 42 | default: 43 | return entityState; 44 | } 45 | } 46 | 47 | private static DelegateSynchronyUnion? RaiseChangingEvent(EntityEntry entry, DbContext dbContext, IServiceProvider serviceProvider, ref Boolean cancel) { 48 | var tDbContext = (TDbContext)dbContext; 49 | var entityType = entry.Entity.GetType(); 50 | var triggerEntityInvoker = GenericServiceCache, TriggerEntityInvoker>.GetOrAdd(dbContext.GetType(), entityType); 51 | switch (entry.State) { 52 | case EntityState.Added: 53 | triggerEntityInvoker.RaiseInserting(serviceProvider, entry.Entity, tDbContext, ref cancel); 54 | return new DelegateSynchronyUnion(context => triggerEntityInvoker.RaiseInserted(serviceProvider, entry.Entity, (TDbContext) context)); 55 | case EntityState.Deleted: 56 | triggerEntityInvoker.RaiseDeleting(serviceProvider, entry.Entity, tDbContext, ref cancel); 57 | return new DelegateSynchronyUnion(context => triggerEntityInvoker.RaiseDeleted(serviceProvider, entry.Entity, (TDbContext) context)); 58 | case EntityState.Modified: 59 | triggerEntityInvoker.RaiseUpdating(serviceProvider, entry.Entity, tDbContext, ref cancel); 60 | return new DelegateSynchronyUnion(context => triggerEntityInvoker.RaiseUpdated(serviceProvider, entry.Entity, (TDbContext) context)); 61 | } 62 | return null; 63 | } 64 | 65 | public void RaiseChangedEvents(DbContext dbContext, IServiceProvider serviceProvider, IEnumerable> afterEvents) { 66 | foreach (var after in afterEvents) 67 | after.Invoke(dbContext); 68 | } 69 | 70 | public Boolean RaiseFailedEvents(DbContext dbContext, IServiceProvider serviceProvider, DbUpdateException dbUpdateException, ref Boolean swallow) { 71 | var context = (TDbContext) dbContext; 72 | 73 | IEnumerable entries; 74 | 75 | if (dbUpdateException.Entries.Any()) { 76 | entries = dbUpdateException.Entries; 77 | } 78 | else { 79 | entries = dbContext.ChangeTracker.Entries().ToArray(); 80 | if (entries.Count() != 1) { 81 | swallow = false; 82 | return swallow; 83 | } 84 | } 85 | RaiseTheFailedEvents(context, serviceProvider, entries, dbUpdateException, ref swallow); 86 | return swallow; 87 | } 88 | 89 | #if !EF_CORE 90 | public Boolean RaiseFailedEvents(DbContext dbContext, IServiceProvider serviceProvider, DbEntityValidationException dbEntityValidationException, ref Boolean swallow) { 91 | var context = (TDbContext) dbContext; 92 | RaiseTheFailedEvents(context, serviceProvider, dbEntityValidationException.EntityValidationErrors.Select(x => x.Entry), dbEntityValidationException, ref swallow); 93 | return swallow; 94 | } 95 | #endif 96 | 97 | public Boolean RaiseFailedEvents(DbContext dbContext, IServiceProvider serviceProvider, Exception exception, ref Boolean swallow) { 98 | var context = (TDbContext) dbContext; 99 | var entries = dbContext.ChangeTracker.Entries().ToArray(); 100 | if (entries.Length != 1) { 101 | swallow = false; 102 | return swallow; 103 | } 104 | RaiseTheFailedEvents(context, serviceProvider, entries, exception, ref swallow); 105 | return swallow; 106 | } 107 | 108 | private static void RaiseTheFailedEvents(TDbContext dbContext, IServiceProvider serviceProvider, IEnumerable entries, Exception exception, ref Boolean swallow) { 109 | foreach (var entry in entries) 110 | RaiseTheFailedEvents(dbContext, serviceProvider, entry, exception, ref swallow); 111 | } 112 | 113 | private static void RaiseTheFailedEvents(TDbContext dbContext, IServiceProvider serviceProvider, EntityEntry entry, Exception exception, ref Boolean swallow) { 114 | switch (entry.State) { 115 | case EntityState.Added: 116 | GetTriggerEntityInvoker(entry.Entity.GetType()).RaiseInsertFailed(serviceProvider, entry.Entity, dbContext, exception, ref swallow); 117 | break; 118 | case EntityState.Modified: 119 | GetTriggerEntityInvoker(entry.Entity.GetType()).RaiseUpdateFailed(serviceProvider, entry.Entity, dbContext, exception, ref swallow); 120 | break; 121 | case EntityState.Deleted: 122 | GetTriggerEntityInvoker(entry.Entity.GetType()).RaiseDeleteFailed(serviceProvider, entry.Entity, dbContext, exception, ref swallow); 123 | break; 124 | } 125 | ITriggerEntityInvoker GetTriggerEntityInvoker(Type entityType) => 126 | GenericServiceCache, TriggerEntityInvoker>.GetOrAdd(dbContext.GetType(), entityType); 127 | } 128 | } -------------------------------------------------------------------------------- /src/EntityFrameworkCore.Triggers/TriggerInvokerAsync.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.EntityFrameworkCore; 6 | using Microsoft.EntityFrameworkCore.ChangeTracking; 7 | 8 | namespace EntityFrameworkCore.Triggers; 9 | 10 | internal class TriggerInvokerAsync : ITriggerInvokerAsync where TDbContext : DbContext 11 | { 12 | public async Task>> RaiseChangingEventsAsync(DbContext dbContext, IServiceProvider serviceProvider) 13 | { 14 | var entries = dbContext.ChangeTracker.Entries().ToList(); 15 | var triggeredEntries = new List(entries.Count); 16 | var afterEvents = new List>(entries.Count); 17 | while (entries.Any()) 18 | { 19 | foreach (var entry in entries) 20 | { 21 | var cancel = false; 22 | DelegateSynchronyUnion? after; 23 | (after, cancel) = await RaiseChangingEventAsync(entry, dbContext, serviceProvider, cancel); 24 | if (after != null && !cancel) 25 | afterEvents.Add(after.Value); 26 | 27 | triggeredEntries.Add(entry); 28 | if (cancel) 29 | entry.State = GetCanceledEntityState(entry.State); 30 | } 31 | var newEntries = dbContext.ChangeTracker.Entries().Except(triggeredEntries, EntityEntryComparer.Default); 32 | entries.Clear(); 33 | entries.AddRange(newEntries); 34 | } 35 | return afterEvents; 36 | } 37 | 38 | private static EntityState GetCanceledEntityState(EntityState entityState) 39 | { 40 | switch (entityState) 41 | { 42 | case EntityState.Added: 43 | return EntityState.Detached; 44 | case EntityState.Deleted: 45 | return EntityState.Modified; 46 | case EntityState.Modified: 47 | return EntityState.Unchanged; 48 | default: 49 | return entityState; 50 | } 51 | } 52 | 53 | private static async Task<(DelegateSynchronyUnion?, Boolean)> RaiseChangingEventAsync(EntityEntry entry, DbContext dbContext, IServiceProvider serviceProvider, Boolean cancel) 54 | { 55 | var tDbContext = (TDbContext)dbContext; 56 | var entityType = entry.Entity.GetType(); 57 | var triggerEntityInvoker = GenericServiceCache, TriggerEntityInvoker>.GetOrAdd(tDbContext.GetType(), entityType); 58 | switch (entry.State) 59 | { 60 | case EntityState.Added: 61 | cancel = await triggerEntityInvoker.RaiseInsertingAsync(serviceProvider, entry.Entity, tDbContext, cancel); 62 | return (new DelegateSynchronyUnion(context => triggerEntityInvoker.RaiseInsertedAsync(serviceProvider, entry.Entity, (TDbContext)context)), cancel); 63 | case EntityState.Deleted: 64 | cancel = await triggerEntityInvoker.RaiseDeletingAsync(serviceProvider, entry.Entity, tDbContext, cancel); 65 | return (new DelegateSynchronyUnion(context => triggerEntityInvoker.RaiseDeletedAsync(serviceProvider, entry.Entity, (TDbContext)context)), cancel); 66 | case EntityState.Modified: 67 | cancel = await triggerEntityInvoker.RaiseUpdatingAsync(serviceProvider, entry.Entity, tDbContext, cancel); 68 | return (new DelegateSynchronyUnion(context => triggerEntityInvoker.RaiseUpdatedAsync(serviceProvider, entry.Entity, (TDbContext)context)), cancel); 69 | } 70 | return default; 71 | } 72 | 73 | public async Task RaiseChangedEventsAsync(DbContext dbContext, IServiceProvider serviceProvider, IEnumerable> afterEvents) 74 | { 75 | foreach (var after in afterEvents) 76 | await after.InvokeAsync(dbContext); 77 | } 78 | 79 | public async Task RaiseFailedEventsAsync(DbContext dbContext, IServiceProvider serviceProvider, DbUpdateException dbUpdateException) 80 | { 81 | var context = (TDbContext)dbContext; 82 | 83 | IEnumerable entries; 84 | 85 | if (dbUpdateException.Entries.Any()) 86 | { 87 | entries = dbUpdateException.Entries; 88 | } 89 | else 90 | { 91 | entries = dbContext.ChangeTracker.Entries().ToArray(); 92 | if (entries.Count() != 1) 93 | { 94 | return false; 95 | } 96 | } 97 | return await RaiseFailedEventsInternalAsync(context, serviceProvider, entries, dbUpdateException); 98 | } 99 | 100 | #if !EF_CORE 101 | public async Task RaiseFailedEventsAsync(DbContext dbContext, IServiceProvider serviceProvider, DbEntityValidationException dbEntityValidationException) { 102 | var context = (TDbContext) dbContext; 103 | return await RaiseFailedEventsInternalAsync(context, serviceProvider, dbEntityValidationException.EntityValidationErrors.Select(x => x.Entry), dbEntityValidationException); 104 | } 105 | #endif 106 | 107 | public async Task RaiseFailedEventsAsync(DbContext dbContext, IServiceProvider serviceProvider, Exception exception) 108 | { 109 | var context = (TDbContext)dbContext; 110 | var entries = dbContext.ChangeTracker.Entries().ToArray(); 111 | if (entries.Length != 1) 112 | { 113 | return false; 114 | } 115 | return await RaiseFailedEventsInternalAsync(context, serviceProvider, entries, exception); 116 | } 117 | 118 | private static async Task RaiseFailedEventsInternalAsync(TDbContext dbContext, IServiceProvider serviceProvider, IEnumerable entries, Exception exception) 119 | { 120 | var swallow = false; 121 | foreach (var entry in entries) 122 | swallow = await RaiseFailedEventsInternalAsync(dbContext, serviceProvider, entry, exception, swallow); 123 | return swallow; 124 | } 125 | 126 | private static async Task RaiseFailedEventsInternalAsync(TDbContext dbContext, IServiceProvider serviceProvider, EntityEntry entry, Exception exception, Boolean swallow) 127 | { 128 | switch (entry.State) 129 | { 130 | case EntityState.Added: 131 | return await GetTriggerEntityInvoker(entry.Entity.GetType()).RaiseInsertFailedAsync(serviceProvider, entry.Entity, dbContext, exception, swallow); 132 | case EntityState.Modified: 133 | return await GetTriggerEntityInvoker(entry.Entity.GetType()).RaiseUpdateFailedAsync(serviceProvider, entry.Entity, dbContext, exception, swallow); 134 | case EntityState.Deleted: 135 | return await GetTriggerEntityInvoker(entry.Entity.GetType()).RaiseDeleteFailedAsync(serviceProvider, entry.Entity, dbContext, exception, swallow); 136 | default: 137 | return default; 138 | } 139 | 140 | ITriggerEntityInvoker GetTriggerEntityInvoker(Type entityType) => 141 | GenericServiceCache, TriggerEntityInvoker>.GetOrAdd(dbContext.GetType(), entityType); 142 | } 143 | } -------------------------------------------------------------------------------- /src/EntityFrameworkCore.Triggers/TriggerInvokerAsyncCache.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | 4 | namespace EntityFrameworkCore.Triggers; 5 | 6 | internal static class TriggerInvokerAsyncCache 7 | { 8 | public static ITriggerInvokerAsync Get(Type dbContextType) 9 | { 10 | return cache.GetOrAdd(dbContextType, ValueFactory); 11 | } 12 | 13 | private static ITriggerInvokerAsync ValueFactory(Type type) 14 | { 15 | var triggerInvokerType = typeof(TriggerInvoker<>).MakeGenericType(type); 16 | return (ITriggerInvokerAsync)Activator.CreateInstance(triggerInvokerType)!; 17 | } 18 | 19 | private static readonly ConcurrentDictionary cache = new ConcurrentDictionary(); 20 | } -------------------------------------------------------------------------------- /src/EntityFrameworkCore.Triggers/Triggers.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore; 3 | 4 | namespace EntityFrameworkCore.Triggers; 5 | 6 | public sealed class Triggers 7 | : ITriggers 8 | , IEquatable 9 | { 10 | private readonly ITriggers triggers; 11 | public Triggers(ITriggers triggers) => this.triggers = triggers; 12 | 13 | IInsertingTriggerEvent ITriggers.Inserting => triggers.Inserting ; 14 | IInsertFailedTriggerEvent ITriggers.InsertFailed => triggers.InsertFailed; 15 | IInsertedTriggerEvent ITriggers.Inserted => triggers.Inserted ; 16 | IDeletingTriggerEvent ITriggers.Deleting => triggers.Deleting ; 17 | IDeleteFailedTriggerEvent ITriggers.DeleteFailed => triggers.DeleteFailed; 18 | IDeletedTriggerEvent ITriggers.Deleted => triggers.Deleted ; 19 | IUpdatingTriggerEvent ITriggers.Updating => triggers.Updating ; 20 | IUpdateFailedTriggerEvent ITriggers.UpdateFailed => triggers.UpdateFailed; 21 | IUpdatedTriggerEvent ITriggers.Updated => triggers.Updated ; 22 | 23 | public static IInsertingTriggerEvent GlobalInserting { get; } = Triggers.GlobalInserting ; 24 | public static IInsertFailedTriggerEvent GlobalInsertFailed { get; } = Triggers.GlobalInsertFailed; 25 | public static IInsertedTriggerEvent GlobalInserted { get; } = Triggers.GlobalInserted ; 26 | public static IDeletingTriggerEvent GlobalDeleting { get; } = Triggers.GlobalDeleting ; 27 | public static IDeleteFailedTriggerEvent GlobalDeleteFailed { get; } = Triggers.GlobalDeleteFailed; 28 | public static IDeletedTriggerEvent GlobalDeleted { get; } = Triggers.GlobalDeleted ; 29 | public static IUpdatingTriggerEvent GlobalUpdating { get; } = Triggers.GlobalUpdating ; 30 | public static IUpdateFailedTriggerEvent GlobalUpdateFailed { get; } = Triggers.GlobalUpdateFailed; 31 | public static IUpdatedTriggerEvent GlobalUpdated { get; } = Triggers.GlobalUpdated ; 32 | 33 | public static event Action> Inserting { add => Triggers.Inserting += value; remove => Triggers.Inserting -= value; } 34 | public static event Action> InsertFailed { add => Triggers.InsertFailed += value; remove => Triggers.InsertFailed -= value; } 35 | public static event Action> Inserted { add => Triggers.Inserted += value; remove => Triggers.Inserted -= value; } 36 | public static event Action> Deleting { add => Triggers.Deleting += value; remove => Triggers.Deleting -= value; } 37 | public static event Action> DeleteFailed { add => Triggers.DeleteFailed += value; remove => Triggers.DeleteFailed -= value; } 38 | public static event Action> Deleted { add => Triggers.Deleted += value; remove => Triggers.Deleted -= value; } 39 | public static event Action> Updating { add => Triggers.Updating += value; remove => Triggers.Updating -= value; } 40 | public static event Action> UpdateFailed { add => Triggers.UpdateFailed += value; remove => Triggers.UpdateFailed -= value; } 41 | public static event Action> Updated { add => Triggers.Updated += value; remove => Triggers.Updated -= value; } 42 | 43 | public override Int32 GetHashCode() => TriggersEqualityComparer.Instance.GetHashCode(triggers); 44 | public override Boolean Equals(Object? obj) => obj is ITriggers other && Equals(other); 45 | 46 | public Boolean Equals(ITriggers? other) => other is Triggers te ? ReferenceEquals(triggers, te.triggers) : TriggersEqualityComparer.Instance.Equals(this, other); 47 | } -------------------------------------------------------------------------------- /src/EntityFrameworkCore.Triggers/TriggersBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Microsoft.EntityFrameworkCore; 4 | 5 | namespace EntityFrameworkCore.Triggers; 6 | 7 | internal sealed class TriggersBuilder : ITriggersBuilder 8 | { 9 | private readonly IServiceProvider serviceProvider; 10 | 11 | public TriggersBuilder(IServiceProvider serviceProvider) => this.serviceProvider = serviceProvider; 12 | 13 | public ITriggers Triggers() 14 | where TEntity : class 15 | where TDbContext : DbContext => 16 | serviceProvider.GetRequiredService>(); 17 | 18 | public ITriggers Triggers() 19 | where TEntity : class => 20 | serviceProvider.GetRequiredService>(); 21 | 22 | public ITriggers Triggers() => 23 | serviceProvider.GetRequiredService(); 24 | } -------------------------------------------------------------------------------- /src/EntityFrameworkCore.Triggers/TriggersEqualityComparer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace EntityFrameworkCore.Triggers; 6 | 7 | public sealed class TriggersEqualityComparer : IEqualityComparer 8 | where TTriggers : class, ITriggers 9 | { 10 | public static readonly TriggersEqualityComparer Instance = new TriggersEqualityComparer(); 11 | 12 | public Boolean Equals(TTriggers? x, TTriggers? y) 13 | { 14 | if (ReferenceEquals(x, y)) 15 | return true; 16 | if (x is null || y is null) 17 | return false; 18 | 19 | foreach (var tes in GetTriggerEvents(x).Zip(GetTriggerEvents(y), (tex, tey) => (tex, tey))) 20 | if (!ReferenceEquals(tes.tex, tes.tey)) 21 | return false; 22 | return true; 23 | } 24 | 25 | public Int32 GetHashCode(TTriggers triggers) 26 | { 27 | var hashCode = 0x51ed270b; 28 | foreach (var triggerEvent in GetTriggerEvents(triggers)) 29 | if (triggerEvent != null) 30 | hashCode = (hashCode * -1521134295) + triggerEvent.GetHashCode(); 31 | return hashCode; 32 | } 33 | 34 | private static IEnumerable GetTriggerEvents(TTriggers triggers) => EventGetters.Select(x => x.Invoke(triggers)); 35 | 36 | private static readonly EventDelegate[] EventGetters = GetEventGetterQuery.ToArray(); 37 | 38 | private static IEnumerable GetEventGetterQuery => 39 | from propertyInfo in typeof(TTriggers).GetProperties() 40 | where typeof(TriggerEvent).IsAssignableFrom(propertyInfo.PropertyType) 41 | orderby propertyInfo.Name 42 | select propertyInfo.GetGetMethod().CreateDelegate(); 43 | 44 | private delegate TriggerEvent EventDelegate(TTriggers triggers); 45 | } -------------------------------------------------------------------------------- /src/EntityFrameworkCore.Triggers/Triggers_1.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore; 3 | 4 | namespace EntityFrameworkCore.Triggers; 5 | 6 | public sealed class Triggers 7 | : ITriggers 8 | , IEquatable> 9 | where TEntity : class 10 | { 11 | private readonly ITriggers triggers; 12 | public Triggers(ITriggers triggers) => this.triggers = triggers; 13 | 14 | IInsertingTriggerEvent ITriggers.Inserting => triggers.Inserting ; 15 | IInsertFailedTriggerEvent ITriggers.InsertFailed => triggers.InsertFailed; 16 | IInsertedTriggerEvent ITriggers.Inserted => triggers.Inserted ; 17 | IDeletingTriggerEvent ITriggers.Deleting => triggers.Deleting ; 18 | IDeleteFailedTriggerEvent ITriggers.DeleteFailed => triggers.DeleteFailed; 19 | IDeletedTriggerEvent ITriggers.Deleted => triggers.Deleted ; 20 | IUpdatingTriggerEvent ITriggers.Updating => triggers.Updating ; 21 | IUpdateFailedTriggerEvent ITriggers.UpdateFailed => triggers.UpdateFailed; 22 | IUpdatedTriggerEvent ITriggers.Updated => triggers.Updated ; 23 | 24 | IInsertingTriggerEvent ITriggers.Inserting => triggers.Inserting ; 25 | IInsertFailedTriggerEvent ITriggers.InsertFailed => triggers.InsertFailed; 26 | IInsertedTriggerEvent ITriggers.Inserted => triggers.Inserted ; 27 | IDeletingTriggerEvent ITriggers.Deleting => triggers.Deleting ; 28 | IDeleteFailedTriggerEvent ITriggers.DeleteFailed => triggers.DeleteFailed; 29 | IDeletedTriggerEvent ITriggers.Deleted => triggers.Deleted ; 30 | IUpdatingTriggerEvent ITriggers.Updating => triggers.Updating ; 31 | IUpdateFailedTriggerEvent ITriggers.UpdateFailed => triggers.UpdateFailed; 32 | IUpdatedTriggerEvent ITriggers.Updated => triggers.Updated ; 33 | 34 | public static IInsertingTriggerEvent GlobalInserting { get; } = Triggers.GlobalInserting ; 35 | public static IInsertFailedTriggerEvent GlobalInsertFailed { get; } = Triggers.GlobalInsertFailed; 36 | public static IInsertedTriggerEvent GlobalInserted { get; } = Triggers.GlobalInserted ; 37 | public static IDeletingTriggerEvent GlobalDeleting { get; } = Triggers.GlobalDeleting ; 38 | public static IDeleteFailedTriggerEvent GlobalDeleteFailed { get; } = Triggers.GlobalDeleteFailed; 39 | public static IDeletedTriggerEvent GlobalDeleted { get; } = Triggers.GlobalDeleted ; 40 | public static IUpdatingTriggerEvent GlobalUpdating { get; } = Triggers.GlobalUpdating ; 41 | public static IUpdateFailedTriggerEvent GlobalUpdateFailed { get; } = Triggers.GlobalUpdateFailed; 42 | public static IUpdatedTriggerEvent GlobalUpdated { get; } = Triggers.GlobalUpdated ; 43 | 44 | public static event Action> Inserting { add => Triggers.Inserting += value; remove => Triggers.Inserting -= value; } 45 | public static event Action> InsertFailed { add => Triggers.InsertFailed += value; remove => Triggers.InsertFailed -= value; } 46 | public static event Action> Inserted { add => Triggers.Inserted += value; remove => Triggers.Inserted -= value; } 47 | public static event Action> Deleting { add => Triggers.Deleting += value; remove => Triggers.Deleting -= value; } 48 | public static event Action> DeleteFailed { add => Triggers.DeleteFailed += value; remove => Triggers.DeleteFailed -= value; } 49 | public static event Action> Deleted { add => Triggers.Deleted += value; remove => Triggers.Deleted -= value; } 50 | public static event Action> Updating { add => Triggers.Updating += value; remove => Triggers.Updating -= value; } 51 | public static event Action> UpdateFailed { add => Triggers.UpdateFailed += value; remove => Triggers.UpdateFailed -= value; } 52 | public static event Action> Updated { add => Triggers.Updated += value; remove => Triggers.Updated -= value; } 53 | 54 | public override Int32 GetHashCode() => TriggersEqualityComparer>.Instance.GetHashCode(triggers); 55 | public override Boolean Equals(Object? obj) => obj is ITriggers other && Equals(other); 56 | 57 | public Boolean Equals(ITriggers? other) => other is Triggers te ? ReferenceEquals(triggers, te.triggers) : TriggersEqualityComparer>.Instance.Equals(this, other); 58 | } -------------------------------------------------------------------------------- /src/EntityFrameworkCore.Triggers/Triggers_2.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore; 3 | 4 | namespace EntityFrameworkCore.Triggers; 5 | 6 | public sealed class Triggers 7 | : ITriggers 8 | , IEquatable> 9 | where TEntity : class 10 | where TDbContext : DbContext 11 | { 12 | private readonly IInsertingTriggerEvent inserting = new InsertingTriggerEvent (); 13 | private readonly IInsertFailedTriggerEvent insertFailed = new InsertFailedTriggerEvent(); 14 | private readonly IInsertedTriggerEvent inserted = new InsertedTriggerEvent (); 15 | private readonly IDeletingTriggerEvent deleting = new DeletingTriggerEvent (); 16 | private readonly IDeleteFailedTriggerEvent deleteFailed = new DeleteFailedTriggerEvent(); 17 | private readonly IDeletedTriggerEvent deleted = new DeletedTriggerEvent (); 18 | private readonly IUpdatingTriggerEvent updating = new UpdatingTriggerEvent (); 19 | private readonly IUpdateFailedTriggerEvent updateFailed = new UpdateFailedTriggerEvent(); 20 | private readonly IUpdatedTriggerEvent updated = new UpdatedTriggerEvent (); 21 | 22 | IInsertingTriggerEvent ITriggers.Inserting => inserting ; 23 | IInsertFailedTriggerEvent ITriggers.InsertFailed => insertFailed; 24 | IInsertedTriggerEvent ITriggers.Inserted => inserted ; 25 | IDeletingTriggerEvent ITriggers.Deleting => deleting ; 26 | IDeleteFailedTriggerEvent ITriggers.DeleteFailed => deleteFailed; 27 | IDeletedTriggerEvent ITriggers.Deleted => deleted ; 28 | IUpdatingTriggerEvent ITriggers.Updating => updating ; 29 | IUpdateFailedTriggerEvent ITriggers.UpdateFailed => updateFailed; 30 | IUpdatedTriggerEvent ITriggers.Updated => updated ; 31 | 32 | IInsertingTriggerEvent ITriggers.Inserting => inserting ; 33 | IInsertFailedTriggerEvent ITriggers.InsertFailed => insertFailed; 34 | IInsertedTriggerEvent ITriggers.Inserted => inserted ; 35 | IDeletingTriggerEvent ITriggers.Deleting => deleting ; 36 | IDeleteFailedTriggerEvent ITriggers.DeleteFailed => deleteFailed; 37 | IDeletedTriggerEvent ITriggers.Deleted => deleted ; 38 | IUpdatingTriggerEvent ITriggers.Updating => updating ; 39 | IUpdateFailedTriggerEvent ITriggers.UpdateFailed => updateFailed; 40 | IUpdatedTriggerEvent ITriggers.Updated => updated ; 41 | 42 | IInsertingTriggerEvent ITriggers.Inserting => inserting ; 43 | IInsertFailedTriggerEvent ITriggers.InsertFailed => insertFailed; 44 | IInsertedTriggerEvent ITriggers.Inserted => inserted ; 45 | IDeletingTriggerEvent ITriggers.Deleting => deleting ; 46 | IDeleteFailedTriggerEvent ITriggers.DeleteFailed => deleteFailed; 47 | IDeletedTriggerEvent ITriggers.Deleted => deleted ; 48 | IUpdatingTriggerEvent ITriggers.Updating => updating ; 49 | IUpdateFailedTriggerEvent ITriggers.UpdateFailed => updateFailed; 50 | IUpdatedTriggerEvent ITriggers.Updated => updated ; 51 | 52 | public static IInsertingTriggerEvent GlobalInserting { get; } = new InsertingTriggerEvent (); 53 | public static IInsertFailedTriggerEvent GlobalInsertFailed { get; } = new InsertFailedTriggerEvent(); 54 | public static IInsertedTriggerEvent GlobalInserted { get; } = new InsertedTriggerEvent (); 55 | public static IDeletingTriggerEvent GlobalDeleting { get; } = new DeletingTriggerEvent (); 56 | public static IDeleteFailedTriggerEvent GlobalDeleteFailed { get; } = new DeleteFailedTriggerEvent(); 57 | public static IDeletedTriggerEvent GlobalDeleted { get; } = new DeletedTriggerEvent (); 58 | public static IUpdatingTriggerEvent GlobalUpdating { get; } = new UpdatingTriggerEvent (); 59 | public static IUpdateFailedTriggerEvent GlobalUpdateFailed { get; } = new UpdateFailedTriggerEvent(); 60 | public static IUpdatedTriggerEvent GlobalUpdated { get; } = new UpdatedTriggerEvent (); 61 | 62 | public static event Action> Inserting { add => GlobalInserting .Add(value); remove => GlobalInserting .Remove(value); } 63 | public static event Action> InsertFailed { add => GlobalInsertFailed.Add(value); remove => GlobalInsertFailed.Remove(value); } 64 | public static event Action> Inserted { add => GlobalInserted .Add(value); remove => GlobalInserted .Remove(value); } 65 | public static event Action> Deleting { add => GlobalDeleting .Add(value); remove => GlobalDeleting .Remove(value); } 66 | public static event Action> DeleteFailed { add => GlobalDeleteFailed.Add(value); remove => GlobalDeleteFailed.Remove(value); } 67 | public static event Action> Deleted { add => GlobalDeleted .Add(value); remove => GlobalDeleted .Remove(value); } 68 | public static event Action> Updating { add => GlobalUpdating .Add(value); remove => GlobalUpdating .Remove(value); } 69 | public static event Action> UpdateFailed { add => GlobalUpdateFailed.Add(value); remove => GlobalUpdateFailed.Remove(value); } 70 | public static event Action> Updated { add => GlobalUpdated .Add(value); remove => GlobalUpdated .Remove(value); } 71 | 72 | public override Int32 GetHashCode() => TriggersEqualityComparer>.Instance.GetHashCode(this); 73 | public override Boolean Equals(Object? obj) => obj is ITriggers other && Equals(other); 74 | 75 | public Boolean Equals(ITriggers? other) => other is Triggers ted ? ReferenceEquals(this, ted) : TriggersEqualityComparer>.Instance.Equals(this, other); 76 | } -------------------------------------------------------------------------------- /src/EntityFrameworkCore.Triggers/TypeExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Reflection; 4 | 5 | namespace EntityFrameworkCore.Triggers; 6 | 7 | public static class TypeExtensions { 8 | public static Type[] GetDeclaredInterfaces(this Type t) { 9 | var allInterfaces = t.GetInterfaces(); 10 | var baseInterfaces = t.GetTypeInfo().BaseType?.GetInterfaces(); 11 | return baseInterfaces == null ? allInterfaces : allInterfaces.Except(baseInterfaces).ToArray(); 12 | } 13 | } -------------------------------------------------------------------------------- /test/Testing/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using EntityFrameworkCore.Triggers; 3 | using Microsoft.EntityFrameworkCore; 4 | using SimpleInjector; 5 | 6 | namespace Testing 7 | { 8 | public interface IInserted 9 | { 10 | DateTime Inserted { get; } 11 | } 12 | 13 | public class Base : IInserted, IDisposable 14 | { 15 | public DateTime Inserted { get; private set; } 16 | public DateTime? Updated { get; private set; } 17 | 18 | static Base() 19 | { 20 | Triggers.Inserting += entry => entry.Entity.Inserted = DateTime.UtcNow; 21 | Triggers.Updating += entry => entry.Entity.Updated = DateTime.UtcNow; 22 | } 23 | 24 | public void Dispose() {} 25 | } 26 | public interface IWhat { } 27 | public class Entity : Base, IInserted, IDisposable, IWhat 28 | { 29 | public Int64 Id { get; private set; } 30 | public String Name { get; set; } 31 | } 32 | 33 | public class Foo 34 | { 35 | private static Int32 instanceCount; 36 | public readonly Int32 Count; 37 | public Foo() => Count = instanceCount += 1; 38 | } 39 | 40 | public class Bar 41 | { 42 | private static Int32 instanceCount; 43 | public readonly Int32 Count; 44 | public Bar() => Count = instanceCount += 10; 45 | } 46 | 47 | public class Context : DbContextWithTriggers, IWhat 48 | { 49 | public Context(IServiceProvider serviceProvider) : base(serviceProvider) {} 50 | //public Context(IServiceProvider serviceProvider, DbContextOptions options) : base(serviceProvider, options) {} 51 | 52 | public virtual DbSet Entities { get; set; } 53 | 54 | protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) 55 | { 56 | if (!optionsBuilder.IsConfigured) 57 | optionsBuilder.UseInMemoryDatabase("what"); 58 | } 59 | } 60 | 61 | class Program 62 | { 63 | static void Foo(Object o) {} 64 | 65 | static void Main(String[] args) 66 | { 67 | Action aaa = (Action)((Action)Foo).Method.CreateDelegate(typeof(Action)); 68 | 69 | 70 | Triggers.Inserting += x => x.Entity.Name = ""; 71 | using (var container = new Container()) 72 | { 73 | container.Register(() => container, Lifestyle.Singleton); 74 | container.Register(Lifestyle.Transient); 75 | container.Register(Lifestyle.Transient); 76 | container.Register(Lifestyle.Transient); 77 | 78 | var triggers = container.GetInstance>(); 79 | var triggers1 = container.GetInstance>(); 80 | var triggers2 = container.GetInstance>(); 81 | var triggers3 = container.GetInstance(); 82 | var triggers4 = container.GetInstance>(); 83 | var bb = triggers1.Equals(triggers2); 84 | var bbb = ReferenceEquals(triggers1, triggers2); 85 | var bbbb = triggers3.Equals(triggers4); 86 | var bbbbb = ReferenceEquals(triggers3, triggers4); 87 | //triggers.Inserting.Add(entry => entry.Entity.Inserted = DateTime.UtcNow); 88 | //triggers.Updating.Add(entry => entry.Entity.Updated = DateTime.UtcNow); 89 | //triggers.Inserting.Add(entry => entry.Entity.Name = entry.Service.Count.ToString()); 90 | triggers.Inserting.Add<(Foo Foo, Bar Bar)>(entry => Console.WriteLine(entry.Service.Foo.Count + " " + entry.Service.Bar.Count)); 91 | triggers.Updating.Add<(Foo Foo, Bar Bar)>(entry => Console.WriteLine(entry.Service.Foo.Count + " " + entry.Service.Bar.Count)); 92 | 93 | using (var context = container.GetInstance()) 94 | { 95 | var a = new Entity(); 96 | var b = new Entity(); 97 | context.Add(a); 98 | context.Add(b); 99 | context.SaveChanges(); 100 | a.Name = "Test"; 101 | context.SaveChanges(); 102 | } 103 | } 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /test/Testing/Testing.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net8.0 6 | latest 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /test/TestingAsync/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using EntityFrameworkCore.Triggers; 5 | using Microsoft.EntityFrameworkCore; 6 | 7 | namespace TestingAsync 8 | { 9 | internal class Context : DbContextWithTriggers 10 | { 11 | protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) 12 | { 13 | if (optionsBuilder.IsConfigured) 14 | return; 15 | optionsBuilder.UseSqlServer($@"Server=(localdb)\mssqllocaldb;Database={GetType().FullName};Trusted_Connection=True;ConnectRetryCount=0"); 16 | } 17 | 18 | protected override void OnModelCreating(ModelBuilder modelBuilder) 19 | { 20 | modelBuilder.Entity
(); 21 | } 22 | } 23 | 24 | internal class Address 25 | { 26 | public Int64 Id { get; private set; } 27 | public String Street { get; set; } 28 | public String City { get; set; } 29 | public Province Province { get; set; } 30 | public String PostalCode { get; set; } 31 | 32 | static Address() => Triggers
.GlobalInserting.Add(entry => Task.Delay(5_000)); 33 | } 34 | 35 | internal enum Province 36 | { 37 | Alberta, 38 | BritishColumbia, 39 | Manitoba, 40 | NewBrunswick, 41 | NewfoundlandAndLabrador, 42 | NovaScotia, 43 | Ontario, 44 | PrinceEdwardIsland, 45 | Quebec, 46 | Saskatchewan, 47 | NorthwestTerritories, 48 | Nunavut, 49 | Yukon, 50 | } 51 | 52 | internal class Program 53 | { 54 | private static class AppDomainCancellation 55 | { 56 | private static readonly CancellationTokenSource CancellationTokenSource = new CancellationTokenSource(); 57 | public static readonly CancellationToken Token = CancellationTokenSource.Token; 58 | static AppDomainCancellation() => AppDomain.CurrentDomain.ProcessExit += (_, __) => { CancellationTokenSource.Cancel(); CancellationTokenSource.Dispose(); }; 59 | } 60 | 61 | private static async Task Main(String[] args) 62 | { 63 | using (var context = new Context()) 64 | { 65 | await context.Database.EnsureDeletedAsync(AppDomainCancellation.Token); 66 | await context.Database.EnsureCreatedAsync(AppDomainCancellation.Token); 67 | context.Add(new Address { Street = "123 Fake St.", City = "Toronto", Province = Province.Ontario, PostalCode = "H0H0H0" }); 68 | await context.SaveChangesAsync(AppDomainCancellation.Token); 69 | context.Add(new Address { Street = "456 Fake St.", City = "Vancouver", Province = Province.BritishColumbia, PostalCode = "H0H0H0" }); 70 | context.SaveChanges(); 71 | } 72 | } 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /test/TestingAsync/TestingAsync.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net8.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /test/TestsCore/AddingEntitiesWithinABeforeTrigger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using Xunit; 5 | 6 | #if EF_CORE 7 | using Microsoft.EntityFrameworkCore; 8 | # if NETCOREAPP2_0 9 | namespace EntityFrameworkCore.Triggers.Tests { 10 | # else 11 | namespace EntityFrameworkCore.Triggers.Tests.Net461 { 12 | # endif 13 | #else 14 | using System.Data.Entity; 15 | using System.Data.Entity.Infrastructure; 16 | using System.Data.Entity.Validation; 17 | namespace EntityFramework.Triggers.Tests { 18 | #endif 19 | public class InsideAnInsertingTriggerInsertAnEntityWhichHasInsertingTriggers : ThingTestBase { 20 | // Note that `Person` has triggers via its base classes `EntityWithTracking` and `EntityWithInsertTracking` 21 | private readonly String lastName = Guid.NewGuid().ToString(); 22 | private void TriggersOnInserting(IBeforeEntry beforeEntry) => beforeEntry.Context.People.Add(new Person { LastName = lastName }); 23 | 24 | protected override void Setup() { 25 | base.Setup(); 26 | Triggers.Inserting += TriggersOnInserting; 27 | } 28 | protected override void Teardown() { 29 | Triggers.Inserting -= TriggersOnInserting; 30 | base.Teardown(); 31 | } 32 | 33 | [Fact] 34 | public void Sync() => DoATest(() => { 35 | var guid = Guid.NewGuid().ToString(); 36 | Context.Things.Add(new Thing { Value = guid }); 37 | Context.SaveChanges(); 38 | Assert.True(Context.Things.SingleOrDefault(x => x.Value == guid) != null); 39 | Assert.True(Context.People.SingleOrDefault(x => x.LastName == lastName) != null); 40 | }); 41 | 42 | [Fact] 43 | public Task Async() => DoATestAsync(async () => { 44 | var guid = Guid.NewGuid().ToString(); 45 | Context.Things.Add(new Thing { Value = guid }); 46 | await Context.SaveChangesAsync().ConfigureAwait(false); 47 | Assert.True(await Context.Things.SingleOrDefaultAsync(x => x.Value == guid).ConfigureAwait(false) != null); 48 | Assert.True(await Context.People.SingleOrDefaultAsync(x => x.LastName == lastName).ConfigureAwait(false) != null); 49 | }); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /test/TestsCore/Context.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | #if EF_CORE 4 | using Microsoft.EntityFrameworkCore; 5 | using EntityFrameworkCore.Triggers; 6 | namespace EntityFrameworkCore.Triggers.Tests { 7 | #else 8 | using EntityFramework.Triggers; 9 | using System.Data.Entity; 10 | using System.Data.Entity.Migrations; 11 | namespace EntityFramework.Triggers.Tests { 12 | #endif 13 | 14 | public class Context : DbContextWithTriggers { 15 | private static String GetConnectionString(String databaseName) => $@"Server=(localdb)\mssqllocaldb;Database={databaseName};Trusted_Connection=True;"; 16 | 17 | #if !EF_CORE 18 | public Context(IServiceProvider serviceProvider) : base(serviceProvider, GetConnectionString(typeof(Context).FullName)) {} 19 | #endif 20 | #if EF_CORE 21 | public Context(IServiceProvider serviceProvider) : base(serviceProvider) { } 22 | public Context(IServiceProvider serviceProvider, DbContextOptions options) : base(serviceProvider, options) {} 23 | 24 | protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { 25 | optionsBuilder.UseSqlServer(GetConnectionString(GetType().FullName)); 26 | } 27 | #endif 28 | 29 | public DbSet People { get; set; } 30 | public DbSet Things { get; set; } 31 | public DbSet Apples { get; set; } 32 | public DbSet RoyalGalas { get; set; } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /test/TestsCore/Entity.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | #if EF_CORE 4 | namespace EntityFrameworkCore.Triggers.Tests { 5 | #else 6 | namespace EntityFramework.Triggers.Tests { 7 | #endif 8 | 9 | public abstract class EntityWithInsertTracking { 10 | public DateTime Inserted { get; private set; } 11 | public Int32 Number { get; private set; } 12 | 13 | static EntityWithInsertTracking() { 14 | Triggers.Inserting += e => e.Entity.Inserted = DateTime.UtcNow; 15 | Triggers.Inserting += e => e.Entity.Number = 42; 16 | } 17 | } 18 | public abstract class EntityWithTracking : EntityWithInsertTracking { 19 | public DateTime Updated { get; private set; } 20 | 21 | static EntityWithTracking() { 22 | Triggers.Inserting += e => e.Entity.Updated = DateTime.UtcNow; 23 | Triggers.Updating += e => e.Entity.Updated = DateTime.UtcNow; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test/TestsCore/GenericServiceCache_2.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Xunit; 3 | 4 | #if EF_CORE 5 | # if NETCOREAPP2_0 6 | namespace EntityFrameworkCore.Triggers.Tests 7 | # else 8 | namespace EntityFrameworkCore.Triggers.Tests.Net461 9 | # endif 10 | #else 11 | using System.Data.Entity; 12 | using System.Data.Entity.Validation; 13 | namespace EntityFramework.Triggers.Tests 14 | #endif 15 | { 16 | public class GenericServiceCacheTests 17 | { 18 | interface IFoo { } 19 | class Foo : IFoo { } 20 | 21 | [Fact] 22 | public void Test() 23 | { 24 | var foo = GenericServiceCache>.GetOrAdd(typeof(Int32)); 25 | Assert.IsType>(foo); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /test/TestsCore/Person.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.DataAnnotations; 3 | 4 | #if EF_CORE 5 | using Microsoft.EntityFrameworkCore; 6 | namespace EntityFrameworkCore.Triggers.Tests { 7 | #else 8 | using System.Data.Entity; 9 | namespace EntityFramework.Triggers.Tests { 10 | #endif 11 | 12 | public class Person : EntityWithTracking { 13 | [Key] 14 | public virtual Int64 Id { get; private set; } 15 | public virtual String FirstName { get; set; } 16 | [Required] 17 | public virtual String LastName { get; set; } 18 | public virtual Boolean IsMarkedDeleted { get; set; } 19 | 20 | public override String ToString() => $"{FirstName} {LastName}"; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test/TestsCore/TestBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Xunit; 5 | 6 | #if EF_CORE 7 | using Microsoft.EntityFrameworkCore; 8 | #else 9 | using System.Data.Entity; 10 | using System.Data.Entity.Validation; 11 | #endif 12 | 13 | [assembly: CollectionBehavior(DisableTestParallelization = true)] 14 | 15 | #if EF_CORE 16 | namespace EntityFrameworkCore.Triggers.Tests { 17 | #else 18 | namespace EntityFramework.Triggers.Tests { 19 | #endif 20 | public abstract class TestBase : IDisposable { 21 | protected abstract void Setup(); 22 | protected abstract void Teardown(); 23 | 24 | protected void DoATest(Action action) { 25 | #if EF_CORE 26 | Context.Database.EnsureCreated(); 27 | #endif 28 | Setup(); 29 | try { 30 | action(); 31 | } 32 | finally { 33 | Teardown(); 34 | } 35 | } 36 | 37 | protected async Task DoATestAsync(Func action) { 38 | Setup(); 39 | try { 40 | await action().ConfigureAwait(false); 41 | } 42 | finally { 43 | Teardown(); 44 | } 45 | } 46 | 47 | protected readonly Context Context = new Context(null); 48 | 49 | public virtual void Dispose() => Context.Dispose(); 50 | } 51 | } -------------------------------------------------------------------------------- /test/TestsCore/TestsCore.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | 6 | false 7 | EF_CORE 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | all 16 | runtime; build; native; contentfiles; analyzers 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /test/TestsCore/Thing.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.ComponentModel.DataAnnotations.Schema; 5 | 6 | 7 | #if EF_CORE 8 | namespace EntityFrameworkCore.Triggers.Tests { 9 | #else 10 | namespace EntityFramework.Triggers.Tests { 11 | #endif 12 | public interface IThing { 13 | Int64 Id { get; } 14 | String Value { get; set; } 15 | Boolean Inserting { get; set; } 16 | Boolean InsertFailed { get; set; } 17 | Boolean Inserted { get; set; } 18 | Boolean Updating { get; set; } 19 | Boolean UpdateFailed { get; set; } 20 | Boolean Updated { get; set; } 21 | Boolean Deleting { get; set; } 22 | Boolean DeleteFailed { get; set; } 23 | Boolean Deleted { get; set; } 24 | List Numbers { get; } 25 | } 26 | 27 | public class Thing : IThing { 28 | [Key] 29 | public virtual Int64 Id { get; private set; } 30 | 31 | [Required] 32 | public virtual String Value { get; set; } 33 | 34 | [NotMapped] public virtual Boolean Inserting { get; set; } 35 | [NotMapped] public virtual Boolean InsertFailed { get; set; } 36 | [NotMapped] public virtual Boolean Inserted { get; set; } 37 | [NotMapped] public virtual Boolean Updating { get; set; } 38 | [NotMapped] public virtual Boolean UpdateFailed { get; set; } 39 | [NotMapped] public virtual Boolean Updated { get; set; } 40 | [NotMapped] public virtual Boolean Deleting { get; set; } 41 | [NotMapped] public virtual Boolean DeleteFailed { get; set; } 42 | [NotMapped] public virtual Boolean Deleted { get; set; } 43 | 44 | [NotMapped] public virtual List Numbers { get; } = new List(); 45 | } 46 | 47 | public interface IApple { 48 | List Numbers { get; } 49 | } 50 | 51 | public class Apple : Thing, IApple 52 | { 53 | public override String ToString() => Value; 54 | } 55 | 56 | public interface IRoyalGala { 57 | List Numbers { get; } 58 | } 59 | 60 | public class RoyalGala : Apple, IRoyalGala { } 61 | } 62 | -------------------------------------------------------------------------------- /test/TestsCore/ThingTestBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using Xunit; 6 | 7 | #if EF_CORE 8 | using Microsoft.EntityFrameworkCore; 9 | namespace EntityFrameworkCore.Triggers.Tests { 10 | #else 11 | using System.Data.Entity; 12 | using System.Data.Entity.Validation; 13 | namespace EntityFramework.Triggers.Tests { 14 | #endif 15 | public abstract class ThingTestBase : TestBase { 16 | protected override void Setup() { 17 | Triggers.Inserting += InsertingTrue; 18 | Triggers.Inserting += InsertingCheckFlags; 19 | 20 | Triggers.InsertFailed += InsertFailedTrue; 21 | Triggers.InsertFailed += InsertFailedCheckFlags; 22 | 23 | Triggers.Inserted += InsertedTrue; 24 | Triggers.Inserted += InsertedCheckFlags; 25 | 26 | Triggers.Updating += UpdatingTrue; 27 | Triggers.Updating += UpdatingCheckFlags; 28 | 29 | Triggers.UpdateFailed += UpdateFailedTrue; 30 | Triggers.UpdateFailed += UpdateFailedCheckFlags; 31 | 32 | Triggers.Updated += UpdatedTrue; 33 | Triggers.Updated += UpdatedCheckFlags; 34 | 35 | Triggers.Deleting += DeletingTrue; 36 | Triggers.Deleting += DeletingCheckFlags; 37 | 38 | Triggers.DeleteFailed += DeleteFailedTrue; 39 | Triggers.DeleteFailed += DeleteFailedCheckFlags; 40 | 41 | Triggers.Deleted += DeletedTrue; 42 | Triggers.Deleted += DeletedCheckFlags; 43 | } 44 | 45 | protected override void Teardown() { 46 | Triggers.Inserting -= InsertingTrue; 47 | Triggers.Inserting -= InsertingCheckFlags; 48 | 49 | Triggers.InsertFailed -= InsertFailedTrue; 50 | Triggers.InsertFailed -= InsertFailedCheckFlags; 51 | 52 | Triggers.Inserted -= InsertedTrue; 53 | Triggers.Inserted -= InsertedCheckFlags; 54 | 55 | Triggers.Updating -= UpdatingTrue; 56 | Triggers.Updating -= UpdatingCheckFlags; 57 | 58 | Triggers.UpdateFailed -= UpdateFailedTrue; 59 | Triggers.UpdateFailed -= UpdateFailedCheckFlags; 60 | 61 | Triggers.Updated -= UpdatedTrue; 62 | Triggers.Updated -= UpdatedCheckFlags; 63 | 64 | Triggers.Deleting -= DeletingTrue; 65 | Triggers.Deleting -= DeletingCheckFlags; 66 | 67 | Triggers.DeleteFailed -= DeleteFailedTrue; 68 | Triggers.DeleteFailed -= DeleteFailedCheckFlags; 69 | 70 | Triggers.Deleted -= DeletedTrue; 71 | Triggers.Deleted -= DeletedCheckFlags; 72 | } 73 | 74 | private static void InsertingTrue (IBeforeEntry e) => InsertingTrue (e.Entity); 75 | private static void InsertingCheckFlags (IBeforeEntry e) => InsertingCheckFlags (e.Entity); 76 | private static void InsertFailedTrue (IFailedEntry e) => InsertFailedTrue (e.Entity); 77 | private static void InsertFailedCheckFlags(IFailedEntry e) => InsertFailedCheckFlags(e.Entity); 78 | private static void InsertedTrue (IAfterEntry e) => InsertedTrue (e.Entity); 79 | private static void InsertedCheckFlags (IAfterEntry e) => InsertedCheckFlags (e.Entity); 80 | private static void UpdatingTrue (IBeforeChangeEntry e) => UpdatingTrue (e.Entity); 81 | private static void UpdatingCheckFlags (IBeforeChangeEntry e) => UpdatingCheckFlags (e.Entity); 82 | private static void UpdateFailedTrue (IChangeFailedEntry e) => UpdateFailedTrue (e.Entity); 83 | private static void UpdateFailedCheckFlags(IChangeFailedEntry e) => UpdateFailedCheckFlags(e.Entity); 84 | private static void UpdatedTrue (IAfterChangeEntry e) => UpdatedTrue (e.Entity); 85 | private static void UpdatedCheckFlags (IAfterChangeEntry e) => UpdatedCheckFlags (e.Entity); 86 | private static void DeletingTrue (IBeforeChangeEntry e) => DeletingTrue (e.Entity); 87 | private static void DeletingCheckFlags (IBeforeChangeEntry e) => DeletingCheckFlags (e.Entity); 88 | private static void DeleteFailedTrue (IChangeFailedEntry e) => DeleteFailedTrue (e.Entity); 89 | private static void DeleteFailedCheckFlags(IChangeFailedEntry e) => DeleteFailedCheckFlags(e.Entity); 90 | private static void DeletedTrue (IAfterChangeEntry e) => DeletedTrue (e.Entity); 91 | private static void DeletedCheckFlags (IAfterChangeEntry e) => DeletedCheckFlags (e.Entity); 92 | 93 | public static void InsertingTrue (Thing thing) => thing.Inserting = true; 94 | public static void InsertingCheckFlags (Thing thing) => CheckFlags(thing, nameof(thing.Inserting)); 95 | public static void InsertFailedTrue (Thing thing) => thing.InsertFailed = true; 96 | public static void InsertFailedCheckFlags(Thing thing) => CheckFlags(thing, nameof(thing.Inserting), nameof(thing.InsertFailed)); 97 | public static void InsertedTrue (Thing thing) => thing.Inserted = true; 98 | public static void InsertedCheckFlags (Thing thing) => CheckFlags(thing, nameof(thing.Inserting), nameof(thing.Inserted)); 99 | public static void UpdatingTrue (Thing thing) => thing.Updating = true; 100 | public static void UpdatingCheckFlags (Thing thing) => CheckFlags(thing, nameof(thing.Updating)); 101 | public static void UpdateFailedTrue (Thing thing) => thing.UpdateFailed = true; 102 | public static void UpdateFailedCheckFlags(Thing thing) => CheckFlags(thing, nameof(thing.Updating), nameof(thing.UpdateFailed)); 103 | public static void UpdatedTrue (Thing thing) => thing.Updated = true; 104 | public static void UpdatedCheckFlags (Thing thing) => CheckFlags(thing, nameof(thing.Updating), nameof(thing.Updated)); 105 | public static void DeletingTrue (Thing thing) => thing.Deleting = true; 106 | public static void DeletingCheckFlags (Thing thing) => CheckFlags(thing, nameof(thing.Deleting)); 107 | public static void DeleteFailedTrue (Thing thing) => thing.DeleteFailed = true; 108 | public static void DeleteFailedCheckFlags(Thing thing) => CheckFlags(thing, nameof(thing.Deleting), nameof(thing.DeleteFailed)); 109 | public static void DeletedTrue (Thing thing) => thing.Deleted = true; 110 | public static void DeletedCheckFlags (Thing thing) => CheckFlags(thing, nameof(thing.Deleting), nameof(thing.Deleted)); 111 | 112 | private static readonly IEnumerable FlagPropertyInfos = typeof(Thing).GetProperties().Where(x => x.PropertyType == typeof(Boolean)); 113 | 114 | private static void CheckFlags(Thing thing, params String[] trueFlagNames) { 115 | foreach (var flagPropertyInfo in FlagPropertyInfos) { 116 | var flagSet = (Boolean) flagPropertyInfo.GetValue(thing, null); 117 | var failedAssertMessage = flagPropertyInfo.Name; 118 | 119 | if (trueFlagNames.Contains(flagPropertyInfo.Name)) 120 | Assert.True(flagSet, failedAssertMessage); 121 | else 122 | Assert.False(flagSet, failedAssertMessage); 123 | } 124 | } 125 | 126 | protected void ResetFlags(Thing thing) { 127 | foreach (var flag in FlagPropertyInfos) 128 | flag.SetValue(thing, false, null); 129 | } 130 | } 131 | } -------------------------------------------------------------------------------- /test/TestsCore/UnitTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using Xunit; 5 | 6 | #if EF_CORE 7 | using Microsoft.EntityFrameworkCore; 8 | # if NETCOREAPP2_0 9 | namespace EntityFrameworkCore.Triggers.Tests 10 | { 11 | # else 12 | namespace EntityFrameworkCore.Triggers.Tests.Net461 { 13 | # endif 14 | #else 15 | using System.Data.Entity; 16 | using System.Data.Entity.Validation; 17 | namespace EntityFramework.Triggers.Tests { 18 | #endif 19 | 20 | public class UnitTests { 21 | // inserting 22 | // insertfailed 23 | // inserted 24 | // updating 25 | // updatefailed 26 | // updated 27 | // deleting 28 | // deletefailed 29 | // deleted 30 | 31 | // cancel inserting 32 | // cancel updating 33 | // cancel deleting 34 | 35 | // DbEntityValidationException 36 | // DbUpdateException 37 | 38 | // event order 39 | // inheritance hierarchy event order 40 | 41 | // original values on updating 42 | // firing 'before' triggers of an entity added by another's "before" trigger, all before actual SaveChanges is executed 43 | 44 | // Cancel property of "before" trigger 45 | // Swallow proprety of "failed" trigger 46 | 47 | // TODO: 48 | // event loops 49 | // calling savechanges in an event handler 50 | // doubly-declared interfaces 51 | 52 | // test ...edFailed exception logic... 53 | // DbUpdateException raises failed triggers and is swallowable if contains entries or changetracker has only one entry 54 | // DbEntityValidationException raises failed triggers and is swallowable if it contains entries 55 | // All other exceptions raises failed triggers and is swallowable if changetracker has only one entry 56 | } 57 | 58 | public class TriggersEnabled : TestBase { 59 | protected override void Setup() => Triggers.Inserting += OnTriggersOnInserting; 60 | protected override void Teardown() => Triggers.Inserting -= OnTriggersOnInserting; 61 | 62 | private void OnTriggersOnInserting(IInsertingEntry e) => e.Entity.Value = "changed"; 63 | 64 | [Fact] 65 | public void Sync() => DoATest(() => { 66 | var thing = new Thing {Value = "okay"}; 67 | Context.Things.Add(thing); 68 | Assert.True(Context.TriggersEnabled); 69 | Context.TriggersEnabled = false; 70 | Context.SaveChanges(); 71 | Assert.True(thing.Value == "okay"); 72 | 73 | var thing2 = new Thing {Value = "yep"}; 74 | Context.Things.Add(thing2); 75 | Context.TriggersEnabled = true; 76 | Context.SaveChanges(); 77 | Assert.True(thing2.Value == "changed"); 78 | }); 79 | 80 | [Fact] 81 | public Task Async() => DoATestAsync(async () => { 82 | var thing = new Thing { Value = "okay" }; 83 | Context.Things.Add(thing); 84 | Assert.True(Context.TriggersEnabled); 85 | Context.TriggersEnabled = false; 86 | await Context.SaveChangesAsync().ConfigureAwait(false); 87 | Assert.True(thing.Value == "okay"); 88 | 89 | var thing2 = new Thing { Value = "yep" }; 90 | Context.Things.Add(thing2); 91 | Context.TriggersEnabled = true; 92 | await Context.SaveChangesAsync().ConfigureAwait(false); 93 | Assert.True(thing2.Value == "changed"); 94 | }); 95 | } 96 | 97 | public class AddRemoveEventHandler : TestBase { 98 | protected override void Setup() => Triggers.Inserting += TriggersOnInserting; 99 | protected override void Teardown() => Triggers.Inserting -= TriggersOnInserting; 100 | 101 | private Int32 triggerCount; 102 | private void TriggersOnInserting(IBeforeEntry beforeEntry) => ++triggerCount; 103 | 104 | [Fact] 105 | public void Sync() => DoATest(() => { 106 | Context.Things.Add(new Thing { Value = "Foo" }); 107 | Context.SaveChanges(); 108 | Assert.True(1 == triggerCount); 109 | 110 | Teardown(); // Remove handler 111 | Context.Things.Add(new Thing { Value = "Foo" }); 112 | Context.SaveChanges(); 113 | Assert.True(1 == triggerCount); 114 | }); 115 | 116 | [Fact] 117 | public Task Async() => DoATestAsync(async () => { 118 | Context.Things.Add(new Thing { Value = "Foo" }); 119 | await Context.SaveChangesAsync().ConfigureAwait(false); 120 | Assert.True(1 == triggerCount); 121 | 122 | Teardown(); // Remove handler 123 | Context.Things.Add(new Thing { Value = "Foo" }); 124 | await Context.SaveChangesAsync().ConfigureAwait(false); 125 | Assert.True(1 == triggerCount); 126 | }); 127 | } 128 | 129 | public class Insert : ThingTestBase { 130 | [Fact] 131 | public void Sync() => DoATest(() => { 132 | var guid = Guid.NewGuid().ToString(); 133 | var thing = new Thing { Value = guid }; 134 | Context.Things.Add(thing); 135 | Context.SaveChanges(); 136 | InsertedCheckFlags(thing); 137 | Assert.True(Context.Things.SingleOrDefault(x => x.Value == guid) != null); 138 | }); 139 | 140 | [Fact] 141 | public Task Async() => DoATestAsync(async () => { 142 | var guid = Guid.NewGuid().ToString(); 143 | var thing = new Thing { Value = guid }; 144 | Context.Things.Add(thing); 145 | await Context.SaveChangesAsync().ConfigureAwait(false); 146 | InsertedCheckFlags(thing); 147 | Assert.True(await Context.Things.SingleOrDefaultAsync(x => x.Value == guid).ConfigureAwait(false) != null); 148 | }); 149 | } 150 | 151 | public class InsertFail : ThingTestBase { 152 | [Fact] 153 | public void Sync() => DoATest(() => { 154 | var thing = new Thing { Value = null }; 155 | Context.Things.Add(thing); 156 | try { 157 | Context.SaveChanges(); 158 | } 159 | #if EF_CORE 160 | catch (DbUpdateException) { 161 | #else 162 | catch (DbEntityValidationException) { 163 | #endif 164 | InsertFailedCheckFlags(thing); 165 | return; 166 | } 167 | Assert.True(false, "Exception not caught"); 168 | }); 169 | 170 | [Fact] 171 | public Task Async() => DoATestAsync(async () => { 172 | var thing = new Thing { Value = null }; 173 | Context.Things.Add(thing); 174 | try { 175 | await Context.SaveChangesAsync().ConfigureAwait(false); 176 | } 177 | #if EF_CORE 178 | catch (DbUpdateException) { 179 | #else 180 | catch (DbEntityValidationException) { 181 | #endif 182 | InsertFailedCheckFlags(thing); 183 | return; 184 | } 185 | Assert.True(false, "Exception not caught"); 186 | }); 187 | } 188 | 189 | public class InsertFailSwallow : TestBase { 190 | protected override void Setup() => Triggers.InsertFailed += OnInsertFailed; 191 | protected override void Teardown() => Triggers.InsertFailed -= OnInsertFailed; 192 | 193 | private static void OnInsertFailed(IFailedEntry e) { 194 | #if EF_CORE 195 | Assert.True(e.Exception is DbUpdateException); 196 | #else 197 | Assert.True(e.Exception is DbEntityValidationException); 198 | #endif 199 | e.Swallow = true; 200 | } 201 | 202 | [Fact] 203 | public void Sync() => DoATest(() => { 204 | Context.Things.Add(new Thing { Value = null }); 205 | Context.SaveChanges(); 206 | }); 207 | 208 | [Fact] 209 | public Task Async() => DoATestAsync(async () => { 210 | Context.Things.Add(new Thing { Value = null }); 211 | await Context.SaveChangesAsync().ConfigureAwait(false); 212 | }); 213 | } 214 | 215 | public class Update : ThingTestBase { 216 | [Fact] 217 | public void Sync() => DoATest(() => { 218 | var thing = new Thing { Value = "Foo" }; 219 | Context.Things.Add(thing); 220 | Context.SaveChanges(); 221 | thing.Value = "Bar"; 222 | ResetFlags(thing); 223 | Context.SaveChanges(); 224 | UpdatedCheckFlags(thing); 225 | }); 226 | 227 | [Fact] 228 | public Task Async() => DoATestAsync(async () => { 229 | var thing = new Thing { Value = "Foo" }; 230 | Context.Things.Add(thing); 231 | await Context.SaveChangesAsync().ConfigureAwait(false); 232 | thing.Value = "Bar"; 233 | ResetFlags(thing); 234 | await Context.SaveChangesAsync().ConfigureAwait(false); 235 | UpdatedCheckFlags(thing); 236 | }); 237 | } 238 | 239 | public class UpdateFail : ThingTestBase { 240 | [Fact] 241 | public void Sync() => DoATest(() => { 242 | var thing = new Thing { Value = "Foo" }; 243 | Context.Things.Add(thing); 244 | Context.SaveChanges(); 245 | thing.Value = null; 246 | ResetFlags(thing); 247 | try { 248 | Context.SaveChanges(); 249 | } 250 | #if EF_CORE 251 | catch (DbUpdateException) { 252 | #else 253 | catch (DbEntityValidationException) { 254 | #endif 255 | UpdateFailedCheckFlags(thing); 256 | return; 257 | } 258 | Assert.True(false, "Exception not caught"); 259 | }); 260 | 261 | [Fact] 262 | public Task Async() => DoATestAsync(async () => { 263 | var thing = new Thing { Value = "Foo" }; 264 | Context.Things.Add(thing); 265 | await Context.SaveChangesAsync().ConfigureAwait(false); 266 | thing.Value = null; 267 | ResetFlags(thing); 268 | try { 269 | await Context.SaveChangesAsync().ConfigureAwait(false); 270 | } 271 | #if EF_CORE 272 | catch (DbUpdateException) { 273 | #else 274 | catch (DbEntityValidationException) { 275 | #endif 276 | UpdateFailedCheckFlags(thing); 277 | return; 278 | } 279 | Assert.True(false, "Exception not caught"); 280 | }); 281 | } 282 | 283 | public class UpdateFailSwallow : TestBase { 284 | protected override void Setup() => Triggers.UpdateFailed += OnUpdateFailed; 285 | protected override void Teardown() => Triggers.UpdateFailed -= OnUpdateFailed; 286 | 287 | private static void OnUpdateFailed(IFailedEntry e) { 288 | #if EF_CORE 289 | Assert.True(e.Exception is DbUpdateException); 290 | #else 291 | Assert.True(e.Exception is DbEntityValidationException); 292 | #endif 293 | e.Swallow = true; 294 | } 295 | 296 | [Fact] 297 | public void Sync() => DoATest(() => { 298 | var thing = new Thing { Value = "Foo" }; 299 | Context.Things.Add(thing); 300 | Context.SaveChanges(); 301 | thing.Value = null; 302 | Context.SaveChanges(); 303 | }); 304 | 305 | [Fact] 306 | public Task Async() => DoATestAsync(async () => { 307 | var thing = new Thing { Value = "Foo" }; 308 | Context.Things.Add(thing); 309 | await Context.SaveChangesAsync().ConfigureAwait(false); 310 | thing.Value = null; 311 | await Context.SaveChangesAsync().ConfigureAwait(false); 312 | }); 313 | } 314 | 315 | public class Delete : ThingTestBase { 316 | [Fact] 317 | public void Sync() => DoATest(() => { 318 | var thing = new Thing { Value = "Foo" }; 319 | Context.Things.Add(thing); 320 | Context.SaveChanges(); 321 | ResetFlags(thing); 322 | Context.Things.Remove(thing); 323 | Context.SaveChanges(); 324 | DeletedCheckFlags(thing); 325 | }); 326 | 327 | [Fact] 328 | public Task Async() => DoATestAsync(async () => { 329 | var thing = new Thing { Value = "Foo" }; 330 | Context.Things.Add(thing); 331 | await Context.SaveChangesAsync().ConfigureAwait(false); 332 | ResetFlags(thing); 333 | Context.Things.Remove(thing); 334 | await Context.SaveChangesAsync().ConfigureAwait(false); 335 | DeletedCheckFlags(thing); 336 | }); 337 | } 338 | 339 | public class DeleteFail : ThingTestBase { 340 | protected override void Setup() { 341 | base.Setup(); 342 | Triggers.Deleting += OnDeleting; 343 | } 344 | 345 | protected override void Teardown() { 346 | Triggers.Deleting -= OnDeleting; 347 | base.Teardown(); 348 | } 349 | 350 | private static void OnDeleting(IBeforeChangeEntry e) { 351 | throw new Exception(); 352 | } 353 | 354 | [Fact] 355 | public void Sync() => DoATest(() => { 356 | var thing = new Thing { Value = "Foo" }; 357 | Context.Things.Add(thing); 358 | Context.SaveChanges(); 359 | ResetFlags(thing); 360 | Context.Things.Remove(thing); 361 | try { 362 | Context.SaveChanges(); 363 | } 364 | catch (Exception) { 365 | DeleteFailedCheckFlags(thing); 366 | return; 367 | } 368 | Assert.True(false, "Exception not caught"); 369 | }); 370 | 371 | [Fact] 372 | public Task Async() => DoATestAsync(async () => { 373 | var thing = new Thing { Value = "Foo" }; 374 | Context.Things.Add(thing); 375 | await Context.SaveChangesAsync().ConfigureAwait(false); 376 | ResetFlags(thing); 377 | Context.Things.Remove(thing); 378 | try { 379 | await Context.SaveChangesAsync().ConfigureAwait(false); 380 | } 381 | catch (Exception) { 382 | DeleteFailedCheckFlags(thing); 383 | return; 384 | } 385 | Assert.True(false, "Exception not caught"); 386 | }); 387 | } 388 | 389 | public class DeleteFailSwallow : TestBase { 390 | protected override void Setup() { 391 | Triggers.Deleting += OnDeleting; 392 | Triggers.DeleteFailed += OnDeleteFailed; 393 | } 394 | 395 | protected override void Teardown() { 396 | Triggers.DeleteFailed -= OnDeleteFailed; 397 | Triggers.Deleting -= OnDeleting; 398 | } 399 | 400 | private static void OnDeleting(IBeforeChangeEntry e) { 401 | throw new Exception(); 402 | } 403 | 404 | private static void OnDeleteFailed(IChangeFailedEntry e) { 405 | e.Swallow = true; 406 | } 407 | 408 | [Fact] 409 | public void Sync() => DoATest(() => { 410 | var thing = new Thing { Value = "Foo" }; 411 | Context.Things.Add(thing); 412 | Context.SaveChanges(); 413 | Context.Things.Remove(thing); 414 | Context.SaveChanges(); 415 | }); 416 | 417 | [Fact] 418 | public Task Async() => DoATestAsync(async () => { 419 | var thing = new Thing { Value = "Foo" }; 420 | Context.Things.Add(thing); 421 | await Context.SaveChangesAsync().ConfigureAwait(false); 422 | Context.Things.Remove(thing); 423 | await Context.SaveChangesAsync().ConfigureAwait(false); 424 | }); 425 | } 426 | 427 | public class InsertingCancel : ThingTestBase { 428 | protected override void Setup() { 429 | base.Setup(); 430 | Triggers.Inserting += Cancel; 431 | Triggers.Inserting += Cancel2; // <-- Note the specified Context class (the `Cancel` property must persist across 432 | } 433 | protected override void Teardown() { 434 | Triggers.Inserting -= Cancel2; 435 | Triggers.Inserting -= Cancel; 436 | base.Teardown(); 437 | } 438 | 439 | protected virtual void Cancel(IBeforeEntry e) => e.Cancel = true; 440 | 441 | private Boolean cancel2Ran; 442 | protected void Cancel2(IBeforeEntry e) { 443 | cancel2Ran = true; 444 | Assert.True(e.Cancel, nameof(e.Cancel) + ": " + e.Cancel); 445 | } 446 | 447 | [Fact] 448 | public void Sync() => DoATest(() => { 449 | var guid = Guid.NewGuid().ToString(); 450 | var thing = new Thing { Value = guid }; 451 | Context.Things.Add(thing); 452 | Assert.False(cancel2Ran, nameof(cancel2Ran) + ": " + cancel2Ran); 453 | Context.SaveChanges(); 454 | InsertingCheckFlags(thing); 455 | Assert.True(cancel2Ran, nameof(cancel2Ran) + ": " + cancel2Ran); 456 | Assert.True(Context.Things.SingleOrDefault(x => x.Value == guid) == null); 457 | }); 458 | 459 | [Fact] 460 | public Task Async() => DoATestAsync(async () => { 461 | var guid = Guid.NewGuid().ToString(); 462 | var thing = new Thing { Value = guid }; 463 | Context.Things.Add(thing); 464 | Assert.False(cancel2Ran, nameof(cancel2Ran) + ": " + cancel2Ran); 465 | await Context.SaveChangesAsync().ConfigureAwait(false); 466 | InsertingCheckFlags(thing); 467 | Assert.True(cancel2Ran, nameof(cancel2Ran) + ": " + cancel2Ran); 468 | Assert.True(await Context.Things.SingleOrDefaultAsync(x => x.Value == guid).ConfigureAwait(false) == null); 469 | }); 470 | } 471 | 472 | public class UpdatingCancel : ThingTestBase { 473 | protected override void Setup() { 474 | base.Setup(); 475 | Triggers.Updating += Cancel; 476 | Triggers.Updating += Cancel2; // <-- Note the specified Context class (the `Cancel` property must persist across 477 | } 478 | protected override void Teardown() { 479 | Triggers.Updating -= Cancel2; 480 | Triggers.Updating -= Cancel; 481 | base.Teardown(); 482 | } 483 | 484 | protected virtual void Cancel(IBeforeChangeEntry e) => e.Cancel = true; 485 | 486 | private Boolean cancel2Ran; 487 | protected void Cancel2(IBeforeEntry e) { 488 | cancel2Ran = true; 489 | Assert.True(e.Cancel, nameof(e.Cancel) + ": " + e.Cancel); 490 | } 491 | 492 | [Fact] 493 | public void Sync() => DoATest(() => { 494 | var guid = Guid.NewGuid().ToString(); 495 | var thing = new Thing { Value = guid }; 496 | Context.Things.Add(thing); 497 | Context.SaveChanges(); 498 | ResetFlags(thing); 499 | var updatedGuid = Guid.NewGuid().ToString(); 500 | thing.Value = updatedGuid; 501 | Context.SaveChanges(); 502 | UpdatingCheckFlags(thing); 503 | Assert.True(cancel2Ran); 504 | Assert.True(Context.Things.SingleOrDefault(x => x.Value == guid) != null); 505 | Assert.True(Context.Things.SingleOrDefault(x => x.Value == updatedGuid) == null); 506 | }); 507 | 508 | [Fact] 509 | public Task Async() => DoATestAsync(async () => { 510 | var guid = Guid.NewGuid().ToString(); 511 | var thing = new Thing { Value = guid }; 512 | Context.Things.Add(thing); 513 | Context.SaveChanges(); 514 | ResetFlags(thing); 515 | var updatedGuid = Guid.NewGuid().ToString(); 516 | thing.Value = updatedGuid; 517 | await Context.SaveChangesAsync(); 518 | UpdatingCheckFlags(thing); 519 | Assert.True(cancel2Ran); 520 | Assert.True(await Context.Things.SingleOrDefaultAsync(x => x.Value == guid).ConfigureAwait(false) != null); 521 | Assert.True(await Context.Things.SingleOrDefaultAsync(x => x.Value == updatedGuid).ConfigureAwait(false) == null); 522 | }); 523 | } 524 | 525 | public class DeletingCancel : ThingTestBase { 526 | protected override void Setup() { 527 | base.Setup(); 528 | Triggers.Deleting += Cancel; 529 | Triggers.Updating += Cancel2; // <-- Note the specified Context class (the `Cancel` property must persist across 530 | } 531 | protected override void Teardown() { 532 | Triggers.Updating -= Cancel2; 533 | Triggers.Deleting -= Cancel; 534 | base.Teardown(); 535 | } 536 | 537 | protected virtual void Cancel(IBeforeChangeEntry e) => e.Cancel = true; 538 | 539 | private Boolean cancel2Ran; 540 | protected void Cancel2(IBeforeEntry e) { 541 | cancel2Ran = true; 542 | Assert.True(e.Cancel, nameof(e.Cancel) + ": " + e.Cancel); 543 | } 544 | 545 | [Fact] 546 | public void Sync() => DoATest(() => { 547 | var guid = Guid.NewGuid().ToString(); 548 | var thing = new Thing { Value = guid }; 549 | Context.Things.Add(thing); 550 | Context.SaveChanges(); 551 | ResetFlags(thing); 552 | Context.Things.Remove(thing); 553 | Context.SaveChanges(); 554 | DeletingCheckFlags(thing); 555 | Assert.False(cancel2Ran); 556 | Assert.True(Context.Things.SingleOrDefault(x => x.Value == guid) != null); 557 | }); 558 | 559 | [Fact] 560 | public Task Async() => DoATestAsync(async () => { 561 | var guid = Guid.NewGuid().ToString(); 562 | var thing = new Thing { Value = guid }; 563 | Context.Things.Add(thing); 564 | await Context.SaveChangesAsync(); 565 | ResetFlags(thing); 566 | Context.Things.Remove(thing); 567 | await Context.SaveChangesAsync(); 568 | DeletingCheckFlags(thing); 569 | Assert.False(cancel2Ran); 570 | Assert.True(await Context.Things.SingleOrDefaultAsync(x => x.Value == guid).ConfigureAwait(false) != null); 571 | }); 572 | } 573 | 574 | public class EventFiringOrderRelativeToAttachment : TestBase { 575 | protected override void Setup() { 576 | Triggers.Inserting += Add1; 577 | Triggers.Inserting += Add2; 578 | Triggers.Inserting += Add3; 579 | } 580 | 581 | protected override void Teardown() { 582 | Triggers.Inserting -= Add1; 583 | Triggers.Inserting -= Add2; 584 | Triggers.Inserting -= Add3; 585 | } 586 | 587 | private static void Add1(IBeforeEntry e) => e.Entity.Numbers.Add(1); 588 | private static void Add2(IBeforeEntry e) => e.Entity.Numbers.Add(2); 589 | private static void Add3(IBeforeEntry e) => e.Entity.Numbers.Add(3); 590 | 591 | [Fact] 592 | public void Sync() => DoATest(() => { 593 | var thing = new Thing { Value = Guid.NewGuid().ToString() }; 594 | Context.Things.Add(thing); 595 | Context.SaveChanges(); 596 | Assert.True(thing.Numbers.SequenceEqual(new [] { 1, 2, 3 })); 597 | }); 598 | 599 | [Fact] 600 | public Task Async() => DoATestAsync(async () => { 601 | var thing = new Thing { Value = Guid.NewGuid().ToString() }; 602 | Context.Things.Add(thing); 603 | await Context.SaveChangesAsync().ConfigureAwait(false); 604 | Assert.True(thing.Numbers.SequenceEqual(new[] { 1, 2, 3 })); 605 | }); 606 | } 607 | 608 | public class EventFiringOrderRelativeToClassHierarchy : TestBase { 609 | protected override void Setup() { 610 | Triggers.Inserting += Add3; 611 | Triggers.Inserting += Add2; 612 | Triggers.Inserting += Add1; 613 | } 614 | 615 | protected override void Teardown() { 616 | Triggers.Inserting -= Add1; 617 | Triggers.Inserting -= Add2; 618 | Triggers.Inserting -= Add3; 619 | } 620 | 621 | private static void Add1(IBeforeEntry e) => e.Entity.Numbers.Add(1); 622 | private static void Add2(IBeforeEntry e) => e.Entity.Numbers.Add(2); 623 | private static void Add3(IBeforeEntry e) => e.Entity.Numbers.Add(3); 624 | 625 | [Fact] 626 | public void Sync() => DoATest(() => { 627 | var royalGala = new RoyalGala { Value = Guid.NewGuid().ToString() }; 628 | Context.RoyalGalas.Add(royalGala); 629 | Context.SaveChanges(); 630 | Assert.True(royalGala.Numbers.SequenceEqual(new[] { 1, 2, 3 })); 631 | }); 632 | 633 | [Fact] 634 | public Task Async() => DoATestAsync(async () => { 635 | var royalGala = new RoyalGala { Value = Guid.NewGuid().ToString() }; 636 | Context.RoyalGalas.Add(royalGala); 637 | await Context.SaveChangesAsync().ConfigureAwait(false); 638 | Assert.True(royalGala.Numbers.SequenceEqual(new[] { 1, 2, 3 })); 639 | }); 640 | } 641 | 642 | public class EventFiringOrderRelativeToClassInterfaceAndDbContextHierarchy : TestBase { 643 | protected override void Setup() { 644 | Triggers.Inserting += Add1; 645 | Triggers.Inserting += Add2; 646 | Triggers.Inserting += Add3; 647 | Triggers.Inserting += Add4; 648 | Triggers.Inserting += Add5; 649 | Triggers.Inserting += Add6; 650 | 651 | Triggers.Inserting += Add11; 652 | Triggers.Inserting += Add22; 653 | Triggers.Inserting += Add33; 654 | Triggers.Inserting += Add44; 655 | Triggers.Inserting += Add55; 656 | Triggers.Inserting += Add66; 657 | 658 | Triggers.Inserting += Add111; 659 | Triggers.Inserting += Add222; 660 | Triggers.Inserting += Add333; 661 | Triggers.Inserting += Add444; 662 | Triggers.Inserting += Add555; 663 | Triggers.Inserting += Add666; 664 | } 665 | 666 | protected override void Teardown() { 667 | Triggers.Inserting -= Add1; 668 | Triggers.Inserting -= Add2; 669 | Triggers.Inserting -= Add3; 670 | Triggers.Inserting -= Add4; 671 | Triggers.Inserting -= Add5; 672 | Triggers.Inserting -= Add6; 673 | 674 | Triggers.Inserting -= Add11; 675 | Triggers.Inserting -= Add22; 676 | Triggers.Inserting -= Add33; 677 | Triggers.Inserting -= Add44; 678 | Triggers.Inserting -= Add55; 679 | Triggers.Inserting -= Add66; 680 | 681 | Triggers.Inserting -= Add111; 682 | Triggers.Inserting -= Add222; 683 | Triggers.Inserting -= Add333; 684 | Triggers.Inserting -= Add444; 685 | Triggers.Inserting -= Add555; 686 | Triggers.Inserting -= Add666; 687 | } 688 | 689 | private static void Add1(IInsertingEntry e) => e.Entity.Numbers.Add(1); 690 | private static void Add2(IInsertingEntry e) => e.Entity.Numbers.Add(2); 691 | private static void Add3(IInsertingEntry e) => e.Entity.Numbers.Add(3); 692 | private static void Add4(IInsertingEntry e) => e.Entity.Numbers.Add(4); 693 | private static void Add5(IInsertingEntry e) => e.Entity.Numbers.Add(5); 694 | private static void Add6(IInsertingEntry e) => e.Entity.Numbers.Add(6); 695 | 696 | private static void Add11(IInsertingEntry e) => e.Entity.Numbers.Add(11); 697 | private static void Add22(IInsertingEntry e) => e.Entity.Numbers.Add(22); 698 | private static void Add33(IInsertingEntry e) => e.Entity.Numbers.Add(33); 699 | private static void Add44(IInsertingEntry e) => e.Entity.Numbers.Add(44); 700 | private static void Add55(IInsertingEntry e) => e.Entity.Numbers.Add(55); 701 | private static void Add66(IInsertingEntry e) => e.Entity.Numbers.Add(66); 702 | 703 | private static void Add111(IInsertingEntry e) => e.Entity.Numbers.Add(111); 704 | private static void Add222(IInsertingEntry e) => e.Entity.Numbers.Add(222); 705 | private static void Add333(IInsertingEntry e) => e.Entity.Numbers.Add(333); 706 | private static void Add444(IInsertingEntry e) => e.Entity.Numbers.Add(444); 707 | private static void Add555(IInsertingEntry e) => e.Entity.Numbers.Add(555); 708 | private static void Add666(IInsertingEntry e) => e.Entity.Numbers.Add(666); 709 | 710 | [Fact] 711 | public void Sync() => DoATest(() => { 712 | var royalGala = new RoyalGala { Value = Guid.NewGuid().ToString() }; 713 | Context.RoyalGalas.Add(royalGala); 714 | Context.SaveChanges(); 715 | Assert.True(royalGala.Numbers.SequenceEqual(new[] { 1, 11, 111, 2, 22, 222, 3, 33, 333, 4, 44, 444, 5, 55, 555, 6, 66, 666 })); 716 | }); 717 | 718 | [Fact] 719 | public Task Async() => DoATestAsync(async () => { 720 | var royalGala = new RoyalGala { Value = Guid.NewGuid().ToString() }; 721 | Context.RoyalGalas.Add(royalGala); 722 | await Context.SaveChangesAsync().ConfigureAwait(false); 723 | Assert.True(royalGala.Numbers.SequenceEqual(new[] { 1, 11, 111, 2, 22, 222, 3, 33, 333, 4, 44, 444, 5, 55, 555, 6, 66, 666 })); 724 | }); 725 | } 726 | 727 | public class OriginalValuesOnUpdating : TestBase { 728 | protected override void Setup() => Triggers.Updating += TriggersOnUpdating; 729 | protected override void Teardown() => Triggers.Updating -= TriggersOnUpdating; 730 | 731 | private void TriggersOnUpdating(IBeforeChangeEntry beforeChangeEntry) { 732 | Assert.True(beforeChangeEntry.Original.Value == guid); 733 | Assert.True(beforeChangeEntry.Entity.Value == guid2); 734 | } 735 | 736 | private String guid; 737 | private String guid2; 738 | 739 | [Fact] 740 | public void Sync() => DoATest(() => { 741 | guid = Guid.NewGuid().ToString(); 742 | guid2 = Guid.NewGuid().ToString(); 743 | var thing = new Thing { Value = guid }; 744 | Context.Things.Add(thing); 745 | Context.SaveChanges(); 746 | thing.Value = guid2; 747 | Context.SaveChanges(); 748 | }); 749 | 750 | [Fact] 751 | public Task Async() => DoATestAsync(async () => { 752 | guid = Guid.NewGuid().ToString(); 753 | guid2 = Guid.NewGuid().ToString(); 754 | var thing = new Thing { Value = guid }; 755 | Context.Things.Add(thing); 756 | await Context.SaveChangesAsync().ConfigureAwait(false); 757 | thing.Value = guid2; 758 | await Context.SaveChangesAsync().ConfigureAwait(false); 759 | }); 760 | } 761 | 762 | public class OriginalValuesOnDeleting : TestBase { 763 | protected override void Setup() => Triggers.Deleting += TriggersOnDeleting; 764 | protected override void Teardown() => Triggers.Deleting -= TriggersOnDeleting; 765 | 766 | private void TriggersOnDeleting(IBeforeChangeEntry beforeChangeEntry) { 767 | Assert.True(guid != null); 768 | Assert.True(guid2 != null); 769 | Assert.True(beforeChangeEntry.Original.Value == guid); 770 | Assert.True(beforeChangeEntry.Entity.Value == guid2); 771 | } 772 | 773 | private String guid; 774 | private String guid2; 775 | 776 | [Fact] 777 | public void Sync() => DoATest(() => { 778 | guid = Guid.NewGuid().ToString(); 779 | guid2 = Guid.NewGuid().ToString(); 780 | var thing = new Thing { Value = guid }; 781 | Context.Things.Add(thing); 782 | Context.SaveChanges(); 783 | thing.Value = guid2; 784 | Context.Things.Remove(thing); 785 | Context.SaveChanges(); 786 | }); 787 | 788 | [Fact] 789 | public Task Async() => DoATestAsync(async () => { 790 | guid = Guid.NewGuid().ToString(); 791 | guid2 = Guid.NewGuid().ToString(); 792 | var thing = new Thing { Value = guid }; 793 | Context.Things.Add(thing); 794 | await Context.SaveChangesAsync().ConfigureAwait(false); 795 | thing.Value = guid2; 796 | Context.Things.Remove(thing); 797 | await Context.SaveChangesAsync().ConfigureAwait(false); 798 | }); 799 | } 800 | 801 | public class Covariance : TestBase { 802 | protected override void Setup() { 803 | Action> triggersOnInserting = entry => { }; // These two will break at runtime without the `CoContra` event-backing-field 804 | Action> triggersOnInserting2 = entry => { }; 805 | Triggers.Inserting += triggersOnInserting; 806 | Triggers.Inserting += triggersOnInserting2; 807 | 808 | Action> triggersOnInserting3 = entry => { }; 809 | Action> triggersOnInserting4 = entry => { }; 810 | Action> triggersOnInserting5 = entry => { }; 811 | Triggers.Inserting += triggersOnInserting3; 812 | Triggers.Inserting += triggersOnInserting5; 813 | Triggers.Inserting += triggersOnInserting5; 814 | Triggers.Inserting += triggersOnInserting4; 815 | 816 | Triggers.Inserting += ObjectInserting6; 817 | Triggers.Inserting += ObjectInserting5; 818 | Triggers.Inserting += ObjectInserting4; 819 | Triggers.Inserting += ObjectInserting3; 820 | Triggers.Inserting += ObjectInserting2; 821 | Triggers.Inserting += ObjectInserting; 822 | Triggers.Inserting += ThingInserting6; 823 | Triggers.Inserting += ThingInserting5; 824 | Triggers.Inserting += ThingInserting4; 825 | Triggers.Inserting += ThingInserting3; 826 | Triggers.Inserting += ThingInserting2; 827 | Triggers.Inserting += ThingInserting; 828 | } 829 | 830 | protected override void Teardown() { 831 | Triggers.Inserting += ThingInserting; 832 | Triggers.Inserting += ThingInserting2; 833 | Triggers.Inserting += ThingInserting3; 834 | Triggers.Inserting += ThingInserting4; 835 | Triggers.Inserting += ThingInserting5; 836 | Triggers.Inserting += ThingInserting6; 837 | Triggers.Inserting += ObjectInserting; 838 | Triggers.Inserting += ObjectInserting2; 839 | Triggers.Inserting += ObjectInserting3; 840 | Triggers.Inserting += ObjectInserting4; 841 | Triggers.Inserting += ObjectInserting5; 842 | Triggers.Inserting += ObjectInserting6; 843 | } 844 | 845 | private Boolean thingInsertingRan; 846 | private Boolean thingInserting2Ran; 847 | private Boolean thingInserting3Ran; 848 | private Boolean objectInsertingRan; 849 | private Boolean objectInserting2Ran; 850 | private Boolean objectInserting3Ran; 851 | 852 | private void ThingInserting(IBeforeEntry entry) => thingInsertingRan = true; 853 | private void ThingInserting2(IBeforeEntry entry) => thingInserting2Ran = true; 854 | private void ThingInserting3(IBeforeEntry entry) => thingInserting3Ran = true; 855 | private void ThingInserting4(IEntry entry) {} 856 | private void ThingInserting5(IEntry entry) {} 857 | private void ThingInserting6(IEntry entry) {} 858 | private void ObjectInserting(IBeforeEntry beforeEntry) => objectInsertingRan = true; 859 | private void ObjectInserting2(IBeforeEntry beforeEntry) => objectInserting2Ran = true; 860 | private void ObjectInserting3(IBeforeEntry beforeEntry) => objectInserting3Ran = true; 861 | private void ObjectInserting4(IEntry beforeEntry) {} 862 | private void ObjectInserting5(IEntry beforeEntry) {} 863 | private void ObjectInserting6(IEntry beforeEntry) {} 864 | 865 | [Fact] 866 | public void Sync() => DoATest(() => { 867 | var guid = Guid.NewGuid().ToString(); 868 | var thing = new Thing { Value = guid }; 869 | Context.Things.Add(thing); 870 | Assert.False(thingInsertingRan); 871 | Assert.False(thingInserting2Ran); 872 | Assert.False(thingInserting3Ran); 873 | Assert.False(objectInsertingRan); 874 | Assert.False(objectInserting2Ran); 875 | Assert.False(objectInserting3Ran); 876 | Context.SaveChanges(); 877 | Assert.True(Context.Things.SingleOrDefault(x => x.Value == guid) != null); 878 | Assert.True(thingInsertingRan); 879 | Assert.True(thingInserting2Ran); 880 | Assert.True(thingInserting3Ran); 881 | Assert.True(objectInsertingRan); 882 | Assert.True(objectInserting2Ran); 883 | Assert.True(objectInserting3Ran); 884 | }); 885 | 886 | [Fact] 887 | public Task Async() => DoATestAsync(async () => { 888 | var guid = Guid.NewGuid().ToString(); 889 | var thing = new Thing { Value = guid }; 890 | Context.Things.Add(thing); 891 | Assert.False(thingInsertingRan); 892 | Assert.False(thingInserting2Ran); 893 | Assert.False(thingInserting3Ran); 894 | Assert.False(objectInsertingRan); 895 | Assert.False(objectInserting2Ran); 896 | Assert.False(objectInserting3Ran); 897 | await Context.SaveChangesAsync().ConfigureAwait(false); 898 | Assert.True(await Context.Things.SingleOrDefaultAsync(x => x.Value == guid).ConfigureAwait(false) != null); 899 | Assert.True(thingInsertingRan); 900 | Assert.True(thingInserting2Ran); 901 | Assert.True(thingInserting3Ran); 902 | Assert.True(objectInsertingRan); 903 | Assert.True(objectInserting2Ran); 904 | Assert.True(objectInserting3Ran); 905 | }); 906 | } 907 | 908 | // public class MultiplyDeclaredInterfaces : TestBase { 909 | // protected override void Setup() {} 910 | // protected override void Teardown() { } 911 | 912 | // [Fact] 913 | // public void Sync() => DoATest(() => { 914 | // }); 915 | 916 | // [Fact] 917 | // public Task Async() => DoATestAsync(async () => { 918 | // }); 919 | // } 920 | 921 | // public interface ICreature { } 922 | // public class Creature : ICreature { 923 | // [Key] 924 | // public virtual Int64 Id { get; protected set; } 925 | // public virtual String Name { get; set; } 926 | // } 927 | // public class Dog : Creature, ICreature { } 928 | } --------------------------------------------------------------------------------