├── .gitattributes ├── .gitignore ├── .travis.yml ├── CommonAssemblyInfo.cs ├── Even.sln ├── GitVersion.yml ├── README.md ├── RELEASE_NOTES.md ├── Samples └── Even.Sample │ ├── Aggregates │ ├── Product.Messages.cs │ └── Product.cs │ ├── App.config │ ├── Even.Sample.csproj │ ├── Program.cs │ ├── Projections │ └── ActiveProducts.cs │ ├── Properties │ └── AssemblyInfo.cs │ └── project.json ├── Src ├── Even.Persistence.SQLite │ ├── Even.Persistence.SQLite.csproj │ ├── Properties │ │ ├── AssemblyInfo.cs │ │ └── CommonAssemblyInfo.cs │ ├── SQLiteStore.cs │ └── project.json ├── Even.Persistence.Sql │ ├── BaseSqlStore.cs │ ├── Even.Persistence.Sql.csproj │ ├── Even.Persistence.Sql.nuspec │ ├── MySqlStore.cs │ ├── Properties │ │ ├── AssemblyInfo.cs │ │ └── CommonAssemblyInfo.cs │ ├── SqlServer2012Store.cs │ └── project.json └── Even │ ├── Aggregate.cs │ ├── AggregateSupervisor.cs │ ├── AkkaAsyncHelper.cs │ ├── Argument.cs │ ├── BufferedEventWriter.cs │ ├── CommandProcessor.cs │ ├── CommandProcessorSupervisor.cs │ ├── CommandResult.cs │ ├── Constants.cs │ ├── DefaultSerializer.cs │ ├── ESCategoryAttribute.cs │ ├── ESEventAttribute.cs │ ├── Even.csproj │ ├── Even.nuspec │ ├── EvenException.cs │ ├── EvenGateway.cs │ ├── EvenMaster.cs │ ├── EvenServices.cs │ ├── EvenSetup.cs │ ├── EvenStorageFormatAttribute.cs │ ├── EventCount.cs │ ├── EventDispatcher.cs │ ├── EventProcessor.cs │ ├── EventProcessorSupervisor.cs │ ├── EventReader.cs │ ├── EventRegistry.cs │ ├── EventStoreReader.cs │ ├── EventStoreWriter.cs │ ├── ExpectedSequence.cs │ ├── ExtensionMethods.cs │ ├── ICommandValidator.cs │ ├── IEventStore.cs │ ├── IPersistedEvent.cs │ ├── IPersistedStreamEvent.cs │ ├── IProjectionStreamPredicate.cs │ ├── ISerializer.cs │ ├── IndexSequenceEntry.cs │ ├── IndexedProjectionStreamReader.cs │ ├── Internals │ ├── CollectionExtensions.cs │ └── Unit.cs │ ├── MessageHandler.cs │ ├── Messages │ ├── CommandMessages.cs │ ├── CommandProcessorMessages.cs │ ├── EventProcessorMessages.cs │ ├── EventReadMessages.cs │ ├── GenericMessages.cs │ ├── IRequest.cs │ ├── InformationMessages.cs │ ├── Initialization.cs │ ├── Persistence.cs │ └── ProjectionMessages.cs │ ├── Options.cs │ ├── PersistedEventFactory.cs │ ├── PersistedRawEvent.cs │ ├── Persistence │ └── InMemoryStore.cs │ ├── Projection.cs │ ├── ProjectionCheckpointWriter.cs │ ├── ProjectionEvent.cs │ ├── ProjectionIndexWriter.cs │ ├── ProjectionState.cs │ ├── ProjectionStream.cs │ ├── ProjectionStreamQuery.cs │ ├── ProjectionStreamSupervisor.cs │ ├── ProjectionSupervisor.cs │ ├── Properties │ └── AssemblyInfo.cs │ ├── PropsFactory.cs │ ├── Query.cs │ ├── QueryExtensions.cs │ ├── ReadWorkers.cs │ ├── RejectReason.cs │ ├── SerialEventStreamWriter.cs │ ├── Stream.cs │ ├── StreamEventReader.cs │ ├── StreamHash.cs │ ├── SystemClock.cs │ ├── Timeout.cs │ ├── UnpersistedEvent.cs │ ├── UnpersistedRawEvent.cs │ ├── Utils │ ├── BatchStringBuilder.cs │ └── EnumerableExtensions.cs │ ├── app.config │ └── project.json ├── Test └── Even.Tests │ ├── AggregateTests.cs │ ├── BasicIntegrationTests.cs │ ├── BufferedEventWriterTests.cs │ ├── Even.Tests.csproj │ ├── EvenGatewayTests.cs │ ├── EvenSetupTests.cs │ ├── EvenTestKit.cs │ ├── EventDispatcherTests.cs │ ├── EventStoreReaderTests.cs │ ├── EventStoreWriterTests.cs │ ├── MessageHandlerTests.cs │ ├── Mocks │ ├── MockEventStore.cs │ ├── MockPersistedEvent.cs │ ├── MockPersistedEventFactory.cs │ ├── MockPersistedStreamEvent.cs │ ├── MockProjectionStore.cs │ ├── MockSerializer.cs │ └── TestStore.cs │ ├── Persistence │ ├── EventStoreTests.cs │ ├── InMemoryEventStoreTests.cs │ ├── MySqlEventStoreTests.cs │ ├── SQLiteEventStoreTests.cs │ └── SqlServer2012StoreTests.cs │ ├── ProjectionCheckpointWriterTests.cs │ ├── ProjectionIndexWriterTests.cs │ ├── ProjectionReplayWorkerTests.cs │ ├── ProjectionStreamTests.cs │ ├── ProjectionTests.cs │ ├── Properties │ └── AssemblyInfo.cs │ ├── QueryTests.cs │ ├── ReadIndexedProjectionStreamWorkerTests.cs │ ├── ReadStreamWorkerTests.cs │ ├── ReadWorkerTests.cs │ ├── SerialEventStreamWriterTests.cs │ ├── StreamTests.cs │ ├── TestConfig.hocon │ ├── TimeoutTests.cs │ ├── Utils │ ├── BatchStringBuilderTests.cs │ ├── EnumerableExtensionsTests.cs │ └── ProbeRelay.cs │ ├── app.config │ └── project.json ├── appveyor.yml ├── build.cake ├── build.ps1 ├── global.json ├── nuget.config └── tools └── packages.config /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp text=auto eol=lf 6 | *.vb diff=csharp text=auto eol=lf 7 | *.fs diff=csharp text=auto eol=lf 8 | *.fsi diff=csharp text=auto eol=lf 9 | *.fsx diff=csharp text=auto eol=lf 10 | *.sln text eol=crlf merge=union 11 | *.csproj merge=union 12 | *.vbproj merge=union 13 | *.fsproj merge=union 14 | *.dbproj merge=union 15 | 16 | # Standard to msysgit 17 | *.doc diff=astextplain 18 | *.DOC diff=astextplain 19 | *.docx diff=astextplain 20 | *.DOCX diff=astextplain 21 | *.dot diff=astextplain 22 | *.DOT diff=astextplain 23 | *.pdf diff=astextplain 24 | *.PDF diff=astextplain 25 | *.rtf diff=astextplain 26 | *.RTF diff=astextplain 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.sln.docstates 8 | 9 | # Xamarin Studio / monodevelop user-specific 10 | *.userprefs 11 | *.dll.mdb 12 | *.exe.mdb 13 | 14 | # Build results 15 | 16 | [Dd]ebug/ 17 | [Rr]elease/ 18 | x64/ 19 | #build/ 20 | [Bb]in/ 21 | [Oo]bj/ 22 | 23 | # MSTest test Results 24 | [Tt]est[Rr]esult*/ 25 | [Bb]uild[Ll]og.* 26 | 27 | *_i.c 28 | *_p.c 29 | *.ilk 30 | *.meta 31 | *.obj 32 | *.pch 33 | *.pdb 34 | *.pgc 35 | *.pgd 36 | *.rsp 37 | *.sbr 38 | *.tlb 39 | *.tli 40 | *.tlh 41 | *.tmp 42 | *.tmp_proj 43 | *.log 44 | *.vspscc 45 | *.vssscc 46 | .builds 47 | *.pidb 48 | *.log 49 | *.scc 50 | 51 | # Visual C++ cache files 52 | ipch/ 53 | *.aps 54 | *.ncb 55 | *.opensdf 56 | *.sdf 57 | *.cachefile 58 | 59 | # Visual Studio profiler 60 | *.psess 61 | *.vsp 62 | *.vspx 63 | 64 | # Other Visual Studio data 65 | .vs/ 66 | 67 | # Guidance Automation Toolkit 68 | *.gpState 69 | 70 | # ReSharper is a .NET coding add-in 71 | _ReSharper*/ 72 | *.[Rr]e[Ss]harper 73 | 74 | # TeamCity is a build add-in 75 | _TeamCity* 76 | 77 | # DotCover is a Code Coverage Tool 78 | *.dotCover 79 | 80 | # NCrunch 81 | *.ncrunch* 82 | .*crunch*.local.xml 83 | 84 | # Installshield output folder 85 | [Ee]xpress/ 86 | 87 | # DocProject is a documentation generator add-in 88 | DocProject/buildhelp/ 89 | DocProject/Help/*.HxT 90 | DocProject/Help/*.HxC 91 | DocProject/Help/*.hhc 92 | DocProject/Help/*.hhk 93 | DocProject/Help/*.hhp 94 | DocProject/Help/Html2 95 | DocProject/Help/html 96 | 97 | # Click-Once directory 98 | publish/ 99 | 100 | # Publish Web Output 101 | *.Publish.xml 102 | 103 | # Enable nuget.exe in the .nuget folder (though normally executables are not tracked) 104 | !.nuget/NuGet.exe 105 | 106 | # Windows Azure Build Output 107 | csx 108 | *.build.csdef 109 | 110 | # Windows Store app package directory 111 | AppPackages/ 112 | 113 | # Others 114 | sql/ 115 | *.Cache 116 | ClientBin/ 117 | [Ss]tyle[Cc]op.* 118 | ~$* 119 | *~ 120 | *.dbmdl 121 | *.[Pp]ublish.xml 122 | *.pfx 123 | *.publishsettings 124 | 125 | # RIA/Silverlight projects 126 | Generated_Code/ 127 | 128 | # Backup & report files from converting an old project file to a newer 129 | # Visual Studio version. Backup files are not needed, because we have git ;-) 130 | _UpgradeReport_Files/ 131 | Backup*/ 132 | UpgradeLog*.XML 133 | UpgradeLog*.htm 134 | 135 | # SQL Server files 136 | App_Data/*.mdf 137 | App_Data/*.ldf 138 | 139 | 140 | #LightSwitch generated files 141 | GeneratedArtifacts/ 142 | _Pvt_Extensions/ 143 | ModelManifest.xml 144 | 145 | # ========================= 146 | # Windows detritus 147 | # ========================= 148 | 149 | # Windows image file caches 150 | Thumbs.db 151 | ehthumbs.db 152 | 153 | # Folder config file 154 | Desktop.ini 155 | 156 | # Recycle Bin used on file shares 157 | $RECYCLE.BIN/ 158 | 159 | # Mac desktop service store files 160 | .DS_Store 161 | 162 | # =================================================== 163 | # Exclude F# project specific directories and files 164 | # =================================================== 165 | 166 | # NuGet Packages Directory 167 | packages/ 168 | 169 | # Generated documentation folder 170 | docs/output/ 171 | 172 | # Temp folder used for publishing docs 173 | temp/ 174 | 175 | # Test results produced by build 176 | TestResults.xml 177 | 178 | # Nuget outputs 179 | nuget/*.nupkg 180 | release.cmd 181 | release.sh 182 | localpackages/ 183 | paket-files 184 | *.orig 185 | .paket/paket.exe 186 | docs/content/license.md 187 | docs/content/release-notes.md 188 | .fake 189 | docs/tools/FSharp.Formatting.svclog 190 | .user[Ss]ettings/ 191 | 192 | # DNX 193 | project.lock.json 194 | artifacts/ 195 | 196 | # Visual Studio Code 197 | .vscode/ 198 | 199 | # Build output 200 | .build/ 201 | build/tools/packages/ 202 | 203 | #More Nuget 204 | *.nuget.props 205 | 206 | .build/ 207 | 208 | # tools 209 | tools/* 210 | !tools/packages.config 211 | /.nuget -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: csharp 2 | 3 | sudo: false # use the new container-based Travis infrastructure 4 | 5 | before_install: 6 | - chmod +x build.sh 7 | 8 | script: 9 | - ./build.sh All 10 | -------------------------------------------------------------------------------- /CommonAssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | [assembly: AssemblyVersion("0.2.0.0")] 4 | [assembly: AssemblyInformationalVersion("0.2.0-akka-upgrade.0+Branch.feature/akka-upgrade-v0.2.0.Sha.55a89037e60e8812cd25c778acb4c10c819d02d1")] 5 | [assembly: AssemblyFileVersion("0.2.0.0")] -------------------------------------------------------------------------------- /Even.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 14 4 | VisualStudioVersion = 14.0.25420.1 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Even", "Src\Even\Even.csproj", "{00546FFA-62B1-4F0A-A5BE-AC476FF3691B}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{4BBB771D-1CF0-45A6-9C3A-F53B95796D5F}" 9 | ProjectSection(SolutionItems) = preProject 10 | .gitattributes = .gitattributes 11 | .gitignore = .gitignore 12 | appveyor.yml = appveyor.yml 13 | build.bat = build.bat 14 | CommonAssemblyInfo.cs = CommonAssemblyInfo.cs 15 | EndProjectSection 16 | EndProject 17 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Even.Persistence.Sql", "Src\Even.Persistence.Sql\Even.Persistence.Sql.csproj", "{179B5E10-8E0A-47B3-B5AD-46466CAA393C}" 18 | EndProject 19 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Even.Tests", "Test\Even.Tests\Even.Tests.csproj", "{BA2F8882-BDF9-47FF-9401-C7EAB6E558FB}" 20 | EndProject 21 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Even.Sample", "Samples\Even.Sample\Even.Sample.csproj", "{83E4FF85-C9D9-4AA8-935E-84D25B9E1430}" 22 | EndProject 23 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{29F91898-5B53-45BF-B788-4DFAA65BEB3B}" 24 | ProjectSection(SolutionItems) = preProject 25 | build\build.ps1 = build\build.ps1 26 | build\default.ps1 = build\default.ps1 27 | build\psake-config.ps1 = build\psake-config.ps1 28 | build\psake.cmd = build\psake.cmd 29 | build\psake.ps1 = build\psake.ps1 30 | build\psake.psm1 = build\psake.psm1 31 | EndProjectSection 32 | EndProject 33 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Modules", "Modules", "{1D39B43D-65D9-4E1F-AA2E-40314989D0CF}" 34 | ProjectSection(SolutionItems) = preProject 35 | build\Modules\BuildFunctions.psm1 = build\Modules\BuildFunctions.psm1 36 | build\Modules\ILMerge.psm1 = build\Modules\ILMerge.psm1 37 | build\Modules\IO.psm1 = build\Modules\IO.psm1 38 | build\Modules\template.xunit = build\Modules\template.xunit 39 | build\Modules\Xunit.psm1 = build\Modules\Xunit.psm1 40 | EndProjectSection 41 | EndProject 42 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Even.Persistence.SQLite", "Src\Even.Persistence.SQLite\Even.Persistence.SQLite.csproj", "{20A9C4E3-F4C5-49F8-B53C-A964DB828C71}" 43 | EndProject 44 | Global 45 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 46 | Debug|Any CPU = Debug|Any CPU 47 | Release|Any CPU = Release|Any CPU 48 | EndGlobalSection 49 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 50 | {00546FFA-62B1-4F0A-A5BE-AC476FF3691B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 51 | {00546FFA-62B1-4F0A-A5BE-AC476FF3691B}.Debug|Any CPU.Build.0 = Debug|Any CPU 52 | {00546FFA-62B1-4F0A-A5BE-AC476FF3691B}.Release|Any CPU.ActiveCfg = Release|Any CPU 53 | {00546FFA-62B1-4F0A-A5BE-AC476FF3691B}.Release|Any CPU.Build.0 = Release|Any CPU 54 | {179B5E10-8E0A-47B3-B5AD-46466CAA393C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 55 | {179B5E10-8E0A-47B3-B5AD-46466CAA393C}.Debug|Any CPU.Build.0 = Debug|Any CPU 56 | {179B5E10-8E0A-47B3-B5AD-46466CAA393C}.Release|Any CPU.ActiveCfg = Release|Any CPU 57 | {179B5E10-8E0A-47B3-B5AD-46466CAA393C}.Release|Any CPU.Build.0 = Release|Any CPU 58 | {BA2F8882-BDF9-47FF-9401-C7EAB6E558FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 59 | {BA2F8882-BDF9-47FF-9401-C7EAB6E558FB}.Debug|Any CPU.Build.0 = Debug|Any CPU 60 | {BA2F8882-BDF9-47FF-9401-C7EAB6E558FB}.Release|Any CPU.ActiveCfg = Release|Any CPU 61 | {BA2F8882-BDF9-47FF-9401-C7EAB6E558FB}.Release|Any CPU.Build.0 = Release|Any CPU 62 | {83E4FF85-C9D9-4AA8-935E-84D25B9E1430}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 63 | {83E4FF85-C9D9-4AA8-935E-84D25B9E1430}.Debug|Any CPU.Build.0 = Debug|Any CPU 64 | {83E4FF85-C9D9-4AA8-935E-84D25B9E1430}.Release|Any CPU.ActiveCfg = Release|Any CPU 65 | {83E4FF85-C9D9-4AA8-935E-84D25B9E1430}.Release|Any CPU.Build.0 = Release|Any CPU 66 | {20A9C4E3-F4C5-49F8-B53C-A964DB828C71}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 67 | {20A9C4E3-F4C5-49F8-B53C-A964DB828C71}.Debug|Any CPU.Build.0 = Debug|Any CPU 68 | {20A9C4E3-F4C5-49F8-B53C-A964DB828C71}.Release|Any CPU.ActiveCfg = Release|Any CPU 69 | {20A9C4E3-F4C5-49F8-B53C-A964DB828C71}.Release|Any CPU.Build.0 = Release|Any CPU 70 | EndGlobalSection 71 | GlobalSection(SolutionProperties) = preSolution 72 | HideSolutionNode = FALSE 73 | EndGlobalSection 74 | GlobalSection(NestedProjects) = preSolution 75 | {1D39B43D-65D9-4E1F-AA2E-40314989D0CF} = {29F91898-5B53-45BF-B788-4DFAA65BEB3B} 76 | EndGlobalSection 77 | EndGlobal 78 | -------------------------------------------------------------------------------- /GitVersion.yml: -------------------------------------------------------------------------------- 1 | assembly-versioning-scheme: MajorMinorPatch 2 | mode: ContinuousDelivery 3 | tag-prefix: '[vV]' 4 | continuous-delivery-fallback-tag: ci 5 | major-version-bump-message: '\+semver:\s?(breaking|major)' 6 | minor-version-bump-message: '\+semver:\s?(feature|minor)' 7 | patch-version-bump-message: '\+semver:\s?(fix|patch)' 8 | legacy-semver-padding: 4 9 | build-metadata-padding: 4 10 | commit-message-incrementing: Enabled 11 | branches: 12 | master: 13 | mode: ContinuousDelivery 14 | tag: 15 | increment: Patch 16 | prevent-increment-of-merged-branch-version: true 17 | track-merge-target: false 18 | releases?[/-]: 19 | mode: ContinuousDelivery 20 | tag: beta 21 | increment: Patch 22 | prevent-increment-of-merged-branch-version: true 23 | track-merge-target: false 24 | features?[/-]: 25 | mode: ContinuousDeployment 26 | tag: useBranchName 27 | increment: Inherit 28 | prevent-increment-of-merged-branch-version: false 29 | track-merge-target: false 30 | (pull|pull\-requests|pr)[/-]: 31 | mode: ContinuousDeployment 32 | tag: PullRequest 33 | increment: Inherit 34 | prevent-increment-of-merged-branch-version: false 35 | tag-number-pattern: '[/-](?\d+)[-/]' 36 | track-merge-target: false 37 | hotfix(es)?[/-]: 38 | mode: ContinuousDelivery 39 | tag: beta 40 | increment: Patch 41 | prevent-increment-of-merged-branch-version: false 42 | track-merge-target: false 43 | support[/-]: 44 | mode: ContinuousDelivery 45 | tag: 46 | increment: Patch 47 | prevent-increment-of-merged-branch-version: true 48 | track-merge-target: false 49 | dev(elop)?(ment)?$: 50 | mode: ContinuousDeployment 51 | tag: preview 52 | increment: Minor 53 | prevent-increment-of-merged-branch-version: false 54 | track-merge-target: true -------------------------------------------------------------------------------- /RELEASE_NOTES.md: -------------------------------------------------------------------------------- 1 | ### 0.0.2-alpha01 - January 14 2016 2 | * Got SQL Persistence working 3 | * Downgraded project/nuget packages to .NET 4.5.1 4 | * Enable CI 5 | 6 | #### 0.0.1-alpha - December 13 2015 7 | * This is an alphe-release intended for development and testing only. 8 | * Check the project on github for more information. 9 | -------------------------------------------------------------------------------- /Samples/Even.Sample/Aggregates/Product.Messages.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace Even.Sample.Aggregates 8 | { 9 | // define commands 10 | public class CreateProduct { public string Name { get; set; } } 11 | public class RenameProduct { public string NewName { get; set; } } 12 | public class DeleteProduct { } 13 | 14 | // define events 15 | public class ProductCreated { public string Name { get; set; } } 16 | public class ProductRenamed { public string NewName { get; set; } } 17 | public class ProductDeleted { } 18 | } 19 | -------------------------------------------------------------------------------- /Samples/Even.Sample/Aggregates/Product.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Text.RegularExpressions; 6 | using System.Threading.Tasks; 7 | 8 | namespace Even.Sample.Aggregates 9 | { 10 | // define the state 11 | public class ProductState 12 | { 13 | public bool IsDeleted { get; set; } 14 | public string Name { get; set; } 15 | } 16 | 17 | // define the aggregate 18 | public class Product : Aggregate 19 | { 20 | public Product() 21 | { 22 | // creation 23 | 24 | OnCommand(c => { 25 | 26 | if (State != null) 27 | Reject("Product already exists"); 28 | 29 | Persist(new ProductCreated { Name = c.Name }); 30 | }); 31 | 32 | OnEvent(e => { 33 | State = new ProductState() 34 | { 35 | Name = e.Name 36 | }; 37 | }); 38 | 39 | // renaming 40 | 41 | OnCommand(async c => 42 | { 43 | if (State == null) 44 | Reject("Product doesn't exist"); 45 | 46 | var alreadyExists = await Task.FromResult(false); 47 | 48 | if (alreadyExists) 49 | Reject("Can't rename, name already taken."); 50 | 51 | Persist(new ProductRenamed { NewName = c.NewName }); 52 | }); 53 | 54 | OnEvent(e => 55 | { 56 | State.Name = e.NewName; 57 | }); 58 | 59 | // deletion 60 | 61 | OnCommand(c => 62 | { 63 | if (State != null && !State.IsDeleted) 64 | Persist(new ProductDeleted()); 65 | }); 66 | 67 | OnEvent(e => 68 | { 69 | State.IsDeleted = true; 70 | }); 71 | } 72 | 73 | protected override bool IsValidStream(string stream) 74 | { 75 | // by default, streams use "category-uuid" pattern 76 | // changing this allows aggregates to use any format 77 | 78 | var pattern = Category + @"-\d+"; 79 | return Regex.IsMatch(stream, pattern); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Samples/Even.Sample/App.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 7 | 8 | 9 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /Samples/Even.Sample/Even.Sample.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {83E4FF85-C9D9-4AA8-935E-84D25B9E1430} 8 | Exe 9 | Properties 10 | Even.Sample 11 | Even.Sample 12 | v4.5.1 13 | 512 14 | true 15 | 16 | 17 | AnyCPU 18 | true 19 | full 20 | false 21 | bin\Debug\ 22 | DEBUG;TRACE 23 | prompt 24 | 4 25 | 26 | 27 | AnyCPU 28 | pdbonly 29 | true 30 | bin\Release\ 31 | TRACE 32 | prompt 33 | 4 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | Properties\CommonAssemblyInfo.cs 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | {179b5e10-8e0a-47b3-b5ad-46466caa393c} 62 | Even.Persistence.Sql 63 | 64 | 65 | {00546ffa-62b1-4f0a-a5be-ac476ff3691b} 66 | Even 67 | 68 | 69 | 70 | 77 | -------------------------------------------------------------------------------- /Samples/Even.Sample/Program.cs: -------------------------------------------------------------------------------- 1 | using Akka.Actor; 2 | using Even.Persistence; 3 | using Even.Sample.Aggregates; 4 | using Even.Sample.Projections; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Text; 9 | using System.Threading; 10 | using System.Threading.Tasks; 11 | 12 | namespace Even.Sample 13 | { 14 | class Program 15 | { 16 | static void Main(string[] args) 17 | { 18 | var actorSystem = ActorSystem.Create("Sample"); 19 | 20 | Task.Run(async () => 21 | { 22 | // this is used to retrieve the events later 23 | var memoryStore = new InMemoryStore(); 24 | 25 | // initialize the event store 26 | var gateway = await actorSystem 27 | .SetupEven() 28 | .UseStore(memoryStore) 29 | .AddProjection() 30 | .Start("even"); 31 | 32 | // send some commands 33 | await Task.WhenAll( 34 | gateway.SendAggregateCommand(1, new CreateProduct { Name = "Product 1" }), 35 | gateway.SendAggregateCommand(2, new CreateProduct { Name = "Product 2" }), 36 | gateway.SendAggregateCommand(3, new CreateProduct { Name = "Product 3" }), 37 | gateway.SendAggregateCommand(2, new RenameProduct { NewName = "Product 2 - Renamed" }), 38 | gateway.SendAggregateCommand(1, new DeleteProduct()) 39 | ); 40 | 41 | // add some delay to make sure the data is flushed to the store 42 | await Task.Delay(100); 43 | 44 | // print the contents of the event store 45 | Console.WriteLine(); 46 | Console.WriteLine("Event Store Data"); 47 | Console.WriteLine("================"); 48 | Console.WriteLine(); 49 | Console.WriteLine($"{"Seq",-6} {"Stream ID",-50} Event Name"); 50 | 51 | foreach (var e in memoryStore.GetEvents()) 52 | Console.WriteLine($"{e.GlobalSequence,-6} {e.Stream,-50} {e.EventType}"); 53 | 54 | }).Wait(); 55 | 56 | Console.WriteLine(); 57 | Console.WriteLine("End"); 58 | Console.ReadLine(); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Samples/Even.Sample/Projections/ActiveProducts.cs: -------------------------------------------------------------------------------- 1 | using Akka.Actor; 2 | using Akka.Event; 3 | using Even.Sample.Aggregates; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Runtime.Remoting.Contexts; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | 11 | namespace Even.Sample.Projections 12 | { 13 | public class ActiveProducts : Projection 14 | { 15 | public ActiveProducts() 16 | { 17 | OnEvent(e => 18 | { 19 | _list.Add(new ProductInfo { ID = e.Stream.Name, Name = e.DomainEvent.Name }); 20 | }); 21 | 22 | OnEvent(e => 23 | { 24 | _list.RemoveAll(i => i.ID == e.Stream.Name); 25 | }); 26 | } 27 | 28 | protected override Task OnReceiveEvent(IPersistedStreamEvent e) 29 | { 30 | Console.WriteLine($"Projection Received Event {e.StreamSequence}: {e.EventType}"); 31 | return Task.FromResult(1); 32 | } 33 | 34 | protected override void OnReady() 35 | { 36 | Receive(_ => 37 | { 38 | var copy = _list.Select(i => i.Clone()).ToList(); 39 | Sender.Tell(copy); 40 | }); 41 | } 42 | 43 | List _list = new List(); 44 | 45 | #region Event Processors 46 | 47 | private void Add(IPersistedEvent pe, ProductCreated e) 48 | { 49 | 50 | } 51 | 52 | private void Rename(IPersistedEvent pe, ProductRenamed e) 53 | { 54 | 55 | } 56 | 57 | private void Delete(IPersistedEvent pe, ProductDeleted e) 58 | { 59 | 60 | } 61 | 62 | #endregion 63 | } 64 | 65 | public class ProductInfo 66 | { 67 | public string ID { get; set; } 68 | public string Name { get; set; } 69 | 70 | public ProductInfo Clone() 71 | { 72 | return (ProductInfo)this.MemberwiseClone(); 73 | } 74 | } 75 | 76 | public class GetActiveProducts 77 | { } 78 | } 79 | -------------------------------------------------------------------------------- /Samples/Even.Sample/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System.Reflection; 3 | 4 | [assembly: AssemblyTitleAttribute("Even.Sample")] 5 | [assembly: AssemblyProductAttribute("Even")] 6 | [assembly: AssemblyDescriptionAttribute("An event sourcing framework on top of Akka.NET")] 7 | -------------------------------------------------------------------------------- /Samples/Even.Sample/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "Akka": "1.1.1", 4 | "DBHelpers": "1.1.0.0" 5 | }, 6 | "frameworks": { 7 | "net451": {} 8 | }, 9 | "runtimes": { 10 | "win": "" 11 | } 12 | } -------------------------------------------------------------------------------- /Src/Even.Persistence.SQLite/Even.Persistence.SQLite.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {20A9C4E3-F4C5-49F8-B53C-A964DB828C71} 8 | Library 9 | Properties 10 | Even.Persistence.SQLite 11 | Even.Persistence.SQLite 12 | v4.5.1 13 | 512 14 | 15 | 16 | 17 | 18 | 19 | true 20 | full 21 | false 22 | bin\Debug\ 23 | DEBUG;TRACE 24 | prompt 25 | 4 26 | 27 | 28 | pdbonly 29 | true 30 | bin\Release\ 31 | TRACE 32 | prompt 33 | 4 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | {179b5e10-8e0a-47b3-b5ad-46466caa393c} 57 | Even.Persistence.Sql 58 | 59 | 60 | {00546ffa-62b1-4f0a-a5be-ac476ff3691b} 61 | Even 62 | 63 | 64 | 65 | 72 | -------------------------------------------------------------------------------- /Src/Even.Persistence.SQLite/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System.Reflection; 3 | 4 | [assembly: AssemblyTitleAttribute("Even.Persistence.SQLite")] 5 | [assembly: AssemblyProductAttribute("Even")] 6 | [assembly: AssemblyDescriptionAttribute("An event sourcing framework on top of Akka.NET")] -------------------------------------------------------------------------------- /Src/Even.Persistence.SQLite/Properties/CommonAssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | [assembly: AssemblyVersionAttribute("0.1.0")] 4 | [assembly: AssemblyFileVersionAttribute("0.1.0")] 5 | -------------------------------------------------------------------------------- /Src/Even.Persistence.SQLite/SQLiteStore.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Data.Common; 3 | using System.Data.SQLite; 4 | using Even.Persistence.Sql; 5 | 6 | namespace Even.Persistence.SQLite 7 | { 8 | public class SQLiteStore : BaseSqlStore 9 | { 10 | public SQLiteStore(DbProviderFactory factory, string connectionString, bool createTables) 11 | : base(factory, connectionString, createTables) 12 | { 13 | } 14 | 15 | public SQLiteStore(string connectionString, bool createTables = false) 16 | : this(DbProviderFactories.GetFactory("System.Data.SQLite"), connectionString, createTables) 17 | { 18 | } 19 | 20 | protected override string CommandText_CreateTables => @" 21 | CREATE TABLE IF NOT EXISTS {0} ( 22 | GlobalSequence integer primary key autoincrement, 23 | EventID uniqueidentifier not null unique, 24 | StreamHash binary(20) not null, 25 | StreamName varchar(200) not null, 26 | EventType varchar(50) not null, 27 | UtcTimestamp datetime not null, 28 | Metadata blob, 29 | Payload blob not null, 30 | PayloadFormat int not null 31 | ); 32 | 33 | CREATE INDEX IF NOT EXISTS IX_{0}_StreamHash ON {0} (StreamHash); 34 | 35 | CREATE TABLE IF NOT EXISTS {1} ( 36 | ProjectionStreamHash binary(20) not null, 37 | ProjectionStreamSequence int not null, 38 | GlobalSequence bigint not null, 39 | primary key (ProjectionStreamHash, ProjectionStreamSequence) 40 | ); 41 | 42 | CREATE UNIQUE INDEX IF NOT EXISTS UIX_{1}_ProjectionStreamHash_GlobalSequence on {1} (ProjectionStreamHash, GlobalSequence); 43 | 44 | CREATE TABLE IF NOT EXISTS {2} ( 45 | ProjectionStreamHash binary(20) not null primary key, 46 | LastGlobalSequence bigint not null 47 | ); 48 | "; 49 | 50 | protected override string CommandText_SelectGeneratedGlobalSequence => "SELECT last_insert_rowid()"; 51 | 52 | protected override string EscapeIdentifier(string identifier) 53 | { 54 | return "[" + identifier + "]"; 55 | } 56 | 57 | protected override void HandleException(Exception ex) 58 | { 59 | var sqlEx = ex as SQLiteException; 60 | 61 | if (sqlEx?.ResultCode == SQLiteErrorCode.Constraint) 62 | throw new DuplicatedEntryException(); 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /Src/Even.Persistence.SQLite/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "System.Data.SQLite.Core": "1.0.102" 4 | }, 5 | "frameworks": { 6 | "net451": {} 7 | }, 8 | "runtimes": { 9 | "win": "" 10 | } 11 | } -------------------------------------------------------------------------------- /Src/Even.Persistence.Sql/Even.Persistence.Sql.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {179B5E10-8E0A-47B3-B5AD-46466CAA393C} 8 | Library 9 | Properties 10 | Even.Persistence.Sql 11 | Even.Persistence.Sql 12 | v4.5.1 13 | 512 14 | 15 | 16 | 17 | true 18 | full 19 | false 20 | bin\Debug\ 21 | DEBUG;TRACE 22 | prompt 23 | 4 24 | 25 | 26 | pdbonly 27 | true 28 | bin\Release\ 29 | TRACE 30 | prompt 31 | 4 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | {00546ffa-62b1-4f0a-a5be-ac476ff3691b} 57 | Even 58 | 59 | 60 | 61 | 68 | -------------------------------------------------------------------------------- /Src/Even.Persistence.Sql/Even.Persistence.Sql.nuspec: -------------------------------------------------------------------------------- 1 |  2 | 5 | 6 | 7 | Even.Persistence.Sql 8 | $version$ 9 | nvivo, DamReev 10 | Even is an event sourcing framework built on top of Akka.NET. 11 | en-US 12 | https://github.com/evendotnet/Even/ 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /Src/Even.Persistence.Sql/MySqlStore.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Data.Common; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace Even.Persistence.Sql 9 | { 10 | public class MySqlStore : BaseSqlStore 11 | { 12 | public MySqlStore(DbProviderFactory factory, string connectionString, bool createTables) 13 | : base(factory, connectionString, createTables) 14 | { } 15 | 16 | public MySqlStore(string connectionString, bool createTables = false) 17 | : this(DbProviderFactories.GetFactory("MySql.Data.MySqlClient"), connectionString, createTables) 18 | { } 19 | 20 | protected override string CommandText_SelectGeneratedGlobalSequence => "SELECT LAST_INSERT_ID()"; 21 | 22 | protected override string CommandText_CreateTables => @" 23 | create table if not exists {0} ( 24 | GlobalSequence bigint not null primary key auto_increment, 25 | EventID char(36) not null, 26 | StreamHash binary(20) not null, 27 | StreamName varchar(200) not null, 28 | EventType varchar(50) not null, 29 | UtcTimestamp datetime not null, 30 | Metadata blob, 31 | Payload mediumblob not null, 32 | PayloadFormat int not null, 33 | 34 | index (StreamHash), 35 | unique index (EventID) 36 | ); 37 | 38 | create table if not exists {1} ( 39 | ProjectionStreamHash binary(20) not null, 40 | ProjectionStreamSequence int not null, 41 | GlobalSequence bigint not null, 42 | primary key (ProjectionStreamHash, ProjectionStreamSequence), 43 | unique index (ProjectionStreamHash, GlobalSequence) 44 | ); 45 | 46 | create table if not exists {2} ( 47 | ProjectionStreamHash binary(20) not null primary key, 48 | LastGlobalSequence bigint not null 49 | ); 50 | "; 51 | 52 | protected override string EscapeIdentifier(string identifier) 53 | { 54 | return "`" + identifier + "`"; 55 | } 56 | 57 | protected override void HandleException(Exception ex) 58 | { 59 | if (ex.Message.IndexOf("Duplicate", StringComparison.OrdinalIgnoreCase) >= 0) 60 | throw new DuplicatedEntryException(); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Src/Even.Persistence.Sql/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System.Reflection; 3 | 4 | [assembly: AssemblyTitleAttribute("Even.Persistence.Sql")] 5 | [assembly: AssemblyProductAttribute("Even")] 6 | [assembly: AssemblyDescriptionAttribute("An event sourcing framework on top of Akka.NET")] 7 | -------------------------------------------------------------------------------- /Src/Even.Persistence.Sql/Properties/CommonAssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | [assembly: AssemblyVersionAttribute("0.1.0")] 4 | [assembly: AssemblyFileVersionAttribute("0.1.0")] 5 | -------------------------------------------------------------------------------- /Src/Even.Persistence.Sql/SqlServer2012Store.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Data.Common; 4 | using System.Data.SqlClient; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace Even.Persistence.Sql 10 | { 11 | public class SqlServer2012Store : BaseSqlStore 12 | { 13 | public SqlServer2012Store(DbProviderFactory factory, string connectionString, bool createTables) 14 | : base(factory, connectionString, createTables) 15 | { } 16 | 17 | public SqlServer2012Store(string connectionString, bool createTables = false) 18 | : this(System.Data.SqlClient.SqlClientFactory.Instance, connectionString, createTables) 19 | { } 20 | 21 | protected override string CommandText_SelectGeneratedGlobalSequence => "SELECT SCOPE_IDENTITY()"; 22 | 23 | protected override string CommandText_CreateTables => @" 24 | if not exists (select * from information_schema.tables where table_name = N'{0}') 25 | begin 26 | create table [{0}] ( 27 | GlobalSequence bigint not null primary key identity, 28 | EventID uniqueidentifier not null unique, 29 | StreamHash binary(20) not null, 30 | StreamName varchar(200) not null, 31 | EventType varchar(50) not null, 32 | UtcTimestamp datetime not null, 33 | Metadata varbinary(max), 34 | Payload varbinary(max) not null, 35 | PayloadFormat int not null, 36 | 37 | index [IX_{0}_StreamHash] (StreamHash) 38 | ); 39 | end; 40 | 41 | if not exists (select * from information_schema.tables where table_name = N'{1}') 42 | begin 43 | create table [{1}] ( 44 | ProjectionStreamHash binary(20) not null, 45 | ProjectionStreamSequence int not null, 46 | GlobalSequence bigint not null, 47 | primary key (ProjectionStreamHash, ProjectionStreamSequence) 48 | ); 49 | 50 | create unique index [UIX_{1}_ProjectionStreamHash_GlobalSequence] on [{1}] (ProjectionStreamHash, GlobalSequence); 51 | end; 52 | 53 | if not exists (select * from information_schema.tables where table_name = N'{2}') 54 | begin 55 | create table [{2}] ( 56 | ProjectionStreamHash binary(20) not null primary key, 57 | LastGlobalSequence bigint not null 58 | ); 59 | end; 60 | "; 61 | 62 | protected override string EscapeIdentifier(string identifier) 63 | { 64 | return "[" + identifier + "]"; 65 | } 66 | 67 | protected override void HandleException(Exception ex) 68 | { 69 | var sqlEx = ex as SqlException; 70 | 71 | if (sqlEx?.Number == 2627 || ex.Message.Contains("duplicate")) 72 | throw new DuplicatedEntryException(); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Src/Even.Persistence.Sql/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "Akka": "1.1.1", 4 | "DBHelpers": "1.1.0.0" 5 | }, 6 | "frameworks": { 7 | "net451": {} 8 | }, 9 | "runtimes": { 10 | "win": "" 11 | } 12 | } -------------------------------------------------------------------------------- /Src/Even/AggregateSupervisor.cs: -------------------------------------------------------------------------------- 1 | using Akka.Actor; 2 | using Even.Messages; 3 | using System; 4 | using System.Collections.Generic; 5 | 6 | namespace Even 7 | { 8 | public class AggregateSupervisor : ReceiveActor 9 | { 10 | IActorRef _reader; 11 | IActorRef _writer; 12 | GlobalOptions _options; 13 | 14 | Dictionary _aggregates = new Dictionary(StringComparer.OrdinalIgnoreCase); 15 | 16 | public static Props CreateProps(IActorRef reader, IActorRef writer, GlobalOptions options) 17 | { 18 | Argument.RequiresNotNull(reader, nameof(reader)); 19 | Argument.RequiresNotNull(writer, nameof(writer)); 20 | Argument.RequiresNotNull(options, nameof(options)); 21 | 22 | return Props.Create(reader, writer, options); 23 | } 24 | 25 | public AggregateSupervisor(IActorRef reader, IActorRef writer, GlobalOptions options) 26 | { 27 | _reader = reader; 28 | _writer = writer; 29 | _options = options; 30 | 31 | Ready(); 32 | } 33 | 34 | private void Ready() 35 | { 36 | Receive(envelope => 37 | { 38 | var command = envelope.Command; 39 | var key = command.Stream.Name + "|" + envelope.AggregateType.FullName; 40 | IActorRef aRef; 41 | 42 | // if the aggregate doesn't exist, create it 43 | if (!_aggregates.TryGetValue(key, out aRef)) 44 | { 45 | var props = PropsFactory.Create(envelope.AggregateType); 46 | aRef = Context.ActorOf(props); 47 | aRef.Tell(new InitializeAggregate(_reader, _writer, _options)); 48 | _aggregates.Add(key, aRef); 49 | Context.Watch(aRef); 50 | } 51 | 52 | aRef.Forward(command); 53 | }); 54 | 55 | // remove children that are going to stop 56 | Receive(_ => 57 | { 58 | if (RemoveActiveProcessor(Sender)) 59 | Sender.Tell(StopNoticeAcknowledged.Instance); 60 | }); 61 | 62 | // remove terminated children 63 | Receive(t => RemoveActiveProcessor(t.ActorRef)); 64 | } 65 | 66 | bool RemoveActiveProcessor(IActorRef aRef) 67 | { 68 | foreach (var kvp in _aggregates) 69 | { 70 | if (aRef.Equals(kvp.Value)) 71 | { 72 | _aggregates.Remove(kvp.Key); 73 | return true; 74 | } 75 | } 76 | 77 | return false; 78 | } 79 | 80 | protected override SupervisorStrategy SupervisorStrategy() 81 | { 82 | // default behavior to handle exceptions in aggregates is to always restart 83 | return new OneForOneStrategy(ex => Directive.Restart); 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Src/Even/AkkaAsyncHelper.cs: -------------------------------------------------------------------------------- 1 | using Akka.Actor; 2 | using Akka.Dispatch; 3 | using Akka.Dispatch.SysMsg; 4 | using System; 5 | using System.Runtime.ExceptionServices; 6 | using System.Threading.Tasks; 7 | 8 | namespace Even 9 | { 10 | internal static class AkkaAsyncHelper 11 | { 12 | /// 13 | /// Causes the actor to await until the task is completed to process new messages. 14 | /// If the task is completed, works synchronously without any overhead 15 | /// 16 | public static void Await(Func func) 17 | { 18 | var task = func(); 19 | 20 | // if task is null, treat as synchronous execution 21 | if (task == null) 22 | return; 23 | 24 | if (task.IsFaulted) 25 | ExceptionDispatchInfo.Capture(task.Exception.InnerException).Throw(); 26 | 27 | // if task is completed, return synchronously 28 | if (task.IsCompleted) 29 | return; 30 | 31 | // dispatch to actor scheduler only if needed 32 | ActorTaskScheduler.RunTask(() => task); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Src/Even/Argument.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace Even 8 | { 9 | internal static class Argument 10 | { 11 | public static void RequiresNotNull(object param, string paramName) 12 | { 13 | Requires(param != null, paramName); 14 | } 15 | 16 | public static void Requires(bool condition, string paramName = null, string message = null) 17 | { 18 | Requires(condition, paramName, message); 19 | } 20 | 21 | public static void Requires(bool condition, string paramName) 22 | where TException : Exception, new() 23 | { 24 | Requires(condition, paramName, null); 25 | } 26 | 27 | public static void Requires(bool condition, string paramName, string message) 28 | where TException : Exception, new() 29 | { 30 | if (!condition) 31 | { 32 | if (typeof(TException) == typeof(ArgumentNullException)) 33 | throw new ArgumentNullException(paramName); 34 | 35 | if (typeof(TException) == typeof(ArgumentOutOfRangeException)) 36 | throw new ArgumentOutOfRangeException(paramName); 37 | 38 | if (typeof(TException) == typeof(ArgumentException)) 39 | throw new ArgumentException(message, paramName); 40 | 41 | throw new TException(); 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Src/Even/CommandProcessorSupervisor.cs: -------------------------------------------------------------------------------- 1 | using Akka.Actor; 2 | using Even.Messages; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace Even 10 | { 11 | public class CommandProcessorSupervisor : ReceiveActor 12 | { 13 | IActorRef _writer; 14 | GlobalOptions _options; 15 | 16 | Dictionary _processors = new Dictionary(); 17 | 18 | public static Props CreateProps(IActorRef writer, GlobalOptions options) 19 | { 20 | Argument.RequiresNotNull(writer, nameof(writer)); 21 | Argument.RequiresNotNull(options, nameof(options)); 22 | 23 | return Props.Create(writer, options); 24 | } 25 | 26 | public CommandProcessorSupervisor(IActorRef writer, GlobalOptions options) 27 | { 28 | this._writer = writer; 29 | this._options = options; 30 | 31 | Ready(); 32 | } 33 | 34 | void Ready() 35 | { 36 | Receive(envelope => 37 | { 38 | var command = envelope.Command; 39 | var key = envelope.ProcessorType; 40 | 41 | IActorRef aRef; 42 | 43 | // if the aggregate doesn't exist, create it 44 | if (!_processors.TryGetValue(key, out aRef)) 45 | { 46 | var props = PropsFactory.Create(envelope.ProcessorType); 47 | aRef = Context.ActorOf(props); 48 | aRef.Tell(new InitializeCommandProcessor(_writer, _options)); 49 | _processors.Add(key, aRef); 50 | Context.Watch(aRef); 51 | } 52 | 53 | aRef.Forward(command); 54 | }); 55 | 56 | // remove children that are going to stop 57 | Receive(_ => 58 | { 59 | RemoveActiveProcessor(Sender); 60 | Sender.Tell(StopNoticeAcknowledged.Instance); 61 | }); 62 | 63 | // remove terminated children 64 | Receive(t => RemoveActiveProcessor(t.ActorRef)); 65 | } 66 | 67 | void RemoveActiveProcessor(IActorRef aRef) 68 | { 69 | foreach (var kvp in _processors) 70 | { 71 | if (aRef.Equals(kvp.Value)) 72 | { 73 | _processors.Remove(kvp.Key); 74 | return; 75 | } 76 | } 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Src/Even/CommandResult.cs: -------------------------------------------------------------------------------- 1 | using Even.Messages; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace Even 9 | { 10 | public class CommandResult 11 | { 12 | public CommandResult() 13 | { 14 | Accepted = true; 15 | } 16 | 17 | public CommandResult(RejectReasons reasons) 18 | { 19 | Accepted = false; 20 | RejectReasons = reasons; 21 | } 22 | 23 | public bool Accepted { get; private set; } 24 | public RejectReasons RejectReasons { get; private set; } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Src/Even/Constants.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace Even 8 | { 9 | public static class Constants 10 | { 11 | public const string AnonymousEventType = "$AnonymousEvent"; 12 | public const string ClrTypeMetadataKey = "$SourceClrType"; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Src/Even/DefaultSerializer.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Newtonsoft.Json.Bson; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | 7 | namespace Even 8 | { 9 | public class DefaultSerializer : ISerializer 10 | { 11 | public virtual object DeserializeEvent(byte[] bytes, int format, Type type) 12 | { 13 | return DeserializeInternal(bytes, type); 14 | } 15 | 16 | public virtual IReadOnlyDictionary DeserializeMetadata(byte[] bytes) 17 | { 18 | return (IReadOnlyDictionary)DeserializeInternal(bytes, typeof(Dictionary)); 19 | } 20 | 21 | public virtual byte[] SerializeEvent(object domainEvent, int format) 22 | { 23 | return SerializeInternal(domainEvent); 24 | } 25 | 26 | public virtual byte[] SerializeMetadata(IReadOnlyDictionary metadata) 27 | { 28 | return SerializeInternal(metadata); 29 | } 30 | 31 | private static byte[] SerializeInternal(object o) 32 | { 33 | using (var ms = new MemoryStream()) 34 | using (var writer = new BsonWriter(ms)) 35 | { 36 | var serializer = new JsonSerializer(); 37 | serializer.Serialize(writer, o); 38 | return ms.ToArray(); 39 | } 40 | } 41 | 42 | private static object DeserializeInternal(byte[] bytes, Type type) 43 | { 44 | using (var ms = new MemoryStream(bytes)) 45 | using (var reader = new BsonReader(ms)) 46 | { 47 | var serializer = new JsonSerializer(); 48 | return serializer.Deserialize(reader, type); 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Src/Even/ESCategoryAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics.Contracts; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace Even 9 | { 10 | [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] 11 | public class ESCategoryAttribute : Attribute 12 | { 13 | public ESCategoryAttribute(string category) 14 | { 15 | Contract.Requires(!String.IsNullOrWhiteSpace(category)); 16 | 17 | this.Category = Category; 18 | } 19 | 20 | public string Category { get; } 21 | 22 | public static string GetCategory(Type type) 23 | { 24 | var a = Attribute.GetCustomAttribute(type, typeof(ESCategoryAttribute)) as ESCategoryAttribute; 25 | return a?.Category ?? type.Name.ToLowerInvariant(); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Src/Even/ESEventAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics.Contracts; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace Even 9 | { 10 | public class ESEventAttribute : Attribute 11 | { 12 | public ESEventAttribute() 13 | { } 14 | 15 | public ESEventAttribute(string eventType) 16 | { 17 | Contract.Requires(!String.IsNullOrEmpty(eventType)); 18 | this.EventType = eventType; 19 | } 20 | 21 | public string EventType { get; } 22 | 23 | public static string GetEventType(Type type) 24 | { 25 | var a = Attribute.GetCustomAttribute(type, typeof(ESEventAttribute)) as ESEventAttribute; 26 | return a.EventType ?? type.Name; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Src/Even/Even.nuspec: -------------------------------------------------------------------------------- 1 |  2 | 5 | 6 | 7 | Even 8 | $version$ 9 | nvivo, DamReev 10 | Even is an event sourcing framework built on top of Akka.NET. 11 | en-US 12 | https://github.com/evendotnet/Even/ 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /Src/Even/EvenException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace Even 8 | { 9 | public class EvenException : Exception 10 | { 11 | public EvenException() 12 | { } 13 | 14 | public EvenException(string message, Exception innerException) 15 | : base(message, innerException) 16 | { } 17 | } 18 | 19 | /// 20 | /// Thrown by the store when a write fails due to the stream not being in a specific sequence. 21 | /// 22 | public class UnexpectedStreamSequenceException : EvenException 23 | { } 24 | 25 | /// 26 | /// Thrown by the store when a write fails due to a duplicated entry. 27 | /// 28 | public class DuplicatedEntryException : EvenException 29 | { 30 | public DuplicatedEntryException() 31 | { } 32 | 33 | public DuplicatedEntryException(Exception innerException) 34 | : base("A duplicated entry was detected.", innerException) 35 | { } 36 | } 37 | 38 | public class MissingIndexEntryException : EvenException 39 | { } 40 | 41 | public class EventOutOfOrderException : EvenException 42 | { 43 | public EventOutOfOrderException(long expectedSequence, long receivedSequence, string message = null) 44 | : base(message, null) 45 | { 46 | this.ExpectedSequence = expectedSequence; 47 | this.ReceivedSequence = receivedSequence; 48 | } 49 | 50 | public long ExpectedSequence { get; } 51 | public long ReceivedSequence { get; } 52 | } 53 | 54 | public class RebuildRequestException : Exception 55 | { } 56 | 57 | public class CommandException : Exception 58 | { 59 | public CommandException(string message) 60 | : base(message) 61 | { } 62 | 63 | public CommandException(string message, Exception innerException) 64 | : base(message, innerException) 65 | { } 66 | } 67 | 68 | public class UnexpectedCommandResponseException : CommandException 69 | { 70 | public UnexpectedCommandResponseException(object response) 71 | : base("An unexpected command response was received.") 72 | { 73 | this.Response = response; 74 | } 75 | 76 | public object Response { get; } 77 | } 78 | 79 | public class QueryException : Exception 80 | { 81 | public QueryException(string message, Exception innerException) 82 | : base(message, innerException) 83 | { } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Src/Even/EvenGateway.cs: -------------------------------------------------------------------------------- 1 | using Akka.Actor; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using Even.Messages; 8 | using System.Diagnostics.Contracts; 9 | 10 | namespace Even 11 | { 12 | public class EvenGateway 13 | { 14 | public EvenGateway(EvenServices services, ActorSystem system, GlobalOptions options) 15 | { 16 | Argument.RequiresNotNull(services, nameof(services)); 17 | Argument.RequiresNotNull(system, nameof(system)); 18 | Argument.RequiresNotNull(options, nameof(options)); 19 | 20 | this._system = system; 21 | this.Services = services; 22 | this._options = options; 23 | } 24 | 25 | public EvenServices Services { get; } 26 | ActorSystem _system; 27 | GlobalOptions _options; 28 | 29 | /// 30 | /// Sends a command to an aggregate using the aggregate's category as the stream id. 31 | /// 32 | public Task SendAggregateCommand(object command, TimeSpan? timeout = null) 33 | where T : Aggregate 34 | { 35 | var stream = ESCategoryAttribute.GetCategory(typeof(T)); 36 | return SendAggregateCommand(stream, command, timeout); 37 | } 38 | 39 | /// 40 | /// Sends a command to an aggregate using the aggregate's category and an id to compose the stream id. 41 | /// The stream will be generated as "category-id". 42 | /// 43 | public Task SendAggregateCommand(object id, object command, TimeSpan? timeout = null) 44 | where T : Aggregate 45 | { 46 | Contract.Requires(id != null); 47 | 48 | var category = ESCategoryAttribute.GetCategory(typeof(T)); 49 | var stream = id != null ? category + "-" + id.ToString() : category; 50 | 51 | return SendAggregateCommand(stream, command, timeout); 52 | } 53 | 54 | /// 55 | /// Sends a command to an aggregate using the specified stream id. 56 | /// 57 | public Task SendAggregateCommand(string stream, object command, TimeSpan? timeout = null) 58 | where T : Aggregate 59 | { 60 | Argument.RequiresNotNull(stream, nameof(stream)); 61 | Argument.RequiresNotNull(command, nameof(command)); 62 | 63 | var to = timeout ?? _options.DefaultCommandTimeout; 64 | 65 | var aggregateCommand = new AggregateCommand(stream, command, to); 66 | var envelope = new AggregateCommandEnvelope(typeof(T), aggregateCommand); 67 | 68 | // TODO: add some threshold to Ask higher than the timeout 69 | return Ask(Services.Aggregates, envelope, to); 70 | } 71 | 72 | private static async Task Ask(IActorRef actor, object msg, TimeSpan timeout) 73 | { 74 | object response; 75 | 76 | try 77 | { 78 | response = (CommandResponse)await actor.Ask(msg, timeout); 79 | } 80 | catch (TaskCanceledException) 81 | { 82 | throw new CommandException("Command timeout"); 83 | } 84 | 85 | if (response is CommandSucceeded) 86 | return new CommandResult(); 87 | 88 | if (response is CommandRejected) 89 | return new CommandResult(((CommandRejected)response).Reasons); 90 | 91 | if (response is CommandFailed) 92 | { 93 | var cf = (CommandFailed)response; 94 | throw new CommandException("An error occoured while processing the command: " + cf.Reason, cf.Exception); 95 | } 96 | 97 | throw new UnexpectedCommandResponseException(response); 98 | } 99 | 100 | public Task Query(object query, TimeSpan? timeout = null) 101 | { 102 | var to = timeout ?? _options.DefaultQueryTimeout; 103 | return _system.Query(query, timeout ?? _options.DefaultQueryTimeout); 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /Src/Even/EvenServices.cs: -------------------------------------------------------------------------------- 1 | using Akka.Actor; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace Even 9 | { 10 | public class EvenServices 11 | { 12 | public IActorRef Reader { get; internal set; } 13 | public IActorRef Writer { get; internal set; } 14 | public IActorRef Aggregates { get; internal set; } 15 | public IActorRef CommandProcessors { get; internal set; } 16 | public IActorRef EventProcessors { get; internal set; } 17 | public IActorRef Projections { get; internal set; } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Src/Even/EvenSetup.cs: -------------------------------------------------------------------------------- 1 | using Akka.Actor; 2 | using Even.Messages; 3 | using Even.Persistence; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Diagnostics.Contracts; 7 | using System.Linq; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | 11 | namespace Even 12 | { 13 | public class EvenSetup 14 | { 15 | ActorSystem _system; 16 | IEventStore _store; 17 | ISerializer _serializer; 18 | GlobalOptions _options; 19 | List _eventProcessors = new List(); 20 | List _projections = new List(); 21 | 22 | public EvenSetup(ActorSystem system) 23 | { 24 | Argument.RequiresNotNull(system, nameof(system)); 25 | 26 | this._system = system; 27 | } 28 | 29 | public EvenSetup UseStore(IEventStore store) 30 | { 31 | Argument.RequiresNotNull(store, nameof(store)); 32 | 33 | _store = store; 34 | 35 | return this; 36 | } 37 | 38 | public EvenSetup UseSerializer(ISerializer serializer) 39 | { 40 | Argument.RequiresNotNull(serializer, nameof(serializer)); 41 | 42 | _serializer = serializer; 43 | 44 | return this; 45 | } 46 | public EvenSetup UseOptions(GlobalOptions options) 47 | { 48 | Argument.RequiresNotNull(options, nameof(options)); 49 | 50 | _options = options; 51 | 52 | return this; 53 | } 54 | 55 | public EvenSetup AddProjections(Func> func) 56 | { 57 | Argument.RequiresNotNull(func, nameof(func)); 58 | 59 | foreach (var t in func()) 60 | _projections.Add(new StartProjection(t)); 61 | 62 | return this; 63 | } 64 | 65 | public EvenSetup AddProjection() 66 | where T : Projection 67 | { 68 | _projections.Add(new StartProjection(typeof(T))); 69 | 70 | return this; 71 | } 72 | 73 | public EvenSetup AddEventProcessors(Func> func) 74 | { 75 | Argument.RequiresNotNull(func, nameof(func)); 76 | 77 | foreach(var t in func()) 78 | _eventProcessors.Add(new StartEventProcessor(t)); 79 | 80 | return this; 81 | } 82 | 83 | public EvenSetup AddEventProcessor() 84 | where T : EventProcessor 85 | { 86 | _eventProcessors.Add(new StartEventProcessor(typeof(T))); 87 | 88 | return this; 89 | } 90 | 91 | public async Task Start(string name = null) 92 | { 93 | var options = _options ?? new GlobalOptions(); 94 | var store = _store ?? new InMemoryStore(); 95 | var serializer = _serializer ?? new DefaultSerializer(); 96 | 97 | var startInfo = new EvenStartInfo(store, serializer, options); 98 | startInfo.Projections.AddRange(_projections); 99 | startInfo.EventProcessors.AddRange(_eventProcessors); 100 | 101 | var props = EvenMaster.CreateProps(startInfo); 102 | var master = _system.ActorOf(props, name); 103 | var timeout = TimeSpan.FromSeconds(5); 104 | 105 | var services = (EvenServices) await master.Ask(new GetEvenServices(), timeout); 106 | 107 | return new EvenGateway(services, _system, options); 108 | } 109 | } 110 | 111 | public static class EvenSetupExtensions 112 | { 113 | public static EvenSetup SetupEven(this ActorSystem system) 114 | { 115 | return new EvenSetup(system); 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /Src/Even/EvenStorageFormatAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace Even 8 | { 9 | [AttributeUsage(AttributeTargets.Class)] 10 | public class EvenStorageFormatAttribute : Attribute 11 | { 12 | public EvenStorageFormatAttribute(int format) 13 | { 14 | this.StorageFormat = format; 15 | } 16 | 17 | public int StorageFormat { get; } 18 | 19 | public static int GetStorageFormat(Type type) 20 | { 21 | if (type == null) 22 | return 0; 23 | 24 | var attr = type.GetCustomAttributes(typeof(EvenStorageFormatAttribute), false).FirstOrDefault() as EvenStorageFormatAttribute; 25 | 26 | return attr?.StorageFormat ?? 0; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Src/Even/EventCount.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace Even 8 | { 9 | public static class EventCount 10 | { 11 | public const int Unlimited = -1; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Src/Even/EventProcessor.cs: -------------------------------------------------------------------------------- 1 | using Akka.Actor; 2 | using Akka.Event; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace Even 10 | { 11 | public class EventProcessor : ReceiveActor 12 | { 13 | PersistedEventHandler _handlers = new PersistedEventHandler(); 14 | 15 | public EventProcessor() 16 | { 17 | Receive(async e => 18 | { 19 | await _handlers.Handle(e); 20 | }); 21 | } 22 | 23 | protected void OnEvent(Func, Task> handler) 24 | { 25 | Context.System.EventStream.Subscribe>(Self); 26 | _handlers.AddHandler(e => handler((IPersistedEvent) e)); 27 | } 28 | 29 | protected void OnEvent(Action> handler) 30 | { 31 | Context.System.EventStream.Subscribe>(Self); 32 | _handlers.AddHandler(e => handler((IPersistedEvent)e)); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Src/Even/EventProcessorSupervisor.cs: -------------------------------------------------------------------------------- 1 | using Akka.Actor; 2 | using Even.Messages; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace Even 10 | { 11 | public class EventProcessorSupervisor: ReceiveActor 12 | { 13 | Dictionary _processors = new Dictionary(); 14 | GlobalOptions _options; 15 | 16 | public static Props CreateProps(GlobalOptions options) 17 | { 18 | return Props.Create(options); 19 | } 20 | 21 | public EventProcessorSupervisor(GlobalOptions options) 22 | { 23 | Argument.Requires(options != null, nameof(options)); 24 | _options = options; 25 | 26 | Ready(); 27 | } 28 | 29 | void Ready() 30 | { 31 | Receive(m => 32 | { 33 | var props = PropsFactory.Create(m.EventProcessorType); 34 | Context.ActorOf(props); 35 | }); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Src/Even/EventReader.cs: -------------------------------------------------------------------------------- 1 | using Akka.Actor; 2 | using Even.Messages; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | 10 | namespace Even 11 | { 12 | public class EventReader : ReceiveActor 13 | { 14 | Props _workerProps; 15 | 16 | public EventReader(IEventStoreReader reader, PersistedEventFactory factory) 17 | { 18 | Argument.Requires(reader != null, nameof(reader)); 19 | Argument.Requires(factory != null, nameof(factory)); 20 | 21 | _workerProps = Props.Create(reader, factory); 22 | Receive(r => HandleRequest(r)); 23 | } 24 | 25 | void HandleRequest(ReadRequest r) 26 | { 27 | var worker = Context.ActorOf(_workerProps); 28 | worker.Forward(r); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Src/Even/EventRegistry.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics.Contracts; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace Even 9 | { 10 | public class EventRegistry 11 | { 12 | public Dictionary _mapping = new Dictionary(StringComparer.InvariantCultureIgnoreCase); 13 | 14 | public void Register(string eventType, Type clrType) 15 | { 16 | Contract.Requires(!String.IsNullOrWhiteSpace(eventType)); 17 | Contract.Requires(clrType != null); 18 | 19 | lock (_mapping) 20 | { 21 | if (_mapping.ContainsKey(eventType)) 22 | { 23 | var existingType = _mapping[eventType]; 24 | throw new Exception($"Cannot register '{clrType.AssemblyQualifiedName}' to event type '{eventType}', it's already registered to '{existingType.AssemblyQualifiedName}'"); 25 | } 26 | 27 | _mapping.Add(eventType, clrType); 28 | } 29 | } 30 | 31 | public Type GetClrType(string eventType) 32 | { 33 | if (eventType == null) 34 | return null; 35 | 36 | Type t; 37 | 38 | lock (_mapping) 39 | { 40 | if (_mapping.TryGetValue(eventType, out t)) 41 | return t; 42 | } 43 | 44 | return null; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Src/Even/EventStoreReader.cs: -------------------------------------------------------------------------------- 1 | using Akka.Actor; 2 | using Even.Messages; 3 | using System.Threading.Tasks; 4 | 5 | namespace Even 6 | { 7 | public class EventStoreReader : ReceiveActor 8 | { 9 | Props _readProps; 10 | Props _readStreamProps; 11 | Props _readIndexedProjectionProps; 12 | Props _readProjectionCheckpointProps; 13 | Props _readHighestGlobalSequenceProps; 14 | 15 | public static Props CreateProps(IEventStore store, IPersistedEventFactory factory, GlobalOptions options) 16 | { 17 | return Props.Create(store, factory, options); 18 | } 19 | 20 | public EventStoreReader(IEventStore store, IPersistedEventFactory factory, GlobalOptions options) 21 | { 22 | _readProps = ReadWorker.CreateProps(store, factory); 23 | _readStreamProps = ReadStreamWorker.CreateProps(store, factory); 24 | _readIndexedProjectionProps = ReadIndexedProjectionStreamWorker.CreateProps(store, factory); 25 | _readProjectionCheckpointProps = ReadProjectionCheckpointWorker.CreateProps(store); 26 | _readHighestGlobalSequenceProps = ReadHighestGlobalSequenceWorker.CreateProps(store); 27 | 28 | Ready(); 29 | } 30 | 31 | // test only 32 | public static Props CreateProps(Props readProps, Props readStreamProps, Props readIndexedProjectionProps, Props readProjectionCheckpointProps, Props readHighestGlobalSequenceProps, GlobalOptions options) 33 | { 34 | return Props.Create(readProps, readStreamProps, readIndexedProjectionProps, readProjectionCheckpointProps, readHighestGlobalSequenceProps, options); 35 | } 36 | 37 | // test only 38 | public EventStoreReader(Props readProps, Props readStreamProps, Props readIndexedProjectionProps, Props readProjectionCheckpointProps, Props readHighestGlobalSequenceProps, GlobalOptions options) 39 | { 40 | _readProps = readProps; 41 | _readStreamProps = readStreamProps; 42 | _readIndexedProjectionProps = readIndexedProjectionProps; 43 | _readProjectionCheckpointProps = readProjectionCheckpointProps; 44 | _readHighestGlobalSequenceProps = readHighestGlobalSequenceProps; 45 | 46 | Ready(); 47 | } 48 | 49 | void Ready() 50 | { 51 | Receive(r => 52 | { 53 | var worker = Context.ActorOf(_readProps); 54 | worker.Forward(r); 55 | }); 56 | 57 | Receive(r => 58 | { 59 | var worker = Context.ActorOf(_readStreamProps); 60 | worker.Forward(r); 61 | }); 62 | 63 | Receive(r => 64 | { 65 | var worker = Context.ActorOf(_readIndexedProjectionProps); 66 | worker.Forward(r); 67 | }); 68 | 69 | Receive(r => 70 | { 71 | var worker = Context.ActorOf(_readProjectionCheckpointProps); 72 | worker.Forward(r); 73 | }); 74 | 75 | Receive(r => 76 | { 77 | var worker = Context.ActorOf(_readHighestGlobalSequenceProps); 78 | worker.Forward(r); 79 | }); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Src/Even/EventStoreWriter.cs: -------------------------------------------------------------------------------- 1 | using Akka.Actor; 2 | using Akka.Event; 3 | using Even.Messages; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Diagnostics.Contracts; 7 | using System.Linq; 8 | using System.Threading.Tasks; 9 | 10 | namespace Even 11 | { 12 | public class EventStoreWriter : ReceiveActor 13 | { 14 | IActorRef _serialWriter; 15 | IActorRef _bufferedWriter; 16 | IActorRef _indexWriter; 17 | IActorRef _checkpointWriter; 18 | 19 | public static Props CreateProps(IEventStore store, ISerializer serializer, IActorRef dispatcher, GlobalOptions options) 20 | { 21 | Argument.RequiresNotNull(store, nameof(store)); 22 | Argument.RequiresNotNull(serializer, nameof(serializer)); 23 | Argument.RequiresNotNull(dispatcher, nameof(dispatcher)); 24 | Argument.RequiresNotNull(options, nameof(options)); 25 | 26 | return Props.Create(store, serializer, dispatcher, options); 27 | } 28 | 29 | public EventStoreWriter(IEventStore store, ISerializer serializer, IActorRef dispatcher, GlobalOptions options) 30 | { 31 | var serialProps = SerialEventStreamWriter.CreateProps(store, serializer, dispatcher, options); 32 | _serialWriter = Context.ActorOf(serialProps, "serial"); 33 | 34 | var bufferedProps = BufferedEventWriter.CreateProps(store, serializer, dispatcher, options); 35 | _bufferedWriter = Context.ActorOf(bufferedProps, "buffered"); 36 | 37 | var indexWriterProps = ProjectionIndexWriter.CreateProps(store, options); 38 | _indexWriter = Context.ActorOf(indexWriterProps, "index"); 39 | 40 | var checkpointWriterProps = ProjectionCheckpointWriter.CreateProps(store, options); 41 | _checkpointWriter = Context.ActorOf(checkpointWriterProps, "checkpoint"); 42 | 43 | Ready(); 44 | } 45 | 46 | // test only 47 | public static Props CreateProps(IActorRef serial, IActorRef buffered, IActorRef indexWriter, IActorRef checkpointWriter, GlobalOptions options) 48 | { 49 | return Props.Create(serial, buffered, indexWriter, checkpointWriter, options); 50 | } 51 | 52 | // test only 53 | public EventStoreWriter(IActorRef serialWriter, IActorRef bufferedWriter, IActorRef indexWriter, IActorRef checkpointWriter, GlobalOptions options) 54 | { 55 | _serialWriter = serialWriter; 56 | _bufferedWriter = bufferedWriter; 57 | _indexWriter = indexWriter; 58 | _checkpointWriter = checkpointWriter; 59 | 60 | Ready(); 61 | } 62 | 63 | public void Ready() 64 | { 65 | Receive(request => 66 | { 67 | if (request.ExpectedStreamSequence == ExpectedSequence.Any) 68 | _bufferedWriter.Forward(request); 69 | else 70 | _serialWriter.Forward(request); 71 | }); 72 | 73 | Receive(request => 74 | { 75 | _indexWriter.Forward(request); 76 | }); 77 | 78 | Receive(request => 79 | { 80 | _checkpointWriter.Forward(request); 81 | }); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Src/Even/ExpectedSequence.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace Even 8 | { 9 | public static class ExpectedSequence 10 | { 11 | /// 12 | /// The writer will ensure the stream doesn't have any events. 13 | /// 14 | public const int None = 0; 15 | 16 | /// 17 | /// No checks will be made by the writer and all events will be appended to the end of the stream. 18 | /// 19 | public const int Any = -1; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Src/Even/ExtensionMethods.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace Even 8 | { 9 | internal static class LinkedListExtensions 10 | { 11 | public static IEnumerable> Nodes(this LinkedList list) 12 | { 13 | var node = list.First; 14 | 15 | while (node != null) 16 | { 17 | yield return node; 18 | node = node.Next; 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Src/Even/ICommandValidator.cs: -------------------------------------------------------------------------------- 1 | using Akka.Actor; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace Even 9 | { 10 | public interface ICommandValidator 11 | { 12 | Task ValidateAsync(object command); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Src/Even/IEventStore.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace Even 7 | { 8 | public interface IEventStore : IEventStoreWriter, IEventStoreReader, IProjectionStoreWriter, IProjectionStoreReader 9 | { 10 | /// 11 | /// Initializes the store driver. This method is called by the Master service every time Even starts. 12 | /// 13 | Task InitializeAsync(); 14 | } 15 | 16 | // the following interfaces exist only for internal use 17 | 18 | public interface IEventStoreWriter 19 | { 20 | Task WriteAsync(IReadOnlyCollection events); 21 | Task WriteStreamAsync(Stream stream, int expectedSequence, IReadOnlyCollection events); 22 | } 23 | 24 | public interface IEventStoreReader 25 | { 26 | Task ReadHighestGlobalSequenceAsync(); 27 | Task ReadAsync(long initialSequence, int count, Action readCallback, CancellationToken ct); 28 | Task ReadStreamAsync(Stream stream, int initialSequence, int count, Action readCallback, CancellationToken ct); 29 | } 30 | 31 | public interface IProjectionStoreWriter 32 | { 33 | Task ClearProjectionIndexAsync(Stream stream); 34 | Task WriteProjectionIndexAsync(Stream stream, int expectedSequence, IReadOnlyCollection globalSequences); 35 | Task WriteProjectionCheckpointAsync(Stream stream, long globalSequence); 36 | } 37 | 38 | public interface IProjectionStoreReader 39 | { 40 | Task ReadProjectionCheckpointAsync(Stream stream); 41 | Task ReadHighestIndexedProjectionGlobalSequenceAsync(Stream stream); 42 | Task ReadHighestIndexedProjectionStreamSequenceAsync(Stream stream); 43 | 44 | Task ReadIndexedProjectionStreamAsync(Stream stream, int initialSequence, int count, Action readCallback, CancellationToken ct); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Src/Even/IPersistedEvent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace Even 8 | { 9 | /// 10 | /// Represents an event that was persisted to the event store. 11 | /// 12 | public interface IPersistedEvent 13 | { 14 | long GlobalSequence { get; } 15 | Guid EventID { get; } 16 | Stream Stream { get; } 17 | string EventType { get; } 18 | DateTime UtcTimestamp { get; } 19 | IReadOnlyDictionary Metadata { get; } 20 | object DomainEvent { get; } 21 | } 22 | 23 | public interface IPersistedEvent : IPersistedEvent 24 | { 25 | new T DomainEvent { get; } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Src/Even/IPersistedStreamEvent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace Even 8 | { 9 | public interface IPersistedStreamEvent : IPersistedEvent 10 | { 11 | int StreamSequence { get; } 12 | } 13 | 14 | public interface IPersistedStreamEvent : IPersistedStreamEvent, IPersistedEvent 15 | { } 16 | } 17 | -------------------------------------------------------------------------------- /Src/Even/IProjectionStreamPredicate.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.Contracts; 3 | 4 | namespace Even 5 | { 6 | public interface IProjectionStreamPredicate 7 | { 8 | bool EventMatches(IPersistedEvent persistedEvent); 9 | object GetDeterministicHashSource(); 10 | } 11 | 12 | public class DomainEventPredicate : IProjectionStreamPredicate 13 | { 14 | public DomainEventPredicate(Type domainEventType) 15 | { 16 | Contract.Requires(domainEventType != null); 17 | _type = domainEventType; 18 | } 19 | 20 | Type _type; 21 | 22 | public bool EventMatches(IPersistedEvent persistedEvent) 23 | { 24 | return _type.IsAssignableFrom(persistedEvent.DomainEvent.GetType()); 25 | } 26 | 27 | public object GetDeterministicHashSource() 28 | { 29 | return GetType().Name + _type.FullName + _type.Assembly.GetName().FullName; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Src/Even/ISerializer.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Newtonsoft.Json.Bson; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using System.Linq; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | 10 | namespace Even 11 | { 12 | public interface ISerializer 13 | { 14 | byte[] SerializeEvent(object domainEvent, int format); 15 | object DeserializeEvent(byte[] bytes, int format, Type type); 16 | 17 | byte[] SerializeMetadata(IReadOnlyDictionary metadata); 18 | IReadOnlyDictionary DeserializeMetadata(byte[] bytes); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Src/Even/IndexSequenceEntry.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace Even 8 | { 9 | public class IndexSequenceEntry 10 | { 11 | public int ProjectionStreamSequence { get; set; } 12 | public long GlobalSequence { get; set; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Src/Even/IndexedProjectionStreamReader.cs: -------------------------------------------------------------------------------- 1 | using Akka.Actor; 2 | using Even.Messages; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace Even 10 | { 11 | public class IndexedProjectionStreamReader : ReceiveActor 12 | { 13 | Props _workerProps; 14 | 15 | public IndexedProjectionStreamReader(IEventStoreReader reader, PersistedEventFactory factory) 16 | { 17 | Argument.Requires(reader != null, nameof(reader)); 18 | Argument.Requires(factory != null, nameof(factory)); 19 | 20 | _workerProps = Props.Create(reader, factory); 21 | Receive(r => HandleRequest(r)); 22 | } 23 | 24 | void HandleRequest(ReadIndexedProjectionStreamRequest r) 25 | { 26 | var worker = Context.ActorOf(_workerProps); 27 | worker.Forward(r); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Src/Even/Internals/CollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | 5 | namespace Even.Internals 6 | { 7 | internal static class CollectionExtensions 8 | { 9 | public static IReadOnlyCollection AsReadOnlyCollection(this LinkedList @this) 10 | { 11 | if (@this == null) throw new ArgumentNullException(nameof(@this)); 12 | return new ReadOnlyLinkedListAdapter(@this); 13 | } 14 | 15 | private class ReadOnlyLinkedListAdapter:IReadOnlyCollection 16 | { 17 | private readonly LinkedList _items; 18 | 19 | public ReadOnlyLinkedListAdapter(LinkedList items) 20 | { 21 | _items = items; 22 | } 23 | 24 | public IEnumerator GetEnumerator() 25 | { 26 | return _items.GetEnumerator(); 27 | } 28 | 29 | IEnumerator IEnumerable.GetEnumerator() 30 | { 31 | return GetEnumerator(); 32 | } 33 | 34 | public int Count 35 | { 36 | get 37 | { 38 | return _items.Count; 39 | } 40 | } 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /Src/Even/Internals/Unit.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Even.Internals 4 | { 5 | internal class Unit 6 | { 7 | public static readonly Unit Instance = new Unit(); 8 | private Unit() 9 | { 10 | } 11 | 12 | public static Task GetCompletedTask() 13 | { 14 | return Task.FromResult(Instance); 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /Src/Even/MessageHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using Even.Internals; 5 | 6 | namespace Even 7 | { 8 | public class MessageHandler 9 | { 10 | /// 11 | /// Creates a new message handler for message of type . 12 | /// 13 | /// 14 | /// Optional. A mapper function that returns the type to match the handler. 15 | /// If null, the actual message type will be used to find the matching handler. 16 | /// 17 | public MessageHandler(Func mapper = null) 18 | { 19 | _mapper = mapper; 20 | } 21 | 22 | Func _mapper; 23 | class HandlerList : LinkedList> { } 24 | 25 | Dictionary _handlers = new Dictionary(); 26 | 27 | public async Task Handle(TMessage message) 28 | { 29 | if (message == null) 30 | return false; 31 | 32 | Type type; 33 | 34 | if (_mapper != null) 35 | { 36 | type = _mapper(message); 37 | 38 | if (type == null) 39 | return false; 40 | } 41 | else 42 | { 43 | type = message.GetType(); 44 | } 45 | 46 | HandlerList list; 47 | 48 | if (_handlers.TryGetValue(type, out list)) 49 | { 50 | foreach (var handler in list) 51 | await handler(message); 52 | 53 | return true; 54 | } 55 | 56 | return false; 57 | } 58 | 59 | public void AddHandler(Func handler) 60 | { 61 | HandlerList list; 62 | 63 | if (!_handlers.TryGetValue(typeof(T), out list)) 64 | { 65 | list = new HandlerList(); 66 | _handlers.Add(typeof(T), list); 67 | } 68 | 69 | list.AddLast(handler); 70 | } 71 | 72 | public void AddHandler(Action handler) 73 | { 74 | AddHandler(msg => 75 | { 76 | handler(msg); 77 | return Unit.GetCompletedTask(); 78 | }); 79 | } 80 | } 81 | 82 | public class PersistedEventHandler : MessageHandler 83 | { 84 | public PersistedEventHandler() 85 | : base(e => e.DomainEvent?.GetType()) 86 | { } 87 | } 88 | 89 | public class QueryHandler : MessageHandler 90 | { 91 | public QueryHandler() 92 | : base(e => e.Message?.GetType()) 93 | { } 94 | } 95 | 96 | public class ObjectHandler : MessageHandler 97 | { } 98 | } 99 | -------------------------------------------------------------------------------- /Src/Even/Messages/CommandMessages.cs: -------------------------------------------------------------------------------- 1 | using Akka.Actor; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Collections.Specialized; 5 | using System.Diagnostics.Contracts; 6 | using System.Linq; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | 10 | namespace Even.Messages 11 | { 12 | public class AggregateCommandEnvelope 13 | { 14 | public AggregateCommandEnvelope(Type aggregateType, AggregateCommand command) 15 | { 16 | Argument.RequiresNotNull(aggregateType, nameof(aggregateType)); 17 | Argument.RequiresNotNull(command, nameof(command)); 18 | 19 | this.AggregateType = aggregateType; 20 | this.Command = command; 21 | } 22 | 23 | public Type AggregateType { get; } 24 | public AggregateCommand Command { get; } 25 | } 26 | 27 | /// 28 | /// Represents a command sent to an aggregate. 29 | /// 30 | public class AggregateCommand 31 | { 32 | internal AggregateCommand(AggregateCommand previous) 33 | { 34 | this.CommandID = previous.CommandID; 35 | this.Stream = previous.Stream; 36 | this.Command = previous.Command; 37 | this.Timeout = previous.Timeout; 38 | } 39 | 40 | public AggregateCommand(Stream stream, object command, TimeSpan timeout) 41 | { 42 | Argument.RequiresNotNull(stream, nameof(stream)); 43 | Argument.RequiresNotNull(command, nameof(command)); 44 | 45 | this.CommandID = Guid.NewGuid(); 46 | this.Stream = stream; 47 | this.Command = command; 48 | this.Timeout = Timeout.In(timeout); 49 | } 50 | 51 | public Guid CommandID { get; } 52 | public Stream Stream { get; } 53 | public object Command { get; } 54 | public Timeout Timeout { get; } 55 | } 56 | 57 | public class RetryAggregateCommand : AggregateCommand 58 | { 59 | public RetryAggregateCommand(AggregateCommand command, int attemptNo) 60 | : base(command) 61 | { 62 | this.Attempt = attemptNo; 63 | } 64 | 65 | public int Attempt { get; } 66 | } 67 | 68 | public class CommandResponse 69 | { 70 | public CommandResponse(Guid commandId) 71 | { 72 | this.CommandID = commandId; 73 | } 74 | 75 | public Guid CommandID { get; } 76 | } 77 | 78 | /// 79 | /// Indicates the command was applied successfully. 80 | /// 81 | public class CommandSucceeded : CommandResponse 82 | { 83 | public CommandSucceeded(Guid commandId) 84 | : base(commandId) 85 | { } 86 | } 87 | 88 | public class CommandRejected : CommandResponse 89 | { 90 | public CommandRejected(Guid commandId, RejectReasons reasons) 91 | : base(commandId) 92 | { 93 | Argument.RequiresNotNull(reasons, nameof(reasons)); 94 | 95 | this.Reasons = reasons; 96 | } 97 | 98 | public RejectReasons Reasons { get; } 99 | } 100 | 101 | /// 102 | /// Indicates the command failed for some reason. 103 | /// 104 | public class CommandFailed : CommandResponse 105 | { 106 | public CommandFailed(Guid commandId, Exception ex) 107 | : base(commandId) 108 | { 109 | this.Exception = ex; 110 | this.Reason = ex.Message; 111 | } 112 | 113 | public CommandFailed(Guid commandId, string reason) 114 | : base(commandId) 115 | { 116 | this.Reason = reason; 117 | } 118 | 119 | public string Reason { get; } 120 | public Exception Exception { get; } 121 | } 122 | 123 | public class CommandTimeout : CommandResponse 124 | { 125 | public CommandTimeout(Guid commandId) 126 | : base(commandId) 127 | { } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /Src/Even/Messages/CommandProcessorMessages.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace Even.Messages 8 | { 9 | public class ProcessorCommandEnvelope 10 | { 11 | public ProcessorCommandEnvelope(Type aggregateType, ProcessorCommand command) 12 | { 13 | Argument.RequiresNotNull(aggregateType, nameof(aggregateType)); 14 | Argument.RequiresNotNull(command, nameof(command)); 15 | 16 | this.ProcessorType = aggregateType; 17 | this.Command = command; 18 | } 19 | 20 | public Type ProcessorType { get; } 21 | public ProcessorCommand Command { get; } 22 | } 23 | 24 | public class ProcessorCommand 25 | { 26 | public ProcessorCommand(object command, Timeout timeout) 27 | { 28 | Argument.RequiresNotNull(command, nameof(command)); 29 | 30 | this.CommandID = Guid.NewGuid(); 31 | this.Command = command; 32 | this.Timeout = timeout; 33 | } 34 | 35 | public Guid CommandID { get; } 36 | public object Command { get; } 37 | public Timeout Timeout { get; } 38 | } 39 | } -------------------------------------------------------------------------------- /Src/Even/Messages/EventProcessorMessages.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Even.Messages 4 | { 5 | public class StartEventProcessor 6 | { 7 | public StartEventProcessor(Type eventProcessorType) 8 | { 9 | Argument.RequiresNotNull(eventProcessorType, nameof(eventProcessorType)); 10 | 11 | this.EventProcessorType = eventProcessorType; 12 | } 13 | 14 | public Type EventProcessorType { get; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Src/Even/Messages/GenericMessages.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace Even.Messages 8 | { 9 | public class CancelRequest : IRequest 10 | { 11 | public CancelRequest(Guid requestId) 12 | { 13 | this.RequestID = requestId; 14 | } 15 | 16 | public Guid RequestID { get; } 17 | } 18 | 19 | public class Cancelled 20 | { 21 | public Cancelled(Guid requestId) 22 | { 23 | this.RequestID = requestId; 24 | } 25 | 26 | public Guid RequestID { get; } 27 | } 28 | 29 | public class Aborted 30 | { 31 | public Aborted(Guid requestId, Exception exception) 32 | { 33 | this.RequestID = requestId; 34 | this.Exception = exception; 35 | } 36 | 37 | public Guid RequestID { get; } 38 | public Exception Exception { get; private set; } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Src/Even/Messages/IRequest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Even.Messages 4 | { 5 | interface IRequest 6 | { 7 | Guid RequestID { get; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Src/Even/Messages/InformationMessages.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace Even.Messages 8 | { 9 | public class GetEvenServices 10 | { } 11 | } 12 | -------------------------------------------------------------------------------- /Src/Even/Messages/Initialization.cs: -------------------------------------------------------------------------------- 1 | using Akka.Actor; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Diagnostics.Contracts; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace Even.Messages 10 | { 11 | public class InitializationResult 12 | { 13 | private InitializationResult() 14 | { } 15 | 16 | public bool Initialized { get; private set; } 17 | public Exception Exception { get; private set; } 18 | 19 | public static InitializationResult Successful() 20 | { 21 | return new InitializationResult { Initialized = true }; 22 | } 23 | 24 | public static InitializationResult Failed(Exception ex) 25 | { 26 | return new InitializationResult { Initialized = false, Exception = ex }; 27 | } 28 | } 29 | 30 | public class InitializeCommandProcessor 31 | { 32 | public InitializeCommandProcessor(IActorRef writer, GlobalOptions options) 33 | { 34 | Argument.RequiresNotNull(writer, nameof(writer)); 35 | Argument.RequiresNotNull(options, nameof(options)); 36 | 37 | this.Writer = writer; 38 | this.Options = options; 39 | } 40 | 41 | public IActorRef Writer { get; set; } 42 | public GlobalOptions Options { get; } 43 | } 44 | 45 | public class InitializeAggregate 46 | { 47 | public InitializeAggregate(IActorRef reader, IActorRef writer, GlobalOptions options) 48 | { 49 | Argument.RequiresNotNull(reader, nameof(reader)); 50 | Argument.RequiresNotNull(writer, nameof(writer)); 51 | Argument.RequiresNotNull(options, nameof(options)); 52 | 53 | this.Reader = reader; 54 | this.Writer = writer; 55 | this.Options = options; 56 | } 57 | 58 | public IActorRef Reader { get; } 59 | public IActorRef Writer { get; } 60 | public GlobalOptions Options { get; } 61 | } 62 | 63 | public class WillStop 64 | { 65 | private WillStop() { } 66 | public static readonly WillStop Instance = new WillStop(); 67 | } 68 | 69 | public class StopNoticeAcknowledged 70 | { 71 | private StopNoticeAcknowledged() { } 72 | public static readonly StopNoticeAcknowledged Instance = new StopNoticeAcknowledged(); 73 | } 74 | 75 | public class InitializeProjection 76 | { 77 | public InitializeProjection(IActorRef projectionStreamSupervisor, GlobalOptions options) 78 | { 79 | Argument.RequiresNotNull(projectionStreamSupervisor, nameof(projectionStreamSupervisor)); 80 | Argument.RequiresNotNull(options, nameof(options)); 81 | 82 | this.ProjectionStreams = projectionStreamSupervisor; 83 | this.Options = options; 84 | } 85 | 86 | public IActorRef ProjectionStreams { get; } 87 | public GlobalOptions Options { get; } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Src/Even/Messages/Persistence.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics.Contracts; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace Even.Messages 9 | { 10 | /// 11 | /// Represents a request to persist events to a stream. 12 | /// 13 | public class PersistenceRequest 14 | { 15 | public PersistenceRequest(IReadOnlyList events) 16 | { 17 | Argument.Requires(events != null && events.Any(), nameof(events), "The argument must contain at least one event."); 18 | 19 | this.Events = events; 20 | } 21 | 22 | public PersistenceRequest(Stream stream, int expectedStreamSequence, IReadOnlyList events) 23 | : this(events) 24 | { 25 | Argument.RequiresNotNull(stream, nameof(stream)); 26 | Argument.Requires(expectedStreamSequence >= 0 || expectedStreamSequence == ExpectedSequence.Any, nameof(expectedStreamSequence)); 27 | Argument.Requires(events.All(e => e.Stream.Equals(stream)), nameof(events), $"All events must belong to the stream '{stream}'"); 28 | 29 | this.Stream = stream; 30 | this.ExpectedStreamSequence = expectedStreamSequence; 31 | this.Events = events; 32 | } 33 | 34 | public Guid PersistenceID { get; } = Guid.NewGuid(); 35 | public Stream Stream { get; } 36 | public int ExpectedStreamSequence { get; } = ExpectedSequence.Any; 37 | public IReadOnlyList Events { get; } 38 | } 39 | 40 | public abstract class PersistenceResponse 41 | { 42 | public PersistenceResponse(Guid persistenceId) 43 | { 44 | PersistenceID = persistenceId; 45 | } 46 | 47 | public Guid PersistenceID { get; } 48 | } 49 | 50 | public class PersistenceSuccess : PersistenceResponse 51 | { 52 | public PersistenceSuccess(Guid persistenceId) 53 | : base(persistenceId) 54 | { } 55 | } 56 | 57 | public class PersistenceFailure : PersistenceResponse 58 | { 59 | public PersistenceFailure(Guid persistenceId, Exception ex, string reason = null) 60 | : base(persistenceId) 61 | { 62 | Exception = ex; 63 | _message = reason; 64 | } 65 | 66 | string _message; 67 | public string Reason => _message ?? Exception?.Message; 68 | public Exception Exception { get; private set; } 69 | } 70 | 71 | public class UnexpectedStreamSequence : PersistenceResponse 72 | { 73 | public UnexpectedStreamSequence(Guid persistenceId) 74 | : base(persistenceId) 75 | { } 76 | } 77 | 78 | public class DuplicatedEntry : PersistenceResponse 79 | { 80 | public DuplicatedEntry(Guid persistenceId) 81 | : base(persistenceId) 82 | { } 83 | } 84 | 85 | public class ProjectionIndexPersistenceRequest 86 | { 87 | public ProjectionIndexPersistenceRequest(Stream projectionStream, int projectionStreamSequence, long globalSequence) 88 | { 89 | Argument.Requires(projectionStream != null, nameof(projectionStream)); 90 | Argument.Requires(projectionStreamSequence >= 0, nameof(projectionStreamSequence)); 91 | Argument.Requires(globalSequence >= 0, nameof(globalSequence)); 92 | 93 | this.ProjectionStream = projectionStream; 94 | this.ProjectionStreamSequence = projectionStreamSequence; 95 | this.GlobalSequence = globalSequence; 96 | } 97 | 98 | public Stream ProjectionStream { get; } 99 | public int ProjectionStreamSequence { get; } 100 | public long GlobalSequence { get; } 101 | } 102 | 103 | public class ProjectionIndexInconsistencyDetected 104 | { } 105 | 106 | public class ProjectionCheckpointPersistenceRequest 107 | { 108 | public ProjectionCheckpointPersistenceRequest(Stream stream, long globalSequence) 109 | { 110 | Argument.Requires(stream != null); 111 | Argument.Requires(globalSequence >= 0); 112 | 113 | this.Stream = stream; 114 | this.GlobalSequence = globalSequence; 115 | } 116 | 117 | public Stream Stream { get; } 118 | public long GlobalSequence { get; } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /Src/Even/Messages/ProjectionMessages.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Even.Messages 4 | { 5 | public class StartProjection 6 | { 7 | public StartProjection(Type projectionType) 8 | { 9 | Argument.RequiresNotNull(projectionType, nameof(projectionType)); 10 | 11 | this.ProjectionType = projectionType; 12 | } 13 | 14 | public Type ProjectionType { get; } 15 | } 16 | 17 | public class ProjectionSubscriptionRequest : IRequest 18 | { 19 | public ProjectionSubscriptionRequest(ProjectionStreamQuery query, int lastKnownSequence) 20 | { 21 | Argument.RequiresNotNull(query, nameof(query)); 22 | Argument.Requires(lastKnownSequence >= 0, nameof(lastKnownSequence)); 23 | 24 | Query = query; 25 | LastKnownSequence = lastKnownSequence; 26 | } 27 | 28 | public Guid RequestID { get; } = Guid.NewGuid(); 29 | public ProjectionStreamQuery Query { get; } 30 | public int LastKnownSequence { get; } 31 | } 32 | 33 | public class ProjectionReplayEvent 34 | { 35 | public ProjectionReplayEvent(Guid requestId, IPersistedStreamEvent persistedEvent) 36 | { 37 | this.RequestID = requestId; 38 | this.Event = persistedEvent; 39 | } 40 | 41 | public Guid RequestID { get; } 42 | public IPersistedStreamEvent Event { get; } 43 | } 44 | 45 | public class ProjectionReplayFinished 46 | { 47 | public ProjectionReplayFinished(Guid requestId) 48 | { 49 | this.RequestID = requestId; 50 | } 51 | 52 | public Guid RequestID { get; } 53 | } 54 | 55 | public class RebuildProjection 56 | { } 57 | 58 | public class ProjectionUnsubscribed 59 | { } 60 | } 61 | -------------------------------------------------------------------------------- /Src/Even/Options.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace Even 8 | { 9 | public interface IProjectionOptions 10 | { 11 | TimeSpan ProjectionReplayTimeout { get; } 12 | } 13 | 14 | public interface IProjectionStreamOptions 15 | { 16 | 17 | } 18 | 19 | public class GlobalOptions 20 | { 21 | /// 22 | /// The maximum number of events per read request. 23 | /// 24 | public int EventsPerReadRequest { get; set; } = 10000; 25 | 26 | /// 27 | /// The default timeout for reveiving read responses. 28 | /// 29 | public TimeSpan ReadRequestTimeout { get; set; } = TimeSpan.FromSeconds(15); 30 | 31 | /// 32 | /// The amount of time the projection will wait before starting to replay; 33 | /// 34 | public TimeSpan ProjectionReplayDelay { get; set; } = TimeSpan.FromSeconds(1); 35 | 36 | public TimeSpan ProjectionReplayRetryInterval { get; set; } = TimeSpan.FromSeconds(1); 37 | public int MaxProjectionReplayRetries { get; set; } = 10; 38 | 39 | /// 40 | /// The default timeout for commands sent through the gateway. 41 | /// 42 | public TimeSpan DefaultCommandTimeout { get; set; } = TimeSpan.FromSeconds(10); 43 | 44 | public TimeSpan AggregateFirstCommandTimeout { get; set; } = TimeSpan.FromSeconds(1); 45 | public TimeSpan AggregateIdleTimeout { get; set; } = TimeSpan.FromSeconds(60); 46 | public TimeSpan AggregatePersistenceTimeout { get; set; } = TimeSpan.FromSeconds(30); 47 | public int MaxAggregateProcessAttempts { get; set; } = 10; 48 | public TimeSpan AggregateStopTimeout { get; set; } = TimeSpan.FromSeconds(10); 49 | public TimeSpan DispatcherRecoveryTimeout { get; set; } = TimeSpan.FromSeconds(5); 50 | public TimeSpan IndexWriterFlushDelay { get; set; } = TimeSpan.FromSeconds(2); 51 | public TimeSpan CheckpointWriterFlushDelay { get; set; } = TimeSpan.FromSeconds(2); 52 | public TimeSpan DefaultQueryTimeout { get; set; } = TimeSpan.FromSeconds(5); 53 | public TimeSpan CommandProcessorPersistenceTimeout { get; set; } = TimeSpan.FromSeconds(30); 54 | public TimeSpan CommandProcessorIdleTimeout { get; set; } = TimeSpan.FromSeconds(10); 55 | public TimeSpan CommandProcessorStopTimeout { get; set; } = TimeSpan.FromSeconds(10); 56 | public ICommandValidator DefaultCommandValidator { get; set; } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Src/Even/PersistedRawEvent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace Even 8 | { 9 | public interface IPersistedRawEvent 10 | { 11 | long GlobalSequence { get; } 12 | Guid EventID { get; } 13 | Stream Stream { get; } 14 | DateTime UtcTimestamp { get; } 15 | string EventType { get; } 16 | byte[] Metadata { get; } 17 | byte[] Payload { get; } 18 | int PayloadFormat { get; } 19 | } 20 | 21 | public class PersistedRawEvent : IPersistedRawEvent 22 | { 23 | public long GlobalSequence { get; set; } 24 | public Guid EventID { get; set; } 25 | public Stream Stream { get; set; } 26 | public DateTime UtcTimestamp { get; set; } 27 | public string EventType { get; set; } 28 | public byte[] Metadata { get; set; } 29 | public byte[] Payload { get; set; } 30 | public int PayloadFormat { get; set; } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Src/Even/ProjectionCheckpointWriter.cs: -------------------------------------------------------------------------------- 1 | using Akka.Actor; 2 | using Akka.Event; 3 | using Even.Messages; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | 10 | namespace Even 11 | { 12 | public class ProjectionCheckpointWriter : ReceiveActor 13 | { 14 | IProjectionStoreWriter _writer; 15 | GlobalOptions _options; 16 | 17 | LinkedList _buffer = new LinkedList(); 18 | bool _flushRequested; 19 | 20 | public static Props CreateProps(IProjectionStoreWriter writer, GlobalOptions options) 21 | { 22 | Argument.RequiresNotNull(writer, nameof(writer)); 23 | Argument.RequiresNotNull(options, nameof(options)); 24 | 25 | return Props.Create(writer, options); 26 | } 27 | 28 | public ProjectionCheckpointWriter(IProjectionStoreWriter writer, GlobalOptions options) 29 | { 30 | _writer = writer; 31 | _options = options; 32 | 33 | Receive(request => Enqueue(request)); 34 | Receive(_ => FlushBuffer()); 35 | } 36 | 37 | void Enqueue(ProjectionCheckpointPersistenceRequest request) 38 | { 39 | _buffer.AddLast(request); 40 | 41 | if (!_flushRequested) 42 | { 43 | _flushRequested = true; 44 | Context.System.Scheduler.ScheduleTellOnce(_options.CheckpointWriterFlushDelay, Self, new FlushBufferCommand(), Self); 45 | } 46 | } 47 | 48 | async Task FlushBuffer() 49 | { 50 | _flushRequested = false; 51 | 52 | var re = from e in _buffer 53 | group e by e.Stream into g 54 | select new { Stream = g.Key, GlobalSequence = g.Max(o => o.GlobalSequence) }; 55 | 56 | foreach (var o in re) 57 | { 58 | try 59 | { 60 | await _writer.WriteProjectionCheckpointAsync(o.Stream, o.GlobalSequence); 61 | } 62 | catch (Exception ex) 63 | { 64 | Context.GetLogger().Error(ex, "Error writing projection checkpoint."); 65 | } 66 | } 67 | 68 | _buffer.Clear(); 69 | } 70 | 71 | class FlushBufferCommand { } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Src/Even/ProjectionEvent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Even 5 | { 6 | public static class ProjectionEventFactory 7 | { 8 | public static IPersistedStreamEvent Create(Stream stream, int streamSequence, IPersistedEvent persistedEvent) 9 | { 10 | Argument.RequiresNotNull(stream, nameof(stream)); 11 | Argument.Requires(streamSequence >= 1, nameof(streamSequence)); 12 | Argument.RequiresNotNull(persistedEvent, nameof(persistedEvent)); 13 | 14 | var eventType = persistedEvent.DomainEvent.GetType(); 15 | 16 | var t = typeof(ProjectionEvent<>).MakeGenericType(eventType); 17 | return (IPersistedStreamEvent)Activator.CreateInstance(t, stream, streamSequence, persistedEvent); 18 | } 19 | 20 | class ProjectionEvent : IPersistedStreamEvent 21 | { 22 | public ProjectionEvent(Stream stream, int streamSequence, IPersistedEvent persistedEvent) 23 | { 24 | Stream = stream; 25 | StreamSequence = streamSequence; 26 | _e = persistedEvent; 27 | } 28 | 29 | public Stream Stream { get; } 30 | public int StreamSequence { get; } 31 | IPersistedEvent _e; 32 | 33 | public long GlobalSequence => _e.GlobalSequence; 34 | public Guid EventID => _e.EventID; 35 | public string EventType => _e.EventType; 36 | public DateTime UtcTimestamp => _e.UtcTimestamp; 37 | public IReadOnlyDictionary Metadata => _e.Metadata; 38 | public T DomainEvent => _e.DomainEvent; 39 | 40 | object IPersistedEvent.DomainEvent => DomainEvent; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Src/Even/ProjectionIndexWriter.cs: -------------------------------------------------------------------------------- 1 | using Akka.Actor; 2 | using Akka.Event; 3 | using Even.Messages; 4 | using Even.Utils; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | 11 | namespace Even 12 | { 13 | public class ProjectionIndexWriter : ReceiveActor 14 | { 15 | IProjectionStoreWriter _writer; 16 | GlobalOptions _options; 17 | 18 | LinkedList _buffer = new LinkedList(); 19 | bool _flushRequested; 20 | 21 | public static Props CreateProps(IProjectionStoreWriter writer, GlobalOptions options) 22 | { 23 | Argument.RequiresNotNull(writer, nameof(writer)); 24 | Argument.RequiresNotNull(options, nameof(options)); 25 | 26 | return Props.Create(writer, options); 27 | } 28 | 29 | public ProjectionIndexWriter(IProjectionStoreWriter writer, GlobalOptions options) 30 | { 31 | _writer = writer; 32 | _options = options; 33 | 34 | Receive(request => Enqueue(request)); 35 | Receive(_ => FlushBuffer()); 36 | } 37 | 38 | void Enqueue(ProjectionIndexPersistenceRequest request) 39 | { 40 | _buffer.AddLast(new BufferEntry { Request = request, Sender = Sender }); 41 | 42 | if (!_flushRequested) 43 | { 44 | _flushRequested = true; 45 | Context.System.Scheduler.ScheduleTellOnce(_options.IndexWriterFlushDelay, Self, new FlushBufferCommand(), Self); 46 | } 47 | } 48 | 49 | async Task FlushBuffer() 50 | { 51 | _flushRequested = false; 52 | 53 | if (_buffer.Count == 0) 54 | return; 55 | 56 | // groups all requests by sender and stream and issues a write for each one at a time 57 | var re = from e in _buffer 58 | group e by new { e.Sender, e.Request.ProjectionStream } into g 59 | select new WriteEntry 60 | { 61 | Sender = g.Key.Sender, 62 | Stream = g.Key.ProjectionStream, 63 | Requests = g.Select(o => o.Request).ToList() 64 | }; 65 | 66 | foreach (var e in re) 67 | { 68 | try 69 | { 70 | await Write(e); 71 | } 72 | catch (Exception ex) when (ex is MissingIndexEntryException || ex is UnexpectedStreamSequenceException || ex is DuplicatedEntryException) 73 | { 74 | e.Sender.Tell(new ProjectionIndexInconsistencyDetected()); 75 | } 76 | catch (Exception ex) 77 | { 78 | Context.GetLogger().Error(ex, "Error writing projection index"); 79 | } 80 | } 81 | 82 | _buffer.Clear(); 83 | } 84 | 85 | async Task Write(WriteEntry entry) 86 | { 87 | var requests = entry.Requests.OrderBy(o => o.ProjectionStreamSequence).ToList(); 88 | 89 | if (!requests.Select(e => e.ProjectionStreamSequence).IsSequential()) 90 | throw new MissingIndexEntryException(); 91 | 92 | var firstSequence = requests.First().ProjectionStreamSequence; 93 | var globalSequences = requests.Select(e => e.GlobalSequence).ToList(); 94 | 95 | await _writer.WriteProjectionIndexAsync(entry.Stream, firstSequence - 1, globalSequences); 96 | } 97 | 98 | class FlushBufferCommand { } 99 | 100 | class BufferEntry 101 | { 102 | public IActorRef Sender; 103 | public ProjectionIndexPersistenceRequest Request; 104 | } 105 | 106 | class WriteEntry 107 | { 108 | public IActorRef Sender; 109 | public Stream Stream; 110 | public IReadOnlyList Requests; 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /Src/Even/ProjectionState.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace Even 8 | { 9 | public class ProjectionState 10 | { 11 | public Stream ProjectionStream { get; set; } 12 | public int ProjectionSequence { get; set; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Src/Even/ProjectionStreamQuery.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Diagnostics.Contracts; 5 | using System.Linq; 6 | using System.Text; 7 | 8 | namespace Even 9 | { 10 | /// 11 | /// Represents a projection stream query. 12 | /// 13 | public class ProjectionStreamQuery : IEquatable 14 | { 15 | public ProjectionStreamQuery(params IProjectionStreamPredicate[] predicates) 16 | { 17 | _predicates = predicates ?? new IProjectionStreamPredicate[0]; 18 | } 19 | 20 | public ProjectionStreamQuery(IEnumerable predicates) 21 | : this(predicates.ToArray()) 22 | { } 23 | 24 | private Stream _stream; 25 | private IProjectionStreamPredicate[] _predicates; 26 | 27 | /// 28 | /// A deterministic stream that will always be the same for the same query. 29 | /// 30 | public Stream ProjectionStream => _stream ?? (_stream = CreateStream()); 31 | 32 | [Obsolete] 33 | public IReadOnlyCollection Predicates => _predicates; 34 | 35 | private Stream CreateStream() 36 | { 37 | var items = _predicates 38 | .Select(q => JsonConvert.SerializeObject(q.GetDeterministicHashSource())) 39 | .OrderBy(s => s, StringComparer.OrdinalIgnoreCase); 40 | 41 | var bytes = Encoding.UTF8.GetBytes(String.Concat(items)); 42 | 43 | return Stream.FromBytes(bytes); 44 | } 45 | 46 | public bool EventMatches(IPersistedEvent e) 47 | { 48 | foreach (var p in _predicates) 49 | if (p.EventMatches(e)) 50 | return true; 51 | 52 | return false; 53 | } 54 | 55 | public bool Equals(ProjectionStreamQuery other) 56 | { 57 | return other != null && ProjectionStream == other.ProjectionStream; 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Src/Even/ProjectionStreamSupervisor.cs: -------------------------------------------------------------------------------- 1 | using Akka.Actor; 2 | using Even.Messages; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace Even 10 | { 11 | public class ProjectionStreamSupervisor : ReceiveActor 12 | { 13 | public static Props CreateProps(IActorRef reader, IActorRef writer, GlobalOptions options) 14 | { 15 | return Props.Create(reader, writer, options); 16 | } 17 | 18 | public ProjectionStreamSupervisor(IActorRef reader, IActorRef writer, GlobalOptions options) 19 | { 20 | Argument.RequiresNotNull(reader, nameof(reader)); 21 | Argument.RequiresNotNull(writer, nameof(writer)); 22 | Argument.RequiresNotNull(options, nameof(options)); 23 | 24 | _reader = reader; 25 | _writer = writer; 26 | _options = options; 27 | 28 | Become(ReceivingRequests); 29 | } 30 | 31 | IActorRef _reader; 32 | IActorRef _writer; 33 | GlobalOptions _options; 34 | 35 | private void ReceivingRequests() 36 | { 37 | Receive(ps => 38 | { 39 | var key = "stream-" + ps.Query.ProjectionStream; 40 | IActorRef pRef = Context.Child(key); 41 | 42 | // if the projection stream doesn't exist, start one 43 | if (pRef == ActorRefs.Nobody) 44 | { 45 | var props = ProjectionStream.CreateProps(ps.Query, _reader, _writer, _options); 46 | pRef = Context.ActorOf(props); 47 | } 48 | 49 | pRef.Forward(ps); 50 | }); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Src/Even/ProjectionSupervisor.cs: -------------------------------------------------------------------------------- 1 | using Akka.Actor; 2 | using Even.Messages; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace Even 10 | { 11 | public class ProjectionSupervisor : ReceiveActor 12 | { 13 | GlobalOptions _options; 14 | IActorRef _projectionStreams; 15 | 16 | public static Props CreateProps(IActorRef projectionStreams, GlobalOptions options) 17 | { 18 | Argument.RequiresNotNull(projectionStreams, nameof(projectionStreams)); 19 | Argument.RequiresNotNull(options, nameof(options)); 20 | 21 | return Props.Create(projectionStreams, options); 22 | } 23 | 24 | public ProjectionSupervisor(IActorRef projectionStreamSupervisor, GlobalOptions options) 25 | { 26 | _projectionStreams = projectionStreamSupervisor; 27 | _options = options; 28 | 29 | Ready(); 30 | } 31 | 32 | void Ready() 33 | { 34 | Receive(sp => 35 | { 36 | try 37 | { 38 | var props = PropsFactory.Create(sp.ProjectionType); 39 | var projection = Context.ActorOf(props); 40 | projection.Ask(new InitializeProjection(_projectionStreams, _options)).PipeTo(Sender); 41 | } 42 | catch (Exception ex) 43 | { 44 | Sender.Tell(InitializationResult.Failed(ex)); 45 | } 46 | }); 47 | } 48 | 49 | protected override SupervisorStrategy SupervisorStrategy() 50 | { 51 | return new OneForOneStrategy(ex => 52 | { 53 | return Directive.Restart; 54 | }); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Src/Even/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System.Reflection; 3 | using System.Runtime.CompilerServices; 4 | 5 | [assembly: AssemblyTitleAttribute("Even")] 6 | [assembly: AssemblyProductAttribute("Even")] 7 | [assembly: AssemblyDescriptionAttribute("An event sourcing framework on top of Akka.NET")] 8 | [assembly: InternalsVisibleTo("Even.Tests")] 9 | -------------------------------------------------------------------------------- /Src/Even/PropsFactory.cs: -------------------------------------------------------------------------------- 1 | using Akka.Actor; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace Even 9 | { 10 | public static class PropsFactory 11 | { 12 | static Func _factory; 13 | 14 | private static void SetProducer(Func factory) => _factory = factory; 15 | 16 | public static Props Create(Type type) 17 | { 18 | if (_factory != null) 19 | return _factory(type); 20 | 21 | return Props.Create(type); 22 | } 23 | 24 | public static Props Create() 25 | { 26 | return Create(typeof(T)); 27 | } 28 | 29 | public static Props Create(params object[] parameters) 30 | { 31 | return Props.Create(typeof(T), parameters); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Src/Even/Query.cs: -------------------------------------------------------------------------------- 1 | using Akka.Actor; 2 | using System; 3 | 4 | namespace Even 5 | { 6 | public interface IQuery 7 | { 8 | IActorRef Sender { get; } 9 | object Message { get; } 10 | Timeout Timeout { get; } 11 | } 12 | 13 | public interface IQuery : IQuery 14 | { 15 | new T Message { get; } 16 | } 17 | 18 | public class Query : IQuery 19 | { 20 | public Query(IActorRef sender, T query, Timeout timeout) 21 | { 22 | this.Sender = sender; 23 | this.Message = query; 24 | this.Timeout = timeout; 25 | } 26 | 27 | public IActorRef Sender { get; private set; } 28 | public T Message { get; private set; } 29 | public Timeout Timeout { get; private set; } 30 | object IQuery.Message => Message; 31 | } 32 | 33 | public static class Query 34 | { 35 | public static IQuery Create(IActorRef sender, object query, TimeSpan timeout) 36 | { 37 | Argument.RequiresNotNull(query, nameof(query)); 38 | 39 | var type = typeof(Query<>).MakeGenericType(query.GetType()); 40 | 41 | return (IQuery) Activator.CreateInstance(type, sender, query, Timeout.In(timeout)); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Src/Even/QueryExtensions.cs: -------------------------------------------------------------------------------- 1 | using Akka.Actor; 2 | using Akka.Actor.Internal; 3 | using System; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace Even 8 | { 9 | public static class QueryExtensions 10 | { 11 | public static Task Query(this ActorSystem system, object query, TimeSpan timeout) 12 | { 13 | return Query(system, null, query, timeout); 14 | } 15 | 16 | public static async Task Query(this ActorSystem system, ICanTell destination, object query, TimeSpan timeout) 17 | { 18 | var provider = (system as ActorSystemImpl)?.Provider; 19 | 20 | if (provider == null) 21 | throw new NotSupportedException("Unable to resolve the target Provider"); 22 | 23 | var refs = AskRefs.Create(provider, timeout); 24 | var iquery = Even.Query.Create(refs.FutureActorRef, query, timeout); 25 | 26 | if (destination != null) 27 | destination.Tell(iquery, refs.FutureActorRef); 28 | else 29 | system.EventStream.Publish(iquery); 30 | 31 | object taskResult; 32 | 33 | try 34 | { 35 | taskResult = await refs.CompletionSource.Task; 36 | } 37 | catch (TaskCanceledException ex) 38 | { 39 | throw new QueryException("Query timeout.", ex); 40 | } 41 | catch (Exception ex) 42 | { 43 | throw new QueryException("Unexpected query exception.", ex); 44 | } 45 | 46 | return (TResponse)taskResult; 47 | } 48 | } 49 | 50 | public class AskRefs 51 | { 52 | public FutureActorRef FutureActorRef { get; } 53 | public TaskCompletionSource CompletionSource { get; } 54 | 55 | public static AskRefs Create(IActorRefProvider provider, TimeSpan timeout) 56 | { 57 | var tcs = new TaskCompletionSource(); 58 | 59 | // logic copied from Ask (Akka.Actor -> Futures.cs) 60 | 61 | if (timeout != System.Threading.Timeout.InfiniteTimeSpan && timeout > default(TimeSpan)) 62 | { 63 | var cancellationSource = new CancellationTokenSource(); 64 | cancellationSource.Token.Register(() => tcs.TrySetCanceled()); 65 | cancellationSource.CancelAfter(timeout); 66 | } 67 | 68 | //create a new tempcontainer path 69 | ActorPath path = provider.TempPath(); 70 | 71 | //callback to unregister from tempcontainer 72 | Action unregister = () => provider.UnregisterTempActor(path); 73 | var future = new FutureActorRef(tcs, unregister, path); 74 | 75 | //The future actor needs to be registered in the temp container 76 | provider.RegisterTempActor(future, path); 77 | 78 | return new AskRefs(future, tcs); 79 | } 80 | 81 | public AskRefs(FutureActorRef futureRef, TaskCompletionSource completionSource) 82 | { 83 | FutureActorRef = futureRef; 84 | CompletionSource = completionSource; 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Src/Even/RejectReason.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Diagnostics; 5 | using System.Linq; 6 | 7 | namespace Even 8 | { 9 | [DebuggerDisplay("FirstReason")] 10 | public class RejectReasons : IEnumerable> 11 | { 12 | private Dictionary> _reasons = new Dictionary>(StringComparer.InvariantCultureIgnoreCase); 13 | 14 | public RejectReasons() 15 | { } 16 | 17 | public RejectReasons(string reason) 18 | { 19 | Add(reason); 20 | } 21 | 22 | public string FirstReason => this.Any() ? this.First().Value : null; 23 | 24 | public void Add(string reason) 25 | { 26 | Add(String.Empty, reason); 27 | } 28 | 29 | public void Add(string key, string reason) 30 | { 31 | Argument.RequiresNotNull(key, nameof(key)); 32 | 33 | List list; 34 | 35 | if (!_reasons.TryGetValue(key, out list)) 36 | { 37 | list = new List(); 38 | _reasons.Add(key, list); 39 | } 40 | 41 | list.Add(reason); 42 | } 43 | 44 | public IEnumerator> GetEnumerator() 45 | { 46 | foreach (var kvp in _reasons) 47 | { 48 | foreach (var s in kvp.Value) 49 | { 50 | yield return new KeyValuePair(kvp.Key, s); 51 | } 52 | } 53 | } 54 | 55 | IEnumerator IEnumerable.GetEnumerator() 56 | { 57 | return GetEnumerator(); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Src/Even/SerialEventStreamWriter.cs: -------------------------------------------------------------------------------- 1 | using Akka.Actor; 2 | using Even.Messages; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace Even 10 | { 11 | public class SerialEventStreamWriter : ReceiveActor 12 | { 13 | IEventStoreWriter _writer; 14 | ISerializer _serializer; 15 | IActorRef _dispatcher; 16 | GlobalOptions _options; 17 | 18 | public static Props CreateProps(IEventStoreWriter writer, ISerializer serializer, IActorRef dispatcher, GlobalOptions options) 19 | { 20 | Argument.RequiresNotNull(writer, nameof(writer)); 21 | Argument.RequiresNotNull(serializer, nameof(serializer)); 22 | Argument.RequiresNotNull(dispatcher, nameof(dispatcher)); 23 | Argument.RequiresNotNull(options, nameof(options)); 24 | 25 | return Props.Create(writer, serializer, dispatcher, options); 26 | } 27 | 28 | public SerialEventStreamWriter(IEventStoreWriter writer, ISerializer serializer, IActorRef dispatcher, GlobalOptions options) 29 | { 30 | this._writer = writer; 31 | this._serializer = serializer; 32 | this._dispatcher = dispatcher; 33 | this._options = options; 34 | 35 | Receive(r => HandleRequest(r)); 36 | } 37 | 38 | private async Task HandleRequest(PersistenceRequest request) 39 | { 40 | try 41 | { 42 | await WriteEvents(request); 43 | Sender.Tell(new PersistenceSuccess(request.PersistenceID)); 44 | } 45 | catch (UnexpectedStreamSequenceException) 46 | { 47 | Sender.Tell(new UnexpectedStreamSequence(request.PersistenceID)); 48 | } 49 | catch (DuplicatedEntryException) 50 | { 51 | Sender.Tell(new DuplicatedEntry(request.PersistenceID)); 52 | } 53 | catch (Exception ex) 54 | { 55 | Sender.Tell(new PersistenceFailure(request.PersistenceID, ex)); 56 | } 57 | } 58 | 59 | protected async Task WriteEvents(PersistenceRequest request) 60 | { 61 | var events = request.Events; 62 | 63 | // serialize the events into raw events 64 | var rawEvents = UnpersistedRawEvent.FromUnpersistedEvents(events, _serializer); 65 | 66 | // writes all events to the store 67 | await _writer.WriteStreamAsync(request.Stream, request.ExpectedStreamSequence, rawEvents); 68 | 69 | // publishes the events in the order they were sent 70 | for (int i = 0, len = events.Count; i < len; i++) { 71 | 72 | var e = events[i]; 73 | var re = rawEvents[i]; 74 | var persistedEvent = PersistedEventFactory.FromUnpersistedEvent(re.GlobalSequence, e); 75 | 76 | // publish to the event stream 77 | _dispatcher.Tell(persistedEvent); 78 | } 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Src/Even/StreamEventReader.cs: -------------------------------------------------------------------------------- 1 | using Akka.Actor; 2 | using Even.Messages; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace Even 10 | { 11 | public class StreamEventReader : ReceiveActor 12 | { 13 | Props _workerProps; 14 | 15 | public StreamEventReader(IEventStoreReader reader, PersistedEventFactory factory) 16 | { 17 | Argument.Requires(reader != null, nameof(reader)); 18 | Argument.Requires(factory != null, nameof(factory)); 19 | 20 | _workerProps = Props.Create(reader, factory); 21 | Receive(r => HandleRequest(r)); 22 | } 23 | 24 | void HandleRequest(ReadStreamRequest r) 25 | { 26 | var worker = Context.ActorOf(_workerProps); 27 | worker.Forward(r); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Src/Even/StreamHash.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics.Contracts; 4 | using System.Linq; 5 | using System.Security.Cryptography; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace Even 10 | { 11 | [Obsolete] 12 | public static class StreamHash 13 | { 14 | /// 15 | /// Determines if the string is a hash string. 16 | /// 17 | public static bool IsStreamHash(string str) 18 | { 19 | if (str == null || str.Length != 40) 20 | return false; 21 | 22 | for (var i = 0; i < 40; i++) 23 | { 24 | var c = str[i]; 25 | 26 | if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) 27 | return false; 28 | } 29 | 30 | return true; 31 | } 32 | 33 | public static string AsHashString(string str) 34 | { 35 | if (IsStreamHash(str)) 36 | return str; 37 | 38 | var bytes = ComputeHash(str); 39 | 40 | var sb = new StringBuilder(bytes.Length * 2); 41 | 42 | foreach (var b in bytes) 43 | sb.Append(b.ToString("x2")); 44 | 45 | return sb.ToString(); 46 | } 47 | 48 | public static byte[] AsHashBytes(string str) 49 | { 50 | if (IsStreamHash(str)) 51 | { 52 | var bytes = new byte[20]; 53 | 54 | for (var i = 0; i < 20; i++) 55 | bytes[i] = Convert.ToByte(str.Substring(i * 2, 2), 16); 56 | 57 | return bytes; 58 | } 59 | 60 | return ComputeHash(str); 61 | } 62 | 63 | private static byte[] ComputeHash(string str) 64 | { 65 | var bytes = Encoding.UTF8.GetBytes(str); 66 | var sha1 = new SHA1Managed(); 67 | return sha1.ComputeHash(bytes); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Src/Even/SystemClock.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace Even 9 | { 10 | public static class SystemClock 11 | { 12 | private static Stopwatch _sw = Stopwatch.StartNew(); 13 | public static long MonotonicTicks => _sw.Elapsed.Ticks; 14 | public static DateTimeOffset Now => DateTimeOffset.Now; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Src/Even/Timeout.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace Even 8 | { 9 | public class Timeout 10 | { 11 | public readonly long Ticks; 12 | 13 | public Timeout(long monotonicTicks) 14 | { 15 | Ticks = monotonicTicks; 16 | } 17 | 18 | public bool IsExpired => SystemClock.MonotonicTicks >= Ticks; 19 | 20 | public static Timeout In(int milliseconds) 21 | { 22 | return new Timeout(SystemClock.MonotonicTicks + TimeSpan.FromMilliseconds(milliseconds).Ticks); 23 | } 24 | 25 | public static Timeout In(TimeSpan timeSpan) 26 | { 27 | return new Timeout(SystemClock.MonotonicTicks + timeSpan.Ticks); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Src/Even/UnpersistedEvent.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Diagnostics.Contracts; 5 | using System.Linq; 6 | using System.Runtime.CompilerServices; 7 | 8 | namespace Even 9 | { 10 | public class UnpersistedEvent 11 | { 12 | public UnpersistedEvent(Stream stream, object domainEvent) 13 | : this(stream, domainEvent, null, null) 14 | { } 15 | 16 | public UnpersistedEvent(Stream stream, object domainEvent, string eventType, Dictionary metadata) 17 | { 18 | Argument.Requires(stream != null, nameof(stream)); 19 | Argument.Requires(domainEvent != null, nameof(domainEvent)); 20 | 21 | this.Stream = stream; 22 | this.DomainEvent = domainEvent; 23 | this.EventType = eventType ?? GetEventType(domainEvent); 24 | 25 | if (eventType != Constants.AnonymousEventType) 26 | { 27 | metadata = metadata ?? new Dictionary(1); 28 | metadata[Constants.ClrTypeMetadataKey] = GetUnversionedQualifiedName(domainEvent.GetType()); 29 | } 30 | 31 | this.Metadata = metadata; 32 | } 33 | 34 | public Guid EventID { get; } = Guid.NewGuid(); 35 | public DateTime UtcTimestamp { get; } = DateTime.UtcNow; 36 | public Stream Stream { get; } 37 | public string EventType { get; } 38 | public object DomainEvent { get; } 39 | public IReadOnlyDictionary Metadata { get; } 40 | 41 | private static string GetEventType(object o) 42 | { 43 | var type = o.GetType(); 44 | 45 | var esEvent = type.GetCustomAttributes(typeof(ESEventAttribute), false).FirstOrDefault() as ESEventAttribute; 46 | 47 | if (esEvent != null) 48 | return esEvent.EventType; 49 | 50 | if (IsAnonymousType(type)) 51 | return Constants.AnonymousEventType; 52 | 53 | return type.Name; 54 | } 55 | 56 | private static bool IsAnonymousType(Type type) 57 | { 58 | return type.GetCustomAttributes(typeof(CompilerGeneratedAttribute), false).Length > 0 && type.FullName.Contains("AnonymousType"); 59 | } 60 | 61 | private static string GetUnversionedQualifiedName(Type type) 62 | { 63 | return type.FullName + ", " + type.Assembly.GetName().Name; 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Src/Even/UnpersistedRawEvent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics.Contracts; 4 | using System.Linq; 5 | 6 | namespace Even 7 | { 8 | public interface IUnpersistedRawEvent 9 | { 10 | long GlobalSequence { get; set; } 11 | Guid EventID { get; } 12 | string EventType { get; } 13 | DateTime UtcTimestamp { get; } 14 | byte[] Metadata { get; } 15 | byte[] Payload { get; } 16 | int PayloadFormat { get; } 17 | } 18 | 19 | public interface IUnpersistedRawStreamEvent : IUnpersistedRawEvent 20 | { 21 | Stream Stream { get; } 22 | } 23 | 24 | public class UnpersistedRawEvent : IUnpersistedRawStreamEvent 25 | { 26 | public UnpersistedRawEvent(Guid eventId, Stream stream, string eventType, DateTime utcTimestamp, byte[] metadata, byte[] payload, int payloadFormat) 27 | { 28 | Argument.Requires(eventId != Guid.Empty); 29 | Argument.RequiresNotNull(stream, nameof(stream)); 30 | Argument.Requires(!String.IsNullOrEmpty(eventType)); 31 | Argument.Requires(utcTimestamp != default(DateTime)); 32 | Argument.Requires(payload != null); 33 | 34 | Stream = stream; 35 | EventID = eventId; 36 | EventType = eventType; 37 | UtcTimestamp = utcTimestamp; 38 | Metadata = metadata; 39 | Payload = payload; 40 | PayloadFormat = payloadFormat; 41 | } 42 | 43 | public long GlobalSequence { get; set; } 44 | public Stream Stream { get; } 45 | public Guid EventID { get; } 46 | public string EventType { get; } 47 | public DateTime UtcTimestamp { get; } 48 | public byte[] Metadata { get; } 49 | public byte[] Payload { get; } 50 | public int PayloadFormat { get; } 51 | 52 | public static List FromUnpersistedEvents(IEnumerable events, ISerializer serializer) 53 | { 54 | Argument.Requires(events != null); 55 | Argument.Requires(serializer != null); 56 | 57 | return events.Select(e => 58 | { 59 | var format = EvenStorageFormatAttribute.GetStorageFormat(e.DomainEvent.GetType()); 60 | var metadata = serializer.SerializeMetadata(e.Metadata); 61 | var payload = serializer.SerializeEvent(e.DomainEvent, format); 62 | 63 | var re = new UnpersistedRawEvent(e.EventID, e.Stream, e.EventType, e.UtcTimestamp, metadata, payload, format); 64 | 65 | return re; 66 | }).ToList(); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Src/Even/Utils/EnumerableExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace Even.Utils 8 | { 9 | public static class EnumerableExtensions 10 | { 11 | /// 12 | /// Returns true if the enumerable contains a values in sequential order, or is empty. Returns false otherwise. 13 | /// 14 | public static bool IsSequential(this IEnumerable enumeration) 15 | { 16 | var gotFirst = false; 17 | int lastValue = 0; 18 | 19 | foreach (var i in enumeration) 20 | { 21 | if (!gotFirst) 22 | { 23 | lastValue = i; 24 | gotFirst = true; 25 | continue; 26 | } 27 | 28 | if (i != lastValue + 1) 29 | return false; 30 | 31 | lastValue = i; 32 | } 33 | 34 | if (!gotFirst) 35 | return false; 36 | 37 | return true; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Src/Even/app.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /Src/Even/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "Akka": "1.1.1" 4 | }, 5 | "frameworks": { 6 | "net451": {} 7 | }, 8 | "runtimes": { 9 | "win": "" 10 | } 11 | } -------------------------------------------------------------------------------- /Test/Even.Tests/BasicIntegrationTests.cs: -------------------------------------------------------------------------------- 1 | using Akka.Actor; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using Xunit; 8 | 9 | namespace Even.Tests 10 | { 11 | public class BasicIntegrationTests : EvenTestKit 12 | { 13 | public class DoSomething {} 14 | public class SomethingDone { public int Sequence { get; set; } } 15 | 16 | public class TestAggregate : Aggregate 17 | { 18 | int sequence = 0; 19 | 20 | public TestAggregate() 21 | { 22 | OnCommand(c => 23 | { 24 | Persist(new SomethingDone { Sequence = sequence + 1 }); 25 | }); 26 | 27 | OnEvent(e => 28 | { 29 | sequence = e.Sequence; 30 | }); 31 | } 32 | } 33 | 34 | public class IsSomeghingDone { } 35 | 36 | public class TestProjection : Projection 37 | { 38 | bool _isDone; 39 | 40 | public TestProjection() 41 | { 42 | OnEvent(e => 43 | { 44 | _isDone = true; 45 | }); 46 | 47 | OnQuery(q => 48 | { 49 | Sender.Tell(_isDone); 50 | }); 51 | } 52 | } 53 | 54 | [Fact] 55 | public async Task Event_is_accepted_and_published() 56 | { 57 | Sys.EventStream.Subscribe(TestActor, typeof(IPersistedEvent)); 58 | var gateway = await Sys.SetupEven().Start(); 59 | var response = await gateway.SendAggregateCommand(new Guid(), new DoSomething()); 60 | 61 | Assert.True(response.Accepted); 62 | 63 | ExpectMsg>(); 64 | } 65 | 66 | [Fact] 67 | public async Task Projection_responds_to_queries() 68 | { 69 | var gateway = await Sys.SetupEven() 70 | .AddProjection() 71 | .Start(); 72 | 73 | var isDone = await Sys.Query(new IsSomeghingDone(), TimeSpan.FromSeconds(1)); 74 | 75 | Assert.False(isDone, "should not be done"); 76 | 77 | await gateway.SendAggregateCommand(new Guid(), new DoSomething()); 78 | 79 | await Task.Delay(1000); 80 | 81 | isDone = await Sys.Query(new IsSomeghingDone(), TimeSpan.FromSeconds(1)); 82 | 83 | Assert.True(isDone, "should be done by now"); 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Test/Even.Tests/BufferedEventWriterTests.cs: -------------------------------------------------------------------------------- 1 | using Akka.Actor; 2 | using Even.Messages; 3 | using Even.Tests.Mocks; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | using Xunit; 10 | 11 | namespace Even.Tests 12 | { 13 | public class BufferedEventWriterTests : EvenTestKit 14 | { 15 | #region Helpers 16 | 17 | class SampleEvent1 { } 18 | class SampleEvent2 { } 19 | class SampleEvent3 { } 20 | 21 | protected PersistenceRequest CreatePersistenceRequest(int eventCount = 1) 22 | { 23 | return CreatePersistenceRequest(Guid.NewGuid().ToString(), ExpectedSequence.Any, eventCount); 24 | } 25 | 26 | protected PersistenceRequest CreatePersistenceRequest(string streamName, int expectedSequence, int eventCount) 27 | { 28 | var list = new List(); 29 | 30 | for (var i = 0; i < eventCount; i++) 31 | list.Add(new UnpersistedEvent(streamName, new SampleEvent1())); 32 | 33 | return new PersistenceRequest(list); 34 | } 35 | 36 | protected IActorRef CreateWriter(IEventStoreWriter writer = null, ISerializer serializer = null, IActorRef dispatcher = null) 37 | { 38 | writer = writer ?? MockEventStore.SuccessfulWriter(); 39 | serializer = serializer ?? new MockSerializer(); 40 | dispatcher = dispatcher ?? CreateTestProbe(); 41 | 42 | var props = BufferedEventWriter.CreateProps(writer, serializer, dispatcher, new GlobalOptions()); 43 | return Sys.ActorOf(props); 44 | } 45 | 46 | #endregion 47 | 48 | [Fact] 49 | public void Writer_tells_persistedevents_to_dispatcher_in_order() 50 | { 51 | var dispatcher = CreateTestProbe(); 52 | var writer = CreateWriter(writer: MockEventStore.SuccessfulWriter(), dispatcher: dispatcher); 53 | 54 | var request = new PersistenceRequest(new[] { 55 | new UnpersistedEvent("a", new SampleEvent3()), 56 | new UnpersistedEvent("a", new SampleEvent1()), 57 | new UnpersistedEvent("a", new SampleEvent2()) 58 | }); 59 | 60 | writer.Tell(request); 61 | 62 | dispatcher.ExpectMsg>(); 63 | dispatcher.ExpectMsg>(); 64 | dispatcher.ExpectMsg>(); 65 | dispatcher.ExpectNoMsg(50); 66 | } 67 | 68 | [Fact] 69 | public void DuplicatedEventException_causes_duplicatedevent_message() 70 | { 71 | var writer = CreateWriter(writer: MockEventStore.ThrowsOnWrite()); 72 | var request = CreatePersistenceRequest(); 73 | writer.Tell(request); 74 | 75 | ExpectMsg(msg => msg.PersistenceID == request.PersistenceID, TimeSpan.FromMinutes(5)); 76 | } 77 | 78 | [Fact] 79 | public void Exception_for_some_items_still_writes_the_others() 80 | { 81 | // the writer will fail on the batch and on the next 3 requests 82 | var writer = CreateWriter(writer: MockEventStore.ThrowsOnWrite(new Exception(), new[] { 1, 2, 3, 4 })); 83 | 84 | var requests = Enumerable.Range(0, 100).Select(_ => CreatePersistenceRequest(1)).ToList(); 85 | 86 | foreach (var r in requests) 87 | writer.Tell(r); 88 | 89 | var list = new List(); 90 | 91 | for (var i = 0; i < requests.Count; i++) 92 | { 93 | var request = requests[i]; 94 | 95 | if (i <= 2) 96 | ExpectMsg(msg => msg.PersistenceID == request.PersistenceID); 97 | else 98 | ExpectMsg(msg => msg.PersistenceID == request.PersistenceID); 99 | } 100 | } 101 | 102 | [Fact] 103 | public void Writer_does_not_publish_to_event_stream() 104 | { 105 | var dispatcher = CreateTestProbe(); 106 | var writer = CreateWriter(writer: MockEventStore.SuccessfulWriter(), dispatcher: dispatcher); 107 | 108 | var request = new PersistenceRequest(new[] { 109 | new UnpersistedEvent("a", new SampleEvent3()), 110 | new UnpersistedEvent("a", new SampleEvent1()), 111 | new UnpersistedEvent("a", new SampleEvent2()) 112 | }); 113 | 114 | var probe = CreateTestProbe(); 115 | Sys.EventStream.Subscribe(probe, typeof(IPersistedEvent)); 116 | 117 | writer.Tell(request); 118 | 119 | probe.ExpectNoMsg(TimeSpan.FromMilliseconds(500)); 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /Test/Even.Tests/EvenGatewayTests.cs: -------------------------------------------------------------------------------- 1 | using Akka.TestKit; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using Xunit; 8 | 9 | namespace Even.Tests 10 | { 11 | public class EvenGatewayTests : EvenTestKit 12 | { 13 | TestProbe ReaderProbe; 14 | TestProbe WriterProbe; 15 | TestProbe AggregatesProbe; 16 | TestProbe CommandProcessorsProbe; 17 | TestProbe EventProcessorsProbe; 18 | TestProbe ProjectionsProbe; 19 | EvenGateway TestGateway; 20 | 21 | public EvenGatewayTests() 22 | { 23 | ReaderProbe = CreateTestProbe(); 24 | WriterProbe = CreateTestProbe(); 25 | AggregatesProbe = CreateTestProbe(); 26 | CommandProcessorsProbe = CreateTestProbe(); 27 | EventProcessorsProbe = CreateTestProbe(); 28 | ProjectionsProbe = CreateTestProbe(); 29 | InitializeTestGateway(); 30 | } 31 | 32 | public void InitializeTestGateway(GlobalOptions options = null) 33 | { 34 | TestGateway = new EvenGateway(new EvenServices 35 | { 36 | Aggregates = AggregatesProbe, 37 | CommandProcessors = CommandProcessorsProbe, 38 | EventProcessors = EventProcessorsProbe, 39 | Projections = ProjectionsProbe, 40 | Reader = ReaderProbe, 41 | Writer = WriterProbe 42 | }, Sys, options ?? new GlobalOptions()); 43 | } 44 | 45 | [Fact] 46 | public void Query_is_published_to_event_stream() 47 | { 48 | Sys.EventStream.Subscribe(TestActor, typeof(IQuery)); 49 | 50 | var query = new object(); 51 | TestGateway.Query(query); 52 | 53 | ExpectMsg(m => m.Message == query); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Test/Even.Tests/EvenSetupTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using Xunit; 7 | using Even; 8 | 9 | namespace Even.Tests 10 | { 11 | public class EvenSetupTests : EvenTestKit 12 | { 13 | [Fact] 14 | public async Task Start_with_default_config_works() 15 | { 16 | await Sys.SetupEven().Start(); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Test/Even.Tests/EvenTestKit.cs: -------------------------------------------------------------------------------- 1 | using Akka.TestKit.Xunit2; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace Even.Tests 10 | { 11 | public class EvenTestKit : TestKit 12 | { 13 | static readonly string Config = File.ReadAllText("TestConfig.hocon"); 14 | 15 | public EvenTestKit() 16 | : base(Config) 17 | { } 18 | 19 | public T ExpectMsgEventually(Predicate isMessage = null, TimeSpan? timeout = null) 20 | { 21 | var to = GetTimeoutOrDefault(timeout); 22 | 23 | var received = Within(to, () => 24 | { 25 | do 26 | { 27 | var msg = ExpectMsg(Remaining); 28 | 29 | if (msg is T) 30 | return (T)msg; 31 | 32 | } while (Remaining > TimeSpan.Zero); 33 | 34 | throw new TimeoutException(); 35 | }); 36 | 37 | if (isMessage == null) 38 | return received; 39 | 40 | if (isMessage(received)) 41 | return received; 42 | 43 | throw new Exception($"Message of type '{typeof(T).FullName}' received, but didn't match the predicate"); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Test/Even.Tests/EventStoreReaderTests.cs: -------------------------------------------------------------------------------- 1 | using Akka.Actor; 2 | using Akka.TestKit; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | using Xunit; 9 | using Even.Tests.Utils; 10 | using Even.Messages; 11 | 12 | namespace Even.Tests 13 | { 14 | public class EventStoreReaderTests : EvenTestKit 15 | { 16 | class TestContainer 17 | { 18 | public TestContainer(EvenTestKit testKit) 19 | { 20 | var a = testKit.CreateTestRelay(); 21 | var b = testKit.CreateTestRelay(); 22 | var c = testKit.CreateTestRelay(); 23 | var e = testKit.CreateTestRelay(); 24 | var d = testKit.CreateTestRelay(); 25 | 26 | ReadProbe = a.Probe; 27 | ReadStreamProbe = b.Probe; 28 | ReadIndexedProjectionProbe = c.Probe; 29 | ReadProjectionIndexCheckpointProbe = d.Probe; 30 | ReadHighestGlobalSequenceProbe = e.Probe; 31 | 32 | var readerProps = EventStoreReader.CreateProps(a.Props, b.Props, c.Props, d.Props, e.Props, new GlobalOptions()); 33 | Reader = testKit.Sys.ActorOf(readerProps); 34 | } 35 | 36 | public IActorRef Reader { get; } 37 | public TestProbe ReadProbe { get; } 38 | public TestProbe ReadStreamProbe { get; } 39 | public TestProbe ReadIndexedProjectionProbe { get; } 40 | public TestProbe ReadProjectionIndexCheckpointProbe { get; } 41 | public TestProbe ReadHighestGlobalSequenceProbe { get; } 42 | } 43 | 44 | readonly TimeSpan DefaultTimeout = TimeSpan.FromMilliseconds(100); 45 | 46 | [Fact] 47 | public void ReadRequests_are_forwarded_to_worker() 48 | { 49 | var o = new TestContainer(this); 50 | 51 | var req = new ReadRequest(1, 1); 52 | o.Reader.Tell(req); 53 | 54 | o.ReadProbe.ExpectMsg(m => m == req); 55 | } 56 | 57 | [Fact] 58 | public void ReadStreamRequests_are_forwarded_to_worker() 59 | { 60 | var o = new TestContainer(this); 61 | 62 | var req = new ReadStreamRequest("a", 1, 1); 63 | o.Reader.Tell(req); 64 | 65 | o.ReadStreamProbe.ExpectMsg(m => m == req); 66 | } 67 | 68 | [Fact] 69 | public void ReadIndexedProjectionStreamRequests_are_forwarded_to_worker() 70 | { 71 | var o = new TestContainer(this); 72 | 73 | var req = new ReadIndexedProjectionStreamRequest("a", 1, 1); 74 | o.Reader.Tell(req); 75 | 76 | o.ReadIndexedProjectionProbe.ExpectMsg(m => m == req); 77 | } 78 | 79 | [Fact] 80 | public void ReadProjectionIndexCheckpointRequests_are_forwarded_to_worker() 81 | { 82 | var o = new TestContainer(this); 83 | 84 | var req = new ReadProjectionCheckpointRequest("a"); 85 | o.Reader.Tell(req); 86 | 87 | o.ReadProjectionIndexCheckpointProbe.ExpectMsg(m => m == req); 88 | } 89 | 90 | [Fact] 91 | public void ReadHighestGlobalSequenceRequests_are_fowarded_to_worker() 92 | { 93 | var o = new TestContainer(this); 94 | 95 | var req = new ReadHighestGlobalSequenceRequest(); 96 | o.Reader.Tell(req); 97 | o.ReadHighestGlobalSequenceProbe.ExpectMsg(m => m == req); 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /Test/Even.Tests/EventStoreWriterTests.cs: -------------------------------------------------------------------------------- 1 | using Even.Messages; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using Xunit; 8 | using Akka.Actor; 9 | 10 | namespace Even.Tests 11 | { 12 | public class EventStoreWriterTests : EvenTestKit 13 | { 14 | #region Helpers 15 | 16 | static readonly TimeSpan NoMsgTimeout = TimeSpan.FromMilliseconds(100); 17 | 18 | #endregion 19 | 20 | [Fact] 21 | public void Requests_with_expected_version_are_forwarded_to_serial_writer_only() 22 | { 23 | var s = CreateTestProbe(); 24 | var b = CreateTestProbe(); 25 | var i = CreateTestProbe(); 26 | var c = CreateTestProbe(); 27 | 28 | var writer = Sys.ActorOf(EventStoreWriter.CreateProps(s, b, i, c, new GlobalOptions())); 29 | 30 | writer.Tell(new PersistenceRequest("a", 0, new[] { new UnpersistedEvent("a", new object()) })); 31 | 32 | s.ExpectMsg(); 33 | 34 | b.ExpectNoMsg(NoMsgTimeout); 35 | i.ExpectNoMsg(NoMsgTimeout); 36 | c.ExpectNoMsg(NoMsgTimeout); 37 | } 38 | 39 | [Fact] 40 | public void Requests_with_any_version_are_forwarded_to_buffered_writer_only() 41 | { 42 | var s = CreateTestProbe(); 43 | var b = CreateTestProbe(); 44 | var i = CreateTestProbe(); 45 | var c = CreateTestProbe(); 46 | 47 | var writer = Sys.ActorOf(EventStoreWriter.CreateProps(s, b, i, c, new GlobalOptions())); 48 | 49 | writer.Tell(new PersistenceRequest("a", ExpectedSequence.Any, new[] { new UnpersistedEvent("a", new object()) })); 50 | 51 | b.ExpectMsg(); 52 | 53 | s.ExpectNoMsg(NoMsgTimeout); 54 | i.ExpectNoMsg(NoMsgTimeout); 55 | c.ExpectNoMsg(NoMsgTimeout); 56 | } 57 | 58 | [Fact] 59 | public void Requests_for_multiple_streams_are_forwarded_to_buffered_writer_only() 60 | { 61 | var s = CreateTestProbe(); 62 | var b = CreateTestProbe(); 63 | var i = CreateTestProbe(); 64 | var c = CreateTestProbe(); 65 | 66 | var writer = Sys.ActorOf(EventStoreWriter.CreateProps(s, b, i, c, new GlobalOptions())); 67 | 68 | writer.Tell(new PersistenceRequest(new[] 69 | { 70 | new UnpersistedEvent("a", new object()), 71 | new UnpersistedEvent("b", new object()) 72 | })); 73 | 74 | b.ExpectMsg(); 75 | 76 | s.ExpectNoMsg(NoMsgTimeout); 77 | i.ExpectNoMsg(NoMsgTimeout); 78 | c.ExpectNoMsg(NoMsgTimeout); 79 | } 80 | 81 | [Fact] 82 | public void Requests_for_index_writes_are_forwarded_to_index_writer_only() 83 | { 84 | var s = CreateTestProbe(); 85 | var b = CreateTestProbe(); 86 | var i = CreateTestProbe(); 87 | var c = CreateTestProbe(); 88 | 89 | var writer = Sys.ActorOf(EventStoreWriter.CreateProps(s, b, i, c, new GlobalOptions())); 90 | 91 | writer.Tell(new ProjectionIndexPersistenceRequest("a", 1, 1)); 92 | 93 | i.ExpectMsg(); 94 | 95 | b.ExpectNoMsg(NoMsgTimeout); 96 | s.ExpectNoMsg(NoMsgTimeout); 97 | c.ExpectNoMsg(NoMsgTimeout); 98 | } 99 | 100 | [Fact] 101 | public void Request_for_checkpoint_writes_are_forwarded_to_checkpoint_writer_only() 102 | { 103 | var s = CreateTestProbe(); 104 | var b = CreateTestProbe(); 105 | var i = CreateTestProbe(); 106 | var c = CreateTestProbe(); 107 | 108 | var writer = Sys.ActorOf(EventStoreWriter.CreateProps(s, b, i, c, new GlobalOptions())); 109 | 110 | writer.Tell(new ProjectionCheckpointPersistenceRequest("a", 1)); 111 | 112 | c.ExpectMsg(); 113 | 114 | b.ExpectNoMsg(NoMsgTimeout); 115 | s.ExpectNoMsg(NoMsgTimeout); 116 | i.ExpectNoMsg(NoMsgTimeout); 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /Test/Even.Tests/MessageHandlerTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using Xunit; 7 | 8 | namespace Even.Tests 9 | { 10 | public class MessageHandlerTests 11 | { 12 | [Fact] 13 | public async Task Handles_messages() 14 | { 15 | var handler = new ObjectHandler(); 16 | 17 | var expected = 42; 18 | int actual = 0; 19 | 20 | handler.AddHandler(s => actual = 100); 21 | handler.AddHandler(s => actual = expected); 22 | 23 | await handler.Handle("go"); 24 | 25 | Assert.Equal(expected, actual); 26 | } 27 | 28 | class SingleChar { } 29 | class MultiChar { } 30 | 31 | [Fact] 32 | public async Task Handles_mapped_messages() 33 | { 34 | var handler = new MessageHandler(s => s.Length == 1 ? typeof(SingleChar) : typeof(MultiChar)); 35 | 36 | var expected = 42; 37 | int actual = 0; 38 | 39 | handler.AddHandler(s => actual = 100); 40 | handler.AddHandler(s => actual = expected); 41 | 42 | await handler.Handle("a"); 43 | 44 | Assert.Equal(expected, actual); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Test/Even.Tests/Mocks/MockEventStore.cs: -------------------------------------------------------------------------------- 1 | using Even.Persistence; 2 | using NSubstitute; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | using Even.Internals; 10 | 11 | namespace Even.Tests.Mocks 12 | { 13 | public static class MockEventStore 14 | { 15 | 16 | 17 | public static IEventStoreWriter SuccessfulWriter() 18 | { 19 | var store = Substitute.For(); 20 | 21 | store.WriteStreamAsync(null, 0, Arg.Do>(events => 22 | { 23 | int i = 1; 24 | 25 | foreach (var e in events) 26 | e.GlobalSequence = i++; 27 | 28 | })).ReturnsForAnyArgs(Unit.GetCompletedTask()); 29 | 30 | store.WriteAsync(Arg.Do>(events => 31 | { 32 | int i = 1; 33 | 34 | foreach (var e in events) 35 | e.GlobalSequence = i++; 36 | 37 | })).ReturnsForAnyArgs(Unit.GetCompletedTask()); 38 | 39 | return store; 40 | } 41 | 42 | public static IEventStoreWriter ThrowsOnWrite(int[] throwOnCalls = null) 43 | where T : Exception, new() 44 | { 45 | return ThrowsOnWrite(new T(), throwOnCalls); 46 | } 47 | 48 | public static IEventStoreWriter ThrowsOnWrite(Exception exception, int[] throwOnCalls = null) 49 | { 50 | throwOnCalls = throwOnCalls ?? new[] { 1 }; 51 | 52 | var store = Substitute.For(); 53 | 54 | var writeCount = 0; 55 | 56 | store.WriteAsync(null).ReturnsForAnyArgs(t => { 57 | 58 | if (throwOnCalls.Contains(++writeCount)) 59 | throw exception; 60 | 61 | return Unit.GetCompletedTask(); 62 | }); 63 | 64 | var writeStreamCount = 0; 65 | 66 | store.WriteStreamAsync(null, 0, null).ReturnsForAnyArgs(t => { 67 | 68 | if (throwOnCalls.Contains(++writeStreamCount)) 69 | throw exception; 70 | 71 | return Unit.GetCompletedTask(); 72 | }); 73 | 74 | return store; 75 | } 76 | 77 | public static IEventStoreReader ThrowsOnReadStreams() 78 | where T : Exception, new() 79 | { 80 | return ThrowsOnReadStreams(new T()); 81 | } 82 | 83 | public static IEventStoreReader ThrowsOnReadStreams(Exception exception) 84 | { 85 | var store = Substitute.For(); 86 | 87 | store.ReadAsync(0, 0, null, default(CancellationToken)) 88 | .ReturnsForAnyArgs(t => 89 | { 90 | throw exception; 91 | }); 92 | 93 | store.ReadStreamAsync(null, 0, 0, null, default(CancellationToken)) 94 | .ReturnsForAnyArgs(t => 95 | { 96 | throw exception; 97 | }); 98 | 99 | store.ReadIndexedProjectionStreamAsync(null, 0, 0, null, default(CancellationToken)) 100 | .ReturnsForAnyArgs(t => 101 | { 102 | throw exception; 103 | }); 104 | 105 | return store; 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /Test/Even.Tests/Mocks/MockPersistedEvent.cs: -------------------------------------------------------------------------------- 1 | using NSubstitute; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace Even.Tests.Mocks 9 | { 10 | public static class MockPersistedEvent 11 | { 12 | public static IPersistedEvent Create(T domainEvent, int globalSequence = 1, Stream stream = null) 13 | { 14 | stream = stream ?? "a"; 15 | 16 | var e = Substitute.For, IPersistedEvent>(); 17 | 18 | e.DomainEvent.Returns(domainEvent); 19 | e.GlobalSequence.Returns(globalSequence); 20 | e.Stream.Returns(stream); 21 | ((IPersistedEvent)e).DomainEvent.Returns(domainEvent); 22 | 23 | return e; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Test/Even.Tests/Mocks/MockPersistedEventFactory.cs: -------------------------------------------------------------------------------- 1 | using NSubstitute; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace Even.Tests.Mocks 9 | { 10 | public class MockPersistedEventFactory : IPersistedEventFactory 11 | { 12 | public IPersistedEvent CreateEvent(IPersistedRawEvent rawEvent) 13 | { 14 | var e = Substitute.For(); 15 | 16 | e.GlobalSequence.Returns(rawEvent.GlobalSequence); 17 | 18 | return e; 19 | } 20 | 21 | public IPersistedStreamEvent CreateStreamEvent(IPersistedRawEvent rawEvent, int streamSequence) 22 | { 23 | var e = Substitute.For(); 24 | 25 | e.GlobalSequence.Returns(rawEvent.GlobalSequence); 26 | e.Stream.Returns(rawEvent.Stream); 27 | e.StreamSequence.Returns(streamSequence); 28 | 29 | return e; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Test/Even.Tests/Mocks/MockPersistedStreamEvent.cs: -------------------------------------------------------------------------------- 1 | using NSubstitute; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace Even.Tests.Mocks 9 | { 10 | public static class MockPersistedStreamEvent 11 | { 12 | public static IPersistedStreamEvent Create(T domainEvent, int globalSequence = 1, int streamSequence = 1, Stream stream = null) 13 | { 14 | stream = stream ?? "a"; 15 | 16 | var e = Substitute.For, IPersistedStreamEvent>(); 17 | 18 | e.DomainEvent.Returns(domainEvent); 19 | e.GlobalSequence.Returns(globalSequence); 20 | e.StreamSequence.Returns(streamSequence); 21 | e.Stream.Returns(stream); 22 | ((IPersistedStreamEvent)e).DomainEvent.Returns(domainEvent); 23 | 24 | return e; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Test/Even.Tests/Mocks/MockProjectionStore.cs: -------------------------------------------------------------------------------- 1 | using NSubstitute; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using Even.Internals; 8 | 9 | namespace Even.Tests.Mocks 10 | { 11 | public static class MockProjectionStore 12 | { 13 | public static IProjectionStoreWriter SuccessfulWriter() 14 | { 15 | var store = Substitute.For(); 16 | 17 | store.WriteProjectionCheckpointAsync(null, 0).ReturnsForAnyArgs(Unit.GetCompletedTask()); 18 | store.WriteProjectionIndexAsync(null, 0, null).ReturnsForAnyArgs(Unit.GetCompletedTask()); 19 | store.ClearProjectionIndexAsync(null).ReturnsForAnyArgs(Unit.GetCompletedTask()); 20 | 21 | return store; 22 | } 23 | 24 | public static IProjectionStoreWriter ThrowsOnWrite(Exception exception) 25 | { 26 | var store = Substitute.For(); 27 | 28 | store.WriteProjectionIndexAsync(null, 0, null).ReturnsForAnyArgs(_ => { throw exception; }); 29 | store.WriteProjectionCheckpointAsync(null, 0).ReturnsForAnyArgs(_ => { throw exception; }); 30 | 31 | return store; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Test/Even.Tests/Mocks/MockSerializer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace Even.Tests 8 | { 9 | public class MockSerializer : ISerializer 10 | { 11 | public object DeserializeEvent(byte[] bytes, int format, Type type) 12 | { 13 | return new object(); 14 | } 15 | 16 | public IReadOnlyDictionary DeserializeMetadata(byte[] bytes) 17 | { 18 | return new Dictionary(); 19 | } 20 | 21 | public byte[] SerializeEvent(object domainEvent, int format) 22 | { 23 | return new byte[0]; 24 | } 25 | 26 | public byte[] SerializeMetadata(IReadOnlyDictionary metadata) 27 | { 28 | return new byte[0]; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Test/Even.Tests/Mocks/TestStore.cs: -------------------------------------------------------------------------------- 1 | using Even.Persistence; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace Even.Tests.Mocks 9 | { 10 | public class TestStore : InMemoryStore 11 | { 12 | public TestStore(IEnumerable events) 13 | { 14 | var rawEvents = UnpersistedRawEvent.FromUnpersistedEvents(events, new DefaultSerializer()); 15 | this.WriteAsync(rawEvents); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Test/Even.Tests/Persistence/InMemoryEventStoreTests.cs: -------------------------------------------------------------------------------- 1 | using Even.Persistence; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using Xunit; 8 | 9 | namespace Even.Tests.Persistence 10 | { 11 | public class InMemoryEventStoreTests : EventStoreTests 12 | { 13 | protected override IEventStore CreateStore() 14 | { 15 | return new InMemoryStore(); 16 | } 17 | 18 | protected override void ResetStore() 19 | { } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Test/Even.Tests/Persistence/MySqlEventStoreTests.cs: -------------------------------------------------------------------------------- 1 | using DBHelpers; 2 | using Even.Persistence; 3 | using Even.Persistence.Sql; 4 | using MySql.Data.MySqlClient; 5 | using System.Data.Common; 6 | using System; 7 | using System.Threading.Tasks; 8 | 9 | namespace Even.Tests.Persistence 10 | { 11 | #if MYSQL 12 | public 13 | #endif 14 | class MySqlEventStoreTests : EventStoreTests 15 | { 16 | private string ConnectionString = "Server=localhost; Uid=even_test; Pwd=even_test; Database=even_test;"; 17 | 18 | protected override IEventStore CreateStore() 19 | { 20 | return new MySqlStore(ConnectionString, true); 21 | } 22 | 23 | protected override void ResetStore() 24 | { 25 | var db = new DBHelper(DbProviderFactories.GetFactory("MySql.Data.MySqlClient"), ConnectionString); 26 | 27 | var store = (MySqlStore)Store; 28 | 29 | var a = store.EventsTable; 30 | var b = store.ProjectionIndexTable; 31 | var c = store.ProjectionCheckpointTable; 32 | 33 | db.ExecuteNonQuery($"TRUNCATE TABLE `{a}`; TRUNCATE TABLE `{b}`; TRUNCATE TABLE `{c}`;"); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Test/Even.Tests/Persistence/SQLiteEventStoreTests.cs: -------------------------------------------------------------------------------- 1 | using System.Data.Common; 2 | using DBHelpers; 3 | using Even.Persistence.SQLite; 4 | 5 | namespace Even.Tests.Persistence 6 | { 7 | #if SQLITE 8 | public 9 | #endif 10 | class SQLiteEventStoreTests : EventStoreTests 11 | { 12 | private string ConnectionString = "Data Source=event_test.db;Version=3;New=True;"; 13 | protected override IEventStore CreateStore() 14 | { 15 | return new SQLiteStore(ConnectionString, true); 16 | } 17 | 18 | protected override void ResetStore() 19 | { 20 | var db = new DBHelper(DbProviderFactories.GetFactory("System.Data.SQLite"), ConnectionString); 21 | 22 | var store = (SQLiteStore)Store; 23 | 24 | var a = store.EventsTable; 25 | var b = store.ProjectionIndexTable; 26 | var c = store.ProjectionCheckpointTable; 27 | 28 | db.ExecuteNonQuery($"DELETE FROM {a}; DELETE FROM SQLITE_SEQUENCE WHERE NAME='{a}'; DELETE FROM {b}; DELETE FROM SQLITE_SEQUENCE WHERE NAME='{b}'; DELETE FROM {c}; DELETE FROM SQLITE_SEQUENCE WHERE NAME='{c}';"); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Test/Even.Tests/Persistence/SqlServer2012StoreTests.cs: -------------------------------------------------------------------------------- 1 | using DBHelpers; 2 | using Even.Persistence; 3 | using Even.Persistence.Sql; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Data.SqlClient; 7 | using System.Linq; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | 11 | namespace Even.Tests.Persistence 12 | { 13 | #if SQL2012 14 | public 15 | #endif 16 | class SqlServer2012StoreTests : EventStoreTests 17 | { 18 | private string ConnectionString = "Server=localhost\\sqlexpress; Trusted_Connection=True; Database=even_test;"; 19 | 20 | protected override IEventStore CreateStore() 21 | { 22 | return new SqlServer2012Store(ConnectionString, true); 23 | } 24 | 25 | protected override void ResetStore() 26 | { 27 | var db = new DBHelper(SqlClientFactory.Instance, ConnectionString); 28 | 29 | var store = (BaseSqlStore)Store; 30 | 31 | var a = store.EventsTable; 32 | var b = store.ProjectionIndexTable; 33 | var c = store.ProjectionCheckpointTable; 34 | 35 | db.ExecuteNonQuery($"TRUNCATE TABLE [{a}]; TRUNCATE TABLE [{b}]; TRUNCATE TABLE [{c}];"); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Test/Even.Tests/ProjectionCheckpointWriterTests.cs: -------------------------------------------------------------------------------- 1 | using Akka.Actor; 2 | using Even.Messages; 3 | using NSubstitute; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | using Xunit; 10 | 11 | namespace Even.Tests 12 | { 13 | public class ProjectionCheckpointWriterTests : EvenTestKit 14 | { 15 | [Fact] 16 | public async Task Writer_buffers_and_writes_only_highest_value() 17 | { 18 | var store = Substitute.For(); 19 | var props = ProjectionCheckpointWriter.CreateProps(store, new GlobalOptions { CheckpointWriterFlushDelay = TimeSpan.FromMilliseconds(10) }); 20 | var writer = Sys.ActorOf(props); 21 | 22 | writer.Tell(new ProjectionCheckpointPersistenceRequest("a", 10)); 23 | writer.Tell(new ProjectionCheckpointPersistenceRequest("a", 20)); 24 | writer.Tell(new ProjectionCheckpointPersistenceRequest("a", 30)); 25 | writer.Tell(new ProjectionCheckpointPersistenceRequest("a", 40)); 26 | writer.Tell(new ProjectionCheckpointPersistenceRequest("a", 50)); 27 | 28 | await Task.Delay(200); 29 | 30 | #pragma warning disable 4014 31 | store.Received().WriteProjectionCheckpointAsync("a", 50); 32 | #pragma warning restore 4014 33 | } 34 | 35 | [Fact] 36 | public void Writer_does_not_reply_failure_messages_on_store_exception() 37 | { 38 | var store = Substitute.For(); 39 | store.WriteProjectionCheckpointAsync(null, 0).ReturnsForAnyArgs(_ => { throw new Exception(); }); 40 | 41 | var props = ProjectionCheckpointWriter.CreateProps(store, new GlobalOptions { CheckpointWriterFlushDelay = TimeSpan.Zero }); 42 | var writer = Sys.ActorOf(props); 43 | 44 | writer.Tell(new ProjectionCheckpointPersistenceRequest("a", 1)); 45 | 46 | ExpectNoMsg(TimeSpan.FromMilliseconds(200)); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Test/Even.Tests/ProjectionIndexWriterTests.cs: -------------------------------------------------------------------------------- 1 | using NSubstitute; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using Xunit; 8 | using Even.Messages; 9 | using Akka.Actor; 10 | using Even.Internals; 11 | using Even.Tests.Mocks; 12 | 13 | namespace Even.Tests 14 | { 15 | public class ProjectionIndexWriterTests : EvenTestKit 16 | { 17 | #region Helpers 18 | 19 | IActorRef CreateWriter(IProjectionStoreWriter writer = null, TimeSpan? flushDelay = null) 20 | { 21 | writer = writer ?? MockProjectionStore.SuccessfulWriter(); 22 | var delay = flushDelay ?? TimeSpan.FromMilliseconds(10); 23 | var props = ProjectionIndexWriter.CreateProps(writer, new GlobalOptions { IndexWriterFlushDelay = delay }); 24 | return Sys.ActorOf(props); 25 | } 26 | 27 | #endregion 28 | 29 | [Fact] 30 | public async Task Writer_buffers_requests_before_writing() 31 | { 32 | var store = MockProjectionStore.SuccessfulWriter(); 33 | store.WriteProjectionIndexAsync(null, 0, null).ReturnsForAnyArgs(Unit.GetCompletedTask()); 34 | 35 | var writer = CreateWriter(store); 36 | writer.Tell(new ProjectionIndexPersistenceRequest("a", 1, 10)); 37 | writer.Tell(new ProjectionIndexPersistenceRequest("a", 2, 20)); 38 | writer.Tell(new ProjectionIndexPersistenceRequest("a", 3, 30)); 39 | 40 | await Task.Delay(200); 41 | var calls = store.ReceivedCalls().ToList(); 42 | 43 | var expectedSequences = new long[] { 10, 20, 30 }; 44 | 45 | #pragma warning disable 4014 46 | store.Received().WriteProjectionIndexAsync("a", 0, Arg.Is>(l => l.SequenceEqual(expectedSequences))); 47 | #pragma warning restore 4014 48 | } 49 | 50 | [Fact] 51 | public void Writer_replies_inconsistency_message_on_missing_sequence() 52 | { 53 | var writer = CreateWriter(); 54 | writer.Tell(new ProjectionIndexPersistenceRequest("a", 1, 10)); 55 | writer.Tell(new ProjectionIndexPersistenceRequest("a", 2, 20)); 56 | writer.Tell(new ProjectionIndexPersistenceRequest("a", 4, 40)); 57 | 58 | ExpectMsg(); 59 | } 60 | 61 | [Fact] 62 | public void Writer_replies_inconsistency_message_on_unexpected_sequence() 63 | { 64 | var store = MockProjectionStore.ThrowsOnWrite(new UnexpectedStreamSequenceException()); 65 | var writer = CreateWriter(store); 66 | 67 | writer.Tell(new ProjectionIndexPersistenceRequest("a", 1, 1)); 68 | ExpectMsg(); 69 | } 70 | 71 | [Fact] 72 | public void Writer_replies_inconsistency_message_on_duplicated_request() 73 | { 74 | var store = MockProjectionStore.ThrowsOnWrite(new DuplicatedEntryException()); 75 | var writer = CreateWriter(store); 76 | 77 | writer.Tell(new ProjectionIndexPersistenceRequest("a", 1, 1)); 78 | ExpectMsg(); 79 | } 80 | 81 | [Fact] 82 | public void Writer_does_not_reply_on_unexpected_exceptions() 83 | { 84 | // in case of unexpected exceptions, like connection issues 85 | // the way to handle is to let the projection detect inconsistencies when the write works 86 | // and rebuild the index if needed, so we don't expect any error messages 87 | 88 | var store = MockProjectionStore.ThrowsOnWrite(new Exception()); 89 | var writer = CreateWriter(store); 90 | 91 | writer.Tell(new ProjectionIndexPersistenceRequest("a", 1, 1)); 92 | ExpectNoMsg(TimeSpan.FromSeconds(1)); 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Test/Even.Tests/ProjectionTests.cs: -------------------------------------------------------------------------------- 1 | using Akka.Actor; 2 | using Akka.TestKit; 3 | using Akka.Actor.Dsl; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | using Xunit; 10 | using Even.Messages; 11 | using NSubstitute; 12 | using Even.Tests.Mocks; 13 | using Even.Tests.Utils; 14 | using System.Threading; 15 | using Even.Internals; 16 | 17 | namespace Even.Tests 18 | { 19 | public class ProjectionTests : EvenTestKit 20 | { 21 | public class TestEvent { } 22 | public class TestQuery { } 23 | 24 | class TestProjection : Projection 25 | { 26 | IActorRef _probe; 27 | 28 | public TestProjection(IActorRef probe) 29 | { 30 | _probe = probe; 31 | 32 | OnEvent(e => 33 | { 34 | _probe.Forward(e); 35 | }); 36 | 37 | OnQuery(q => 38 | { 39 | _probe.Forward(q); 40 | }); 41 | } 42 | 43 | protected override void OnReady() 44 | { 45 | Receive(async delay => 46 | { 47 | await Task.Delay(delay); 48 | }); 49 | } 50 | 51 | protected override Task PrepareToRebuild() 52 | { 53 | _probe.Tell("rebuild"); 54 | return Unit.GetCompletedTask(); 55 | } 56 | 57 | protected override Task OnExpiredQuery(object query) 58 | { 59 | _probe.Tell("expired"); 60 | return Unit.GetCompletedTask(); 61 | } 62 | } 63 | 64 | private Stream _testStream; 65 | 66 | private IActorRef CreateAndInitializeTestProjection(GlobalOptions options = null) 67 | { 68 | var sup = Sys.ActorOf(conf => 69 | { 70 | conf.Receive((r, ctx) => 71 | { 72 | _testStream = r.Query.ProjectionStream; 73 | ctx.Sender.Tell(new ProjectionReplayFinished(r.RequestID)); 74 | }); 75 | }); 76 | 77 | var props = Props.Create(TestActor); 78 | var proj = Sys.ActorOf(props); 79 | 80 | proj.Tell(new InitializeProjection(sup, options ?? new GlobalOptions())); 81 | ExpectMsg(i => i.Initialized); 82 | 83 | return proj; 84 | } 85 | 86 | [Fact] 87 | public void Projection_requests_subscription_on_start() 88 | { 89 | var sup = CreateTestProbe(); 90 | var proj = Sys.ActorOf(); 91 | proj.Tell(new InitializeProjection(sup, new GlobalOptions())); 92 | 93 | sup.ExpectMsg(); 94 | } 95 | 96 | [Trait("Category","Projection")] 97 | [Trait("Investigate", "True")] 98 | public void Receives_events_after_replay() 99 | { 100 | var proj = CreateAndInitializeTestProjection(); 101 | 102 | var e = MockPersistedStreamEvent.Create(new TestEvent(), stream: _testStream); 103 | proj.Tell(e); 104 | 105 | ExpectMsg>(TimeSpan.FromSeconds(15)); 106 | } 107 | 108 | [Fact] 109 | public void Rebuilds_on_RebuildProjection_message() 110 | { 111 | var proj = CreateAndInitializeTestProjection(); 112 | 113 | proj.Tell(new RebuildProjection()); 114 | 115 | ExpectMsg(s => s == "rebuild"); 116 | } 117 | 118 | [Fact] 119 | public void Handles_queries() 120 | { 121 | var proj = CreateAndInitializeTestProjection(); 122 | 123 | var q = new TestQuery(); 124 | Sys.EventStream.Publish(new Query(CreateTestProbe(), q, Timeout.In(1000))); 125 | 126 | ExpectMsg(o => o == q); 127 | } 128 | 129 | [Fact] 130 | public void Does_not_handle_expired_queries() 131 | { 132 | var proj = CreateAndInitializeTestProjection(); 133 | 134 | // create an event to expire in 10 ms 135 | var q = new Query(CreateTestProbe(), new TestQuery(), Timeout.In(10)); 136 | 137 | // forces the projection to sleep for 100 ms 138 | proj.Tell(100); 139 | 140 | // publish the expired event 141 | Sys.EventStream.Publish(q); 142 | 143 | ExpectMsg(s => s == "expired"); 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /Test/Even.Tests/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System.Reflection; 3 | 4 | [assembly: AssemblyTitleAttribute("Even.Tests")] 5 | [assembly: AssemblyProductAttribute("Even")] 6 | [assembly: AssemblyDescriptionAttribute("An event sourcing framework on top of Akka.NET")] 7 | -------------------------------------------------------------------------------- /Test/Even.Tests/QueryTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using Akka.Actor; 7 | using Akka.Actor.Dsl; 8 | using Xunit; 9 | 10 | namespace Even.Tests 11 | { 12 | public class QueryTests : EvenTestKit 13 | { 14 | public class TestQuery { public Guid ID { get; } = Guid.NewGuid(); } 15 | public class TestResponse { public Guid ID { get; set; } } 16 | 17 | [Fact] 18 | public async Task Query_using_eventstream_gets_response() 19 | { 20 | var responder = Sys.ActorOf(conf => 21 | { 22 | conf.Receive>((q, ctx) => q.Sender.Tell(new TestResponse { ID = q.Message.ID }, ctx.Self)); 23 | }); 24 | 25 | Sys.EventStream.Subscribe(responder, typeof(IQuery)); 26 | 27 | var query = new TestQuery(); 28 | var response = await Sys.Query(query, TimeSpan.FromSeconds(1)); 29 | 30 | Assert.Equal(query.ID, response.ID); 31 | } 32 | 33 | 34 | [Fact] 35 | public async Task Query_using_actorpath_gets_response() 36 | { 37 | var responder = Sys.ActorOf(conf => 38 | { 39 | conf.Receive>((q, ctx) => ctx.Sender.Tell(new TestResponse { ID = q.Message.ID })); 40 | }); 41 | 42 | var path = responder.Path.ToString(); 43 | 44 | 45 | 46 | var query = new TestQuery(); 47 | var response = await Sys.Query(ActorSelection(path), query, TimeSpan.FromSeconds(1)); 48 | 49 | Assert.Equal(query.ID, response.ID); 50 | } 51 | 52 | [Fact] 53 | public async Task Query_using_eventstream_and_wrong_response_throws_exception() 54 | { 55 | var responder = Sys.ActorOf(conf => 56 | { 57 | conf.Receive>((q, ctx) => ctx.Sender.Tell(new TestResponse { ID = q.Message.ID })); 58 | }); 59 | 60 | Sys.EventStream.Subscribe(responder, typeof(IQuery)); 61 | 62 | await Assert.ThrowsAnyAsync(async () => 63 | { 64 | await Sys.Query(new TestQuery(), TimeSpan.FromSeconds(1)); 65 | }); 66 | } 67 | 68 | [Fact] 69 | public async Task Query_using_actorpath_and_wrong_response_throws_exception() 70 | { 71 | var responder = Sys.ActorOf(conf => 72 | { 73 | conf.Receive>((q, ctx) => ctx.Sender.Tell(new TestResponse { ID = q.Message.ID })); 74 | }); 75 | 76 | var path = responder.Path.ToString(); 77 | 78 | await Assert.ThrowsAnyAsync(async () => 79 | { 80 | await Sys.Query(ActorSelection(path), new TestQuery(), TimeSpan.FromSeconds(1)); 81 | }); 82 | } 83 | 84 | [Fact] 85 | public async Task Query_using_eventstream_throws_exception_on_timeout() 86 | { 87 | await Assert.ThrowsAsync(async () => 88 | { 89 | await Sys.Query(new TestQuery(), TimeSpan.FromMilliseconds(10)); 90 | }); 91 | } 92 | 93 | [Fact] 94 | public async Task Query_using_actorpath_throws_exception_on_timeout() 95 | { 96 | await Assert.ThrowsAsync(async () => 97 | { 98 | await Sys.Query(new TestQuery(), TimeSpan.FromMilliseconds(10)); 99 | }); 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /Test/Even.Tests/ReadWorkerTests.cs: -------------------------------------------------------------------------------- 1 | using Akka.Actor; 2 | using Even.Messages; 3 | using Even.Tests.Mocks; 4 | using NSubstitute; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | using Xunit; 11 | 12 | namespace Even.Tests 13 | { 14 | public class ReadWorkerTests : EvenTestKit 15 | { 16 | const int TotalEvents = 10; 17 | 18 | protected IActorRef CreateReader() 19 | { 20 | var list = new List(); 21 | 22 | for (var i = 0; i < TotalEvents; i++) 23 | list.Add(new UnpersistedEvent("a", new object())); 24 | 25 | var store = new TestStore(list); 26 | var factory = new MockPersistedEventFactory(); 27 | var props = Props.Create(store, factory); 28 | 29 | return Sys.ActorOf(props); 30 | } 31 | 32 | [Theory] 33 | [InlineData(1)] 34 | [InlineData(4)] 35 | [InlineData(10)] 36 | public void Reads_single_event(int sequenceToRead) 37 | { 38 | var reader = CreateReader(); 39 | 40 | var req = new ReadRequest(sequenceToRead, 1); 41 | reader.Tell(req); 42 | 43 | ExpectMsg(m => m.RequestID == req.RequestID && m.Event.GlobalSequence == sequenceToRead); 44 | ExpectMsg(m => m.RequestID == req.RequestID); 45 | } 46 | 47 | [Fact] 48 | public void Reads_all_events() 49 | { 50 | var reader = CreateReader(); 51 | 52 | var req = new ReadRequest(1, EventCount.Unlimited); 53 | 54 | reader.Tell(req); 55 | 56 | for (int i = 1; i <= TotalEvents; i++) 57 | ExpectMsg(m => m.RequestID == req.RequestID && m.Event.GlobalSequence == i); 58 | 59 | ExpectMsg(m => m.RequestID == req.RequestID); 60 | } 61 | 62 | [Fact] 63 | public void Reads_with_sequence_above_highest_sends_ReadFinished() 64 | { 65 | var reader = CreateReader(); 66 | 67 | var req = new ReadRequest(1000, EventCount.Unlimited); 68 | reader.Tell(req); 69 | 70 | ExpectMsg(m => m.RequestID == req.RequestID); 71 | } 72 | 73 | [Fact] 74 | public void Reads_with_count_zero_sends_ReadFinished() 75 | { 76 | var reader = CreateReader(); 77 | 78 | var req = new ReadRequest(1, 0); 79 | reader.Tell(req); 80 | 81 | ExpectMsg(m => m.RequestID == req.RequestID); 82 | } 83 | 84 | [Fact] 85 | public void Reads_with_initialsequence_and_counts() 86 | { 87 | var reader = CreateReader(); 88 | 89 | var req = new ReadRequest(7, 3); 90 | reader.Tell(req); 91 | 92 | ExpectMsg(m => m.RequestID == req.RequestID && m.Event.GlobalSequence == 7); 93 | ExpectMsg(m => m.RequestID == req.RequestID && m.Event.GlobalSequence == 8); 94 | ExpectMsg(m => m.RequestID == req.RequestID && m.Event.GlobalSequence == 9); 95 | ExpectMsg(m => m.RequestID == req.RequestID); 96 | } 97 | 98 | [Fact] 99 | public void Receives_ReadCancelled_on_cancel_request() 100 | { 101 | var reader = CreateReader(); 102 | 103 | var req = new ReadRequest(1, EventCount.Unlimited); 104 | reader.Tell(req); 105 | reader.Tell(new CancelRequest(req.RequestID)); 106 | 107 | ExpectMsgEventually(m => m.RequestID == req.RequestID); 108 | } 109 | 110 | [Fact] 111 | public void Receives_ReadAborted_on_store_exception() 112 | { 113 | var ex = new Exception(); 114 | var store = MockEventStore.ThrowsOnReadStreams(ex); 115 | var factory = Substitute.For(); 116 | 117 | var props = Props.Create(store, factory); 118 | var actor = Sys.ActorOf(props); 119 | 120 | var req = new ReadRequest(1, EventCount.Unlimited); 121 | actor.Tell(req); 122 | 123 | ExpectMsg(m => m.RequestID == req.RequestID && m.Exception == ex); 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /Test/Even.Tests/SerialEventStreamWriterTests.cs: -------------------------------------------------------------------------------- 1 | using Akka.Actor; 2 | using Even.Messages; 3 | using Even.Tests.Mocks; 4 | using System; 5 | using System.Collections.Generic; 6 | using Xunit; 7 | 8 | namespace Even.Tests 9 | { 10 | public class SerialEventStreamWriterTests : EvenTestKit 11 | { 12 | #region Helpers 13 | 14 | class SampleEvent1 { } 15 | class SampleEvent2 { } 16 | class SampleEvent3 { } 17 | 18 | protected PersistenceRequest CreatePersistenceRequest(int eventCount = 1) 19 | { 20 | return CreatePersistenceRequest(Guid.NewGuid().ToString(), ExpectedSequence.Any, eventCount); 21 | } 22 | 23 | protected PersistenceRequest CreatePersistenceRequest(string streamName, int expectedSequence, int eventCount) 24 | { 25 | var list = new List(); 26 | 27 | for (var i = 0; i < eventCount; i++) 28 | list.Add(new UnpersistedEvent(streamName, new SampleEvent1())); 29 | 30 | return new PersistenceRequest(streamName, expectedSequence, list); 31 | } 32 | 33 | protected IActorRef CreateWriter(IEventStoreWriter writer = null, ISerializer serializer = null, IActorRef dispatcher = null) 34 | { 35 | writer = writer ?? MockEventStore.SuccessfulWriter(); 36 | serializer = serializer ?? new MockSerializer(); 37 | dispatcher = dispatcher ?? CreateTestProbe(); 38 | 39 | var props = SerialEventStreamWriter.CreateProps(writer, serializer, dispatcher, new GlobalOptions()); 40 | return Sys.ActorOf(props); 41 | } 42 | 43 | #endregion 44 | 45 | [Fact] 46 | public void Writer_tells_persistedevents_to_dispatcher_in_order() 47 | { 48 | var dispatcher = CreateTestProbe(); 49 | var writer = CreateWriter(writer: MockEventStore.SuccessfulWriter(), dispatcher: dispatcher); 50 | 51 | var request = new PersistenceRequest(new[] { 52 | new UnpersistedEvent("a", new SampleEvent3()), 53 | new UnpersistedEvent("a", new SampleEvent1()), 54 | new UnpersistedEvent("a", new SampleEvent2()) 55 | }); 56 | 57 | writer.Tell(request); 58 | 59 | dispatcher.ExpectMsg>(); 60 | dispatcher.ExpectMsg>(); 61 | dispatcher.ExpectMsg>(); 62 | dispatcher.ExpectNoMsg(50); 63 | } 64 | 65 | [Fact] 66 | public void UnexpectedStreamSequenceException_causes_unexpectedstreamsequence_message() 67 | { 68 | var writer = CreateWriter(writer: MockEventStore.ThrowsOnWrite()); 69 | 70 | var request = CreatePersistenceRequest(); 71 | writer.Tell(request); 72 | 73 | ExpectMsg(msg => msg.PersistenceID == request.PersistenceID); 74 | } 75 | 76 | [Fact] 77 | public void DuplicatedEventException_causes_duplicatedevent_message() 78 | { 79 | var writer = CreateWriter(writer: MockEventStore.ThrowsOnWrite()); 80 | var request = CreatePersistenceRequest(); 81 | writer.Tell(request); 82 | 83 | ExpectMsg(msg => msg.PersistenceID == request.PersistenceID); 84 | } 85 | 86 | [Theory] 87 | [InlineData(typeof(ArgumentException))] 88 | [InlineData(typeof(TimeoutException))] 89 | [InlineData(typeof(Exception))] 90 | public void UnexpectedExceptions_during_write_causes_reply_with_persistencefailure(Type exceptionType) 91 | { 92 | var exception = Activator.CreateInstance(exceptionType) as Exception; 93 | 94 | var writer = CreateWriter(writer: MockEventStore.ThrowsOnWrite(exception)); 95 | 96 | var request = CreatePersistenceRequest(); 97 | writer.Tell(request); 98 | 99 | ExpectMsg(msg => msg.PersistenceID == request.PersistenceID && msg.Exception == exception); 100 | } 101 | 102 | [Fact] 103 | public void Writer_does_not_publish_to_event_stream() 104 | { 105 | var dispatcher = CreateTestProbe(); 106 | var writer = CreateWriter(writer: MockEventStore.SuccessfulWriter(), dispatcher: dispatcher); 107 | var request = CreatePersistenceRequest(); 108 | 109 | var probe = CreateTestProbe(); 110 | Sys.EventStream.Subscribe(probe, typeof(IPersistedEvent)); 111 | 112 | writer.Tell(request); 113 | 114 | probe.ExpectNoMsg(TimeSpan.FromMilliseconds(500)); 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /Test/Even.Tests/StreamTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using Xunit; 7 | 8 | namespace Even.Tests 9 | { 10 | public class StreamTests 11 | { 12 | [Fact] 13 | public void Same_name_generates_equal_streams() 14 | { 15 | var a = new Stream("123"); 16 | var b = new Stream("123"); 17 | 18 | Assert.Equal(a, b); 19 | } 20 | 21 | [Fact] 22 | public void Names_must_be_case_insensitive() 23 | { 24 | var a = new Stream("foobar"); 25 | var b = new Stream("FooBAR"); 26 | 27 | Assert.Equal(a, b); 28 | } 29 | 30 | [Fact] 31 | public void Same_hashes_generate_equal_streams() 32 | { 33 | var hash1 = Enumerable.Range(0, 20).Select(i => (byte)i).ToArray(); 34 | var hash2 = Enumerable.Range(0, 20).Select(i => (byte)i).ToArray(); 35 | 36 | var a = new Stream(hash1); 37 | var b = new Stream(hash2); 38 | 39 | Assert.Equal(a, b); 40 | } 41 | 42 | [Fact] 43 | public void Different_names_generate_different_hashes() 44 | { 45 | var a = new Stream("abc"); 46 | var b = new Stream("xyz"); 47 | 48 | Assert.NotEqual(a, b); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Test/Even.Tests/TestConfig.hocon: -------------------------------------------------------------------------------- 1 | akka { 2 | test.single-expect-default = 3s 3 | log-config-on-start = off 4 | stdout-loglevel = INFO 5 | loglevel = ERROR 6 | actor { 7 | debug { 8 | receive = on 9 | autoreceive = on 10 | lifecycle = on 11 | event-stream = on 12 | unhandled = on 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /Test/Even.Tests/TimeoutTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using Xunit; 9 | 10 | namespace Even.Tests 11 | { 12 | public class TimeoutTests 13 | { 14 | [Theory] 15 | [InlineData(100)] 16 | [InlineData(250)] 17 | [InlineData(500)] 18 | [InlineData(1000)] 19 | [Trait("Category","Unstable")] 20 | public void Timeout_does_not_expires_before_time(int milliseconds) 21 | { 22 | var timeout = Timeout.In(TimeSpan.FromMilliseconds(milliseconds)); 23 | 24 | Thread.Sleep(milliseconds - 10); 25 | 26 | Assert.False(timeout.IsExpired); 27 | } 28 | 29 | [Theory] 30 | [InlineData(100)] 31 | [InlineData(250)] 32 | [InlineData(500)] 33 | [InlineData(1000)] 34 | [Trait("Category", "Unstable")] 35 | public void Timeout_is_expired_after_expected_time(int milliseconds) 36 | { 37 | var timeout = Timeout.In(TimeSpan.FromMilliseconds(milliseconds)); 38 | 39 | Thread.Sleep(milliseconds + 1); 40 | 41 | Assert.True(timeout.IsExpired); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Test/Even.Tests/Utils/EnumerableExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | using Even.Utils; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using Xunit; 8 | 9 | namespace Even.Tests.Utils 10 | { 11 | public class EnumerableExtensionsTests 12 | { 13 | [Fact] 14 | public void IsSequential_returns_false_for_empty_enumerables() 15 | { 16 | Assert.False(new int[0].IsSequential()); 17 | } 18 | 19 | public static IEnumerable SequentialValues() 20 | { 21 | yield return new object[] { new [] { 0 } }; 22 | yield return new object[] { new [] { 1000 } }; 23 | yield return new object[] { new [] { 0, 1 } }; 24 | yield return new object[] { new [] { 0, 1, 2, 3, 4, 5 } }; 25 | yield return new object[] { new [] { 1000, 1001, 1002, 1003 } }; 26 | yield return new object[] { new [] { Int32.MinValue, Int32.MinValue + 1, Int32.MinValue + 2 } }; 27 | yield return new object[] { new [] { Int32.MaxValue - 2, Int32.MaxValue - 1, Int32.MaxValue } }; 28 | yield return new object[] { new [] { -10, -9, -8, -7 } }; 29 | yield return new object[] { new [] { -2, -1, 0, 1, 2 } }; 30 | } 31 | 32 | [Theory] 33 | [MemberData("SequentialValues")] 34 | public void IsSequential_returns_true(int[] values) 35 | { 36 | Assert.True(values.IsSequential()); 37 | } 38 | 39 | public static IEnumerable NonSequentialValues() 40 | { 41 | yield return new object[] { new [] { 1, 2, 3, 5, 6, 7 } }; 42 | yield return new object[] { new [] { 1000, 1001, 1002, 1030 } }; 43 | yield return new object[] { new [] { Int32.MinValue, Int32.MaxValue } }; 44 | yield return new object[] { new [] { 1, 0, -1 } }; 45 | } 46 | 47 | [Theory] 48 | [MemberData("NonSequentialValues")] 49 | public void IsSequential_returns_false(int[] values) 50 | { 51 | Assert.False(values.IsSequential()); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Test/Even.Tests/Utils/ProbeRelay.cs: -------------------------------------------------------------------------------- 1 | using Akka.Actor; 2 | using Akka.TestKit; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace Even.Tests.Utils 10 | { 11 | public class ProbeRelay : ReceiveActor 12 | { 13 | public ProbeRelay(IActorRef target) 14 | { 15 | ReceiveAny(o => target.Forward(o)); 16 | } 17 | } 18 | 19 | public static class ProbeRelayExtensions 20 | { 21 | public static ProbeRelayObjects CreateTestRelay(this TestKitBase testKit) 22 | { 23 | var probe = testKit.CreateTestProbe(); 24 | var props = Props.Create(probe); 25 | 26 | return new ProbeRelayObjects 27 | { 28 | Props = props, 29 | Probe = probe 30 | }; 31 | } 32 | } 33 | 34 | public class ProbeRelayObjects 35 | { 36 | public Props Props { get; set; } 37 | public TestProbe Probe { get; set; } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Test/Even.Tests/app.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /Test/Even.Tests/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "Akka.TestKit.Xunit2": "1.1.1", 4 | "MySql.Data": "7.0.2-DMR", 5 | "NSubstitute": "1.10.0", 6 | "System.Data.SQLite.Core": "1.0.102", 7 | "xunit": "2.1.0", 8 | "xunit.runner.visualstudio": "2.1.0" 9 | }, 10 | "frameworks": { 11 | "net451": {} 12 | }, 13 | "runtimes": { 14 | "win": "" 15 | } 16 | } -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | init: 2 | - git config --global core.autocrlf input 3 | build_script: 4 | - ps: .\build.ps1 -Target BuildAndPackage 5 | test: off 6 | artifacts: 7 | - path: '.build\**\testResults\*Test*.xml' 8 | name: testResults 9 | 10 | - path: '.build\**\*.nupkg' 11 | name: packages 12 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "projects": [ "src", "test" ], 3 | "sdk": { 4 | "version": "1.0.0-rc1-final" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /nuget.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /tools/packages.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | --------------------------------------------------------------------------------