├── .gitattributes ├── .gitignore ├── DataLayer ├── DataLayer.csproj └── ExampleDbContext.cs ├── EntityClasses ├── Book.cs ├── DomainEvents │ ├── AllocateProductEvent.cs │ ├── DeDupEvent.cs │ ├── NewBookEvent.cs │ ├── NewBookEventButBeforeSave.cs │ ├── OrderCreatedEvent.cs │ ├── OrderReadyToDispatchEvent.cs │ └── TaxRateChangedEvent.cs ├── EntityClasses.csproj ├── LineItem.cs ├── Order.cs ├── ProductStock.cs ├── Review.cs ├── SupportClasses │ ├── BasketItemDto.cs │ └── ICreatedUpdated.cs └── TaxRate.cs ├── GenericEventRunner.DomainParts ├── EntityEventsBase.cs ├── EventToSend.cs ├── GenericEventRunner.DomainParts.csproj ├── GenericEventsRunnerDomainNuGetIcon128.png ├── IEntityEvent.cs ├── IEntityWithAfterSaveEvents.cs ├── IEntityWithBeforeSaveEvents.cs ├── IEntityWithDuringSaveEvents.cs ├── MakeDuringEventRunBeforeSaveChangesAttribute.cs └── RemoveDuplicateEventsAttribute.cs ├── GenericEventRunner.sln ├── GenericEventRunner ├── ForDbContext │ ├── DbContextWithEvents.cs │ ├── GenericEventRunnerStatusException.cs │ ├── IEventsRunner.cs │ └── IStatusFromLastSaveChanges.cs ├── ForHandlers │ ├── EventHandlerConfigAttribute.cs │ ├── EventsRunner.cs │ ├── GenericEventRunnerException.cs │ ├── IAfterSaveEventHandler.cs │ ├── IAfterSaveEventHandlerAsync.cs │ ├── IBeforeSaveEventHandler.cs │ ├── IBeforeSaveEventHandlerAsync.cs │ ├── IDuringSaveEventHandler.cs │ ├── IDuringSaveEventHandlerAsync.cs │ └── Internal │ │ ├── AfterEntityAndEvent.cs │ │ ├── AfterSaveEventHandler.cs │ │ ├── AfterSaveEventHandlerAsync.cs │ │ ├── AsyncHelper.cs │ │ ├── BeforeSaveEventHandler.cs │ │ ├── BeforeSaveEventHandlerAsync.cs │ │ ├── DuringEventsExtensions.cs │ │ ├── DuringSaveEventHandler.cs │ │ ├── DuringSaveEventHandlerAsync.cs │ │ ├── EntityAndEvent.cs │ │ ├── FindHandlers.cs │ │ ├── FindRunHandlers.cs │ │ ├── HandlerAndWrapper.cs │ │ ├── RunEachTypeOfEvents.cs │ │ └── ValueTaskSyncCheckers.cs ├── ForSetup │ ├── GenericEventRunnerConfig.cs │ ├── IGenericEventRunnerConfig.cs │ ├── Internal │ │ └── RegisterIfNotThere.cs │ ├── RegisterGenericEventRunnerExtensions.cs │ ├── ServiceDescriptorIncludeLifeTimeCompare.cs │ └── ServiceDescriptorNoLifeTimeCompare.cs ├── GenericEventRunner.csproj └── GenericEventsRunnerNuGetIcon128.png ├── GenericEventRunnerTypesOfEvents.png ├── GenericEventsRunnerDomainNuGetIcon128.png ├── GenericEventsRunnerNuGetIcon128.png ├── Infrastructure ├── AfterEventHandlers │ ├── DeDupAfterEventHandler.cs │ └── OrderReadyToDispatchAfterHandler.cs ├── BeforeEventHandlers │ ├── AllocateProductHander.cs │ ├── DeDupBeforeEventHandler.cs │ ├── Internal │ │ └── TaxRateLookup.cs │ ├── OrderCreatedHandler.cs │ ├── OrderDispatchedBeforeHandler.cs │ └── TaxRateChangedHandler.cs ├── DuringEventHandlers │ ├── DeDupDuringEventHandler.cs │ ├── NewBookDuringButBeforeSaveEventHandler.cs │ ├── NewBookDuringButBeforeSaveEventHandlerAsync.cs │ ├── NewBookDuringEventHandler.cs │ └── NewBookDuringEventHandlerAsync.cs └── Infrastructure.csproj ├── LICENSE ├── OnlyAfterHandlers ├── AfterHandler.cs └── OnlyAfterHandlers.csproj ├── OnlyBeforeHandlers ├── BeforeHandler.cs └── OnlyBeforeHandlers.csproj ├── OnlyDuringHandlers ├── DuringHandler.cs └── OnlyDuringHandlers.csproj ├── README.md ├── ReleaseNotes.md └── Test ├── EfHelpers ├── SeedExtensions.cs ├── SetupToTestEvents.cs └── SqlServerWithExecution.cs ├── EventsAndHandlers ├── AfterHandlerDoNothing.cs ├── AfterHandlerDoNothingAsync.cs ├── AfterHandlerThrowsException.cs ├── BeforeHandlerCircularEvent.cs ├── BeforeHandlerDoNothing.cs ├── BeforeHandlerDoNothingAsync.cs ├── BeforeHandlerReturnsErrorStatus.cs ├── BeforeHandlerThrowsException.cs ├── BeforeHandlerThrowsExceptionWithAttribute.cs ├── DuringHandlerDoNothing.cs ├── DuringHandlerReturnsErrorStatus.cs ├── DuringHandlerReturnsErrorStatusAsync.cs ├── DuringHandlerThrowsException.cs ├── DuringHandlerThrowsExceptionAsync.cs ├── DuringPreHandlerReturnsErrorStatus.cs ├── DuringPreHandlerReturnsErrorStatusAsync.cs ├── DuringPreHandlerThrowsException.cs ├── DuringPreHandlerThrowsExceptionAsync.cs ├── EventAfterHandlerThrowsException.cs ├── EventCircularEvent.cs ├── EventDoNothing.cs ├── EventTestAfterExceptionHandler.cs ├── EventTestBeforeExceptionHandler.cs ├── EventTestBeforeReturnError.cs ├── EventTestDuringExceptionHandler.cs ├── EventTestDuringPreExceptionHandler.cs ├── EventTestDuringPreReturnError.cs ├── EventTestDuringReturnError.cs ├── EventTestExceptionHandlerWithAttribute.cs └── EventWithNoHandler.cs ├── Test.csproj ├── UnitTests ├── DataLayerTests │ └── TestExampleDbContext.cs └── InfrastructureTests │ ├── TestAsyncEventHandlers.cs │ ├── TestDeDupEvents.cs │ ├── TestEventSaveChangesAsync.cs │ ├── TestEventSaveChangesExceptionHandler.cs │ ├── TestEventSaveChangesSync.cs │ ├── TestEventSaveChangesTransactionsAsync.cs │ ├── TestEventSaveChangesTransactionsSync.cs │ ├── TestEventSaveChangesWithStatusAsync.cs │ ├── TestEventSaveChangesWithStatusSync.cs │ ├── TestRegisterEventHandlers.cs │ └── TestRegisterEventHandlersIfAlreadyRegistered.cs └── appsettings.json /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Dd]ebugPublic/ 19 | [Rr]elease/ 20 | [Rr]eleases/ 21 | x64/ 22 | x86/ 23 | [Aa][Rr][Mm]/ 24 | [Aa][Rr][Mm]64/ 25 | bld/ 26 | [Bb]in/ 27 | [Oo]bj/ 28 | [Ll]og/ 29 | 30 | # Visual Studio 2015/2017 cache/options directory 31 | .vs/ 32 | # Uncomment if you have tasks that create the project's static files in wwwroot 33 | #wwwroot/ 34 | 35 | # Visual Studio 2017 auto generated files 36 | Generated\ Files/ 37 | 38 | # MSTest test Results 39 | [Tt]est[Rr]esult*/ 40 | [Bb]uild[Ll]og.* 41 | 42 | # NUNIT 43 | *.VisualState.xml 44 | TestResult.xml 45 | 46 | # Build Results of an ATL Project 47 | [Dd]ebugPS/ 48 | [Rr]eleasePS/ 49 | dlldata.c 50 | 51 | # Benchmark Results 52 | BenchmarkDotNet.Artifacts/ 53 | 54 | # .NET Core 55 | project.lock.json 56 | project.fragment.lock.json 57 | artifacts/ 58 | 59 | # StyleCop 60 | StyleCopReport.xml 61 | 62 | # Files built by Visual Studio 63 | *_i.c 64 | *_p.c 65 | *_h.h 66 | *.ilk 67 | *.meta 68 | *.obj 69 | *.iobj 70 | *.pch 71 | *.pdb 72 | *.ipdb 73 | *.pgc 74 | *.pgd 75 | *.rsp 76 | *.sbr 77 | *.tlb 78 | *.tli 79 | *.tlh 80 | *.tmp 81 | *.tmp_proj 82 | *_wpftmp.csproj 83 | *.log 84 | *.vspscc 85 | *.vssscc 86 | .builds 87 | *.pidb 88 | *.svclog 89 | *.scc 90 | 91 | # Chutzpah Test files 92 | _Chutzpah* 93 | 94 | # Visual C++ cache files 95 | ipch/ 96 | *.aps 97 | *.ncb 98 | *.opendb 99 | *.opensdf 100 | *.sdf 101 | *.cachefile 102 | *.VC.db 103 | *.VC.VC.opendb 104 | 105 | # Visual Studio profiler 106 | *.psess 107 | *.vsp 108 | *.vspx 109 | *.sap 110 | 111 | # Visual Studio Trace Files 112 | *.e2e 113 | 114 | # TFS 2012 Local Workspace 115 | $tf/ 116 | 117 | # Guidance Automation Toolkit 118 | *.gpState 119 | 120 | # ReSharper is a .NET coding add-in 121 | _ReSharper*/ 122 | *.[Rr]e[Ss]harper 123 | *.DotSettings.user 124 | 125 | # JustCode is a .NET coding add-in 126 | .JustCode 127 | 128 | # TeamCity is a build add-in 129 | _TeamCity* 130 | 131 | # DotCover is a Code Coverage Tool 132 | *.dotCover 133 | 134 | # AxoCover is a Code Coverage Tool 135 | .axoCover/* 136 | !.axoCover/settings.json 137 | 138 | # Visual Studio code coverage results 139 | *.coverage 140 | *.coveragexml 141 | 142 | # NCrunch 143 | _NCrunch_* 144 | .*crunch*.local.xml 145 | nCrunchTemp_* 146 | 147 | # MightyMoose 148 | *.mm.* 149 | AutoTest.Net/ 150 | 151 | # Web workbench (sass) 152 | .sass-cache/ 153 | 154 | # Installshield output folder 155 | [Ee]xpress/ 156 | 157 | # DocProject is a documentation generator add-in 158 | DocProject/buildhelp/ 159 | DocProject/Help/*.HxT 160 | DocProject/Help/*.HxC 161 | DocProject/Help/*.hhc 162 | DocProject/Help/*.hhk 163 | DocProject/Help/*.hhp 164 | DocProject/Help/Html2 165 | DocProject/Help/html 166 | 167 | # Click-Once directory 168 | publish/ 169 | 170 | # Publish Web Output 171 | *.[Pp]ublish.xml 172 | *.azurePubxml 173 | # Note: Comment the next line if you want to checkin your web deploy settings, 174 | # but database connection strings (with potential passwords) will be unencrypted 175 | *.pubxml 176 | *.publishproj 177 | 178 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 179 | # checkin your Azure Web App publish settings, but sensitive information contained 180 | # in these scripts will be unencrypted 181 | PublishScripts/ 182 | 183 | # NuGet Packages 184 | *.nupkg 185 | # The packages folder can be ignored because of Package Restore 186 | **/[Pp]ackages/* 187 | # except build/, which is used as an MSBuild target. 188 | !**/[Pp]ackages/build/ 189 | # Uncomment if necessary however generally it will be regenerated when needed 190 | #!**/[Pp]ackages/repositories.config 191 | # NuGet v3's project.json files produces more ignorable files 192 | *.nuget.props 193 | *.nuget.targets 194 | 195 | # Microsoft Azure Build Output 196 | csx/ 197 | *.build.csdef 198 | 199 | # Microsoft Azure Emulator 200 | ecf/ 201 | rcf/ 202 | 203 | # Windows Store app package directories and files 204 | AppPackages/ 205 | BundleArtifacts/ 206 | Package.StoreAssociation.xml 207 | _pkginfo.txt 208 | *.appx 209 | 210 | # Visual Studio cache files 211 | # files ending in .cache can be ignored 212 | *.[Cc]ache 213 | # but keep track of directories ending in .cache 214 | !?*.[Cc]ache/ 215 | 216 | # Others 217 | ClientBin/ 218 | ~$* 219 | *~ 220 | *.dbmdl 221 | *.dbproj.schemaview 222 | *.jfm 223 | *.pfx 224 | *.publishsettings 225 | orleans.codegen.cs 226 | 227 | # Including strong name files can present a security risk 228 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 229 | #*.snk 230 | 231 | # Since there are multiple workflows, uncomment next line to ignore bower_components 232 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 233 | #bower_components/ 234 | 235 | # RIA/Silverlight projects 236 | Generated_Code/ 237 | 238 | # Backup & report files from converting an old project file 239 | # to a newer Visual Studio version. Backup files are not needed, 240 | # because we have git ;-) 241 | _UpgradeReport_Files/ 242 | Backup*/ 243 | UpgradeLog*.XML 244 | UpgradeLog*.htm 245 | ServiceFabricBackup/ 246 | *.rptproj.bak 247 | 248 | # SQL Server files 249 | *.mdf 250 | *.ldf 251 | *.ndf 252 | 253 | # Business Intelligence projects 254 | *.rdl.data 255 | *.bim.layout 256 | *.bim_*.settings 257 | *.rptproj.rsuser 258 | *- Backup*.rdl 259 | 260 | # Microsoft Fakes 261 | FakesAssemblies/ 262 | 263 | # GhostDoc plugin setting file 264 | *.GhostDoc.xml 265 | 266 | # Node.js Tools for Visual Studio 267 | .ntvs_analysis.dat 268 | node_modules/ 269 | 270 | # Visual Studio 6 build log 271 | *.plg 272 | 273 | # Visual Studio 6 workspace options file 274 | *.opt 275 | 276 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 277 | *.vbw 278 | 279 | # Visual Studio LightSwitch build output 280 | **/*.HTMLClient/GeneratedArtifacts 281 | **/*.DesktopClient/GeneratedArtifacts 282 | **/*.DesktopClient/ModelManifest.xml 283 | **/*.Server/GeneratedArtifacts 284 | **/*.Server/ModelManifest.xml 285 | _Pvt_Extensions 286 | 287 | # Paket dependency manager 288 | .paket/paket.exe 289 | paket-files/ 290 | 291 | # FAKE - F# Make 292 | .fake/ 293 | 294 | # JetBrains Rider 295 | .idea/ 296 | *.sln.iml 297 | 298 | # CodeRush personal settings 299 | .cr/personal 300 | 301 | # Python Tools for Visual Studio (PTVS) 302 | __pycache__/ 303 | *.pyc 304 | 305 | # Cake - Uncomment if you are using it 306 | # tools/** 307 | # !tools/packages.config 308 | 309 | # Tabs Studio 310 | *.tss 311 | 312 | # Telerik's JustMock configuration file 313 | *.jmconfig 314 | 315 | # BizTalk build output 316 | *.btp.cs 317 | *.btm.cs 318 | *.odx.cs 319 | *.xsd.cs 320 | 321 | # OpenCover UI analysis results 322 | OpenCover/ 323 | 324 | # Azure Stream Analytics local run output 325 | ASALocalRun/ 326 | 327 | # MSBuild Binary and Structured Log 328 | *.binlog 329 | 330 | # NVidia Nsight GPU debugger configuration file 331 | *.nvuser 332 | 333 | # MFractors (Xamarin productivity tool) working folder 334 | .mfractor/ 335 | 336 | # Local History for Visual Studio 337 | .localhistory/ 338 | 339 | # BeatPulse healthcheck temp database 340 | healthchecksdb -------------------------------------------------------------------------------- /DataLayer/DataLayer.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /DataLayer/ExampleDbContext.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using EntityClasses; 5 | using GenericEventRunner.ForDbContext; 6 | using Microsoft.EntityFrameworkCore; 7 | 8 | namespace DataLayer 9 | { 10 | public class ExampleDbContext : DbContextWithEvents 11 | { 12 | public ExampleDbContext(DbContextOptions options, 13 | IEventsRunner eventRunner = null) 14 | : base(options, eventRunner) 15 | { 16 | } 17 | 18 | public DbSet Orders { get; set; } 19 | public DbSet LineItems { get; set; } 20 | public DbSet ProductStocks { get; set; } 21 | public DbSet TaxRates { get; set; } 22 | public DbSet Books { get; set; } 23 | 24 | protected override void OnModelCreating(ModelBuilder modelBuilder) 25 | { 26 | modelBuilder.Entity().HasKey(x => x.ProductName); 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /EntityClasses/Book.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using EntityClasses.DomainEvents; 8 | using EntityClasses.SupportClasses; 9 | using GenericEventRunner.DomainParts; 10 | using Microsoft.EntityFrameworkCore; 11 | using Microsoft.EntityFrameworkCore.ChangeTracking; 12 | 13 | namespace EntityClasses 14 | { 15 | public class Book : EntityEventsBase, ICreatedUpdated 16 | { 17 | 18 | private HashSet _reviews; 19 | 20 | public int BookId { get; private set; } 21 | public string Title { get; private set; } 22 | 23 | public DateTime WhenCreatedUtc { get; private set; } 24 | public DateTime LastUpdatedUtc { get; private set; } 25 | public void LogChange(bool added, EntityEntry entry) 26 | { 27 | var timeNow = DateTime.UtcNow; 28 | LastUpdatedUtc = timeNow; 29 | if (added) 30 | { 31 | WhenCreatedUtc = timeNow; 32 | } 33 | else 34 | { 35 | entry.Property(nameof(ICreatedUpdated.LastUpdatedUtc)) 36 | .IsModified = true; 37 | } 38 | } 39 | 40 | public IReadOnlyCollection Reviews => _reviews?.ToList(); 41 | 42 | private Book(){} 43 | 44 | public static Book CreateBookWithEvent(string title) 45 | { 46 | var result = new Book 47 | { 48 | Title = title, 49 | _reviews = new HashSet() 50 | }; 51 | result.AddEvent(new NewBookEvent(), EventToSend.DuringSave); 52 | return result; 53 | } 54 | 55 | public void ChangeTitle(string newTitle) 56 | { 57 | Title = newTitle; 58 | } 59 | 60 | public void AddReview(int numStars, string comment, string voterName) 61 | { 62 | if (_reviews == null) 63 | throw new InvalidOperationException("The Reviews collection must be loaded before calling this method"); 64 | _reviews.Add(new Review(numStars, comment, voterName)); 65 | } 66 | 67 | 68 | //This works with the GenericServices' IncludeThen Attribute to pre-load the Reviews collection 69 | public void RemoveReview(int reviewId) 70 | { 71 | if (_reviews == null) 72 | throw new InvalidOperationException("The Reviews collection must be loaded before calling this method"); 73 | var localReview = _reviews.SingleOrDefault(x => x.ReviewId == reviewId); 74 | if (localReview == null) 75 | throw new InvalidOperationException("The review with that key was not found in the book's Reviews."); 76 | _reviews.Remove(localReview); 77 | } 78 | } 79 | } -------------------------------------------------------------------------------- /EntityClasses/DomainEvents/AllocateProductEvent.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using GenericEventRunner.DomainParts; 6 | 7 | namespace EntityClasses.DomainEvents 8 | { 9 | public class AllocateProductEvent : IEntityEvent 10 | { 11 | public AllocateProductEvent(string productName, int numOrdered) 12 | { 13 | ProductName = productName; 14 | NumOrdered = numOrdered; 15 | } 16 | 17 | public string ProductName { get; } 18 | public int NumOrdered { get; } 19 | } 20 | } -------------------------------------------------------------------------------- /EntityClasses/DomainEvents/DeDupEvent.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using GenericEventRunner.DomainParts; 6 | 7 | namespace EntityClasses.DomainEvents 8 | { 9 | [RemoveDuplicateEvents] 10 | public class DeDupEvent : IEntityEvent 11 | { 12 | public DeDupEvent(Action actionToCall) 13 | { 14 | ActionToCall = actionToCall; 15 | } 16 | 17 | public Action ActionToCall { get; } 18 | } 19 | } -------------------------------------------------------------------------------- /EntityClasses/DomainEvents/NewBookEvent.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using GenericEventRunner.DomainParts; 5 | 6 | namespace EntityClasses.DomainEvents 7 | { 8 | public class NewBookEvent : IEntityEvent 9 | { 10 | } 11 | } -------------------------------------------------------------------------------- /EntityClasses/DomainEvents/NewBookEventButBeforeSave.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using GenericEventRunner.DomainParts; 5 | 6 | namespace EntityClasses.DomainEvents 7 | { 8 | [MakeDuringEventRunBeforeSaveChanges] 9 | public class NewBookEventButBeforeSave : IEntityEvent 10 | { 11 | } 12 | } -------------------------------------------------------------------------------- /EntityClasses/DomainEvents/OrderCreatedEvent.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using GenericEventRunner.DomainParts; 6 | 7 | namespace EntityClasses.DomainEvents 8 | { 9 | public class OrderCreatedEvent : IEntityEvent 10 | { 11 | public OrderCreatedEvent(DateTime expectedDispatchDate, Action setTaxRatePercent) 12 | { 13 | ExpectedDispatchDate = expectedDispatchDate; 14 | SetTaxRatePercent = setTaxRatePercent; 15 | } 16 | 17 | public DateTime ExpectedDispatchDate { get; } 18 | 19 | public Action SetTaxRatePercent { get; } 20 | } 21 | } -------------------------------------------------------------------------------- /EntityClasses/DomainEvents/OrderReadyToDispatchEvent.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using GenericEventRunner.DomainParts; 6 | 7 | namespace EntityClasses.DomainEvents 8 | { 9 | public class OrderReadyToDispatchEvent : IEntityEvent 10 | { 11 | public OrderReadyToDispatchEvent(DateTime actualDispatchDate, Action setTaxRatePercent) 12 | { 13 | ActualDispatchDate = actualDispatchDate; 14 | SetTaxRatePercent = setTaxRatePercent; 15 | } 16 | 17 | public DateTime ActualDispatchDate { get; } 18 | 19 | public Action SetTaxRatePercent { get; } 20 | } 21 | } -------------------------------------------------------------------------------- /EntityClasses/DomainEvents/TaxRateChangedEvent.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using GenericEventRunner.DomainParts; 6 | 7 | namespace EntityClasses.DomainEvents 8 | { 9 | public class TaxRateChangedEvent : IEntityEvent 10 | { 11 | public TaxRateChangedEvent(decimal newTaxRate, Action refreshGrandTotalPrice) 12 | { 13 | NewTaxRate = newTaxRate; 14 | RefreshGrandTotalPrice = refreshGrandTotalPrice; 15 | } 16 | 17 | public decimal NewTaxRate { get; } 18 | 19 | public Action RefreshGrandTotalPrice { get; } 20 | } 21 | } -------------------------------------------------------------------------------- /EntityClasses/EntityClasses.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /EntityClasses/LineItem.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | 6 | namespace EntityClasses 7 | { 8 | public class LineItem 9 | { 10 | internal LineItem(int lineNum, string productName, decimal productPrice, int numOrdered) 11 | { 12 | LineNum = lineNum; 13 | ProductName = productName; 14 | ProductPrice = productPrice; 15 | NumOrdered = numOrdered; 16 | } 17 | 18 | 19 | public int LineItemId { get; private set; } 20 | 21 | public int LineNum { get; private set; } 22 | 23 | public string ProductName { get; private set; } 24 | 25 | public decimal ProductPrice { get; private set; } 26 | 27 | public int NumOrdered { get; private set; } 28 | 29 | //------------------------------------------ 30 | //relationships 31 | 32 | public int OrderId { get; private set; } 33 | } 34 | } -------------------------------------------------------------------------------- /EntityClasses/Order.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using EntityClasses.DomainEvents; 8 | using EntityClasses.SupportClasses; 9 | using GenericEventRunner.DomainParts; 10 | 11 | namespace EntityClasses 12 | { 13 | public class Order : EntityEventsBase 14 | { 15 | private HashSet _LineItems; 16 | 17 | public int OrderId { get; private set; } 18 | public string UserId { get; private set; } 19 | 20 | //The date we expect to dispatch the order 21 | public DateTime DispatchDate { get; private set; } 22 | public decimal TotalPriceNoTax { get; private set; } 23 | 24 | //Price and tax 25 | private decimal _taxRatePercent; 26 | public decimal TaxRatePercent 27 | { 28 | get => _taxRatePercent; 29 | private set 30 | { 31 | if (value != _taxRatePercent) 32 | AddEvent(new TaxRateChangedEvent(value, RefreshGrandTotalPrice)); 33 | _taxRatePercent = value; 34 | } 35 | } 36 | 37 | private void RefreshGrandTotalPrice() 38 | { 39 | GrandTotalPrice = TotalPriceNoTax * (1 + TaxRatePercent / 100); 40 | } 41 | 42 | private void SetTaxRatePercent(decimal newValue) 43 | { 44 | TaxRatePercent = newValue; 45 | } 46 | 47 | public decimal GrandTotalPrice { get; private set; } // should be set by RefreshGrandTotalPrice method 48 | 49 | //---------------------------------------------- 50 | //Relationships 51 | 52 | public IEnumerable LineItems => _LineItems.ToList(); 53 | 54 | private Order() { } //For EF Core 55 | 56 | public Order(string userId, DateTime expectedDispatchDate, ICollection orderLines) 57 | { 58 | UserId = userId; 59 | DispatchDate = expectedDispatchDate; 60 | AddEvent(new OrderCreatedEvent(expectedDispatchDate, SetTaxRatePercent)); 61 | 62 | var lineNum = 1; 63 | _LineItems = new HashSet(orderLines 64 | .Select(x => new LineItem(lineNum++, x.ProductName, x.ProductPrice, x.NumOrdered))); 65 | 66 | TotalPriceNoTax = 0; 67 | foreach (var basketItem in orderLines) 68 | { 69 | TotalPriceNoTax += basketItem.ProductPrice * basketItem.NumOrdered; 70 | AddEvent(new AllocateProductEvent(basketItem.ProductName, basketItem.NumOrdered)); 71 | } 72 | } 73 | 74 | public void OrderReadyForDispatch(DateTime newDispatchDate) 75 | { 76 | if (OrderId == 0) 77 | throw new InvalidOperationException("You cannot call this method until the Order is written to the database."); 78 | 79 | DispatchDate = newDispatchDate; 80 | AddEvent(new OrderReadyToDispatchEvent(DispatchDate, SetTaxRatePercent), EventToSend.BeforeAndAfterSave); 81 | } 82 | 83 | } 84 | } -------------------------------------------------------------------------------- /EntityClasses/ProductStock.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.ComponentModel.DataAnnotations; 5 | 6 | namespace EntityClasses 7 | { 8 | public class ProductStock 9 | { 10 | public ProductStock(string productName, int numInStock) 11 | { 12 | ProductName = productName; 13 | NumInStock = numInStock; 14 | NumAllocated = 0; 15 | } 16 | 17 | public string ProductName { get; set; } 18 | 19 | public int NumInStock { get; set; } 20 | 21 | /// 22 | /// This is used for checking the handling of concurrency issues. 23 | /// 24 | [ConcurrencyCheck] 25 | public int NumAllocated { get; set; } 26 | } 27 | } -------------------------------------------------------------------------------- /EntityClasses/Review.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | namespace EntityClasses 5 | { 6 | public class Review 7 | { 8 | private Review() { } 9 | 10 | internal Review(int numStars, string comment, string voterName, int bookId = default) 11 | { 12 | NumStars = numStars; 13 | Comment = comment; 14 | VoterName = voterName; 15 | BookId = bookId; 16 | } 17 | 18 | public int ReviewId { get; private set; } 19 | 20 | public string VoterName { get; private set; } 21 | 22 | public int NumStars { get; private set; } 23 | public string Comment { get; private set; } 24 | 25 | //----------------------------------------- 26 | //Relationships 27 | 28 | public int BookId { get; private set; } 29 | } 30 | } -------------------------------------------------------------------------------- /EntityClasses/SupportClasses/BasketItemDto.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | 6 | namespace EntityClasses.SupportClasses 7 | { 8 | public class BasketItemDto 9 | { 10 | public string ProductName { get; set; } 11 | 12 | public decimal ProductPrice { get; set; } 13 | public int NumOrdered { get; set; } 14 | } 15 | } -------------------------------------------------------------------------------- /EntityClasses/SupportClasses/ICreatedUpdated.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using Microsoft.EntityFrameworkCore.ChangeTracking; 6 | 7 | namespace EntityClasses.SupportClasses 8 | { 9 | public interface ICreatedUpdated 10 | { 11 | DateTime WhenCreatedUtc { get; } 12 | DateTime LastUpdatedUtc { get; } 13 | 14 | void LogChange(bool added, EntityEntry entry); 15 | } 16 | 17 | } -------------------------------------------------------------------------------- /EntityClasses/TaxRate.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using GenericEventRunner.DomainParts; 6 | 7 | namespace EntityClasses 8 | { 9 | public class TaxRate : EntityEventsBase 10 | { 11 | public TaxRate(DateTime effectiveFrom, decimal taxRatePercent) 12 | { 13 | EffectiveFrom = effectiveFrom; 14 | TaxRatePercent = taxRatePercent; 15 | } 16 | 17 | public int TaxRateId { get; set; } 18 | public DateTime EffectiveFrom { get; set; } 19 | public decimal TaxRatePercent { get; set; } 20 | } 21 | } -------------------------------------------------------------------------------- /GenericEventRunner.DomainParts/EntityEventsBase.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | 7 | namespace GenericEventRunner.DomainParts 8 | { 9 | /// 10 | /// This is a class that the EF Core entity classes inherit to add events 11 | /// 12 | public abstract class EntityEventsBase : IEntityWithBeforeSaveEvents, IEntityWithDuringSaveEvents, IEntityWithAfterSaveEvents 13 | { 14 | //Events are NOT stored in the database - they are transitory events 15 | //Events are created within a single DBContext and are cleared every time SaveChanges/SaveChangesAsync is called 16 | 17 | //This holds events that are run before SaveChanges is called 18 | private readonly List _beforeSaveEvents = new List(); 19 | 20 | //This holds events that are run within a transaction containing a call to SaveChanges 21 | private readonly List _duringSaveEvents = new List(); 22 | 23 | //This holds events that are run after SaveChanges finishes successfully 24 | private readonly List _afterSaveChangesEvents = new List(); 25 | 26 | /// 27 | /// This allows an entity to add an event to this class 28 | /// 29 | /// This is the domain event you want to sent 30 | /// This allows you to send the event to either BeforeSave, DuringSave or AfterSave. Default is BeforeSave List 31 | public void AddEvent(IEntityEvent dEvent, EventToSend eventToSend = EventToSend.BeforeSave) 32 | { 33 | if (eventToSend == EventToSend.DuringSave) 34 | _duringSaveEvents.Add(dEvent); 35 | if (eventToSend == EventToSend.BeforeSave || eventToSend == EventToSend.BeforeAndAfterSave) 36 | _beforeSaveEvents.Add(dEvent); 37 | if (eventToSend == EventToSend.AfterSave || eventToSend == EventToSend.BeforeAndAfterSave) 38 | _afterSaveChangesEvents.Add(dEvent); 39 | } 40 | 41 | /// 42 | /// This gets all the events in the BeforeSaveEvents list, and clears that list at the same time 43 | /// 44 | public ICollection GetBeforeSaveEventsThenClear() 45 | { 46 | var eventCopy = _beforeSaveEvents.ToList(); 47 | _beforeSaveEvents.Clear(); 48 | return eventCopy; 49 | } 50 | 51 | /// 52 | /// This returns the events that should be run within a transaction containing a call to SaveChanges 53 | /// 54 | public ICollection GetDuringSaveEvents() 55 | { 56 | return _duringSaveEvents; 57 | } 58 | 59 | /// 60 | /// This clears all the during save events once the code within the transaction has finished 61 | /// 62 | public void ClearDuringSaveEvents() 63 | { 64 | _duringSaveEvents.Clear(); 65 | } 66 | 67 | /// 68 | /// This gets all the events in the AfterSaveEvents list, and clears that list at the same time 69 | /// 70 | public ICollection GetAfterSaveEventsThenClear() 71 | { 72 | var eventCopy = _afterSaveChangesEvents.ToList(); 73 | _afterSaveChangesEvents.Clear(); 74 | return eventCopy; 75 | } 76 | 77 | } 78 | } -------------------------------------------------------------------------------- /GenericEventRunner.DomainParts/EventToSend.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | namespace GenericEventRunner.DomainParts 5 | { 6 | /// 7 | /// This allows you to control which list, the BeforeSave and AfterSave, to add the event to. 8 | /// 9 | public enum EventToSend 10 | { 11 | /// 12 | /// This puts an event into BeforeSaveEvents list 13 | /// 14 | BeforeSave, 15 | /// 16 | /// This puts an event into the DuringSaveEvents list 17 | /// 18 | DuringSave, 19 | /// 20 | /// This puts an event into AfterSaveEvents list 21 | /// 22 | AfterSave, 23 | /// 24 | /// This puts an event into both the BeforeSaveEvents list and the AfterSaveEvents list 25 | /// 26 | BeforeAndAfterSave 27 | 28 | } 29 | } -------------------------------------------------------------------------------- /GenericEventRunner.DomainParts/GenericEventRunner.DomainParts.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | 6 | 7 | 8 | true 9 | 2.2.1 10 | 2.2.1 11 | 2.2.1.0 12 | 2.2.1.0 13 | EfCore.GenericEventRunner.DomainParts 14 | Jon P Smith 15 | Selective Analytics 16 | EfCore.GenericEventRunner.DomainParts 17 | Defines basic interfaces/classes for EfCore.GenericEventRunner when using Clean Code architecture. 18 | Copyright (c) 2020 Jon P Smith 19 | https://github.com/JonPSmith/EfCore.GenericEventRunner/blob/master/LICENSE 20 | https://github.com/JonPSmith/EfCore.GenericEventRunner 21 | https://github.com/JonPSmith/EfCore.GenericEventRunner 22 | GitHub 23 | EfCore.GenericServices, EfCore.GenericEventRunner 24 | 25 | - New Feature: RemoveDuplicateEvents attribute allows you to mark an event so that duplicate events are rolled into one (see wiki for more info) 26 | 27 | https://raw.githubusercontent.com/JonPSmith/EfCore.GenericEventRunner/master/GenericEventsRunnerDomainNuGetIcon128.png 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /GenericEventRunner.DomainParts/GenericEventsRunnerDomainNuGetIcon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JonPSmith/EfCore.GenericEventRunner/e556e175b9d8ea9bf742974b8891a11b37cf5f4a/GenericEventRunner.DomainParts/GenericEventsRunnerDomainNuGetIcon128.png -------------------------------------------------------------------------------- /GenericEventRunner.DomainParts/IEntityEvent.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | namespace GenericEventRunner.DomainParts 5 | { 6 | /// 7 | /// This is an empty interface that events should inherit 8 | /// 9 | public interface IEntityEvent { } 10 | } -------------------------------------------------------------------------------- /GenericEventRunner.DomainParts/IEntityWithAfterSaveEvents.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.Collections.Generic; 5 | 6 | namespace GenericEventRunner.DomainParts 7 | { 8 | /// 9 | /// Add this interface to an entity class to support AfterSaveEvents 10 | /// 11 | public interface IEntityWithAfterSaveEvents 12 | { 13 | /// 14 | /// This gets all the events in the AfterSaveEvents list, and clears that list at the same time 15 | /// 16 | ICollection GetAfterSaveEventsThenClear(); 17 | } 18 | } -------------------------------------------------------------------------------- /GenericEventRunner.DomainParts/IEntityWithBeforeSaveEvents.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.Collections.Generic; 5 | 6 | namespace GenericEventRunner.DomainParts 7 | { 8 | /// 9 | /// Add this interface to an entity class to support BeforeSaveEvents 10 | /// 11 | public interface IEntityWithBeforeSaveEvents 12 | { 13 | /// 14 | /// This gets all the events in the BeforeSaveEvents list, and clears that list at the same time 15 | /// 16 | ICollection GetBeforeSaveEventsThenClear(); 17 | } 18 | } -------------------------------------------------------------------------------- /GenericEventRunner.DomainParts/IEntityWithDuringSaveEvents.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.Collections.Generic; 5 | 6 | namespace GenericEventRunner.DomainParts 7 | { 8 | /// 9 | /// Add this interface to an entity class to support tDuringSaveEvents 10 | /// 11 | public interface IEntityWithDuringSaveEvents 12 | { 13 | /// 14 | /// This returns the events that should be run within a transaction containing a call to SaveChanges 15 | /// 16 | ICollection GetDuringSaveEvents(); 17 | 18 | /// 19 | /// This clears all the during save events once the code within the transaction has finished 20 | /// 21 | void ClearDuringSaveEvents(); 22 | } 23 | } -------------------------------------------------------------------------------- /GenericEventRunner.DomainParts/MakeDuringEventRunBeforeSaveChangesAttribute.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | 6 | namespace GenericEventRunner.DomainParts 7 | { 8 | /// 9 | /// Add this attribute to a During to make the event handler run before SaveChanges 10 | /// 11 | [AttributeUsage(AttributeTargets.Class)] 12 | public class MakeDuringEventRunBeforeSaveChangesAttribute : Attribute 13 | { 14 | } 15 | } -------------------------------------------------------------------------------- /GenericEventRunner.DomainParts/RemoveDuplicateEventsAttribute.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | 6 | namespace GenericEventRunner.DomainParts 7 | { 8 | /// 9 | /// Add this attribute to a and the EventRunner will remove events that a) have the same type, and b) come from the same entity 10 | /// 11 | [AttributeUsage(AttributeTargets.Class)] 12 | public class RemoveDuplicateEventsAttribute : Attribute 13 | { 14 | 15 | } 16 | } -------------------------------------------------------------------------------- /GenericEventRunner.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29424.173 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GenericEventRunner", "GenericEventRunner\GenericEventRunner.csproj", "{4F20200E-71FC-40FB-9E29-571081A50BC2}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EntityClasses", "EntityClasses\EntityClasses.csproj", "{D48D1BDB-6AB3-4A4D-88D6-0477CD9241B9}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DataLayer", "DataLayer\DataLayer.csproj", "{FA7550E5-9D97-46BF-B6E5-3A65C4AD9433}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Infrastructure", "Infrastructure\Infrastructure.csproj", "{6EFA5E3F-2A46-4AC5-B636-A40D9CF40DB9}" 13 | EndProject 14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Test", "Test\Test.csproj", "{2DB798EC-BC1A-496B-82C1-9542E34AA1E8}" 15 | EndProject 16 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{DC1820DB-E4BC-4D89-91E3-12743A3A7C06}" 17 | ProjectSection(SolutionItems) = preProject 18 | GenericEventRunnerTypesOfEvents.png = GenericEventRunnerTypesOfEvents.png 19 | GenericEventsRunnerDomainNuGetIcon128.png = GenericEventsRunnerDomainNuGetIcon128.png 20 | GenericEventsRunnerNuGetIcon128.png = GenericEventsRunnerNuGetIcon128.png 21 | LICENSE = LICENSE 22 | README.md = README.md 23 | ReleaseNotes.md = ReleaseNotes.md 24 | EndProjectSection 25 | EndProject 26 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GenericEventRunner.DomainParts", "GenericEventRunner.DomainParts\GenericEventRunner.DomainParts.csproj", "{B8044A25-8009-41BB-B9AC-45D45DA47B21}" 27 | EndProject 28 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OnlyBeforeHandlers", "OnlyBeforeHandlers\OnlyBeforeHandlers.csproj", "{0824BF78-5361-437E-91BF-E650FDDDF634}" 29 | EndProject 30 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OnlyDuringHandlers", "OnlyDuringHandlers\OnlyDuringHandlers.csproj", "{F8156BEE-0161-4F6A-ADD4-D39F964C6E8D}" 31 | EndProject 32 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OnlyAfterHandlers", "OnlyAfterHandlers\OnlyAfterHandlers.csproj", "{FE2D69FF-5EE4-4986-ADE7-64842056ADA4}" 33 | EndProject 34 | Global 35 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 36 | Debug|Any CPU = Debug|Any CPU 37 | Release|Any CPU = Release|Any CPU 38 | EndGlobalSection 39 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 40 | {4F20200E-71FC-40FB-9E29-571081A50BC2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 41 | {4F20200E-71FC-40FB-9E29-571081A50BC2}.Debug|Any CPU.Build.0 = Debug|Any CPU 42 | {4F20200E-71FC-40FB-9E29-571081A50BC2}.Release|Any CPU.ActiveCfg = Release|Any CPU 43 | {4F20200E-71FC-40FB-9E29-571081A50BC2}.Release|Any CPU.Build.0 = Release|Any CPU 44 | {D48D1BDB-6AB3-4A4D-88D6-0477CD9241B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 45 | {D48D1BDB-6AB3-4A4D-88D6-0477CD9241B9}.Debug|Any CPU.Build.0 = Debug|Any CPU 46 | {D48D1BDB-6AB3-4A4D-88D6-0477CD9241B9}.Release|Any CPU.ActiveCfg = Release|Any CPU 47 | {D48D1BDB-6AB3-4A4D-88D6-0477CD9241B9}.Release|Any CPU.Build.0 = Release|Any CPU 48 | {FA7550E5-9D97-46BF-B6E5-3A65C4AD9433}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 49 | {FA7550E5-9D97-46BF-B6E5-3A65C4AD9433}.Debug|Any CPU.Build.0 = Debug|Any CPU 50 | {FA7550E5-9D97-46BF-B6E5-3A65C4AD9433}.Release|Any CPU.ActiveCfg = Release|Any CPU 51 | {FA7550E5-9D97-46BF-B6E5-3A65C4AD9433}.Release|Any CPU.Build.0 = Release|Any CPU 52 | {6EFA5E3F-2A46-4AC5-B636-A40D9CF40DB9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 53 | {6EFA5E3F-2A46-4AC5-B636-A40D9CF40DB9}.Debug|Any CPU.Build.0 = Debug|Any CPU 54 | {6EFA5E3F-2A46-4AC5-B636-A40D9CF40DB9}.Release|Any CPU.ActiveCfg = Release|Any CPU 55 | {6EFA5E3F-2A46-4AC5-B636-A40D9CF40DB9}.Release|Any CPU.Build.0 = Release|Any CPU 56 | {2DB798EC-BC1A-496B-82C1-9542E34AA1E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 57 | {2DB798EC-BC1A-496B-82C1-9542E34AA1E8}.Debug|Any CPU.Build.0 = Debug|Any CPU 58 | {2DB798EC-BC1A-496B-82C1-9542E34AA1E8}.Release|Any CPU.ActiveCfg = Release|Any CPU 59 | {2DB798EC-BC1A-496B-82C1-9542E34AA1E8}.Release|Any CPU.Build.0 = Release|Any CPU 60 | {B8044A25-8009-41BB-B9AC-45D45DA47B21}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 61 | {B8044A25-8009-41BB-B9AC-45D45DA47B21}.Debug|Any CPU.Build.0 = Debug|Any CPU 62 | {B8044A25-8009-41BB-B9AC-45D45DA47B21}.Release|Any CPU.ActiveCfg = Release|Any CPU 63 | {B8044A25-8009-41BB-B9AC-45D45DA47B21}.Release|Any CPU.Build.0 = Release|Any CPU 64 | {0824BF78-5361-437E-91BF-E650FDDDF634}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 65 | {0824BF78-5361-437E-91BF-E650FDDDF634}.Debug|Any CPU.Build.0 = Debug|Any CPU 66 | {0824BF78-5361-437E-91BF-E650FDDDF634}.Release|Any CPU.ActiveCfg = Release|Any CPU 67 | {0824BF78-5361-437E-91BF-E650FDDDF634}.Release|Any CPU.Build.0 = Release|Any CPU 68 | {F8156BEE-0161-4F6A-ADD4-D39F964C6E8D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 69 | {F8156BEE-0161-4F6A-ADD4-D39F964C6E8D}.Debug|Any CPU.Build.0 = Debug|Any CPU 70 | {F8156BEE-0161-4F6A-ADD4-D39F964C6E8D}.Release|Any CPU.ActiveCfg = Release|Any CPU 71 | {F8156BEE-0161-4F6A-ADD4-D39F964C6E8D}.Release|Any CPU.Build.0 = Release|Any CPU 72 | {FE2D69FF-5EE4-4986-ADE7-64842056ADA4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 73 | {FE2D69FF-5EE4-4986-ADE7-64842056ADA4}.Debug|Any CPU.Build.0 = Debug|Any CPU 74 | {FE2D69FF-5EE4-4986-ADE7-64842056ADA4}.Release|Any CPU.ActiveCfg = Release|Any CPU 75 | {FE2D69FF-5EE4-4986-ADE7-64842056ADA4}.Release|Any CPU.Build.0 = Release|Any CPU 76 | EndGlobalSection 77 | GlobalSection(SolutionProperties) = preSolution 78 | HideSolutionNode = FALSE 79 | EndGlobalSection 80 | GlobalSection(ExtensibilityGlobals) = postSolution 81 | SolutionGuid = {B80DB149-6811-4CA4-87C2-087E9AF5D5BC} 82 | EndGlobalSection 83 | EndGlobal 84 | -------------------------------------------------------------------------------- /GenericEventRunner/ForDbContext/DbContextWithEvents.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Linq; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using GenericEventRunner.DomainParts; 9 | using GenericEventRunner.ForHandlers; 10 | using Microsoft.EntityFrameworkCore; 11 | using StatusGeneric; 12 | 13 | namespace GenericEventRunner.ForDbContext 14 | { 15 | /// 16 | /// If you want to add GenericEventsRunner to your DbContext then inherit this instead of DbContext 17 | /// This overrides the base SaveChanges/SaveChangesAsync to add the event runner before and after the call the base SaveChanges/SaveChangesAsync 18 | /// 19 | /// If you don't like inheriting this class then you can copy this code directly into your own DbContext 20 | /// 21 | /// 22 | public class DbContextWithEvents : DbContext, IStatusFromLastSaveChanges where T : DbContext 23 | { 24 | private readonly IEventsRunner _eventsRunner; 25 | 26 | /// 27 | /// This returns the Status of last SaveChanges/Async and SaveChangesWithStatus/Async 28 | /// NOTE: This is null if no event handler is provided, or if none of the SaveChanges/Async etc. has not been called yet. 29 | /// 30 | public IStatusGeneric StatusFromLastSaveChanges { get; private set; } 31 | 32 | 33 | /// 34 | /// This sets up the DbContext options and adds the eventRunner 35 | /// 36 | /// normal EF Core options for a database 37 | /// The Generic Event Runner - can be null which will turn off domain event handling 38 | protected DbContextWithEvents(DbContextOptions options, IEventsRunner eventsRunner) : base(options) 39 | { 40 | _eventsRunner = eventsRunner; 41 | } 42 | 43 | /// 44 | /// This is a spacial form of SaveChanges that returns an status 45 | /// 46 | /// normal SaveChanges option 47 | /// Status, with a Result that is the number of updates down by SaveChanges 48 | public IStatusGeneric SaveChangesWithStatus(bool acceptAllChangesOnSuccess = true) 49 | { 50 | if (_eventsRunner == null) 51 | throw new GenericEventRunnerException($"The {nameof(SaveChangesWithStatus)} cannot be used unless the event runner is present"); 52 | 53 | StatusFromLastSaveChanges = _eventsRunner.RunEventsBeforeDuringAfterSaveChanges(this, 54 | () => base.SaveChanges(acceptAllChangesOnSuccess)); 55 | 56 | return StatusFromLastSaveChanges; 57 | } 58 | 59 | /// 60 | /// This is a spacial form of SaveChangesAsync that returns an status 61 | /// 62 | /// 63 | /// 64 | /// Status, with a Result that is the number of updates down by SaveChangesAsync 65 | public async Task> SaveChangesWithStatusAsync(bool acceptAllChangesOnSuccess = true, 66 | CancellationToken cancellationToken = default) 67 | { 68 | if (_eventsRunner == null) 69 | throw new GenericEventRunnerException($"The {nameof(SaveChangesWithStatusAsync)} cannot be used unless the event runner is present"); 70 | 71 | StatusFromLastSaveChanges = await _eventsRunner.RunEventsBeforeDuringAfterSaveChangesAsync(this, 72 | () => base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken), cancellationToken).ConfigureAwait(false); 73 | 74 | return StatusFromLastSaveChanges; 75 | } 76 | 77 | //I only have to override these two version of SaveChanges, as the other two versions call these 78 | 79 | /// 80 | /// EF Core's SaveChanges, but with domain event handling added 81 | /// Throws an exception if any of the BeforeSave event handlers return a status with an error in it. 82 | /// 83 | /// 84 | /// number of writes done to the database 85 | public override int SaveChanges(bool acceptAllChangesOnSuccess) 86 | { 87 | if (_eventsRunner == null) 88 | return base.SaveChanges(acceptAllChangesOnSuccess); 89 | 90 | StatusFromLastSaveChanges = SaveChangesWithStatus(acceptAllChangesOnSuccess); 91 | 92 | if (StatusFromLastSaveChanges.IsValid) 93 | return StatusFromLastSaveChanges.Result; 94 | 95 | throw new GenericEventRunnerStatusException(StatusFromLastSaveChanges); 96 | } 97 | 98 | /// 99 | /// EF Core's SaveChanges, but with domain event handling added 100 | /// Throws an exception if any of the BeforeSave event handlers return a status with an error in it. 101 | /// 102 | /// 103 | /// 104 | /// 105 | public override async Task SaveChangesAsync(bool acceptAllChangesOnSuccess, 106 | CancellationToken cancellationToken = default) 107 | { 108 | if (_eventsRunner == null) 109 | return await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken).ConfigureAwait(false); 110 | 111 | StatusFromLastSaveChanges = await SaveChangesWithStatusAsync(acceptAllChangesOnSuccess, cancellationToken) 112 | .ConfigureAwait(false); 113 | 114 | if (StatusFromLastSaveChanges.IsValid) 115 | return StatusFromLastSaveChanges.Result; 116 | 117 | throw new GenericEventRunnerStatusException(StatusFromLastSaveChanges); 118 | } 119 | } 120 | } -------------------------------------------------------------------------------- /GenericEventRunner/ForDbContext/GenericEventRunnerStatusException.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License file in the project root for license information. 3 | 4 | using System; 5 | using StatusGeneric; 6 | 7 | namespace GenericEventRunner.ForDbContext 8 | { 9 | /// 10 | /// This exception is thrown in the overridden SaveChanges/Async method if any of the BeforeSave handlers return errors. 11 | /// 12 | public class GenericEventRunnerStatusException : Exception 13 | { 14 | /// 15 | /// This 16 | /// 17 | /// The status returned from the BeforeSave event handlers 18 | public GenericEventRunnerStatusException(IStatusGeneric status) 19 | : base($"{status.Message}{Environment.NewLine}{status.GetAllErrors()}") 20 | { 21 | } 22 | 23 | } 24 | } -------------------------------------------------------------------------------- /GenericEventRunner/ForDbContext/IEventsRunner.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using Microsoft.EntityFrameworkCore; 8 | using StatusGeneric; 9 | 10 | namespace GenericEventRunner.ForDbContext 11 | { 12 | /// 13 | /// This is the interface for the Events Runner that is in the DbContext 14 | /// 15 | public interface IEventsRunner 16 | { 17 | /// 18 | /// This Handles the running of the BeforeSave Event Handlers 19 | /// 20 | /// 21 | /// This calls the base SaveChanges. 22 | /// 23 | IStatusGeneric RunEventsBeforeDuringAfterSaveChanges(DbContext context, 24 | Func callBaseSaveChanges); 25 | 26 | /// 27 | /// This Handles the running of the BeforeSave Event Handlers 28 | /// 29 | /// 30 | /// This calls the base SaveChangesAsync. 31 | /// 32 | /// 33 | Task> RunEventsBeforeDuringAfterSaveChangesAsync(DbContext context, 34 | Func> callBaseSaveChangesAsync, CancellationToken cancellationToken); 35 | } 36 | } -------------------------------------------------------------------------------- /GenericEventRunner/ForDbContext/IStatusFromLastSaveChanges.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License file in the project root for license information. 3 | 4 | using StatusGeneric; 5 | 6 | namespace GenericEventRunner.ForDbContext 7 | { 8 | /// 9 | /// Interface to access Status filled in by EventsRunner 10 | /// 11 | public interface IStatusFromLastSaveChanges 12 | { 13 | /// 14 | /// This returns the Status of last SaveChanges/Async and SaveChangesWithStatus/Async done by the GenericEventRunner 15 | /// Useful if you are capturing the GenericEventRunnerStatusException and want to get the Status that goes with it. 16 | /// NOTE: This is null if no event handler is provided, or SaveChanges/Async etc. have not been called yet. 17 | /// 18 | IStatusGeneric StatusFromLastSaveChanges { get; } 19 | } 20 | } -------------------------------------------------------------------------------- /GenericEventRunner/ForHandlers/EventHandlerConfigAttribute.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using Microsoft.Extensions.DependencyInjection; 6 | 7 | namespace GenericEventRunner.ForHandlers 8 | { 9 | /// 10 | /// TYou can add this attribute to a event handler to override some of the default or configuration settings 11 | /// 12 | [AttributeUsage(AttributeTargets.Class)] 13 | public class EventHandlerConfigAttribute : Attribute 14 | { 15 | /// 16 | /// This allows you to alter some of the aspects of a handler 17 | /// 18 | /// This controls the lifetime of a handler when registered in the DI. Default = Transient 19 | public EventHandlerConfigAttribute(ServiceLifetime handlerLifetime = ServiceLifetime.Transient) 20 | { 21 | HandlerLifetime = handlerLifetime; 22 | } 23 | 24 | /// 25 | /// This holds the Lifetime of the class when created by via DI 26 | /// 27 | public ServiceLifetime HandlerLifetime { get; } 28 | 29 | } 30 | } -------------------------------------------------------------------------------- /GenericEventRunner/ForHandlers/GenericEventRunnerException.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using GenericEventRunner.DomainParts; 6 | 7 | namespace GenericEventRunner.ForHandlers 8 | { 9 | /// 10 | /// This is used to report any problems in the GenericEventRunner 11 | /// 12 | public class GenericEventRunnerException : Exception 13 | { 14 | /// 15 | /// This creates an exception just with a message 16 | /// 17 | /// 18 | public GenericEventRunnerException(string message) 19 | : base(message) 20 | { 21 | } 22 | 23 | /// 24 | /// This allows you to create an exception with the callingEntity and domainEvent type names 25 | /// 26 | /// 27 | /// 28 | /// 29 | public GenericEventRunnerException(string message, object callingEntity, IEntityEvent entityEvent) 30 | : base(message) 31 | { 32 | Data.Add("CallingEntityType", callingEntity.GetType().FullName); 33 | Data.Add("DomainEventType", entityEvent.GetType().FullName); 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /GenericEventRunner/ForHandlers/IAfterSaveEventHandler.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using GenericEventRunner.DomainParts; 5 | 6 | namespace GenericEventRunner.ForHandlers 7 | { 8 | /// 9 | /// Place this on any event handler that should be called after SaveChanges has updated the database 10 | /// 11 | /// This should be the domain event that this handler is looking for 12 | public interface IAfterSaveEventHandler where T : IEntityEvent 13 | { 14 | /// 15 | /// This is the method you must define to produce a AfterSave event handler 16 | /// 17 | /// 18 | /// 19 | void Handle(object callingEntity, T domainEvent); 20 | } 21 | } -------------------------------------------------------------------------------- /GenericEventRunner/ForHandlers/IAfterSaveEventHandlerAsync.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.Threading.Tasks; 5 | using GenericEventRunner.DomainParts; 6 | 7 | namespace GenericEventRunner.ForHandlers 8 | { 9 | /// 10 | /// Place this on any async event handler that should be called after SaveChanges has updated the database 11 | /// 12 | /// This should be the domain event that this handler is looking for 13 | public interface IAfterSaveEventHandlerAsync where T : IEntityEvent 14 | { 15 | /// 16 | /// This is the method you must define to produce a AfterSave event handler 17 | /// 18 | /// 19 | /// 20 | Task HandleAsync(object callingEntity, T domainEvent); 21 | } 22 | } -------------------------------------------------------------------------------- /GenericEventRunner/ForHandlers/IBeforeSaveEventHandler.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using GenericEventRunner.DomainParts; 5 | using StatusGeneric; 6 | 7 | namespace GenericEventRunner.ForHandlers 8 | { 9 | /// 10 | /// Place this on any event handler that should be called before SaveChanges has updated the database 11 | /// 12 | /// This should be the domain event that this handler is looking for 13 | public interface IBeforeSaveEventHandler where T : IEntityEvent 14 | { 15 | /// 16 | /// This is the method you must define to produce a BeforeSave event handler 17 | /// 18 | /// 19 | /// 20 | /// This can be null if you don't want to return a status, otherwise it should be a IStatusGeneric type 21 | IStatusGeneric Handle(object callingEntity, T domainEvent); 22 | } 23 | } -------------------------------------------------------------------------------- /GenericEventRunner/ForHandlers/IBeforeSaveEventHandlerAsync.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.Threading.Tasks; 5 | using GenericEventRunner.DomainParts; 6 | using StatusGeneric; 7 | 8 | namespace GenericEventRunner.ForHandlers 9 | { 10 | /// 11 | /// Place this on any async event handler that should be called before SaveChanges has updated the database 12 | /// 13 | /// This should be the domain event that this handler is looking for 14 | public interface IBeforeSaveEventHandlerAsync where T : IEntityEvent 15 | { 16 | /// 17 | /// This is the method you must define to produce a BeforeSave event handler 18 | /// 19 | /// 20 | /// 21 | /// This can be null if you don't want to return a status, otherwise it should be a IStatusGeneric type 22 | Task HandleAsync(object callingEntity, T domainEvent); 23 | } 24 | } -------------------------------------------------------------------------------- /GenericEventRunner/ForHandlers/IDuringSaveEventHandler.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using GenericEventRunner.DomainParts; 6 | using StatusGeneric; 7 | 8 | namespace GenericEventRunner.ForHandlers 9 | { 10 | /// 11 | /// Place this on any event handler that should be called within a transaction containing a call to SaveChanges 12 | /// 13 | /// This should be the domain event that this handler is looking for 14 | public interface IDuringSaveEventHandler where T : IEntityEvent 15 | { 16 | /// 17 | /// This is the method you must define to produce a AfterSave event handler 18 | /// 19 | /// 20 | /// 21 | /// A unique value per transaction. This allows you to detect retries of transactions 22 | /// You must return a IStatusGeneric. If has an error the transaction will be rolled back 23 | IStatusGeneric Handle(object callingEntity, T domainEvent, Guid uniqueKey); 24 | } 25 | } -------------------------------------------------------------------------------- /GenericEventRunner/ForHandlers/IDuringSaveEventHandlerAsync.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Threading.Tasks; 6 | using GenericEventRunner.DomainParts; 7 | using StatusGeneric; 8 | 9 | namespace GenericEventRunner.ForHandlers 10 | { 11 | /// 12 | /// Place this on any event handler that should be called within a transaction containing a call to SaveChanges 13 | /// 14 | /// This should be the domain event that this handler is looking for 15 | public interface IDuringSaveEventHandlerAsync where T : IEntityEvent 16 | { 17 | /// 18 | /// This is the method you must define to produce a AfterSave event handler 19 | /// 20 | /// 21 | /// 22 | /// A unique value per transaction. This allows you to detect retries of transactions 23 | /// You must return a IStatusGeneric. If has an error the transaction will be rolled back 24 | Task HandleAsync(object callingEntity, T domainEvent, Guid uniqueKey); 25 | } 26 | } -------------------------------------------------------------------------------- /GenericEventRunner/ForHandlers/Internal/AfterEntityAndEvent.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using GenericEventRunner.DomainParts; 5 | 6 | namespace GenericEventRunner.ForHandlers.Internal 7 | { 8 | internal class AfterEntityAndEvent 9 | { 10 | public AfterEntityAndEvent(IEntityWithAfterSaveEvents callingEntity, IEntityEvent entityEvent) 11 | { 12 | CallingEntity = callingEntity; 13 | EntityEvent = entityEvent; 14 | } 15 | 16 | public IEntityWithAfterSaveEvents CallingEntity { get; } 17 | public IEntityEvent EntityEvent { get; } 18 | } 19 | } -------------------------------------------------------------------------------- /GenericEventRunner/ForHandlers/Internal/AfterSaveEventHandler.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.Runtime.Serialization; 5 | using GenericEventRunner.DomainParts; 6 | 7 | namespace GenericEventRunner.ForHandlers.Internal 8 | { 9 | internal abstract class AfterSaveEventHandler 10 | { 11 | public abstract void Handle(object callingEntity, IEntityEvent entityEvent); 12 | } 13 | 14 | internal class AfterSaveHandler : AfterSaveEventHandler 15 | where T : IEntityEvent 16 | { 17 | private readonly IAfterSaveEventHandler _handler; 18 | 19 | public AfterSaveHandler(IAfterSaveEventHandler handler) 20 | { 21 | _handler = handler; 22 | } 23 | 24 | public override void Handle(object callingEntity, IEntityEvent entityEvent) 25 | { 26 | _handler.Handle(callingEntity, (T)entityEvent); 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /GenericEventRunner/ForHandlers/Internal/AfterSaveEventHandlerAsync.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.Threading.Tasks; 5 | using GenericEventRunner.DomainParts; 6 | 7 | namespace GenericEventRunner.ForHandlers.Internal 8 | { 9 | internal abstract class AfterSaveEventHandlerAsync 10 | { 11 | public abstract Task HandleAsync(object callingEntity, IEntityEvent entityEvent); 12 | } 13 | 14 | internal class AfterSaveHandlerAsync : AfterSaveEventHandlerAsync 15 | where T : IEntityEvent 16 | { 17 | private readonly IAfterSaveEventHandlerAsync _handler; 18 | 19 | public AfterSaveHandlerAsync(IAfterSaveEventHandlerAsync handler) 20 | { 21 | _handler = handler; 22 | } 23 | 24 | public override Task HandleAsync(object callingEntity, IEntityEvent entityEvent) 25 | { 26 | return _handler.HandleAsync(callingEntity, (T)entityEvent); 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /GenericEventRunner/ForHandlers/Internal/AsyncHelper.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace GenericEventRunner.ForHandlers.Internal 9 | { 10 | //Thanks to https://cpratt.co/async-tips-tricks/ 11 | internal static class AsyncHelper 12 | { 13 | private static readonly TaskFactory TaskFactory = new 14 | TaskFactory(CancellationToken.None, 15 | TaskCreationOptions.None, 16 | TaskContinuationOptions.None, 17 | TaskScheduler.Default); 18 | 19 | public static TResult RunSync(Func> func) 20 | => TaskFactory 21 | .StartNew(func) 22 | .Unwrap() 23 | .GetAwaiter() 24 | .GetResult(); 25 | 26 | public static void RunSync(Func func) 27 | => TaskFactory 28 | .StartNew(func) 29 | .Unwrap() 30 | .GetAwaiter() 31 | .GetResult(); 32 | } 33 | 34 | } -------------------------------------------------------------------------------- /GenericEventRunner/ForHandlers/Internal/BeforeSaveEventHandler.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using GenericEventRunner.DomainParts; 5 | using StatusGeneric; 6 | 7 | namespace GenericEventRunner.ForHandlers.Internal 8 | { 9 | internal abstract class BeforeSaveEventHandler 10 | { 11 | public abstract IStatusGeneric Handle(object callingEntity, IEntityEvent entityEvent); 12 | } 13 | 14 | internal class BeforeSaveHandler : BeforeSaveEventHandler 15 | where T : IEntityEvent 16 | { 17 | private readonly IBeforeSaveEventHandler _handler; 18 | 19 | public BeforeSaveHandler(IBeforeSaveEventHandler handler) 20 | { 21 | _handler = handler; 22 | } 23 | 24 | public override IStatusGeneric Handle(object callingEntity, IEntityEvent entityEvent) 25 | { 26 | return _handler.Handle(callingEntity, (T)entityEvent); 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /GenericEventRunner/ForHandlers/Internal/BeforeSaveEventHandlerAsync.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.Threading.Tasks; 5 | using GenericEventRunner.DomainParts; 6 | using StatusGeneric; 7 | 8 | namespace GenericEventRunner.ForHandlers.Internal 9 | { 10 | internal abstract class BeforeSaveEventHandlerAsync 11 | { 12 | public abstract Task HandleAsync(object callingEntity, IEntityEvent entityEvent); 13 | } 14 | 15 | internal class BeforeSaveHandlerAsync : BeforeSaveEventHandlerAsync 16 | where T : IEntityEvent 17 | { 18 | private readonly IBeforeSaveEventHandlerAsync _handler; 19 | 20 | public BeforeSaveHandlerAsync(IBeforeSaveEventHandlerAsync handler) 21 | { 22 | _handler = handler; 23 | } 24 | 25 | public override Task HandleAsync(object callingEntity, IEntityEvent entityEvent) 26 | { 27 | return _handler.HandleAsync(callingEntity, (T)entityEvent); 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /GenericEventRunner/ForHandlers/Internal/DuringEventsExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.Linq; 5 | using GenericEventRunner.DomainParts; 6 | using Microsoft.EntityFrameworkCore; 7 | 8 | namespace GenericEventRunner.ForHandlers.Internal 9 | { 10 | internal static class DuringEventsExtensions 11 | { 12 | 13 | public static void ClearDuringEvents(this DbContext context) 14 | { 15 | context.ChangeTracker.Entries().ToList() 16 | .ForEach(x => x.Entity.ClearDuringSaveEvents()); 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /GenericEventRunner/ForHandlers/Internal/DuringSaveEventHandler.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using GenericEventRunner.DomainParts; 6 | using StatusGeneric; 7 | 8 | namespace GenericEventRunner.ForHandlers.Internal 9 | { 10 | internal abstract class DuringSaveEventHandler 11 | { 12 | public abstract IStatusGeneric Handle(object callingEntity, IEntityEvent entityEvent, Guid uniqueKey); 13 | } 14 | 15 | internal class DuringSaveHandler : DuringSaveEventHandler 16 | where T : IEntityEvent 17 | { 18 | private readonly IDuringSaveEventHandler _handler; 19 | 20 | public DuringSaveHandler(IDuringSaveEventHandler handler) 21 | { 22 | _handler = handler; 23 | } 24 | 25 | public override IStatusGeneric Handle(object callingEntity, IEntityEvent entityEvent, Guid uniqueKey) 26 | { 27 | return _handler.Handle(callingEntity, (T)entityEvent, uniqueKey); 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /GenericEventRunner/ForHandlers/Internal/DuringSaveEventHandlerAsync.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Threading.Tasks; 6 | using GenericEventRunner.DomainParts; 7 | using StatusGeneric; 8 | 9 | namespace GenericEventRunner.ForHandlers.Internal 10 | { 11 | internal abstract class DuringSaveEventHandlerAsync 12 | { 13 | public abstract Task HandleAsync(object callingEntity, IEntityEvent entityEvent, Guid uniqueKey); 14 | } 15 | 16 | internal class DuringSaveHandlerAsync : DuringSaveEventHandlerAsync 17 | where T : IEntityEvent 18 | { 19 | private readonly IDuringSaveEventHandlerAsync _handler; 20 | 21 | public DuringSaveHandlerAsync(IDuringSaveEventHandlerAsync handler) 22 | { 23 | _handler = handler; 24 | } 25 | 26 | public override Task HandleAsync(object callingEntity, IEntityEvent entityEvent, Guid uniqueKey) 27 | { 28 | return _handler.HandleAsync(callingEntity, (T)entityEvent, uniqueKey); 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /GenericEventRunner/ForHandlers/Internal/EntityAndEvent.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Reflection; 6 | using System.Runtime.CompilerServices; 7 | using GenericEventRunner.DomainParts; 8 | 9 | [assembly: InternalsVisibleTo("Test")] 10 | 11 | namespace GenericEventRunner.ForHandlers.Internal 12 | { 13 | internal class EntityAndEvent 14 | { 15 | public EntityAndEvent(object callingEntity, IEntityEvent entityEvent) 16 | { 17 | CallingEntity = callingEntity ?? throw new ArgumentNullException(nameof(callingEntity)); 18 | EntityEvent = entityEvent ?? throw new ArgumentNullException(nameof(entityEvent)); 19 | HasRemoveDuplicateAttribute = EntityEvent 20 | .GetType() 21 | .GetCustomAttribute() != null; 22 | HasDuringEventRunBeforeSave = EntityEvent 23 | .GetType() 24 | .GetCustomAttribute() != null; 25 | } 26 | 27 | public object CallingEntity { get; } 28 | public IEntityEvent EntityEvent { get; } 29 | 30 | public bool HasRemoveDuplicateAttribute { get; } 31 | public bool HasDuringEventRunBeforeSave { get; } 32 | 33 | private bool Equals(EntityAndEvent other) 34 | { 35 | //Only equal if class has the RemoveDuplicate attribute 36 | return HasRemoveDuplicateAttribute && 37 | EntityEvent.GetType() == other.EntityEvent.GetType() && 38 | ReferenceEquals(CallingEntity, other.CallingEntity); 39 | } 40 | 41 | public override bool Equals(object obj) 42 | { 43 | return obj is EntityAndEvent other && Equals(other); 44 | } 45 | 46 | //see https://stackoverflow.com/questions/21402465/iequalitycomparer-not-working-as-intended 47 | public override int GetHashCode() 48 | { 49 | return HashCode.Combine(CallingEntity, EntityEvent.GetType(), HasRemoveDuplicateAttribute); 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /GenericEventRunner/ForHandlers/Internal/FindHandlers.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Runtime.CompilerServices; 8 | using System.Xml.Schema; 9 | using Microsoft.Extensions.DependencyInjection; 10 | using Microsoft.Extensions.Logging; 11 | 12 | namespace GenericEventRunner.ForHandlers.Internal 13 | { 14 | internal class FindHandlers 15 | { 16 | private readonly IServiceProvider _serviceProvider; 17 | private readonly ILogger _logger; 18 | 19 | public FindHandlers(IServiceProvider serviceProvider, ILogger logger) 20 | { 21 | _serviceProvider = serviceProvider; 22 | _logger = logger; 23 | } 24 | 25 | public List GetHandlers(EntityAndEvent entityAndEvent, BeforeDuringOrAfter beforeDuringOrAfter, bool lookForAsyncHandlers) 26 | { 27 | var eventType = entityAndEvent.EntityEvent.GetType(); 28 | var asyncHandlers = new List(); 29 | 30 | List GetAsyncHandlers() 31 | { 32 | var asyncHandlerInterface = GetEventHandlerGenericType(beforeDuringOrAfter, true) 33 | .MakeGenericType(eventType); 34 | asyncHandlers = _serviceProvider.GetServices(asyncHandlerInterface).ToList(); 35 | return asyncHandlers; 36 | } 37 | 38 | if(lookForAsyncHandlers) 39 | { 40 | asyncHandlers = GetAsyncHandlers(); 41 | } 42 | var syncHandlerInterface = GetEventHandlerGenericType(beforeDuringOrAfter, false) 43 | .MakeGenericType(eventType); 44 | 45 | var syncHandler = _serviceProvider.GetServices(syncHandlerInterface) 46 | //This removes sync event handlers that have the same name 47 | .Where(x => asyncHandlers.All(y => 48 | !string.Equals(x.GetType().Name + "Async", y.GetType().Name, StringComparison.InvariantCultureIgnoreCase))) 49 | .ToList(); 50 | 51 | var result = asyncHandlers.Select(x => new HandlerAndWrapper(x, eventType, beforeDuringOrAfter, true)) 52 | .Union(syncHandler.Select(x => new HandlerAndWrapper(x, eventType, beforeDuringOrAfter, false))).ToList(); 53 | 54 | if (!result.Any()) 55 | { 56 | var suffix = GetAsyncHandlers().Any() ? " Their was a suitable async event handler available, but you didn't call SaveChangesAsync." : ""; 57 | _logger.LogError($"Missing handler for event of type {eventType.FullName} for {beforeDuringOrAfter} event handler.{suffix}"); 58 | throw new GenericEventRunnerException( 59 | $"Could not find a {beforeDuringOrAfter} event handler for the event {eventType.Name}.{suffix}", 60 | entityAndEvent.CallingEntity, entityAndEvent.EntityEvent); 61 | } 62 | 63 | return result; 64 | } 65 | 66 | private Type GetEventHandlerGenericType(BeforeDuringOrAfter beforeDuringOrAfter, bool lookForAsyncHandlers) 67 | { 68 | switch (beforeDuringOrAfter, lookForAsyncHandlers) 69 | { 70 | case (BeforeDuringOrAfter.BeforeSave, false): 71 | return typeof(IBeforeSaveEventHandler<>); 72 | case (BeforeDuringOrAfter.BeforeSave, true): 73 | return typeof(IBeforeSaveEventHandlerAsync<>); 74 | case (BeforeDuringOrAfter.DuringBeforeSave, false): 75 | case (BeforeDuringOrAfter.DuringSave, false): 76 | return typeof(IDuringSaveEventHandler<>); 77 | case (BeforeDuringOrAfter.DuringBeforeSave, true): 78 | case (BeforeDuringOrAfter.DuringSave, true): 79 | return typeof(IDuringSaveEventHandlerAsync<>); 80 | case (BeforeDuringOrAfter.AfterSave, false): 81 | return typeof(IAfterSaveEventHandler<>); 82 | case (BeforeDuringOrAfter.AfterSave, true): 83 | return typeof(IAfterSaveEventHandlerAsync<>); 84 | default: 85 | throw new ArgumentOutOfRangeException(); 86 | } 87 | } 88 | } 89 | } -------------------------------------------------------------------------------- /GenericEventRunner/ForHandlers/Internal/FindRunHandlers.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | using GenericEventRunner.ForSetup; 8 | using Microsoft.Extensions.Logging; 9 | using StatusGeneric; 10 | 11 | namespace GenericEventRunner.ForHandlers.Internal 12 | { 13 | internal class FindRunHandlers 14 | { 15 | private readonly IServiceProvider _serviceProvider; 16 | private readonly ILogger _logger; 17 | private readonly IGenericEventRunnerConfig _config; 18 | 19 | private readonly FindHandlers _findHandlers; 20 | 21 | //This is used by During events to handle retry of a transaction 22 | private readonly Guid _uniqueValue = Guid.NewGuid(); 23 | 24 | public FindRunHandlers(IServiceProvider serviceProvider, ILogger logger, IGenericEventRunnerConfig config) 25 | { 26 | _serviceProvider = serviceProvider; 27 | _logger = logger; 28 | _config = config; 29 | 30 | _findHandlers = new FindHandlers(serviceProvider, logger); 31 | } 32 | 33 | /// 34 | /// This finds either sync or async handlers for the event and runs the handlers with the input event 35 | /// 36 | /// 37 | /// This gives the loop number for the RunBefore/AfterSaveChangesEvents 38 | /// tells you what type of event to find/Run 39 | /// true if async is allowed 40 | /// Returns a Task containing the combined status from all the event handlers that ran 41 | public async ValueTask RunHandlersForEventAsync(EntityAndEvent entityAndEvent, int loopCount, 42 | BeforeDuringOrAfter beforeDuringOrAfter, bool allowAsync) 43 | { 44 | var status = new StatusGenericHandler 45 | { 46 | Message = "Successfully saved." 47 | }; 48 | 49 | var handlersAndWrappers = _findHandlers.GetHandlers(entityAndEvent, beforeDuringOrAfter, allowAsync); 50 | foreach (var handlerWrapper in handlersAndWrappers) 51 | { 52 | LogEventHandlerRun(loopCount, beforeDuringOrAfter, handlerWrapper); 53 | if (beforeDuringOrAfter == BeforeDuringOrAfter.BeforeSave) 54 | { 55 | var handlerStatus = handlerWrapper.IsAsync 56 | ? await ((BeforeSaveEventHandlerAsync)Activator.CreateInstance(handlerWrapper.WrapperType, handlerWrapper.EventHandler)) 57 | .HandleAsync(entityAndEvent.CallingEntity, entityAndEvent.EntityEvent).ConfigureAwait(false) 58 | : ((BeforeSaveEventHandler)Activator.CreateInstance(handlerWrapper.WrapperType, handlerWrapper.EventHandler)) 59 | .Handle(entityAndEvent.CallingEntity, entityAndEvent.EntityEvent); 60 | if (handlerStatus != null) 61 | status.CombineStatuses(handlerStatus); 62 | } 63 | else if (beforeDuringOrAfter == BeforeDuringOrAfter.AfterSave) 64 | { 65 | if (handlerWrapper.IsAsync) 66 | await ((AfterSaveEventHandlerAsync) Activator.CreateInstance(handlerWrapper.WrapperType, 67 | handlerWrapper.EventHandler)) 68 | .HandleAsync(entityAndEvent.CallingEntity, entityAndEvent.EntityEvent).ConfigureAwait(false); 69 | else 70 | ((AfterSaveEventHandler) Activator.CreateInstance(handlerWrapper.WrapperType, 71 | handlerWrapper.EventHandler)) 72 | .Handle(entityAndEvent.CallingEntity, entityAndEvent.EntityEvent); 73 | } 74 | else 75 | { 76 | //Its either of the during events 77 | 78 | var handlerStatus = handlerWrapper.IsAsync 79 | ? await ((DuringSaveEventHandlerAsync)Activator.CreateInstance(handlerWrapper.WrapperType, handlerWrapper.EventHandler)) 80 | .HandleAsync(entityAndEvent.CallingEntity, entityAndEvent.EntityEvent, _uniqueValue) 81 | .ConfigureAwait(false) 82 | : ((DuringSaveEventHandler)Activator.CreateInstance(handlerWrapper.WrapperType, handlerWrapper.EventHandler)) 83 | .Handle(entityAndEvent.CallingEntity, entityAndEvent.EntityEvent, _uniqueValue); 84 | if (handlerStatus != null) 85 | status.CombineStatuses(handlerStatus); 86 | } 87 | } 88 | 89 | return status; 90 | } 91 | 92 | private void LogEventHandlerRun(int loopCount, BeforeDuringOrAfter beforeDuringOrAfter, HandlerAndWrapper handlerWrapper) 93 | { 94 | _logger.LogInformation( 95 | $"{beforeDuringOrAfter.ToString()[0]}{loopCount}: About to run a {beforeDuringOrAfter} event handler {handlerWrapper.EventHandler.GetType().FullName}."); 96 | } 97 | } 98 | } -------------------------------------------------------------------------------- /GenericEventRunner/ForHandlers/Internal/HandlerAndWrapper.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | 6 | namespace GenericEventRunner.ForHandlers.Internal 7 | { 8 | internal class HandlerAndWrapper 9 | { 10 | public HandlerAndWrapper(object eventHandler, Type eventType, BeforeDuringOrAfter beforeDuringOrAfter, bool isAsync) 11 | { 12 | EventHandler = eventHandler; 13 | IsAsync = isAsync; 14 | 15 | switch (beforeDuringOrAfter, isAsync) 16 | { 17 | case (BeforeDuringOrAfter.BeforeSave, false): 18 | WrapperType = typeof(BeforeSaveHandler<>).MakeGenericType(eventType); 19 | break; 20 | case (BeforeDuringOrAfter.BeforeSave, true): 21 | WrapperType = typeof(BeforeSaveHandlerAsync<>).MakeGenericType(eventType); 22 | break; 23 | case (BeforeDuringOrAfter.DuringBeforeSave, false): 24 | case (BeforeDuringOrAfter.DuringSave, false): 25 | WrapperType = typeof(DuringSaveHandler<>).MakeGenericType(eventType); 26 | break; 27 | case (BeforeDuringOrAfter.DuringBeforeSave, true): 28 | case (BeforeDuringOrAfter.DuringSave, true): 29 | WrapperType = typeof(DuringSaveHandlerAsync<>).MakeGenericType(eventType); 30 | break; 31 | case (BeforeDuringOrAfter.AfterSave, false): 32 | WrapperType = typeof(AfterSaveHandler<>).MakeGenericType(eventType); 33 | break; 34 | case (BeforeDuringOrAfter.AfterSave, true): 35 | WrapperType = typeof(AfterSaveHandlerAsync<>).MakeGenericType(eventType); 36 | break; 37 | default: 38 | throw new ArgumentOutOfRangeException(); 39 | } 40 | } 41 | 42 | public object EventHandler { get; } 43 | 44 | public Type WrapperType { get; } 45 | public bool IsAsync { get; } 46 | } 47 | } -------------------------------------------------------------------------------- /GenericEventRunner/ForHandlers/Internal/RunEachTypeOfEvents.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Reflection; 8 | using System.Threading.Tasks; 9 | using GenericEventRunner.DomainParts; 10 | using GenericEventRunner.ForSetup; 11 | using Microsoft.EntityFrameworkCore; 12 | using Microsoft.Extensions.Logging; 13 | using StatusGeneric; 14 | 15 | namespace GenericEventRunner.ForHandlers.Internal 16 | { 17 | internal enum BeforeDuringOrAfter { BeforeSave, DuringBeforeSave, DuringSave, AfterSave } 18 | 19 | internal class RunEachTypeOfEvents 20 | { 21 | private readonly FindRunHandlers _findRunHandlers; 22 | private readonly IGenericEventRunnerConfig _config; 23 | 24 | private List _duringBeforeEvents; 25 | private List _duringAfterEvents; 26 | 27 | public RunEachTypeOfEvents(IServiceProvider serviceProvider, ILogger logger, IGenericEventRunnerConfig config) 28 | { 29 | _config = config ?? throw new ArgumentNullException(nameof(config)); 30 | _findRunHandlers = new FindRunHandlers(serviceProvider, logger, _config); 31 | } 32 | 33 | public bool SetupDuringEvents(DbContext context) 34 | { 35 | var duringEvents = new List(); 36 | foreach (var entityEntry in context.ChangeTracker.Entries()) 37 | { 38 | duringEvents.AddRange(entityEntry.Entity.GetDuringSaveEvents() 39 | .Select(x => new EntityAndEvent(entityEntry.Entity, x)) 40 | .Distinct()); 41 | } 42 | 43 | if (!duringEvents.Any()) 44 | return false; 45 | 46 | //Find the events marked to run before/after SaveChanges 47 | _duringBeforeEvents = duringEvents.Where(x => x.HasDuringEventRunBeforeSave).ToList(); 48 | _duringAfterEvents = duringEvents.Where(x => !x.HasDuringEventRunBeforeSave).ToList(); 49 | 50 | return true; 51 | } 52 | 53 | public async ValueTask RunBeforeSaveChangesEventsAsync(DbContext context, bool allowAsync) 54 | { 55 | var status = new StatusGenericHandler(); 56 | 57 | //This has to run until there are no new events, because one event might trigger another event 58 | bool shouldRunAgain; 59 | int loopCount = 1; 60 | do 61 | { 62 | var eventsToRun = new List(); 63 | foreach (var entityEntry in context.ChangeTracker.Entries()) 64 | { 65 | eventsToRun.AddRange(entityEntry.Entity.GetBeforeSaveEventsThenClear() 66 | .Select(x => new EntityAndEvent(entityEntry.Entity, x)) 67 | .Distinct()); 68 | } 69 | 70 | shouldRunAgain = false; 71 | foreach (var entityAndEvent in eventsToRun) 72 | { 73 | shouldRunAgain = true; 74 | if (allowAsync) 75 | status.CombineStatuses(await _findRunHandlers.RunHandlersForEventAsync( 76 | entityAndEvent, loopCount, BeforeDuringOrAfter.BeforeSave, allowAsync) 77 | .ConfigureAwait(false)); 78 | else 79 | { 80 | var findRunStatus = 81 | _findRunHandlers.RunHandlersForEventAsync(entityAndEvent, loopCount, BeforeDuringOrAfter.BeforeSave, allowAsync); 82 | findRunStatus.CheckSyncValueTaskWorked(); 83 | status.CombineStatuses(findRunStatus.Result); 84 | } 85 | if (!status.IsValid && _config.StopOnFirstBeforeHandlerThatHasAnError) 86 | break; 87 | } 88 | if (loopCount++ > _config.MaxTimesToLookForBeforeEvents) 89 | throw new GenericEventRunnerException( 90 | $"The BeforeSave event loop exceeded the config's {nameof(GenericEventRunnerConfig.MaxTimesToLookForBeforeEvents)}" + 91 | $" value of {_config.MaxTimesToLookForBeforeEvents}. This implies a circular sets of events. " + 92 | "Look at EventsRunner Information logs for more information on what event handlers were running.", 93 | eventsToRun.Last().CallingEntity, eventsToRun.Last().EntityEvent); 94 | } while (shouldRunAgain && (status.IsValid || !_config.StopOnFirstBeforeHandlerThatHasAnError)); 95 | 96 | if (!status.IsValid) 97 | { 98 | //If errors then clear any extra before/after events. 99 | //We need to do this to ensure another call to SaveChanges doesn't get the old events 100 | context.ChangeTracker.Entries() 101 | .ToList().ForEach(x => x.Entity.GetBeforeSaveEventsThenClear()); 102 | context.ClearDuringEvents(); 103 | context.ChangeTracker.Entries() 104 | .ToList().ForEach(x => x.Entity.GetAfterSaveEventsThenClear()); 105 | } 106 | 107 | return status; 108 | } 109 | 110 | public async ValueTask RunDuringSaveChangesEventsAsync(bool postSaveChanges, bool allowAsync) 111 | { 112 | var status = new StatusGenericHandler(); 113 | 114 | if (_config.NotUsingDuringSaveHandlers) 115 | //Skip this stage if NotUsingDuringSaveHandlers is true 116 | return status; 117 | 118 | var eventType = postSaveChanges 119 | ? BeforeDuringOrAfter.DuringSave 120 | : BeforeDuringOrAfter.DuringBeforeSave; 121 | foreach (var entityAndEvent in postSaveChanges ? _duringAfterEvents : _duringBeforeEvents) 122 | { 123 | if (allowAsync) 124 | status.CombineStatuses(await _findRunHandlers.RunHandlersForEventAsync( 125 | entityAndEvent, 1, eventType, true) 126 | .ConfigureAwait(false)); 127 | else 128 | { 129 | var findRunStatus = 130 | _findRunHandlers.RunHandlersForEventAsync(entityAndEvent, 1, eventType, false); 131 | findRunStatus.CheckSyncValueTaskWorked(); 132 | status.CombineStatuses(findRunStatus.Result); 133 | } 134 | } 135 | 136 | return status; 137 | } 138 | 139 | //NOTE: This had problems throwing an exception (don't know why - RunBeforeSaveChangesEventsAsync work!?). 140 | //Having it return an exception fixed it 141 | public async ValueTask RunAfterSaveChangesEventsAsync(DbContext context, bool allowAsync) 142 | { 143 | var status = new StatusGenericHandler(); 144 | 145 | if (_config.NotUsingAfterSaveHandlers) 146 | //Skip this stage if NotUsingAfterSaveHandlers is true 147 | return; 148 | 149 | var eventsToRun = new List(); 150 | foreach (var entityEntry in context.ChangeTracker.Entries()) 151 | { 152 | eventsToRun.AddRange(entityEntry.Entity.GetAfterSaveEventsThenClear() 153 | .Select(x => new EntityAndEvent(entityEntry.Entity, x)) 154 | .Distinct()); 155 | } 156 | 157 | foreach (var entityAndEvent in eventsToRun) 158 | { 159 | if (allowAsync) 160 | status.CombineStatuses(await _findRunHandlers.RunHandlersForEventAsync(entityAndEvent, 1, BeforeDuringOrAfter.AfterSave, allowAsync) 161 | .ConfigureAwait(false)); 162 | else 163 | { 164 | var findRunStatus = 165 | _findRunHandlers.RunHandlersForEventAsync(entityAndEvent, 1, BeforeDuringOrAfter.AfterSave, allowAsync); 166 | findRunStatus.CheckSyncValueTaskWorked(); 167 | status.CombineStatuses(findRunStatus.Result); 168 | } 169 | } 170 | } 171 | } 172 | } -------------------------------------------------------------------------------- /GenericEventRunner/ForHandlers/Internal/ValueTaskSyncCheckers.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | 8 | namespace GenericEventRunner.ForHandlers.Internal 9 | { 10 | internal static class ValueTaskSyncCheckers 11 | { 12 | /// 13 | /// This will check the returned 14 | /// by a method and ensure it didn't run any async methods. 15 | /// Also, if the method threw an exception it will throw that exception. 16 | /// 17 | /// The ValueTask from a method that didn't call any async methods 18 | public static void CheckSyncValueTaskWorked(this ValueTask valueTask) 19 | { 20 | if (!valueTask.IsCompleted) 21 | throw new InvalidOperationException("Expected a sync task, but got an async task"); 22 | if (valueTask.IsFaulted) 23 | valueTask.GetAwaiter().GetResult(); 24 | } 25 | 26 | /// 27 | /// This will check the returned 28 | /// by a method and ensure it didn't run any async methods. 29 | /// Also, if the method threw an exception it will throw that exception. 30 | /// 31 | /// The ValueTask from a method that didn't call any async methods 32 | public static void CheckSyncValueTaskWorked(this ValueTask valueTask) 33 | { 34 | if (!valueTask.IsCompleted) 35 | throw new InvalidOperationException("Expected a sync task, but got an async task"); 36 | if (valueTask.IsFaulted) 37 | valueTask.GetAwaiter().GetResult(); 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /GenericEventRunner/ForSetup/GenericEventRunnerConfig.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Collections.Immutable; 7 | using Microsoft.EntityFrameworkCore; 8 | using StatusGeneric; 9 | 10 | namespace GenericEventRunner.ForSetup 11 | { 12 | /// 13 | /// This holds the configuration settings for the GenericEventRunner 14 | /// NOTE: This is registered as a singleton, i.e. the values cannot be changes dynamically 15 | /// 16 | public class GenericEventRunnerConfig : IGenericEventRunnerConfig 17 | { 18 | private readonly List<(Type dbContextType, Action action)> _actionsToRunAfterDetectChanges 19 | = new List<(Type dbContextType, Action action)>(); 20 | private readonly Dictionary> _exceptionHandlerDictionary 21 | = new Dictionary>(); 22 | 23 | /// 24 | /// This limits the number of times it will look for new events from the BeforeSave events. 25 | /// This stops circular sets of events 26 | /// The event runner will throw an exception if the BeforeSave loop goes round move than this number. 27 | /// 28 | public int MaxTimesToLookForBeforeEvents { get; set; } = 6; 29 | 30 | /// 31 | /// If this is set to true, then the DuringSave event handlers aren't used 32 | /// NOTE: This is set to true if the RegisterGenericEventRunner doesn't find any DuringSave event handlers 33 | /// 34 | public bool NotUsingDuringSaveHandlers { get; set; } 35 | 36 | /// 37 | /// If this is set to true, then the AfterSave event handlers aren't used 38 | /// NOTE: This is set to true if the RegisterGenericEventRunner doesn't find any AfterSave event handlers 39 | /// 40 | public bool NotUsingAfterSaveHandlers { get; set; } 41 | 42 | /// 43 | /// If true (which is the default value) then the first BeforeSave event handler that returns an error will stop the event runner. 44 | /// The use cases for each setting is: 45 | /// true: Once you have a error, then its not worth going on so stopping quickly is good. 46 | /// false: If your events have a lot of different checks then this setting gets all the possible errors. 47 | /// NOTE: Because this is very event-specific you can override this on a per-handler basis via the EventHandlerConfig Attribute 48 | /// 49 | public bool StopOnFirstBeforeHandlerThatHasAnError { get; set; } = true; 50 | 51 | /// 52 | /// Add a method which should be executed after ChangeTracker.DetectChanges() has been run for the given DbContext 53 | /// This is useful to add code that uses the State of entities to 54 | /// NOTES: 55 | /// - DetectChanges won't be called again, so you must ensure that an changes must be manually applied. 56 | /// - The BeforeSaveEvents will be run before this action is called 57 | /// 58 | /// Must be a DbContext that uses the GenericEventRunner 59 | /// 60 | public void AddActionToRunAfterDetectChanges(Action runAfterDetectChanges) where TContext : DbContext 61 | { 62 | _actionsToRunAfterDetectChanges.Add((dbContextType: typeof(TContext), action: runAfterDetectChanges)); 63 | } 64 | 65 | /// 66 | /// This holds the list of actions to be run after DetectChanges is called, but before SaveChanges is called 67 | /// NOTE: The BeforeSaveEvents will be run before these actions 68 | /// 69 | public IReadOnlyList<(Type dbContextType, Action action)> ActionsToRunAfterDetectChanges => 70 | _actionsToRunAfterDetectChanges.AsReadOnly(); 71 | 72 | /// 73 | /// This method allows you to register an exception handler for a specific DbContext type 74 | /// When SaveChangesWithValidation is called if there is an exception then this method is called (if present) 75 | /// a) If it returns null then the error is rethrown. This means the exception handler can't handle that exception. 76 | /// b) If it returns a status with errors then those are combined into the GenericEventRunner status. 77 | /// c) If it returns a valid status (i.e. no errors) then it calls SaveChanges again, still with exception capture. 78 | /// Item b) is useful for turning SQL errors into user-friendly error message, and c) is good for handling a DbUpdateConcurrencyException 79 | /// 80 | public void RegisterSaveChangesExceptionHandler( 81 | Func exceptionHandler) where TContext : DbContext 82 | { 83 | if (_exceptionHandlerDictionary.ContainsKey(typeof(TContext))) 84 | throw new InvalidOperationException( 85 | $"You can only register one exception handler per DbContext type. You all ready have registered {typeof(TContext).Name}"); 86 | _exceptionHandlerDictionary[typeof(TContext)] = exceptionHandler; 87 | } 88 | 89 | /// 90 | /// This holds the Dictionary of exception handlers for a specific DbContext 91 | /// 92 | public ImmutableDictionary> ExceptionHandlerDictionary => 93 | _exceptionHandlerDictionary.ToImmutableDictionary(); 94 | } 95 | } -------------------------------------------------------------------------------- /GenericEventRunner/ForSetup/IGenericEventRunnerConfig.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Collections.Immutable; 7 | using Microsoft.EntityFrameworkCore; 8 | using StatusGeneric; 9 | 10 | namespace GenericEventRunner.ForSetup 11 | { 12 | /// 13 | /// Definition of the properties etc. for configuring how the EventsRunner works 14 | /// 15 | public interface IGenericEventRunnerConfig 16 | { 17 | /// 18 | /// This limits the number of times it will look for new events from the BeforeSave events. 19 | /// This stops circular sets of events 20 | /// The event runner will throw an exception if the BeforeSave loop goes round move than this number. 21 | /// The default value is 6 22 | /// 23 | int MaxTimesToLookForBeforeEvents { get; } 24 | 25 | /// 26 | /// If this is set to true, then the DuringSave event handlers aren't used 27 | /// NOTE: This is set to true if the RegisterGenericEventRunner doesn't find any DuringSave event handlers 28 | /// 29 | bool NotUsingDuringSaveHandlers { get; set; } 30 | 31 | /// 32 | /// If this is set to true, then the AfterSave event handlers aren't used 33 | /// NOTE: This is set to true if the RegisterGenericEventRunner doesn't find any AfterSave event handlers 34 | /// 35 | bool NotUsingAfterSaveHandlers { get; set; } 36 | 37 | /// 38 | /// If true (which is the default value) then the first BeforeSave event handler that returns an error will stop the event runner. 39 | /// The use cases for each setting is: 40 | /// true: Once you have a error, then its not worth going on so stopping quickly is good. 41 | /// false: If your events have a lot of different checks then this setting gets all the possible errors. 42 | /// NOTE: Because this is very event-specific you can override this on a per-handler basis via the EventHandlerConfig Attribute 43 | /// 44 | bool StopOnFirstBeforeHandlerThatHasAnError { get; } 45 | 46 | /// 47 | /// Add a method which should be executed after ChangeTracker.DetectChanges() has been run for the given DbContext 48 | /// This is useful to add code that uses the State of entities to 49 | /// NOTES: 50 | /// - DetectChanges won't be called again, so you must ensure that an changes must be manually applied. 51 | /// - The BeforeSaveEvents will be run before this action is called 52 | /// 53 | /// Must be a DbContext that uses the GenericEventRunner 54 | /// 55 | void AddActionToRunAfterDetectChanges(Action runAfterDetectChanges) 56 | where TContext : DbContext; 57 | 58 | /// 59 | /// This method allows you to register an exception handler for a specific DbContext type 60 | /// When SaveChangesWithValidation is called if there is an exception then this method is called (if present) 61 | /// a) If it returns null then the error is rethrown. This means the exception handler can't handle that exception. 62 | /// b) If it returns a status with errors then those are combined into the GenericEventRunner status. 63 | /// c) If it returns a valid status (i.e. no errors) then it calls SaveChanges again, still with exception capture. 64 | /// Item b) is useful for turning SQL errors into user-friendly error message, and c) is good for handling a DbUpdateConcurrencyException 65 | /// 66 | void RegisterSaveChangesExceptionHandler( 67 | Func exceptionHandler) where TContext : DbContext; 68 | 69 | /// 70 | /// This holds the list of actions to be run after DetectChanges is called, but before SaveChanges is called 71 | /// NOTE: The BeforeSaveEvents will be run before these actions 72 | /// 73 | public IReadOnlyList<(Type dbContextType, Action action)> ActionsToRunAfterDetectChanges { get; } 74 | 75 | /// 76 | /// This holds the Dictionary of exception handlers for a specific DbContext 77 | /// 78 | ImmutableDictionary> ExceptionHandlerDictionary { get; } 79 | } 80 | } -------------------------------------------------------------------------------- /GenericEventRunner/ForSetup/Internal/RegisterIfNotThere.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.Collections.Generic; 5 | 6 | namespace GenericEventRunner.ForSetup.Internal 7 | { 8 | public class RegisterIfNotThere 9 | { 10 | public List DebugLogs = new List(); 11 | 12 | 13 | } 14 | } -------------------------------------------------------------------------------- /GenericEventRunner/ForSetup/RegisterGenericEventRunnerExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Net.NetworkInformation; 8 | using System.Reflection; 9 | using GenericEventRunner.ForDbContext; 10 | using GenericEventRunner.ForHandlers; 11 | using Microsoft.Extensions.DependencyInjection; 12 | 13 | namespace GenericEventRunner.ForSetup 14 | { 15 | /// 16 | /// This contains the extensions methods to register the GenericEventRunner and the various event handlers you have created 17 | /// 18 | public static class RegisterGenericEventRunnerExtensions 19 | { 20 | /// 21 | /// This register the Generic EventRunner and the various event handlers you have created in the assemblies you provide. 22 | /// NOTE: Uses default GenericEventRunnerConfig settings. 23 | /// 24 | /// 25 | /// Series of assemblies to scan. If not provided then scans the calling assembly 26 | public static List RegisterGenericEventRunner(this IServiceCollection services, 27 | params Assembly[] assembliesToScan) 28 | { 29 | return services.RegisterGenericEventRunner(new GenericEventRunnerConfig(), assembliesToScan); 30 | } 31 | 32 | /// 33 | /// This register the Generic EventRunner and the various event handlers you have created in the assemblies you provide. 34 | /// 35 | /// 36 | /// A GenericEventRunnerConfig instance with your own settings. 37 | /// Series of assemblies to scan. If not provided then scans the calling assembly 38 | /// List of what it registered - useful for debugging 39 | public static List RegisterGenericEventRunner(this IServiceCollection services, 40 | IGenericEventRunnerConfig config, 41 | params Assembly[] assembliesToScan) 42 | { 43 | var debugLogs = new List(); 44 | 45 | if (config == null) throw new ArgumentNullException(nameof(config)); 46 | 47 | if (!assembliesToScan.Any()) 48 | { 49 | assembliesToScan = new Assembly[]{ Assembly.GetCallingAssembly()}; 50 | debugLogs.Add("No assemblies provided so only scanning calling assembly."); 51 | } 52 | 53 | var someDuringSaveHandlersFound = false; 54 | var someAfterSaveHandlersFound = false; 55 | foreach (var assembly in assembliesToScan) 56 | { 57 | debugLogs.Add($"Starting scanning assembly {assembly.GetName().Name} for event handlers."); 58 | services.RegisterHandlersIfNotAlreadyRegistered(debugLogs, 59 | typeof(IBeforeSaveEventHandler<>), assembly); 60 | services.RegisterHandlersIfNotAlreadyRegistered(debugLogs, 61 | typeof(IBeforeSaveEventHandlerAsync<>), assembly); 62 | 63 | if (!config.NotUsingDuringSaveHandlers) 64 | { 65 | someDuringSaveHandlersFound |= services.RegisterHandlersIfNotAlreadyRegistered(debugLogs, 66 | typeof(IDuringSaveEventHandler<>), assembly); 67 | someDuringSaveHandlersFound |= services.RegisterHandlersIfNotAlreadyRegistered(debugLogs, 68 | typeof(IDuringSaveEventHandlerAsync<>), assembly); 69 | } 70 | 71 | if (!config.NotUsingAfterSaveHandlers) 72 | { 73 | someAfterSaveHandlersFound |= services.RegisterHandlersIfNotAlreadyRegistered(debugLogs, 74 | typeof(IAfterSaveEventHandler<>), assembly); 75 | someAfterSaveHandlersFound |= services.RegisterHandlersIfNotAlreadyRegistered(debugLogs, 76 | typeof(IAfterSaveEventHandlerAsync<>), assembly); 77 | } 78 | } 79 | 80 | if (!someDuringSaveHandlersFound) 81 | { 82 | debugLogs.Add(config.NotUsingDuringSaveHandlers 83 | ? "You manually turned off During event handlers." 84 | : "No During event handlers were found, so turned that part off."); 85 | 86 | config.NotUsingDuringSaveHandlers = true; 87 | } 88 | if (!someAfterSaveHandlersFound) 89 | { 90 | debugLogs.Add(config.NotUsingAfterSaveHandlers 91 | ? "You manually turned off After event handlers." 92 | : "No After event handlers were found, so turned that part off."); 93 | 94 | config.NotUsingAfterSaveHandlers = true; 95 | } 96 | 97 | if (services.Contains(new ServiceDescriptor(typeof(IEventsRunner), typeof(EventsRunner), ServiceLifetime.Transient), 98 | new ServiceDescriptorNoLifeTimeCompare())) 99 | throw new InvalidOperationException("You can only call this method once to register the GenericEventRunner and event handlers."); 100 | services.AddSingleton(config); 101 | services.AddTransient(); 102 | debugLogs.Add($"Finished by registering the {nameof(EventsRunner)} and {nameof(GenericEventRunnerConfig)}"); 103 | 104 | return debugLogs; 105 | } 106 | 107 | private static bool RegisterHandlersIfNotAlreadyRegistered(this IServiceCollection services, 108 | List debugLogs, 109 | Type interfaceToLookFor, Assembly assembly) 110 | { 111 | var noLifeTimeCompare = new ServiceDescriptorNoLifeTimeCompare(); 112 | var someFound = false; 113 | foreach (var (implementationType, interfaceType) in ClassesWithGivenEventHandlerType(interfaceToLookFor, assembly)) 114 | { 115 | var attr = implementationType.GetCustomAttribute(); 116 | var lifeTime = attr?.HandlerLifetime ?? ServiceLifetime.Transient; 117 | 118 | var genericPart = interfaceType.GetGenericArguments(); 119 | var indexCharToRemove = interfaceType.Name.IndexOf('`'); 120 | var displayInterface = $"{interfaceType.Name.Substring(0, indexCharToRemove)}<{genericPart.Single().Name}>"; 121 | 122 | if (services.Contains(new ServiceDescriptor(interfaceType, implementationType, lifeTime), noLifeTimeCompare)) 123 | debugLogs.Add($"Already registered {implementationType.Name} as {displayInterface}"); 124 | else 125 | { 126 | if (lifeTime == ServiceLifetime.Transient) 127 | services.AddTransient(interfaceType, implementationType); 128 | else if (lifeTime == ServiceLifetime.Scoped) 129 | services.AddScoped(interfaceType, implementationType); 130 | else 131 | services.AddSingleton(interfaceType, implementationType); 132 | debugLogs.Add($"Registered {implementationType.Name} as {displayInterface}. Lifetime: {lifeTime}"); 133 | } 134 | someFound = true; 135 | } 136 | 137 | return someFound; 138 | } 139 | 140 | private static IEnumerable<(Type classType, Type interfaceType)> ClassesWithGivenEventHandlerType(Type interfaceToLookFor, Assembly assembly) 141 | { 142 | var allGenericClasses = assembly.GetExportedTypes() 143 | .Where(y => y.IsClass && !y.IsAbstract && !y.IsGenericType && !y.IsNested); 144 | var classesWithIHandle = from classType in allGenericClasses 145 | let interfaceType = classType.GetInterfaces() 146 | .SingleOrDefault(y => y.IsGenericType && y.GetGenericTypeDefinition() == interfaceToLookFor) 147 | where interfaceType != null 148 | select (classType, interfaceType); 149 | return classesWithIHandle; 150 | } 151 | 152 | } 153 | } -------------------------------------------------------------------------------- /GenericEventRunner/ForSetup/ServiceDescriptorIncludeLifeTimeCompare.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using Microsoft.Extensions.DependencyInjection; 7 | 8 | namespace GenericEventRunner.ForSetup 9 | { 10 | public class ServiceDescriptorIncludeLifeTimeCompare : IEqualityComparer 11 | { 12 | public bool Equals(ServiceDescriptor x, ServiceDescriptor y) 13 | { 14 | return x.ServiceType == y.ServiceType 15 | && x.ImplementationType == y.ImplementationType 16 | && x.Lifetime == y.Lifetime; 17 | } 18 | 19 | public int GetHashCode(ServiceDescriptor obj) 20 | { 21 | throw new NotImplementedException(); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /GenericEventRunner/ForSetup/ServiceDescriptorNoLifeTimeCompare.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using Microsoft.Extensions.DependencyInjection; 7 | 8 | namespace GenericEventRunner.ForSetup 9 | { 10 | public class ServiceDescriptorNoLifeTimeCompare : IEqualityComparer 11 | { 12 | public bool Equals(ServiceDescriptor x, ServiceDescriptor y) 13 | { 14 | return x.ServiceType == y.ServiceType 15 | && x.ImplementationType == y.ImplementationType; 16 | } 17 | 18 | public int GetHashCode(ServiceDescriptor obj) 19 | { 20 | throw new NotImplementedException(); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /GenericEventRunner/GenericEventRunner.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.1 5 | 6 | 7 | 8 | true 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | true 19 | 2.3.3 20 | 2.3.3 21 | 2.3.3.0 22 | 2.3.3.0 23 | EfCore.GenericEventRunner 24 | Jon P Smith 25 | Selective Analytics 26 | EfCore.GenericEventRunner 27 | A library to provide domain event handling to EF Core. 28 | Copyright (c) 2019 Jon P Smith 29 | https://github.com/JonPSmith/EfCore.GenericEventRunner 30 | https://github.com/JonPSmith/EfCore.GenericEventRunner 31 | GitHub 32 | EfCore.GenericServices, EfCore.GenericEventRunner 33 | 34 | - Bug Fix - calling `SaveChangesAsync` does not return the number of database updates - see issue #6 35 | 36 | GenericEventsRunnerNuGetIcon128.png 37 | MIT 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /GenericEventRunner/GenericEventsRunnerNuGetIcon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JonPSmith/EfCore.GenericEventRunner/e556e175b9d8ea9bf742974b8891a11b37cf5f4a/GenericEventRunner/GenericEventsRunnerNuGetIcon128.png -------------------------------------------------------------------------------- /GenericEventRunnerTypesOfEvents.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JonPSmith/EfCore.GenericEventRunner/e556e175b9d8ea9bf742974b8891a11b37cf5f4a/GenericEventRunnerTypesOfEvents.png -------------------------------------------------------------------------------- /GenericEventsRunnerDomainNuGetIcon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JonPSmith/EfCore.GenericEventRunner/e556e175b9d8ea9bf742974b8891a11b37cf5f4a/GenericEventsRunnerDomainNuGetIcon128.png -------------------------------------------------------------------------------- /GenericEventsRunnerNuGetIcon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JonPSmith/EfCore.GenericEventRunner/e556e175b9d8ea9bf742974b8891a11b37cf5f4a/GenericEventsRunnerNuGetIcon128.png -------------------------------------------------------------------------------- /Infrastructure/AfterEventHandlers/DeDupAfterEventHandler.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using EntityClasses.DomainEvents; 5 | using GenericEventRunner.ForHandlers; 6 | 7 | namespace Infrastructure.AfterEventHandlers 8 | { 9 | public class DeDupAfterEventHandler : IAfterSaveEventHandler 10 | { 11 | public void Handle(object callingEntity, DeDupEvent domainEvent) 12 | { 13 | domainEvent.ActionToCall(); 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /Infrastructure/AfterEventHandlers/OrderReadyToDispatchAfterHandler.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using DataLayer; 5 | using EntityClasses.DomainEvents; 6 | using GenericEventRunner.DomainParts; 7 | using GenericEventRunner.ForHandlers; 8 | 9 | namespace Infrastructure.AfterEventHandlers 10 | { 11 | public class OrderReadyToDispatchAfterHandler : IAfterSaveEventHandler 12 | { 13 | public void Handle(object callingEntity, OrderReadyToDispatchEvent domainEvent) 14 | { 15 | //Send message to dispatch that order has been checked and is ready to go 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /Infrastructure/BeforeEventHandlers/AllocateProductHander.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using DataLayer; 6 | using EntityClasses; 7 | using EntityClasses.DomainEvents; 8 | using GenericEventRunner.DomainParts; 9 | using GenericEventRunner.ForHandlers; 10 | using StatusGeneric; 11 | 12 | namespace Infrastructure.BeforeEventHandlers 13 | { 14 | public class AllocateProductHandler : IBeforeSaveEventHandler 15 | { 16 | private readonly ExampleDbContext _context; 17 | 18 | public AllocateProductHandler(ExampleDbContext context) 19 | { 20 | _context = context; 21 | } 22 | 23 | public IStatusGeneric Handle(object callingEntity, AllocateProductEvent domainEvent) 24 | { 25 | var status = new StatusGenericHandler(); 26 | var stock = _context.Find(domainEvent.ProductName); 27 | if (stock == null) 28 | throw new ApplicationException($"could not find the stock for product called {domainEvent.ProductName} "); 29 | 30 | if (stock.NumInStock < domainEvent.NumOrdered) 31 | return status.AddError( 32 | $"I could not accept this order because there wasn't enough {domainEvent.ProductName} in stock."); 33 | 34 | stock.NumAllocated += domainEvent.NumOrdered; 35 | return status; 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /Infrastructure/BeforeEventHandlers/DeDupBeforeEventHandler.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using EntityClasses.DomainEvents; 5 | using GenericEventRunner.ForHandlers; 6 | using StatusGeneric; 7 | 8 | namespace Infrastructure.BeforeEventHandlers 9 | { 10 | public class DeDupBeforeEventHandler : IBeforeSaveEventHandler 11 | { 12 | public IStatusGeneric Handle(object callingEntity, DeDupEvent domainEvent) 13 | { 14 | domainEvent.ActionToCall(); 15 | return null; 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /Infrastructure/BeforeEventHandlers/Internal/TaxRateLookup.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Linq; 6 | using DataLayer; 7 | 8 | namespace Infrastructure.BeforeEventHandlers.Internal 9 | { 10 | public class TaxRateLookup 11 | { 12 | private readonly ExampleDbContext _context; 13 | 14 | public TaxRateLookup(ExampleDbContext context) 15 | { 16 | _context = context; 17 | } 18 | 19 | public decimal GetTaxRateInEffect(DateTime expectedDispatchDate) 20 | { 21 | var taxRateToUse = _context.TaxRates.OrderByDescending(x => x.EffectiveFrom) 22 | .FirstOrDefault(x => x.EffectiveFrom <= expectedDispatchDate); 23 | 24 | if (taxRateToUse == null) 25 | throw new InvalidOperationException($"There was no take rate valid for the date {expectedDispatchDate:yyyy MMMM dd}"); 26 | 27 | return taxRateToUse.TaxRatePercent; 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /Infrastructure/BeforeEventHandlers/OrderCreatedHandler.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Linq; 6 | using DataLayer; 7 | using EntityClasses.DomainEvents; 8 | using GenericEventRunner.DomainParts; 9 | using GenericEventRunner.ForHandlers; 10 | using Infrastructure.BeforeEventHandlers.Internal; 11 | using StatusGeneric; 12 | 13 | namespace Infrastructure.BeforeEventHandlers 14 | { 15 | public class OrderCreatedHandler : IBeforeSaveEventHandler 16 | { 17 | private readonly TaxRateLookup _rateFinder; 18 | 19 | public OrderCreatedHandler(ExampleDbContext context) 20 | { 21 | _rateFinder = new TaxRateLookup(context); 22 | } 23 | 24 | public IStatusGeneric Handle(object callingEntity, OrderCreatedEvent domainEvent) 25 | { 26 | var tax = _rateFinder.GetTaxRateInEffect(domainEvent.ExpectedDispatchDate); 27 | domainEvent.SetTaxRatePercent(tax); 28 | 29 | return null; 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /Infrastructure/BeforeEventHandlers/OrderDispatchedBeforeHandler.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.Linq; 5 | using DataLayer; 6 | using EntityClasses; 7 | using EntityClasses.DomainEvents; 8 | using GenericEventRunner.DomainParts; 9 | using GenericEventRunner.ForHandlers; 10 | using Infrastructure.BeforeEventHandlers.Internal; 11 | using StatusGeneric; 12 | 13 | namespace Infrastructure.BeforeEventHandlers 14 | { 15 | public class OrderDispatchedBeforeHandler : IBeforeSaveEventHandler 16 | { 17 | private readonly ExampleDbContext _context; 18 | private readonly TaxRateLookup _rateFinder; 19 | 20 | public OrderDispatchedBeforeHandler(ExampleDbContext context) 21 | { 22 | _context = context; 23 | _rateFinder = new TaxRateLookup(context); 24 | } 25 | 26 | public IStatusGeneric Handle(object callingEntity, OrderReadyToDispatchEvent domainEvent) 27 | { 28 | var status = new StatusGenericHandler(); 29 | //Update the rate as the date may have changed 30 | domainEvent.SetTaxRatePercent(_rateFinder.GetTaxRateInEffect(domainEvent.ActualDispatchDate)); 31 | 32 | var orderId = ((Order) callingEntity).OrderId; 33 | foreach (var lineItem in _context.LineItems.Where(x => x.OrderId == orderId)) 34 | { 35 | var stock = _context.Find(lineItem.ProductName); 36 | if (stock.NumInStock < lineItem.NumOrdered) 37 | return status.AddError( 38 | $"I could not dispatch this order because there wasn't enough {lineItem.ProductName} in stock."); 39 | stock.NumAllocated -= lineItem.NumOrdered; 40 | stock.NumInStock -= lineItem.NumOrdered; 41 | } 42 | 43 | return status; 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /Infrastructure/BeforeEventHandlers/TaxRateChangedHandler.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using EntityClasses.DomainEvents; 5 | using GenericEventRunner.DomainParts; 6 | using GenericEventRunner.ForHandlers; 7 | using StatusGeneric; 8 | 9 | namespace Infrastructure.BeforeEventHandlers 10 | { 11 | public class TaxRateChangedHandler : IBeforeSaveEventHandler 12 | { 13 | public IStatusGeneric Handle(object callingEntity, TaxRateChangedEvent domainEvent) 14 | { 15 | //do something with the new tax rate 16 | 17 | domainEvent.RefreshGrandTotalPrice(); //example of using Action/Func to set something inside the calling class 18 | 19 | return new StatusGenericHandler(); 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /Infrastructure/DuringEventHandlers/DeDupDuringEventHandler.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using EntityClasses.DomainEvents; 6 | using GenericEventRunner.ForHandlers; 7 | using StatusGeneric; 8 | 9 | namespace Infrastructure.DuringEventHandlers 10 | { 11 | public class DeDupDuringEventHandler : IDuringSaveEventHandler 12 | { 13 | public IStatusGeneric Handle(object callingEntity, DeDupEvent domainEvent, Guid uniqueKey) 14 | { 15 | domainEvent.ActionToCall(); 16 | return null; 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /Infrastructure/DuringEventHandlers/NewBookDuringButBeforeSaveEventHandler.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using EntityClasses.DomainEvents; 6 | using GenericEventRunner.ForHandlers; 7 | using Microsoft.Extensions.Logging; 8 | using StatusGeneric; 9 | 10 | namespace Infrastructure.DuringEventHandlers 11 | { 12 | public class NewBookDuringButBeforeSaveEventHandler : IDuringSaveEventHandler 13 | { 14 | private readonly ILogger _logger; 15 | 16 | public NewBookDuringButBeforeSaveEventHandler(ILogger logger) 17 | { 18 | _logger = logger; 19 | } 20 | 21 | public IStatusGeneric Handle(object callingEntity, NewBookEventButBeforeSave domainEvent, Guid uniqueKey) 22 | { 23 | _logger.LogInformation($"Log from {GetType().Name}. Unique value = {uniqueKey}"); 24 | return new StatusGenericHandler(); 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /Infrastructure/DuringEventHandlers/NewBookDuringButBeforeSaveEventHandlerAsync.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Threading.Tasks; 6 | using EntityClasses.DomainEvents; 7 | using GenericEventRunner.ForHandlers; 8 | using Microsoft.Extensions.Logging; 9 | using StatusGeneric; 10 | 11 | namespace Infrastructure.DuringEventHandlers 12 | { 13 | public class NewBookDuringButBeforeSaveEventHandlerAsync : IDuringSaveEventHandlerAsync 14 | { 15 | private readonly ILogger _logger; 16 | 17 | public NewBookDuringButBeforeSaveEventHandlerAsync(ILogger logger) 18 | { 19 | _logger = logger; 20 | } 21 | 22 | public async Task HandleAsync(object callingEntity, NewBookEventButBeforeSave domainEvent, Guid uniqueKey) 23 | { 24 | _logger.LogInformation($"Log from {GetType().Name}. Unique value = {uniqueKey}"); 25 | return new StatusGenericHandler(); 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /Infrastructure/DuringEventHandlers/NewBookDuringEventHandler.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using EntityClasses.DomainEvents; 6 | using GenericEventRunner.ForHandlers; 7 | using Microsoft.Extensions.Logging; 8 | using StatusGeneric; 9 | 10 | namespace Infrastructure.DuringEventHandlers 11 | { 12 | public class NewBookDuringEventHandler : IDuringSaveEventHandler 13 | { 14 | private readonly ILogger _logger; 15 | 16 | public NewBookDuringEventHandler(ILogger logger) 17 | { 18 | _logger = logger; 19 | } 20 | 21 | public IStatusGeneric Handle(object callingEntity, NewBookEvent domainEvent, Guid uniqueKey) 22 | { 23 | _logger.LogInformation($"Log from {GetType().Name}. Unique value = {uniqueKey}"); 24 | return new StatusGenericHandler(); 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /Infrastructure/DuringEventHandlers/NewBookDuringEventHandlerAsync.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Threading.Tasks; 6 | using EntityClasses.DomainEvents; 7 | using GenericEventRunner.ForHandlers; 8 | using Microsoft.Extensions.Logging; 9 | using StatusGeneric; 10 | 11 | namespace Infrastructure.DuringEventHandlers 12 | { 13 | public class NewBookDuringEventHandlerAsync : IDuringSaveEventHandlerAsync 14 | { 15 | private readonly ILogger _logger; 16 | 17 | public NewBookDuringEventHandlerAsync(ILogger logger) 18 | { 19 | _logger = logger; 20 | } 21 | 22 | public async Task HandleAsync(object callingEntity, NewBookEvent domainEvent, Guid uniqueKey) 23 | { 24 | _logger.LogInformation($"Log from {GetType().Name}. Unique value = {uniqueKey}"); 25 | return new StatusGenericHandler(); 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /Infrastructure/Infrastructure.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Jon P Smith - Selective Analytics Ltd. 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 | -------------------------------------------------------------------------------- /OnlyAfterHandlers/AfterHandler.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using EntityClasses.DomainEvents; 5 | using GenericEventRunner.ForHandlers; 6 | 7 | namespace OnlyAfterHandlers 8 | { 9 | public class AfterHandler : IAfterSaveEventHandler 10 | { 11 | public void Handle(object callingEntity, OrderReadyToDispatchEvent domainEvent) 12 | { 13 | //Send message to dispatch that order has been checked and is ready to go 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /OnlyAfterHandlers/OnlyAfterHandlers.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /OnlyBeforeHandlers/BeforeHandler.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using EntityClasses.DomainEvents; 5 | using GenericEventRunner.ForHandlers; 6 | using StatusGeneric; 7 | 8 | namespace OnlyBeforeHandlers 9 | { 10 | public class BeforeHandler : IBeforeSaveEventHandler 11 | { 12 | 13 | public IStatusGeneric Handle(object callingEntity, OrderCreatedEvent domainEvent) 14 | { 15 | return null; 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /OnlyBeforeHandlers/OnlyBeforeHandlers.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /OnlyDuringHandlers/DuringHandler.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using EntityClasses.DomainEvents; 6 | using GenericEventRunner.ForHandlers; 7 | using StatusGeneric; 8 | 9 | namespace OnlyDuringHandlers 10 | { 11 | public class DuringHandler : IDuringSaveEventHandler 12 | { 13 | public IStatusGeneric Handle(object callingEntity, NewBookEvent domainEvent, Guid uniqueKey) 14 | { 15 | return new StatusGenericHandler(); 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /OnlyDuringHandlers/OnlyDuringHandlers.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EfCore.GenericEventRunner 2 | 3 | This library allows users of Entity Framework Core (EF Core) to add events to their entity classes, i.e. the classes that EF Core maps to a database. It is useful if you have business rules that are triggered by a property changing, or an event such as receiving an customer order and you need to check some things before you can accept it. 4 | 5 | This is an open source project (MIT license) available [on GitHub](https://github.com/JonPSmith/EfCore.GenericEventRunner) and as a [NuGet package](https://www.nuget.org/packages/EfCore.GenericEventRunner/). See [ReleaseNotes](https://github.com/JonPSmith/EfCore.GenericEventRunner/blob/master/ReleaseNotes.md) for more information. 6 | 7 | Documentation and links to articles can be found via the [Documentation link](https://github.com/JonPSmith/EfCore.GenericEventRunner/wiki). 8 | 9 | ## Three types of events 10 | 11 | The following image shows the three types of events and when they are called. 12 | 13 | ![Three types of events](https://github.com/JonPSmith/EfCore.GenericEventRunner/blob/master/GenericEventRunnerTypesOfEvents.png) 14 | 15 | ## Useful articles 16 | 17 | * [Article about this event-driven architecture](https://www.thereformedprogrammer.net/a-robust-event-driven-architecture-for-using-with-entity-framework-core/) - good to get an idea of what the library is trying to do. 18 | * [The "how" and "why" of the EfCore.GenericEventRunner library](https://www.thereformedprogrammer.net/efcore-genericeventrunner-an-event-driven-library-that-works-with-ef-core/) - read this for detailed documentation. 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /ReleaseNotes.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JonPSmith/EfCore.GenericEventRunner/e556e175b9d8ea9bf742974b8891a11b37cf5f4a/ReleaseNotes.md -------------------------------------------------------------------------------- /Test/EfHelpers/SeedExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using DataLayer; 7 | using EntityClasses; 8 | using GenericEventRunner.ForSetup; 9 | using Microsoft.EntityFrameworkCore; 10 | using TestSupport.EfHelpers; 11 | 12 | namespace Test.EfHelpers 13 | { 14 | public static class SeedExtensions 15 | { 16 | 17 | public static ExampleDbContext CreateAndSeedDbWithDiForHandlers(this DbContextOptions options, 18 | List logs = null, IGenericEventRunnerConfig config = null) where TRunner : class 19 | { 20 | var context = options.CreateDbWithDiForHandlers(logs, config); 21 | 22 | context.Database.EnsureCreated(); 23 | context.SeedTaxAndStock(); 24 | 25 | return context; 26 | } 27 | 28 | public static List SeedTaxAndStock(this ExampleDbContext context) 29 | { 30 | context.SeedTwoTaxRates(); 31 | return context.SeedExampleProductStock(); 32 | } 33 | 34 | public static List SeedExampleProductStock(this ExampleDbContext context) 35 | { 36 | var prodStocks = new List 37 | { 38 | new ProductStock("Product1", 5), 39 | new ProductStock("Product2", 10), 40 | new ProductStock("Product3", 20), 41 | }; 42 | context.AddRange(prodStocks); 43 | context.SaveChanges(); 44 | return prodStocks; 45 | } 46 | 47 | public static List SeedTwoTaxRates(this ExampleDbContext context) 48 | { 49 | var rateNow = new TaxRate(DateTime.Today, 4); 50 | var rate2Days = new TaxRate(DateTime.Today.AddDays(2), 9); 51 | context.AddRange(rateNow, rate2Days); 52 | context.SaveChanges(); 53 | return new List{ rateNow, rate2Days}; 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /Test/EfHelpers/SetupToTestEvents.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Reflection; 8 | using GenericEventRunner.ForDbContext; 9 | using GenericEventRunner.ForHandlers; 10 | using GenericEventRunner.ForSetup; 11 | using Microsoft.EntityFrameworkCore; 12 | using Microsoft.Extensions.DependencyInjection; 13 | using Microsoft.Extensions.Logging; 14 | using Microsoft.Extensions.Logging.Abstractions; 15 | using TestSupport.EfHelpers; 16 | 17 | namespace Test.EfHelpers 18 | { 19 | public static class SetupToTestEvents 20 | { 21 | 22 | /// 23 | /// This extension method provides a way to set up a DbContext with an EventsRunner and also registers all 24 | /// the event handlers in the assembly that the TRunner class is in. 25 | /// 26 | /// Your DbContext type 27 | /// The type of one of your event handlers. 28 | /// The whole assembly that the TRunner is in will be scanned for event handlers 29 | /// The T:DbContextOptions{TContext} for your DbContext 30 | /// Optional. If provided the it uses the EfCore.TestSupport logging provider to return logs 31 | /// Optional. Allows you to change the configuration setting for GenericEventRunner 32 | /// An instance of the DbContext created by DI and therefore containing the EventsRunner 33 | public static TContext CreateDbWithDiForHandlers(this DbContextOptions options, 34 | List logs = null, IGenericEventRunnerConfig config = null) where TContext : DbContext where THandler : class 35 | { 36 | var services = new ServiceCollection(); 37 | if (logs != null) 38 | { 39 | services.AddSingleton>(new Logger(new LoggerFactory(new[] { new MyLoggerProvider(logs) }))); 40 | } 41 | else 42 | { 43 | services.AddSingleton>(new NullLogger()); 44 | } 45 | 46 | var assembliesToScan = new Assembly[] 47 | { 48 | Assembly.GetAssembly(typeof(THandler)), 49 | Assembly.GetExecutingAssembly() //This will pick up any event handlers in your unit tests assembly 50 | }; 51 | 52 | var debug = services.RegisterGenericEventRunner(config ?? new GenericEventRunnerConfig(), assembliesToScan); 53 | services.AddSingleton(options); 54 | services.AddScoped(); 55 | var serviceProvider = services.BuildServiceProvider(); 56 | var context = serviceProvider.GetRequiredService(); 57 | return context; 58 | } 59 | 60 | /// 61 | /// This registers the GenericEventRunner parts, plus the list of specific event handlers you have provided 62 | /// It then creates the given TContext which should use the GenericEventRunner 63 | /// 64 | /// 65 | /// 66 | /// 67 | /// 68 | /// 69 | public static TContext CreateDbWithDiForHandlers(this DbContextOptions options, 70 | List logs, params Type[] eventHandlers) where TContext : DbContext 71 | { 72 | var services = new ServiceCollection(); 73 | services.AddSingleton>( 74 | new Logger(new LoggerFactory(new[] {new MyLoggerProvider(logs)}))); 75 | services.AddSingleton(new GenericEventRunnerConfig()); 76 | services.AddScoped(); 77 | 78 | foreach (var eventHandler in eventHandlers) 79 | { 80 | var typeInterface = eventHandler.GetInterfaces().Single(); 81 | services.AddTransient(typeInterface ,eventHandler); 82 | } 83 | 84 | services.AddSingleton(options); 85 | services.AddScoped(); 86 | var serviceProvider = services.BuildServiceProvider(); 87 | var context = serviceProvider.GetRequiredService(); 88 | return context; 89 | } 90 | } 91 | } -------------------------------------------------------------------------------- /Test/EfHelpers/SqlServerWithExecution.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using Microsoft.EntityFrameworkCore; 5 | using TestSupport.Helpers; 6 | 7 | namespace Test.EfHelpers 8 | { 9 | public static class SqlServerWithExecution 10 | { 11 | public static DbContextOptions CreateOptionsWithRetryExecutions(this object callingClass) where TContext : DbContext 12 | { 13 | var connectionString = callingClass.GetUniqueDatabaseConnectionString(); 14 | var builder = new DbContextOptionsBuilder(); 15 | builder.UseSqlServer(connectionString, options => options.EnableRetryOnFailure()); 16 | 17 | return builder.Options; 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /Test/EventsAndHandlers/AfterHandlerDoNothing.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using GenericEventRunner.DomainParts; 5 | using GenericEventRunner.ForHandlers; 6 | using StatusGeneric; 7 | 8 | namespace Test.EventsAndHandlers 9 | { 10 | public class AfterHandlerDoNothing : IAfterSaveEventHandler 11 | { 12 | public void Handle(object callingEntity, EventDoNothing domainEvent) 13 | { 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /Test/EventsAndHandlers/AfterHandlerDoNothingAsync.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.Threading.Tasks; 5 | using GenericEventRunner.DomainParts; 6 | using GenericEventRunner.ForHandlers; 7 | 8 | namespace Test.EventsAndHandlers 9 | { 10 | public class AfterHandlerDoNothingAsync : IAfterSaveEventHandlerAsync 11 | { 12 | 13 | public Task HandleAsync(object callingEntity, EventDoNothing domainEvent) 14 | { 15 | return Task.CompletedTask; 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /Test/EventsAndHandlers/AfterHandlerThrowsException.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using GenericEventRunner.DomainParts; 6 | using GenericEventRunner.ForHandlers; 7 | 8 | namespace Test.EventsAndHandlers 9 | { 10 | public class AfterHandlerThrowsException : IAfterSaveEventHandler 11 | { 12 | public void Handle(object callingEntity, EventTestAfterExceptionHandler domainEvent) 13 | { 14 | throw new ApplicationException(nameof(AfterHandlerThrowsException)); 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /Test/EventsAndHandlers/BeforeHandlerCircularEvent.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using GenericEventRunner.DomainParts; 5 | using GenericEventRunner.ForHandlers; 6 | using StatusGeneric; 7 | 8 | namespace Test.EventsAndHandlers 9 | { 10 | public class BeforeHandlerCircularEvent : IBeforeSaveEventHandler 11 | { 12 | public IStatusGeneric Handle(object callingEntity, EventCircularEvent domainEvent) 13 | { 14 | ((EntityEventsBase)callingEntity).AddEvent(domainEvent); 15 | return null; 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /Test/EventsAndHandlers/BeforeHandlerDoNothing.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using GenericEventRunner.DomainParts; 5 | using GenericEventRunner.ForHandlers; 6 | using StatusGeneric; 7 | 8 | namespace Test.EventsAndHandlers 9 | { 10 | public class BeforeHandlerDoNothing : IBeforeSaveEventHandler 11 | { 12 | public IStatusGeneric Handle(object callingEntity, EventDoNothing domainEvent) 13 | { 14 | return null; 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /Test/EventsAndHandlers/BeforeHandlerDoNothingAsync.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.Threading.Tasks; 5 | using GenericEventRunner.DomainParts; 6 | using GenericEventRunner.ForHandlers; 7 | using StatusGeneric; 8 | 9 | namespace Test.EventsAndHandlers 10 | { 11 | public class BeforeHandlerDoNothingAsync : IBeforeSaveEventHandlerAsync 12 | { 13 | public Task HandleAsync(object callingEntity, EventDoNothing domainEvent) 14 | { 15 | return Task.FromResult(null); 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /Test/EventsAndHandlers/BeforeHandlerReturnsErrorStatus.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using GenericEventRunner.DomainParts; 5 | using GenericEventRunner.ForHandlers; 6 | using StatusGeneric; 7 | 8 | namespace Test.EventsAndHandlers 9 | { 10 | public class BeforeHandlerReturnsErrorStatus : IBeforeSaveEventHandler 11 | { 12 | public IStatusGeneric Handle(object callingEntity, EventTestBeforeReturnError domainEvent) 13 | { 14 | ((EntityEventsBase)callingEntity).AddEvent(new EventDoNothing()); 15 | return new StatusGenericHandler().AddError("This is a test"); 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /Test/EventsAndHandlers/BeforeHandlerThrowsException.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using GenericEventRunner.DomainParts; 6 | using GenericEventRunner.ForHandlers; 7 | using StatusGeneric; 8 | 9 | namespace Test.EventsAndHandlers 10 | { 11 | public class BeforeHandlerThrowsException : IBeforeSaveEventHandler 12 | { 13 | public IStatusGeneric Handle(object callingEntity, EventTestBeforeExceptionHandler domainEvent) 14 | { 15 | throw new ApplicationException(nameof(BeforeHandlerThrowsException)); 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /Test/EventsAndHandlers/BeforeHandlerThrowsExceptionWithAttribute.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using GenericEventRunner.DomainParts; 6 | using GenericEventRunner.ForHandlers; 7 | using Microsoft.Extensions.DependencyInjection; 8 | using StatusGeneric; 9 | 10 | namespace Test.EventsAndHandlers 11 | { 12 | [EventHandlerConfig(ServiceLifetime.Scoped)] 13 | public class BeforeHandlerThrowsExceptionWithAttribute : IBeforeSaveEventHandler 14 | { 15 | public IStatusGeneric Handle(object callingEntity, EventTestExceptionHandlerWithAttribute domainEvent) 16 | { 17 | throw new ApplicationException(nameof(BeforeHandlerThrowsExceptionWithAttribute)); 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /Test/EventsAndHandlers/DuringHandlerDoNothing.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using GenericEventRunner.ForHandlers; 6 | using StatusGeneric; 7 | 8 | namespace Test.EventsAndHandlers 9 | { 10 | public class DuringHandlerDoNothing : IDuringSaveEventHandler 11 | { 12 | public IStatusGeneric Handle(object callingEntity, EventDoNothing domainEvent, Guid uniqueGuid) 13 | { 14 | return new StatusGenericHandler(); 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /Test/EventsAndHandlers/DuringHandlerReturnsErrorStatus.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using GenericEventRunner.DomainParts; 6 | using GenericEventRunner.ForHandlers; 7 | using StatusGeneric; 8 | 9 | namespace Test.EventsAndHandlers 10 | { 11 | public class DuringHandlerReturnsErrorStatus : IDuringSaveEventHandler 12 | { 13 | public IStatusGeneric Handle(object callingEntity, EventTestDuringReturnError domainEvent, Guid uniqueKey) 14 | { 15 | ((EntityEventsBase)callingEntity).AddEvent(new EventDoNothing()); 16 | return new StatusGenericHandler().AddError("This is a test"); 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /Test/EventsAndHandlers/DuringHandlerReturnsErrorStatusAsync.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Threading.Tasks; 6 | using GenericEventRunner.DomainParts; 7 | using GenericEventRunner.ForHandlers; 8 | using StatusGeneric; 9 | 10 | namespace Test.EventsAndHandlers 11 | { 12 | public class DuringHandlerReturnsErrorStatusAsync : IDuringSaveEventHandlerAsync 13 | { 14 | public async Task HandleAsync(object callingEntity, EventTestDuringReturnError domainEvent, Guid uniqueKey) 15 | { 16 | ((EntityEventsBase)callingEntity).AddEvent(new EventDoNothing()); 17 | return new StatusGenericHandler().AddError("This is a test"); 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /Test/EventsAndHandlers/DuringHandlerThrowsException.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using GenericEventRunner.ForHandlers; 6 | using StatusGeneric; 7 | 8 | namespace Test.EventsAndHandlers 9 | { 10 | public class DuringHandlerThrowsException : IDuringSaveEventHandler 11 | { 12 | public IStatusGeneric Handle(object callingEntity, EventTestDuringExceptionHandler domainEvent, Guid uniqueKey) 13 | { 14 | throw new ApplicationException(nameof(DuringHandlerThrowsException)); 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /Test/EventsAndHandlers/DuringHandlerThrowsExceptionAsync.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Threading.Tasks; 6 | using GenericEventRunner.ForHandlers; 7 | using StatusGeneric; 8 | 9 | namespace Test.EventsAndHandlers 10 | { 11 | public class DuringHandlerThrowsExceptionAsync : IDuringSaveEventHandlerAsync 12 | { 13 | public async Task HandleAsync(object callingEntity, EventTestDuringExceptionHandler domainEvent, Guid uniqueKey) 14 | { 15 | throw new ApplicationException(nameof(DuringHandlerThrowsException)); 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /Test/EventsAndHandlers/DuringPreHandlerReturnsErrorStatus.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using GenericEventRunner.DomainParts; 6 | using GenericEventRunner.ForHandlers; 7 | using StatusGeneric; 8 | 9 | namespace Test.EventsAndHandlers 10 | { 11 | public class DuringPreHandlerReturnsErrorStatus : IDuringSaveEventHandler 12 | { 13 | public IStatusGeneric Handle(object callingEntity, EventTestDuringPreReturnError domainEvent, Guid uniqueKey) 14 | { 15 | ((EntityEventsBase)callingEntity).AddEvent(new EventDoNothing()); 16 | return new StatusGenericHandler().AddError("This is a test"); 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /Test/EventsAndHandlers/DuringPreHandlerReturnsErrorStatusAsync.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Threading.Tasks; 6 | using GenericEventRunner.DomainParts; 7 | using GenericEventRunner.ForHandlers; 8 | using StatusGeneric; 9 | 10 | namespace Test.EventsAndHandlers 11 | { 12 | public class DuringPreHandlerReturnsErrorStatusAsync : IDuringSaveEventHandlerAsync 13 | { 14 | public async Task HandleAsync(object callingEntity, EventTestDuringPreReturnError domainEvent, Guid uniqueKey) 15 | { 16 | ((EntityEventsBase)callingEntity).AddEvent(new EventDoNothing()); 17 | return new StatusGenericHandler().AddError("This is a test"); 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /Test/EventsAndHandlers/DuringPreHandlerThrowsException.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using GenericEventRunner.ForHandlers; 6 | using StatusGeneric; 7 | 8 | namespace Test.EventsAndHandlers 9 | { 10 | public class DuringPreHandlerThrowsException : IDuringSaveEventHandler 11 | { 12 | public IStatusGeneric Handle(object callingEntity, EventTestDuringPreExceptionHandler domainEvent, Guid uniqueKey) 13 | { 14 | throw new ApplicationException(nameof(DuringHandlerThrowsException)); 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /Test/EventsAndHandlers/DuringPreHandlerThrowsExceptionAsync.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Threading.Tasks; 6 | using GenericEventRunner.ForHandlers; 7 | using StatusGeneric; 8 | 9 | namespace Test.EventsAndHandlers 10 | { 11 | public class DuringPreHandlerThrowsExceptionAsync : IDuringSaveEventHandlerAsync 12 | { 13 | public async Task HandleAsync(object callingEntity, EventTestDuringPreExceptionHandler domainEvent, Guid uniqueKey) 14 | { 15 | throw new ApplicationException(nameof(DuringHandlerThrowsException)); 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /Test/EventsAndHandlers/EventAfterHandlerThrowsException.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using GenericEventRunner.DomainParts; 5 | 6 | namespace Test.EventsAndHandlers 7 | { 8 | public class EventAfterHandlerThrowsException : IEntityEvent 9 | { 10 | 11 | } 12 | } -------------------------------------------------------------------------------- /Test/EventsAndHandlers/EventCircularEvent.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using GenericEventRunner.DomainParts; 5 | 6 | namespace Test.EventsAndHandlers 7 | { 8 | public class EventCircularEvent : IEntityEvent 9 | { 10 | 11 | } 12 | } -------------------------------------------------------------------------------- /Test/EventsAndHandlers/EventDoNothing.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using GenericEventRunner.DomainParts; 5 | 6 | namespace Test.EventsAndHandlers 7 | { 8 | public class EventDoNothing : IEntityEvent 9 | { 10 | 11 | } 12 | } -------------------------------------------------------------------------------- /Test/EventsAndHandlers/EventTestAfterExceptionHandler.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using GenericEventRunner.DomainParts; 5 | 6 | namespace Test.EventsAndHandlers 7 | { 8 | public class EventTestAfterExceptionHandler : IEntityEvent 9 | { 10 | 11 | } 12 | } -------------------------------------------------------------------------------- /Test/EventsAndHandlers/EventTestBeforeExceptionHandler.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using GenericEventRunner.DomainParts; 5 | 6 | namespace Test.EventsAndHandlers 7 | { 8 | public class EventTestBeforeExceptionHandler : IEntityEvent 9 | { 10 | 11 | } 12 | } -------------------------------------------------------------------------------- /Test/EventsAndHandlers/EventTestBeforeReturnError.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using GenericEventRunner.DomainParts; 5 | 6 | namespace Test.EventsAndHandlers 7 | { 8 | public class EventTestBeforeReturnError : IEntityEvent 9 | { 10 | 11 | } 12 | } -------------------------------------------------------------------------------- /Test/EventsAndHandlers/EventTestDuringExceptionHandler.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using GenericEventRunner.DomainParts; 5 | 6 | namespace Test.EventsAndHandlers 7 | { 8 | public class EventTestDuringExceptionHandler : IEntityEvent 9 | { 10 | 11 | } 12 | } -------------------------------------------------------------------------------- /Test/EventsAndHandlers/EventTestDuringPreExceptionHandler.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using GenericEventRunner.DomainParts; 5 | 6 | namespace Test.EventsAndHandlers 7 | { 8 | [MakeDuringEventRunBeforeSaveChanges()] 9 | public class EventTestDuringPreExceptionHandler : IEntityEvent 10 | { 11 | 12 | } 13 | } -------------------------------------------------------------------------------- /Test/EventsAndHandlers/EventTestDuringPreReturnError.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using GenericEventRunner.DomainParts; 5 | 6 | namespace Test.EventsAndHandlers 7 | { 8 | public class EventTestDuringPreReturnError : IEntityEvent 9 | { 10 | 11 | } 12 | } -------------------------------------------------------------------------------- /Test/EventsAndHandlers/EventTestDuringReturnError.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using GenericEventRunner.DomainParts; 5 | 6 | namespace Test.EventsAndHandlers 7 | { 8 | public class EventTestDuringReturnError : IEntityEvent 9 | { 10 | 11 | } 12 | } -------------------------------------------------------------------------------- /Test/EventsAndHandlers/EventTestExceptionHandlerWithAttribute.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using GenericEventRunner.DomainParts; 5 | 6 | namespace Test.EventsAndHandlers 7 | { 8 | public class EventTestExceptionHandlerWithAttribute : IEntityEvent 9 | { 10 | 11 | } 12 | } -------------------------------------------------------------------------------- /Test/EventsAndHandlers/EventWithNoHandler.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using GenericEventRunner.DomainParts; 5 | 6 | namespace Test.EventsAndHandlers 7 | { 8 | public class EventWithNoHandler : IEntityEvent 9 | { 10 | 11 | } 12 | } -------------------------------------------------------------------------------- /Test/Test.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.1 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | all 17 | runtime; build; native; contentfiles; analyzers; buildtransitive 18 | 19 | 20 | all 21 | runtime; build; native; contentfiles; analyzers; buildtransitive 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /Test/UnitTests/DataLayerTests/TestExampleDbContext.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | using DataLayer; 8 | using EntityClasses; 9 | using Test.EfHelpers; 10 | using TestSupport.EfHelpers; 11 | using Xunit; 12 | using Xunit.Extensions.AssertExtensions; 13 | 14 | namespace Test.UnitTests.DataLayerTests 15 | { 16 | public class TestExampleDbContextNoEvents 17 | { 18 | [Fact] 19 | public void TestCreateDatabaseSeedTaxAndStock() 20 | { 21 | //SETUP 22 | var options = SqliteInMemory.CreateOptions(); 23 | using (var context = new ExampleDbContext(options)) 24 | { 25 | 26 | //ATTEMPT 27 | context.Database.EnsureCreated(); 28 | context.SeedTaxAndStock(); 29 | 30 | //VERIFY 31 | context.TaxRates.Count().ShouldEqual(2); 32 | context.ProductStocks.Count().ShouldEqual(3); 33 | } 34 | } 35 | 36 | [Fact] 37 | public void TestCheckSaveChangesReturningCount() 38 | { 39 | //SETUP 40 | var options = SqliteInMemory.CreateOptions(); 41 | using (var context = new ExampleDbContext(options)) 42 | { 43 | context.Database.EnsureCreated(); 44 | 45 | //ATTEMPT 46 | context.Add(new TaxRate(DateTime.UtcNow, 4)); 47 | var numUpdates = context.SaveChanges(); 48 | 49 | //VERIFY 50 | numUpdates.ShouldEqual(1); 51 | } 52 | } 53 | 54 | [Fact] 55 | public async Task TestCheckSaveChangesAsyncReturningCount() 56 | { 57 | //SETUP 58 | var options = SqliteInMemory.CreateOptions(); 59 | using (var context = new ExampleDbContext(options)) 60 | { 61 | context.Database.EnsureCreated(); 62 | 63 | //ATTEMPT 64 | context.Add(new TaxRate(DateTime.UtcNow, 4)); 65 | var numUpdates = await context.SaveChangesAsync(); 66 | 67 | //VERIFY 68 | numUpdates.ShouldEqual(1); 69 | } 70 | } 71 | 72 | } 73 | } -------------------------------------------------------------------------------- /Test/UnitTests/InfrastructureTests/TestAsyncEventHandlers.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Threading.Tasks; 8 | using DataLayer; 9 | using EntityClasses; 10 | using GenericEventRunner.DomainParts; 11 | using GenericEventRunner.ForHandlers; 12 | using Test.EfHelpers; 13 | using Test.EventsAndHandlers; 14 | using TestSupport.EfHelpers; 15 | using Xunit; 16 | using Xunit.Extensions.AssertExtensions; 17 | 18 | namespace Test.UnitTests.InfrastructureTests 19 | { 20 | public class TestAsyncEventHandlers 21 | { 22 | [Fact] 23 | public void TestCreateDbWithDiForHandlersOneEvent() 24 | { 25 | //SETUP 26 | var options = SqliteInMemory.CreateOptions(); 27 | var logs = new List(); 28 | var context = options.CreateDbWithDiForHandlers(logs, typeof(BeforeHandlerDoNothing)); 29 | { 30 | context.Database.EnsureCreated(); 31 | var entity = context.SeedTwoTaxRates().First(); 32 | 33 | //ATTEMPT 34 | entity.AddEvent(new EventDoNothing()); 35 | context.SaveChanges(); 36 | 37 | //VERIFY 38 | logs.Count.ShouldEqual(1); 39 | logs[0].Message.ShouldEqual("B1: About to run a BeforeSave event handler Test.EventsAndHandlers.BeforeHandlerDoNothing."); 40 | } 41 | } 42 | 43 | [Fact] 44 | public void TestCreateDbWithDiForHandlersTwoEvents() 45 | { 46 | //SETUP 47 | var options = SqliteInMemory.CreateOptions(); 48 | var logs = new List(); 49 | var context = options.CreateDbWithDiForHandlers(logs, typeof(BeforeHandlerDoNothing), typeof(AfterHandlerDoNothing)); 50 | { 51 | context.Database.EnsureCreated(); 52 | var entity = context.SeedTwoTaxRates().First(); 53 | 54 | //ATTEMPT 55 | entity.AddEvent(new EventDoNothing(), EventToSend.BeforeAndAfterSave); 56 | context.SaveChanges(); 57 | 58 | //VERIFY 59 | logs.Count.ShouldEqual(2); 60 | logs[0].Message.ShouldEqual("B1: About to run a BeforeSave event handler Test.EventsAndHandlers.BeforeHandlerDoNothing."); 61 | logs[1].Message.ShouldEqual("A1: About to run a AfterSave event handler Test.EventsAndHandlers.AfterHandlerDoNothing."); 62 | } 63 | } 64 | 65 | [Fact] 66 | public async Task TestCreateDbWithDiForHandlersBeforeHandlerDoNothingAsyncOk() 67 | { 68 | //SETUP 69 | var options = SqliteInMemory.CreateOptions(); 70 | var logs = new List(); 71 | var context = options.CreateDbWithDiForHandlers(logs, typeof(BeforeHandlerDoNothingAsync)); 72 | { 73 | context.Database.EnsureCreated(); 74 | var entity = context.SeedTwoTaxRates().First(); 75 | 76 | //ATTEMPT 77 | entity.AddEvent(new EventDoNothing()); 78 | await context.SaveChangesAsync(); 79 | 80 | //VERIFY 81 | logs.Count.ShouldEqual(1); 82 | logs[0].Message.ShouldEqual("B1: About to run a BeforeSave event handler Test.EventsAndHandlers.BeforeHandlerDoNothingAsync."); 83 | } 84 | } 85 | 86 | [Fact] 87 | public async Task TestCreateDbWithDiForHandlersBeforeHandlerDoNothingSyncAndAsyncOk() 88 | { 89 | //SETUP 90 | var options = SqliteInMemory.CreateOptions(); 91 | var logs = new List(); 92 | var context = options.CreateDbWithDiForHandlers(logs, typeof(BeforeHandlerDoNothing), typeof(BeforeHandlerDoNothingAsync)); 93 | { 94 | context.Database.EnsureCreated(); 95 | var entity = context.SeedTwoTaxRates().First(); 96 | 97 | //ATTEMPT 98 | entity.AddEvent(new EventDoNothing()); 99 | await context.SaveChangesAsync(); 100 | 101 | //VERIFY 102 | logs.Count.ShouldEqual(1); 103 | logs[0].Message.ShouldEqual("B1: About to run a BeforeSave event handler Test.EventsAndHandlers.BeforeHandlerDoNothingAsync."); 104 | } 105 | } 106 | 107 | [Fact] 108 | public void TestCreateDbWithDiForHandlersBeforeHandlerDoNothingAsyncBad() 109 | { 110 | //SETUP 111 | var options = SqliteInMemory.CreateOptions(); 112 | var logs = new List(); 113 | var context = options.CreateDbWithDiForHandlers(logs, typeof(BeforeHandlerDoNothingAsync)); 114 | { 115 | context.Database.EnsureCreated(); 116 | var entity = context.SeedTwoTaxRates().First(); 117 | 118 | //ATTEMPT 119 | entity.AddEvent(new EventDoNothing()); 120 | var ex = Assert.Throws(() => context.SaveChanges()); 121 | 122 | //VERIFY 123 | ex.Message.ShouldEqual("Could not find a BeforeSave event handler for the event EventDoNothing. Their was a suitable async event handler available, but you didn't call SaveChangesAsync."); 124 | } 125 | } 126 | 127 | [Fact] 128 | public async Task TestCreateDbWithDiForHandlersAfterHandlerDoNothingAsyncOk() 129 | { 130 | //SETUP 131 | var options = SqliteInMemory.CreateOptions(); 132 | var logs = new List(); 133 | var context = options.CreateDbWithDiForHandlers(logs, typeof(AfterHandlerDoNothingAsync)); 134 | { 135 | context.Database.EnsureCreated(); 136 | var entity = context.SeedTwoTaxRates().First(); 137 | 138 | //ATTEMPT 139 | entity.AddEvent(new EventDoNothing(), EventToSend.AfterSave); 140 | await context.SaveChangesAsync(); 141 | 142 | //VERIFY 143 | logs.Count.ShouldEqual(1); 144 | logs[0].Message.ShouldEqual("A1: About to run a AfterSave event handler Test.EventsAndHandlers.AfterHandlerDoNothingAsync."); 145 | } 146 | } 147 | 148 | [Fact] 149 | public async Task TestCreateDbWithDiForHandlersAfterHandlerDoNothingSyncAndAsyncOk() 150 | { 151 | //SETUP 152 | var options = SqliteInMemory.CreateOptions(); 153 | var logs = new List(); 154 | var context = options.CreateDbWithDiForHandlers(logs, typeof(AfterHandlerDoNothing), typeof(AfterHandlerDoNothingAsync)); 155 | { 156 | context.Database.EnsureCreated(); 157 | var entity = context.SeedTwoTaxRates().First(); 158 | 159 | //ATTEMPT 160 | entity.AddEvent(new EventDoNothing(), EventToSend.AfterSave); 161 | await context.SaveChangesAsync(); 162 | 163 | //VERIFY 164 | logs.Count.ShouldEqual(1); 165 | logs[0].Message.ShouldEqual("A1: About to run a AfterSave event handler Test.EventsAndHandlers.AfterHandlerDoNothingAsync."); 166 | } 167 | } 168 | 169 | [Fact] 170 | public void TestCreateDbWithDiForHandlersAfterHandlerDoNothingAsyncBad() 171 | { 172 | //SETUP 173 | var options = SqliteInMemory.CreateOptions(); 174 | var logs = new List(); 175 | var context = options.CreateDbWithDiForHandlers(logs, typeof(AfterHandlerDoNothingAsync)); 176 | { 177 | context.Database.EnsureCreated(); 178 | var entity = context.SeedTwoTaxRates().First(); 179 | 180 | //ATTEMPT 181 | entity.AddEvent(new EventDoNothing(), EventToSend.AfterSave); 182 | var ex = Assert.Throws(() => context.SaveChanges()); 183 | 184 | //VERIFY 185 | ex.Message.ShouldEqual("Could not find a AfterSave event handler for the event EventDoNothing. Their was a suitable async event handler available, but you didn't call SaveChangesAsync."); 186 | } 187 | } 188 | } 189 | 190 | } -------------------------------------------------------------------------------- /Test/UnitTests/InfrastructureTests/TestDeDupEvents.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using DataLayer; 8 | using EntityClasses; 9 | using EntityClasses.DomainEvents; 10 | using GenericEventRunner.DomainParts; 11 | using GenericEventRunner.ForHandlers.Internal; 12 | using Infrastructure.BeforeEventHandlers; 13 | using Test.EfHelpers; 14 | using TestSupport.EfHelpers; 15 | using Xunit; 16 | using Xunit.Abstractions; 17 | using Xunit.Extensions.AssertExtensions; 18 | 19 | namespace Test.UnitTests.InfrastructureTests 20 | { 21 | public class TestDeDupEvents 22 | { 23 | private readonly ITestOutputHelper _output; 24 | 25 | public TestDeDupEvents(ITestOutputHelper output) 26 | { 27 | _output = output; 28 | } 29 | 30 | 31 | [Fact] 32 | public void TestEntityAndEventComparer() 33 | { 34 | var tax1 = new TaxRate(DateTime.Now, 123); 35 | var tax2 = new TaxRate(DateTime.Now, 123); 36 | var d1 = new EntityAndEvent(tax1, new DeDupEvent(() => { })); 37 | var d2 = new EntityAndEvent(tax1, new DeDupEvent(() => { })); 38 | 39 | var d3 = new EntityAndEvent(tax2, new DeDupEvent(() => { })); 40 | 41 | var n1 = new EntityAndEvent(tax1, new NewBookEvent()); 42 | var n2 = new EntityAndEvent(tax1, new NewBookEvent()); 43 | 44 | d1.Equals(d2).ShouldBeTrue(); 45 | d1.Equals(d3).ShouldBeFalse(); 46 | 47 | 48 | d1.Equals(n1).ShouldBeFalse(); 49 | n1.Equals(n2).ShouldBeFalse(); 50 | 51 | var ees = (new List { d1,d2,d3,n1,n2 }).Distinct().ToList(); 52 | ees.Select(x => x.EntityEvent.GetType().Name).ShouldEqual(new []{ "DeDupEvent", "DeDupEvent", "NewBookEvent", "NewBookEvent" }); 53 | ees[0].ShouldEqual(d1); 54 | ees[1].ShouldEqual(d3); 55 | } 56 | 57 | [Fact] 58 | public void TestEntityAndEventComparerDistinctOrder() 59 | { 60 | var tax1 = new TaxRate(DateTime.Now, 123); 61 | var tax2 = new TaxRate(DateTime.Now, 123); 62 | var d1 = new EntityAndEvent(tax1, new DeDupEvent(() => { })); 63 | var d2 = new EntityAndEvent(tax1, new DeDupEvent(() => { })); 64 | 65 | var d3 = new EntityAndEvent(tax2, new DeDupEvent(() => { })); 66 | var d4 = new EntityAndEvent(tax2, new DeDupEvent(() => { })); 67 | 68 | var ees = (new List { d1, d2, d3, d2, d4}).Distinct().ToList(); 69 | ees.Count.ShouldEqual(2); 70 | ReferenceEquals(ees[0], d1).ShouldBeTrue(); 71 | ReferenceEquals(ees[0], d1).ShouldBeTrue(); 72 | ees[0].ShouldEqual(d1); 73 | ees[1].ShouldEqual(d3); 74 | } 75 | 76 | [Fact] 77 | public void TestDeDupBeforeEvents() 78 | { 79 | //SETUP 80 | var options = SqliteInMemory.CreateOptions(); 81 | var logs = new List(); 82 | var context = options.CreateAndSeedDbWithDiForHandlers(logs); 83 | { 84 | var tax = new TaxRate(DateTime.Now, 123); 85 | context.Add(tax); 86 | context.SaveChanges(); 87 | logs.Clear(); 88 | 89 | //ATTEMPT 90 | var normalCount = 0; 91 | var deDepCount = 0; 92 | tax.AddEvent(new TaxRateChangedEvent(123, () => normalCount++)); 93 | tax.AddEvent(new TaxRateChangedEvent(123, () => normalCount++)); 94 | tax.AddEvent(new DeDupEvent(() => deDepCount++)); 95 | tax.AddEvent(new DeDupEvent(() => deDepCount++)); 96 | 97 | context.SaveChanges(); 98 | 99 | //VERIFY 100 | normalCount.ShouldEqual(2); 101 | deDepCount.ShouldEqual(1); 102 | foreach (var logOutput in logs) 103 | { 104 | _output.WriteLine(logOutput.Message); 105 | } 106 | } 107 | } 108 | 109 | [Fact] 110 | public void TestDeDupBeforeDuringAfterEvents() 111 | { 112 | //SETUP 113 | var options = SqliteInMemory.CreateOptions(); 114 | var logs = new List(); 115 | var context = options.CreateAndSeedDbWithDiForHandlers(logs); 116 | { 117 | var tax = new TaxRate(DateTime.Now, 123); 118 | context.Add(tax); 119 | context.SaveChanges(); 120 | logs.Clear(); 121 | 122 | //ATTEMPT 123 | var deDepCount = 0; 124 | tax.AddEvent(new DeDupEvent(() => deDepCount++), EventToSend.BeforeAndAfterSave); 125 | tax.AddEvent(new DeDupEvent(() => deDepCount++), EventToSend.BeforeAndAfterSave); 126 | tax.AddEvent(new DeDupEvent(() => deDepCount++), EventToSend.DuringSave); 127 | tax.AddEvent(new DeDupEvent(() => deDepCount++), EventToSend.DuringSave); 128 | 129 | context.SaveChanges(); 130 | 131 | //VERIFY 132 | deDepCount.ShouldEqual(3); 133 | foreach (var logOutput in logs) 134 | { 135 | _output.WriteLine(logOutput.Message); 136 | } 137 | } 138 | } 139 | } 140 | } -------------------------------------------------------------------------------- /Test/UnitTests/InfrastructureTests/TestEventSaveChangesExceptionHandler.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | using DataLayer; 8 | using EntityClasses; 9 | using GenericEventRunner.ForSetup; 10 | using Infrastructure.BeforeEventHandlers; 11 | using Microsoft.EntityFrameworkCore; 12 | using StatusGeneric; 13 | using Test.EfHelpers; 14 | using TestSupport.EfHelpers; 15 | using Xunit; 16 | using Xunit.Extensions.AssertExtensions; 17 | 18 | namespace Test.UnitTests.InfrastructureTests 19 | { 20 | public class TestEventSaveChangesExceptionHandler 21 | { 22 | [Fact] 23 | public void TestUpdateProductStockOk() 24 | { 25 | //SETUP 26 | var options = SqliteInMemory.CreateOptions(); 27 | var context = options.CreateAndSeedDbWithDiForHandlers(); 28 | { 29 | var stock = context.ProductStocks.OrderBy(x => x.NumInStock).First(); 30 | 31 | //ATTEMPT 32 | stock.NumAllocated = 2; 33 | context.SaveChanges(); 34 | 35 | //VERIFY 36 | var foundStock = context.Find(stock.ProductName); 37 | foundStock.NumAllocated.ShouldEqual(2); 38 | } 39 | } 40 | 41 | [Fact] 42 | public void TestUpdateProductStockConcurrencyNoHandler() 43 | { 44 | //SETUP 45 | var options = SqliteInMemory.CreateOptions(); 46 | var context = options.CreateAndSeedDbWithDiForHandlers(); 47 | { 48 | var stock = context.ProductStocks.OrderBy(x => x.NumInStock).First(); 49 | 50 | //ATTEMPT 51 | stock.NumAllocated = 2; 52 | context.Database.ExecuteSqlRaw( 53 | "UPDATE ProductStocks SET NumAllocated = @p0 WHERE ProductName = @p1", 3, stock.ProductName); 54 | var ex = Assert.Throws(() => context.SaveChanges()); 55 | 56 | //VERIFY 57 | ex.Message.ShouldStartWith("Database operation expected to affect 1 row(s) but actually affected 0 row(s)."); 58 | } 59 | } 60 | 61 | [Fact] 62 | public void TestUpdateProductStockConcurrencyWithHandler() 63 | { 64 | //SETUP 65 | var options = SqliteInMemory.CreateOptions(); 66 | var config = new GenericEventRunnerConfig(); 67 | config.RegisterSaveChangesExceptionHandler(CatchAndFixConcurrencyException); 68 | var context = options.CreateAndSeedDbWithDiForHandlers( config: config); 69 | { 70 | var stock = context.ProductStocks.OrderBy(x => x.NumInStock).First(); 71 | 72 | //ATTEMPT 73 | stock.NumAllocated = 2; 74 | context.Database.ExecuteSqlRaw( 75 | "UPDATE ProductStocks SET NumAllocated = @p0 WHERE ProductName = @p1", 3, stock.ProductName); 76 | context.SaveChanges(); 77 | 78 | //VERIFY 79 | var foundStock = context.Find(stock.ProductName); 80 | foundStock.NumAllocated.ShouldEqual(5); 81 | } 82 | } 83 | 84 | [Fact] 85 | public async Task TestUpdateProductStockConcurrencyWithHandlerAsync() 86 | { 87 | //SETUP 88 | var options = SqliteInMemory.CreateOptions(); 89 | var config = new GenericEventRunnerConfig(); 90 | config.RegisterSaveChangesExceptionHandler(CatchAndFixConcurrencyException); 91 | var context = options.CreateAndSeedDbWithDiForHandlers(config: config); 92 | { 93 | var stock = context.ProductStocks.OrderBy(x => x.NumInStock).First(); 94 | 95 | //ATTEMPT 96 | stock.NumAllocated = 2; 97 | context.Database.ExecuteSqlRaw( 98 | "UPDATE ProductStocks SET NumAllocated = @p0 WHERE ProductName = @p1", 3, stock.ProductName); 99 | await context.SaveChangesAsync(); 100 | 101 | //VERIFY 102 | var foundStock = context.Find(stock.ProductName); 103 | foundStock.NumAllocated.ShouldEqual(5); 104 | } 105 | } 106 | 107 | //--------------------------------------------- 108 | // private methods 109 | 110 | private IStatusGeneric CatchAndFixConcurrencyException(Exception ex, DbContext context) 111 | { 112 | var dbUpdateEx = ex as DbUpdateConcurrencyException; 113 | if (dbUpdateEx == null || dbUpdateEx.Entries.Count != 1) 114 | return null; //can't handle this error 115 | 116 | var entry = dbUpdateEx.Entries.Single(); 117 | if (!(entry.Entity is ProductStock failedUpdate)) 118 | return null; 119 | 120 | var status = new StatusGenericHandler(); 121 | //This entity MUST be read as NoTracking otherwise it will interfere with the same entity we are trying to write 122 | var overwroteData 123 | = context.Set().AsNoTracking().SingleOrDefault(p => p.ProductName == failedUpdate.ProductName); 124 | if (overwroteData == null) 125 | //The ProductStock was deleted 126 | return status.AddError("The product you were interested in has been removed from our stock."); 127 | 128 | var addedChange = failedUpdate.NumAllocated - (int)entry.Property(nameof(ProductStock.NumAllocated)).OriginalValue ; 129 | var combinedAlloc = overwroteData.NumAllocated + addedChange; 130 | 131 | entry.Property(nameof(ProductStock.NumAllocated)).CurrentValue = combinedAlloc; 132 | entry.Property(nameof(ProductStock.NumAllocated)).OriginalValue = overwroteData.NumAllocated; 133 | 134 | return status; 135 | } 136 | 137 | 138 | } 139 | } -------------------------------------------------------------------------------- /Test/UnitTests/InfrastructureTests/TestRegisterEventHandlers.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Linq; 6 | using System.Reflection; 7 | using EntityClasses.DomainEvents; 8 | using GenericEventRunner.ForHandlers; 9 | using GenericEventRunner.ForSetup; 10 | using Microsoft.Extensions.DependencyInjection; 11 | using Test.EventsAndHandlers; 12 | using Xunit; 13 | using Xunit.Abstractions; 14 | using Xunit.Extensions.AssertExtensions; 15 | 16 | namespace Test.UnitTests.InfrastructureTests 17 | { 18 | public class TestRegisterEventHandlers 19 | { 20 | private readonly ITestOutputHelper _output; 21 | 22 | public TestRegisterEventHandlers(ITestOutputHelper output) 23 | { 24 | _output = output; 25 | } 26 | 27 | [Fact] 28 | public void TestRegisterTwiceBad() 29 | { 30 | //SETUP 31 | var services = new ServiceCollection(); 32 | 33 | //ATTEMPT 34 | services.AddTransient(); 35 | services.AddTransient(); 36 | 37 | //VERIFY 38 | services.Count.ShouldEqual(2); 39 | } 40 | 41 | [Fact] 42 | public void TestRegisterEventHandlersNormalOk() 43 | { 44 | //SETUP 45 | var services = new ServiceCollection(); 46 | 47 | //ATTEMPT 48 | var logs = services.RegisterGenericEventRunner(Assembly 49 | .GetAssembly(typeof(BeforeHandlerThrowsExceptionWithAttribute))); 50 | 51 | //VERIFY 52 | //Before event handlers 53 | services.Contains(new ServiceDescriptor(typeof(IBeforeSaveEventHandler), 54 | typeof(BeforeHandlerCircularEvent), 55 | ServiceLifetime.Transient), new ServiceDescriptorIncludeLifeTimeCompare()).ShouldBeTrue(); 56 | services.Contains(new ServiceDescriptor(typeof(IBeforeSaveEventHandler), 57 | typeof(BeforeHandlerThrowsExceptionWithAttribute), 58 | ServiceLifetime.Scoped), new ServiceDescriptorIncludeLifeTimeCompare()).ShouldBeTrue(); 59 | 60 | //During event handlers 61 | services.Contains(new ServiceDescriptor(typeof(IDuringSaveEventHandler), 62 | typeof(DuringHandlerDoNothing), 63 | ServiceLifetime.Transient), new ServiceDescriptorIncludeLifeTimeCompare()).ShouldBeTrue(); 64 | 65 | //After event handlers 66 | services.Contains(new ServiceDescriptor(typeof(IAfterSaveEventHandler), 67 | typeof(AfterHandlerThrowsException), 68 | ServiceLifetime.Transient), new ServiceDescriptorIncludeLifeTimeCompare()).ShouldBeTrue(); 69 | //Async event handlers 70 | services.Contains(new ServiceDescriptor(typeof(IBeforeSaveEventHandlerAsync), 71 | typeof(BeforeHandlerDoNothingAsync), 72 | ServiceLifetime.Transient), new ServiceDescriptorIncludeLifeTimeCompare()).ShouldBeTrue(); 73 | services.Contains(new ServiceDescriptor(typeof(IAfterSaveEventHandlerAsync), 74 | typeof(AfterHandlerDoNothingAsync), 75 | ServiceLifetime.Transient), new ServiceDescriptorIncludeLifeTimeCompare()).ShouldBeTrue(); 76 | 77 | foreach (var log in logs) 78 | { 79 | _output.WriteLine(log); 80 | } 81 | } 82 | 83 | [Fact] 84 | public void TestRegisterEventHandlersWithConfigTuningOffDuringHandlers() 85 | { 86 | //SETUP 87 | var services = new ServiceCollection(); 88 | var config = new GenericEventRunnerConfig {NotUsingDuringSaveHandlers = true}; 89 | 90 | //ATTEMPT 91 | services.RegisterGenericEventRunner(config, Assembly.GetAssembly(typeof(BeforeHandlerThrowsExceptionWithAttribute))); 92 | 93 | //VERIFY 94 | services.Contains(new ServiceDescriptor(typeof(IBeforeSaveEventHandler), 95 | typeof(BeforeHandlerCircularEvent), 96 | ServiceLifetime.Transient), new ServiceDescriptorIncludeLifeTimeCompare()).ShouldBeTrue(); 97 | services.Contains(new ServiceDescriptor(typeof(IBeforeSaveEventHandler), 98 | typeof(BeforeHandlerThrowsExceptionWithAttribute), 99 | ServiceLifetime.Scoped), new ServiceDescriptorIncludeLifeTimeCompare()).ShouldBeTrue(); 100 | services.Contains(new ServiceDescriptor(typeof(IAfterSaveEventHandler), 101 | typeof(AfterHandlerThrowsException), 102 | ServiceLifetime.Transient), new ServiceDescriptorIncludeLifeTimeCompare()).ShouldBeTrue(); 103 | services.Contains(new ServiceDescriptor(typeof(IAfterSaveEventHandlerAsync), 104 | typeof(AfterHandlerDoNothingAsync), 105 | ServiceLifetime.Transient), new ServiceDescriptorIncludeLifeTimeCompare()).ShouldBeTrue(); 106 | 107 | //During event handlers 108 | services.Contains(new ServiceDescriptor(typeof(IDuringSaveEventHandler), 109 | typeof(DuringHandlerDoNothing), 110 | ServiceLifetime.Transient), new ServiceDescriptorIncludeLifeTimeCompare()).ShouldBeFalse(); 111 | } 112 | 113 | [Fact] 114 | public void TestRegisterEventHandlersWithConfigTuningOffAfterHandlers() 115 | { 116 | //SETUP 117 | var services = new ServiceCollection(); 118 | var config = new GenericEventRunnerConfig { NotUsingAfterSaveHandlers = true }; 119 | 120 | //ATTEMPT 121 | services.RegisterGenericEventRunner(config, Assembly.GetAssembly(typeof(BeforeHandlerThrowsExceptionWithAttribute))); 122 | 123 | //VERIFY 124 | services.Contains(new ServiceDescriptor(typeof(IBeforeSaveEventHandler), 125 | typeof(BeforeHandlerCircularEvent), 126 | ServiceLifetime.Transient), new ServiceDescriptorIncludeLifeTimeCompare()).ShouldBeTrue(); 127 | services.Contains(new ServiceDescriptor(typeof(IBeforeSaveEventHandler), 128 | typeof(BeforeHandlerThrowsExceptionWithAttribute), 129 | ServiceLifetime.Scoped), new ServiceDescriptorIncludeLifeTimeCompare()).ShouldBeTrue(); 130 | 131 | //During event handlers 132 | services.Contains(new ServiceDescriptor(typeof(IDuringSaveEventHandler), 133 | typeof(DuringHandlerDoNothing), 134 | ServiceLifetime.Transient), new ServiceDescriptorIncludeLifeTimeCompare()).ShouldBeTrue(); 135 | 136 | //after event handlers 137 | services.Contains(new ServiceDescriptor(typeof(IAfterSaveEventHandler), 138 | typeof(AfterHandlerThrowsException), 139 | ServiceLifetime.Transient), new ServiceDescriptorIncludeLifeTimeCompare()).ShouldBeFalse(); 140 | services.Contains(new ServiceDescriptor(typeof(IAfterSaveEventHandlerAsync), 141 | typeof(AfterHandlerDoNothingAsync), 142 | ServiceLifetime.Transient), new ServiceDescriptorIncludeLifeTimeCompare()).ShouldBeFalse(); 143 | } 144 | 145 | [Fact] 146 | public void TestRegisterEventHandlersTwiceBad() 147 | { 148 | //SETUP 149 | var services = new ServiceCollection(); 150 | 151 | //ATTEMPT 152 | services.RegisterGenericEventRunner(Assembly.GetAssembly(typeof(BeforeHandlerThrowsExceptionWithAttribute))); 153 | var ex = Assert.Throws(() => 154 | services.RegisterGenericEventRunner( 155 | Assembly.GetAssembly(typeof(BeforeHandlerThrowsExceptionWithAttribute)))); 156 | 157 | //VERIFY 158 | ex.Message.ShouldEqual("You can only call this method once to register the GenericEventRunner and event handlers."); 159 | } 160 | 161 | 162 | } 163 | } -------------------------------------------------------------------------------- /Test/UnitTests/InfrastructureTests/TestRegisterEventHandlersIfAlreadyRegistered.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.Linq; 5 | using System.Reflection; 6 | using EntityClasses.DomainEvents; 7 | using GenericEventRunner.ForHandlers; 8 | using GenericEventRunner.ForSetup; 9 | using Microsoft.Extensions.DependencyInjection; 10 | using OnlyAfterHandlers; 11 | using OnlyBeforeHandlers; 12 | using OnlyDuringHandlers; 13 | using Xunit; 14 | using Xunit.Abstractions; 15 | using Xunit.Extensions.AssertExtensions; 16 | 17 | namespace Test.UnitTests.InfrastructureTests 18 | { 19 | public class TestRegisterEventHandlersIfAlreadyRegistered 20 | { 21 | private readonly ITestOutputHelper _output; 22 | 23 | public TestRegisterEventHandlersIfAlreadyRegistered(ITestOutputHelper output) 24 | { 25 | _output = output; 26 | } 27 | 28 | [Fact] 29 | public void TestRegisterEventHandlersThreeProjects() 30 | { 31 | //SETUP 32 | var services = new ServiceCollection(); 33 | var config = new GenericEventRunnerConfig(); 34 | 35 | //ATTEMPT 36 | var logs = services.RegisterGenericEventRunner(config, 37 | Assembly.GetAssembly(typeof(BeforeHandler)), 38 | Assembly.GetAssembly(typeof(DuringHandler)), 39 | Assembly.GetAssembly(typeof(AfterHandler)) 40 | ); 41 | 42 | //VERIFY 43 | services.Contains(new ServiceDescriptor(typeof(IBeforeSaveEventHandler), 44 | typeof(BeforeHandler), 45 | ServiceLifetime.Transient), new ServiceDescriptorIncludeLifeTimeCompare()).ShouldBeTrue(); 46 | services.Contains(new ServiceDescriptor(typeof(IDuringSaveEventHandler), 47 | typeof(DuringHandler), 48 | ServiceLifetime.Transient), new ServiceDescriptorIncludeLifeTimeCompare()).ShouldBeTrue(); 49 | services.Contains(new ServiceDescriptor(typeof(IAfterSaveEventHandler), 50 | typeof(AfterHandler), 51 | ServiceLifetime.Transient), new ServiceDescriptorIncludeLifeTimeCompare()).ShouldBeTrue(); 52 | 53 | foreach (var log in logs) 54 | { 55 | _output.WriteLine(log); 56 | } 57 | 58 | config.NotUsingDuringSaveHandlers.ShouldBeFalse(); 59 | config.NotUsingAfterSaveHandlers.ShouldBeFalse(); 60 | services.Count.ShouldEqual(5); 61 | } 62 | 63 | 64 | [Fact] 65 | public void TestRegisterEventHandlersOnlyBefore() 66 | { 67 | //SETUP 68 | var services = new ServiceCollection(); 69 | var config = new GenericEventRunnerConfig(); 70 | 71 | //ATTEMPT 72 | var logs = services.RegisterGenericEventRunner(config, 73 | Assembly.GetAssembly(typeof(BeforeHandler)) 74 | ); 75 | 76 | //VERIFY 77 | services.Contains(new ServiceDescriptor(typeof(IBeforeSaveEventHandler), 78 | typeof(BeforeHandler), 79 | ServiceLifetime.Transient), new ServiceDescriptorIncludeLifeTimeCompare()).ShouldBeTrue(); 80 | 81 | foreach (var log in logs) 82 | { 83 | _output.WriteLine(log); 84 | } 85 | 86 | config.NotUsingDuringSaveHandlers.ShouldBeTrue(); 87 | config.NotUsingAfterSaveHandlers.ShouldBeTrue(); 88 | services.Count.ShouldEqual(3); 89 | } 90 | 91 | [Fact] 92 | public void TestRegisterEventHandlersBeforeAlreadyRegisteredThreeProjects() 93 | { 94 | //SETUP 95 | var services = new ServiceCollection(); 96 | var config = new GenericEventRunnerConfig(); 97 | services.AddTransient, BeforeHandler>(); 98 | 99 | //ATTEMPT 100 | var logs = services.RegisterGenericEventRunner(config, 101 | Assembly.GetAssembly(typeof(BeforeHandler)), 102 | Assembly.GetAssembly(typeof(DuringHandler)), 103 | Assembly.GetAssembly(typeof(AfterHandler)) 104 | ); 105 | 106 | //VERIFY 107 | services.Contains(new ServiceDescriptor(typeof(IBeforeSaveEventHandler), 108 | typeof(BeforeHandler), 109 | ServiceLifetime.Transient), new ServiceDescriptorIncludeLifeTimeCompare()).ShouldBeTrue(); 110 | services.Contains(new ServiceDescriptor(typeof(IDuringSaveEventHandler), 111 | typeof(DuringHandler), 112 | ServiceLifetime.Transient), new ServiceDescriptorIncludeLifeTimeCompare()).ShouldBeTrue(); 113 | services.Contains(new ServiceDescriptor(typeof(IAfterSaveEventHandler), 114 | typeof(AfterHandler), 115 | ServiceLifetime.Transient), new ServiceDescriptorIncludeLifeTimeCompare()).ShouldBeTrue(); 116 | 117 | foreach (var log in logs) 118 | { 119 | _output.WriteLine(log); 120 | } 121 | 122 | config.NotUsingDuringSaveHandlers.ShouldBeFalse(); 123 | config.NotUsingAfterSaveHandlers.ShouldBeFalse(); 124 | services.Count.ShouldEqual(5); 125 | } 126 | 127 | [Fact] 128 | public void TestRegisterEventHandlersBeforeAlreadyRegisteredJustBefore() 129 | { 130 | //SETUP 131 | var services = new ServiceCollection(); 132 | var config = new GenericEventRunnerConfig(); 133 | services.AddTransient, BeforeHandler>(); 134 | 135 | //ATTEMPT 136 | var logs = services.RegisterGenericEventRunner(config, 137 | Assembly.GetAssembly(typeof(BeforeHandler)) 138 | ); 139 | 140 | //VERIFY 141 | services.Contains(new ServiceDescriptor(typeof(IBeforeSaveEventHandler), 142 | typeof(BeforeHandler), 143 | ServiceLifetime.Transient), new ServiceDescriptorIncludeLifeTimeCompare()).ShouldBeTrue(); ; 144 | 145 | foreach (var log in logs) 146 | { 147 | _output.WriteLine(log); 148 | } 149 | 150 | config.NotUsingDuringSaveHandlers.ShouldBeTrue(); 151 | config.NotUsingAfterSaveHandlers.ShouldBeTrue(); 152 | services.Count.ShouldEqual(3); 153 | } 154 | 155 | [Fact] 156 | public void TestRegisterEventHandlersAllAlreadyRegisteredThreeProjects() 157 | { 158 | //SETUP 159 | var services = new ServiceCollection(); 160 | var config = new GenericEventRunnerConfig(); 161 | services.AddTransient, BeforeHandler>(); 162 | services.AddTransient, DuringHandler>(); 163 | services.AddTransient, AfterHandler>(); 164 | 165 | //ATTEMPT 166 | var logs = services.RegisterGenericEventRunner(config, 167 | Assembly.GetAssembly(typeof(BeforeHandler)), 168 | Assembly.GetAssembly(typeof(DuringHandler)), 169 | Assembly.GetAssembly(typeof(AfterHandler)) 170 | ); 171 | 172 | //VERIFY 173 | services.Contains(new ServiceDescriptor(typeof(IBeforeSaveEventHandler), 174 | typeof(BeforeHandler), 175 | ServiceLifetime.Transient), new ServiceDescriptorIncludeLifeTimeCompare()).ShouldBeTrue(); 176 | services.Contains(new ServiceDescriptor(typeof(IDuringSaveEventHandler), 177 | typeof(DuringHandler), 178 | ServiceLifetime.Transient), new ServiceDescriptorIncludeLifeTimeCompare()).ShouldBeTrue(); 179 | services.Contains(new ServiceDescriptor(typeof(IAfterSaveEventHandler), 180 | typeof(AfterHandler), 181 | ServiceLifetime.Transient), new ServiceDescriptorIncludeLifeTimeCompare()).ShouldBeTrue(); 182 | 183 | foreach (var log in logs) 184 | { 185 | _output.WriteLine(log); 186 | } 187 | 188 | config.NotUsingDuringSaveHandlers.ShouldBeFalse(); 189 | config.NotUsingAfterSaveHandlers.ShouldBeFalse(); 190 | services.Count.ShouldEqual(5); 191 | } 192 | } 193 | } -------------------------------------------------------------------------------- /Test/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "ConnectionStrings": { 3 | "UnitTestConnection": "Server=(localdb)\\mssqllocaldb;Database=EfCore.GenericEventRunner-Test;Trusted_Connection=True;MultipleActiveResultSets=true" 4 | } 5 | } 6 | --------------------------------------------------------------------------------