├── .gitignore ├── LICENSE ├── README.md ├── appveyor.yml └── src ├── .gitattributes ├── .gitignore ├── DominoEventStore ├── AMapFromEventDataToObject.cs ├── ARewriteEvent.cs ├── AppendResult.cs ├── BatchOperation.cs ├── Commit.cs ├── CommittedEvents.cs ├── ConcurrencyException.cs ├── DominoEventStore.csproj ├── DuplicateCommitException.cs ├── EntityEvents.cs ├── EntityStreamData.cs ├── EventStore.cs ├── EventStoreSettings.cs ├── EventsRewriter.cs ├── IAdvancedFeatures.cs ├── IConfigMigration.cs ├── IConfigReadModelGeneration.cs ├── IConfigureEventStore.cs ├── IConfigureQuery.cs ├── IConfigureQueryByDate.cs ├── IMapEventDataToObject.cs ├── IMapEventVersionAction.cs ├── IMapEventVersionCondition.cs ├── IRewriteEventData.cs ├── ISpecificDbStorage.cs ├── IStoreBatchProgress.cs ├── IStoreEvents.cs ├── IStoreUnitOfWork.cs ├── IWorkWithSnapshots.cs ├── JsonedEvent.cs ├── MigrationConfig.cs ├── ProcessedCommitsCount.cs ├── ProviderExtensions.cs ├── Providers │ ├── ASqlDbProvider.cs │ ├── BatchProgress.cs │ ├── InMemory.cs │ ├── SqlServerProvider.cs │ └── SqliteProvider.cs ├── QueryConfig.cs ├── ReadModelGenerationConfig.cs ├── Snapshot.cs ├── SomeData.cs ├── SomeDataMapper.cs ├── StoreFacade.cs ├── UnversionedCommit.cs └── Utils.cs ├── Tests ├── BatchOperationsTests.cs ├── Event1.cs ├── Event2.cs ├── GetEventsAndSnapshotTests.cs ├── IntegrationTests.cs ├── JsonStuff.cs ├── MigratingEventsFacadeStoreTests.cs ├── Providers │ ├── ASpecificStorageTests.cs │ ├── InMemoryStoreTests.cs │ ├── SqlServerTests.cs │ └── SqliteTests.cs ├── ReadModelGenFacadeTests.cs ├── RewriteEvent1.cs ├── Setup.cs ├── SomeEvent.cs ├── SomeMemento.cs ├── Tests.csproj └── UpcastEvent1.cs └── domino.sln /.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 | *.userosscache 8 | *.sln.docstates 9 | 10 | ======= 11 | # User-specific files (MonoDevelop/Xamarin Studio) 12 | *.userprefs 13 | 14 | # Build results 15 | [Dd]ebug/ 16 | [Dd]ebugPublic/ 17 | [Rr]elease/ 18 | [Rr]eleases/ 19 | x64/ 20 | x86/ 21 | bld/ 22 | [Bb]in/ 23 | [Oo]bj/ 24 | temp/ 25 | 26 | .idea/ 27 | 28 | project.lock.json 29 | 30 | artifacts 31 | 32 | 33 | # Roslyn cache directories 34 | *.ide/ 35 | ======= 36 | 37 | build/tools 38 | build/.fake 39 | 40 | 41 | 42 | !nuget.exe 43 | 44 | bld/ 45 | [Bb]in/ 46 | [Oo]bj/ 47 | 48 | # Visual Studo 2015 cache/options directory 49 | .vs/ 50 | 51 | 52 | # MSTest test Results 53 | [Tt]est[Rr]esult*/ 54 | [Bb]uild[Ll]og.* 55 | 56 | 57 | *.VisualState.xml 58 | TestResult.xml 59 | 60 | # Build Results of an ATL Project 61 | [Dd]ebugPS/ 62 | [Rr]eleasePS/ 63 | dlldata.c 64 | 65 | *_i.c 66 | *_p.c 67 | *_i.h 68 | *.ilk 69 | *.meta 70 | *.obj 71 | *.pch 72 | *.pdb 73 | *.pgc 74 | *.pgd 75 | *.rsp 76 | *.sbr 77 | *.tlb 78 | *.tli 79 | *.tlh 80 | *.tmp 81 | *.tmp_proj 82 | *.log 83 | *.vspscc 84 | *.vssscc 85 | .builds 86 | *.pidb 87 | *.svclog 88 | *.scc 89 | 90 | # Chutzpah Test files 91 | _Chutzpah* 92 | 93 | # Visual C++ cache files 94 | ipch/ 95 | *.aps 96 | *.ncb 97 | *.opensdf 98 | *.sdf 99 | *.cachefile 100 | 101 | # Visual Studio profiler 102 | *.psess 103 | *.vsp 104 | *.vspx 105 | 106 | # TFS 2012 Local Workspace 107 | $tf/ 108 | 109 | # Guidance Automation Toolkit 110 | *.gpState 111 | 112 | # ReSharper is a .NET coding add-in 113 | _ReSharper*/ 114 | *.[Rr]e[Ss]harper 115 | *.DotSettings.user 116 | 117 | # JustCode is a .NET coding addin-in 118 | .JustCode 119 | 120 | # TeamCity is a build add-in 121 | _TeamCity* 122 | 123 | # DotCover is a Code Coverage Tool 124 | *.dotCover 125 | 126 | # NCrunch 127 | _NCrunch_* 128 | .*crunch*.local.xml 129 | 130 | # MightyMoose 131 | *.mm.* 132 | AutoTest.Net/ 133 | 134 | # Web workbench (sass) 135 | .sass-cache/ 136 | 137 | # Installshield output folder 138 | [Ee]xpress/ 139 | 140 | # DocProject is a documentation generator add-in 141 | DocProject/buildhelp/ 142 | DocProject/Help/*.HxT 143 | DocProject/Help/*.HxC 144 | DocProject/Help/*.hhc 145 | DocProject/Help/*.hhk 146 | DocProject/Help/*.hhp 147 | DocProject/Help/Html2 148 | DocProject/Help/html 149 | 150 | # Click-Once directory 151 | publish/ 152 | 153 | # Publish Web Output 154 | *.[Pp]ublish.xml 155 | *.azurePubxml 156 | # TODO: Comment the next line if you want to checkin your web deploy settings 157 | # but database connection strings (with potential passwords) will be unencrypted 158 | *.pubxml 159 | *.publishproj 160 | 161 | # NuGet Packages 162 | *.nupkg 163 | # The packages folder can be ignored because of Package Restore 164 | **/packages/* 165 | # except build/, which is used as an MSBuild target. 166 | !**/packages/build/ 167 | 168 | #!**/packages/repositories.config 169 | 170 | # Windows Azure Build Output 171 | csx/ 172 | *.build.csdef 173 | 174 | # Windows Store app package directory 175 | AppPackages/ 176 | 177 | # Others 178 | 179 | sql/ 180 | *.Cache 181 | ======= 182 | *.[Cc]ache 183 | 184 | ClientBin/ 185 | [Ss]tyle[Cc]op.* 186 | ~$* 187 | *~ 188 | *.dbmdl 189 | *.dbproj.schemaview 190 | *.pfx 191 | *.publishsettings 192 | node_modules/ 193 | 194 | 195 | # RIA/Silverlight projects 196 | Generated_Code/ 197 | 198 | # Backup & report files from converting an old project file 199 | # to a newer Visual Studio version. Backup files are not needed, 200 | # because we have git ;-) 201 | _UpgradeReport_Files/ 202 | Backup*/ 203 | UpgradeLog*.XML 204 | UpgradeLog*.htm 205 | 206 | # SQL Server files 207 | *.mdf 208 | *.ldf 209 | 210 | # Business Intelligence projects 211 | *.rdl.data 212 | *.bim.layout 213 | *.bim_*.settings 214 | 215 | # Microsoft Fakes 216 | FakesAssemblies/ 217 | 218 | 219 | # Thumbnails 220 | ._* 221 | 222 | # Files that might appear on external disk 223 | .Spotlight-V100 224 | .Trashes 225 | 226 | # Directories potentially created on remote AFP share 227 | .AppleDB 228 | .AppleDesktop 229 | Network Trash Folder 230 | Temporary Items 231 | .apdisk 232 | 233 | # Windows 234 | # ========================= 235 | 236 | # Windows image file caches 237 | Thumbs.db 238 | ehthumbs.db 239 | 240 | # Folder config file 241 | Desktop.ini 242 | 243 | # Recycle Bin used on file shares 244 | $RECYCLE.BIN/ 245 | 246 | # Windows Installer files 247 | *.cab 248 | *.msi 249 | *.msm 250 | *.msp 251 | 252 | # Windows shortcuts 253 | *.lnk 254 | ======= 255 | # Node.js Tools for Visual Studio 256 | .ntvs_analysis.dat 257 | 258 | # Visual Studio 6 build log 259 | *.plg 260 | 261 | # Visual Studio 6 workspace options file 262 | *.opt 263 | 264 | src/CavemanTools/PortabilityAnalysis.html 265 | 266 | 267 | /.vscode 268 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Domino Event Store 2 | 3 | [![Appveyor stat](https://ci.appveyor.com/api/projects/status/github/sapiens/dominoeventstore?svg=true)](https://ci.appveyor.com/project/sapiens/dominoeventstore) [![NuGet](https://img.shields.io/nuget/v/DominoES.svg)](https://www.nuget.org/packages/DominoES) 4 | 5 | 6 | An alternative to the good but somewhat outdated [NEventStore](https://github.com/NEventStore/NEventStore/wiki/Quick-Start), DominoES is inspired by Greg Young's [excellent book](https://leanpub.com/esversioning/read#leanpub-auto-weak-schema) and it's designed to be easy to use, lightweight and versatile. 7 | 8 | It sits on top of an existing RDBMS (SqlServer, Sqlite), providing an event store for small to medium applications that are using Event Sourcing and don't want to use a separated, dedicated Event Store solution. Great to get you started with ES as it's easy to setup (just Nuget-it) and it takes at most 5 minutes to learn how to use it. 9 | 10 | ## Features 11 | 12 | * Domino ES is a netstandard 2.0 library, therefore it runs on multiple platforms such as .Net Core and .Net Framework 4.6+ 13 | * Multi-tenant support 14 | * Bulk read model generation assistance 15 | * Easy migrations and event data rewriting support (for those special cases) 16 | * Easy to setup/use 17 | * Great for small to medium non-distributed apps. 18 | 19 | ## Beta Breaking Changes! 20 | 21 | 1.0.0-beta8 -> 1.0.0-beta9 22 | 23 | * Changed how Snapshot data is serialiazed 24 | * Due to using a different json lib, currently stored events might not deserialize properly 25 | * Renamed the `Complete` method to `Commit` 26 | * Netstandard 2.0 27 | 28 | ## Usage 29 | 30 | Configuration is done inside the `Build` function. At a minimum you need to specify what db provider to use. DominoES uses Serilog for its logging, but you still have to specify sinks. 31 | 32 | ```csharp 33 | //create and configure the event store singleton . Add it as singleton to your favourite DI Container 34 | var eventStore=EventStore.WithLogger(/* Serilog instance */).Build(c => 35 | { 36 | c.UseMSSql(SqlClientFactory.Instance.CreateConnection,ConnectionString); 37 | //or 38 | c.UseSqlite(SQLiteFactory.Instance.CreateConnection,ConnectionString); 39 | } 40 | ); 41 | 42 | 43 | //use it in your app services 44 | 45 | //add events 46 | await _store.Append(myEntityId,commitId,myEvents); 47 | 48 | //commit events from more than one aggregate 49 | using (var t=_store.StartCommit(commitId)) 50 | { 51 | t.Append(entity1,events1); 52 | t.Append(entity2,events2); 53 | await t.Complete().ConfigureFalse(); 54 | } 55 | 56 | //query events 57 | var evs=await _store.GetEvents(myEntityId); 58 | 59 | //advanced query 60 | var evs=await _store.GetEvents(q=>q.WithCommitDate.OlderThan(myDate).IncludeSnapshots(false).OfEntity(myEntityId).FromBeginningUntilVersion(someAggregateVersion)); 61 | 62 | ``` 63 | 64 | ## Advanced Usage 65 | 66 | ## When domain events change 67 | 68 | A normal occurrence in any domain, some business events do change over time. It's important to identify first if we're dealing with the same event with slightly changed structure, or the business semantics have changed. In case of the latter, just create a new specific event. 69 | 70 | In case of the former, the traditional approach is to up-cast existing events at the app level, which introduces a new layer of complexity inside the app. Another method is to map directly the old event data to the new event. DominoES is designed around this technique, allowing you to keep the codebase maintainable. Focusing on data, instead of the event itself,it allows further advanced and edge scenarios, such as migrating an old event store. 71 | 72 | ```csharp 73 | 74 | //define mapper, it will be treated as a singleton 75 | class MyMapper : AMapFromEventDataToObject 76 | { 77 | public override SomeEvent Map(dynamic oldData, SomeEvent newEvent, DateTimeOffset commitDate) 78 | { 79 | //change values of newEvent 80 | return newEvent; 81 | } 82 | } 83 | 84 | //register it when configuring DominoES 85 | 86 | EventStore.Build(c => 87 | { 88 | c.UseMSSql(SqlClientFactory.Instance.CreateConnection, SqlServerTests.ConnectionString); 89 | c.AddMapper(new MyMapper()); 90 | }); 91 | 92 | ``` 93 | 94 | ## Read model generation 95 | 96 | Sometimes you need to (re)generate read models from existing events. DominoES makes your life easier in this regard. I suggest to have a `ReadModelUpdater` kind of class consisting of `Handle(event)` methods. 97 | 98 | ```csharp 99 | 100 | public class ReadModelUpdater 101 | { 102 | 103 | public void Handle(TransactionDeleted ev) 104 | { 105 | } 106 | 107 | public void Handle(CashFlowEntryUndone ev) 108 | { 109 | 110 | } 111 | 112 | /** etc **/ 113 | } 114 | 115 | var updater=new ReadModelUpdater(); 116 | _store.Advanced.GenerateReadModel("balanceSheet",e=> updater.Handle(e)); 117 | 118 | ``` 119 | 120 | ## Store migration 121 | 122 | As your app evolves, at one point you might need to move the existing events. One useful but **dangerous** (but _very handy_) feature is support for event rewriting. No, I don't mean changing the past, I mean rewriting the event data for technical or legal reasons. It's an edge case, a feature that shouldn't be used lightly, but nevertheless, if you need it, it will make your life easier. 123 | 124 | ```csharp 125 | 126 | //moving from sqlite to sql server 127 | _dest = EventStore.Build(c => 128 | { 129 | c.UseMSSql(SqlClientFactory.Instance.CreateConnection, SqlServerTests.ConnectionString); 130 | }); 131 | 132 | 133 | _src = EventStore.Build(c => 134 | c.UseSqlite(SQLiteFactory.Instance.CreateConnection, SqliteTests.ConnectionString)); 135 | 136 | //optional event rewriter 137 | public class RewriteEvent : ARewriteEvent 138 | { 139 | public override Event1 Rewrite(dynamic jsonData, Event1 deserializedEvent, DateTimeOffset commitDate) 140 | { 141 | deserializedEvent.Nr += 60; 142 | return deserializedEvent; 143 | } 144 | } 145 | 146 | //do migration 147 | _src.Advanced.MigrateEventsTo(_dest, "migration",c=>c.AddConverters(new RewriteEvent())); 148 | 149 | ``` 150 | 151 | **Note**: Both model generation and store migration are long running processes. These features shouldn't be used inside your main app. Create a console app or cloud job (Azure function, AWS lambda etc) to use them. 152 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: 1.0.0 2 | 3 | image: Visual Studio 2017 4 | 5 | configuration: Release 6 | 7 | assembly_info: 8 | 9 | patch: true 10 | 11 | file: '**\AssemblyInfo.*' 12 | 13 | assembly_version: '{version}' 14 | 15 | assembly_file_version: '{version}' 16 | 17 | assembly_informational_version: '{version}' 18 | 19 | dotnet_csproj: 20 | 21 | patch: true 22 | 23 | file: '**\*.csproj' 24 | 25 | version: '{version}' 26 | 27 | package_version: '{version}' 28 | 29 | skip_commits: 30 | files: 31 | - appveyor* 32 | - '*gitignore' 33 | - '**/*.md' 34 | 35 | services: mssql2016 36 | 37 | before_build: 38 | 39 | - cmd: dotnet restore src\ 40 | 41 | build: 42 | 43 | project: src\DominoEventStore\DominoEventStore.csproj 44 | 45 | verbosity: quiet 46 | 47 | test_script: 48 | 49 | - cmd: >- 50 | 51 | cd src/tests 52 | 53 | dotnet test 54 | 55 | artifacts: 56 | 57 | - path: '**\Release\**\*.nupkg' 58 | 59 | name: nuget 60 | 61 | deploy: 62 | 63 | - provider: NuGet 64 | 65 | api_key: 66 | 67 | secure: zSSdZy/TonHwA3Ltpb2LbBy5zD8+YfYFBayFHPSlkUwR2bj5L9/1MSpMVCqPCCwj 68 | -------------------------------------------------------------------------------- /src/.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /src/.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 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | project.fragment.lock.json 46 | artifacts/ 47 | 48 | *_i.c 49 | *_p.c 50 | *_i.h 51 | *.ilk 52 | *.meta 53 | *.obj 54 | *.pch 55 | *.pdb 56 | *.pgc 57 | *.pgd 58 | *.rsp 59 | *.sbr 60 | *.tlb 61 | *.tli 62 | *.tlh 63 | *.tmp 64 | *.tmp_proj 65 | *.log 66 | *.vspscc 67 | *.vssscc 68 | .builds 69 | *.pidb 70 | *.svclog 71 | *.scc 72 | 73 | # Chutzpah Test files 74 | _Chutzpah* 75 | 76 | # Visual C++ cache files 77 | ipch/ 78 | *.aps 79 | *.ncb 80 | *.opendb 81 | *.opensdf 82 | *.sdf 83 | *.cachefile 84 | *.VC.db 85 | *.VC.VC.opendb 86 | 87 | # Visual Studio profiler 88 | *.psess 89 | *.vsp 90 | *.vspx 91 | *.sap 92 | 93 | # TFS 2012 Local Workspace 94 | $tf/ 95 | 96 | # Guidance Automation Toolkit 97 | *.gpState 98 | 99 | # ReSharper is a .NET coding add-in 100 | _ReSharper*/ 101 | *.[Rr]e[Ss]harper 102 | *.DotSettings.user 103 | 104 | # JustCode is a .NET coding add-in 105 | .JustCode 106 | 107 | # TeamCity is a build add-in 108 | _TeamCity* 109 | 110 | # DotCover is a Code Coverage Tool 111 | *.dotCover 112 | 113 | # NCrunch 114 | _NCrunch_* 115 | .*crunch*.local.xml 116 | nCrunchTemp_* 117 | 118 | # MightyMoose 119 | *.mm.* 120 | AutoTest.Net/ 121 | 122 | # Web workbench (sass) 123 | .sass-cache/ 124 | 125 | # Installshield output folder 126 | [Ee]xpress/ 127 | 128 | # DocProject is a documentation generator add-in 129 | DocProject/buildhelp/ 130 | DocProject/Help/*.HxT 131 | DocProject/Help/*.HxC 132 | DocProject/Help/*.hhc 133 | DocProject/Help/*.hhk 134 | DocProject/Help/*.hhp 135 | DocProject/Help/Html2 136 | DocProject/Help/html 137 | 138 | # Click-Once directory 139 | publish/ 140 | 141 | # Publish Web Output 142 | *.[Pp]ublish.xml 143 | *.azurePubxml 144 | # TODO: Comment the next line if you want to checkin your web deploy settings 145 | # but database connection strings (with potential passwords) will be unencrypted 146 | #*.pubxml 147 | *.publishproj 148 | 149 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 150 | # checkin your Azure Web App publish settings, but sensitive information contained 151 | # in these scripts will be unencrypted 152 | PublishScripts/ 153 | 154 | # NuGet Packages 155 | *.nupkg 156 | # The packages folder can be ignored because of Package Restore 157 | **/packages/* 158 | # except build/, which is used as an MSBuild target. 159 | !**/packages/build/ 160 | # Uncomment if necessary however generally it will be regenerated when needed 161 | #!**/packages/repositories.config 162 | # NuGet v3's project.json files produces more ignoreable files 163 | *.nuget.props 164 | *.nuget.targets 165 | 166 | # Microsoft Azure Build Output 167 | csx/ 168 | *.build.csdef 169 | 170 | # Microsoft Azure Emulator 171 | ecf/ 172 | rcf/ 173 | 174 | # Windows Store app package directories and files 175 | AppPackages/ 176 | BundleArtifacts/ 177 | Package.StoreAssociation.xml 178 | _pkginfo.txt 179 | 180 | # Visual Studio cache files 181 | # files ending in .cache can be ignored 182 | *.[Cc]ache 183 | # but keep track of directories ending in .cache 184 | !*.[Cc]ache/ 185 | 186 | # Others 187 | ClientBin/ 188 | ~$* 189 | *~ 190 | *.dbmdl 191 | *.dbproj.schemaview 192 | *.jfm 193 | *.pfx 194 | *.publishsettings 195 | node_modules/ 196 | orleans.codegen.cs 197 | 198 | # Since there are multiple workflows, uncomment next line to ignore bower_components 199 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 200 | #bower_components/ 201 | 202 | # RIA/Silverlight projects 203 | Generated_Code/ 204 | 205 | # Backup & report files from converting an old project file 206 | # to a newer Visual Studio version. Backup files are not needed, 207 | # because we have git ;-) 208 | _UpgradeReport_Files/ 209 | Backup*/ 210 | UpgradeLog*.XML 211 | UpgradeLog*.htm 212 | 213 | # SQL Server files 214 | *.mdf 215 | *.ldf 216 | 217 | # Business Intelligence projects 218 | *.rdl.data 219 | *.bim.layout 220 | *.bim_*.settings 221 | 222 | # Microsoft Fakes 223 | FakesAssemblies/ 224 | 225 | # GhostDoc plugin setting file 226 | *.GhostDoc.xml 227 | 228 | # Node.js Tools for Visual Studio 229 | .ntvs_analysis.dat 230 | 231 | # Visual Studio 6 build log 232 | *.plg 233 | 234 | # Visual Studio 6 workspace options file 235 | *.opt 236 | 237 | # Visual Studio LightSwitch build output 238 | **/*.HTMLClient/GeneratedArtifacts 239 | **/*.DesktopClient/GeneratedArtifacts 240 | **/*.DesktopClient/ModelManifest.xml 241 | **/*.Server/GeneratedArtifacts 242 | **/*.Server/ModelManifest.xml 243 | _Pvt_Extensions 244 | 245 | # Paket dependency manager 246 | .paket/paket.exe 247 | paket-files/ 248 | 249 | # FAKE - F# Make 250 | .fake/ 251 | 252 | # JetBrains Rider 253 | .idea/ 254 | *.sln.iml 255 | 256 | # CodeRush 257 | .cr/ 258 | 259 | # Python Tools for Visual Studio (PTVS) 260 | __pycache__/ 261 | *.pyc -------------------------------------------------------------------------------- /src/DominoEventStore/AMapFromEventDataToObject.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace DominoEventStore 5 | { 6 | public abstract class AMapFromEventDataToObject : IMapEventDataToObject where T:class 7 | { 8 | public bool Handles(Type type)=> typeof(T) == type; 9 | 10 | public object Map(IDictionary existingData, object deserializedEvent, DateTimeOffset commitDate) 11 | => Map(existingData, deserializedEvent as T, commitDate); 12 | 13 | /// 14 | /// When the event structure changes, this tells EventStore how to treat the old data. 15 | /// By default, it's just deserialized to the specified event type, ignoring fields that don't match 16 | /// 17 | /// Stored event data as expando 18 | /// Event with values automatically deserialized from the old data 19 | /// 20 | /// 21 | public abstract T Map(IDictionary existingData, T deserializedEvent, DateTimeOffset commitDate); 22 | } 23 | } -------------------------------------------------------------------------------- /src/DominoEventStore/ARewriteEvent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DominoEventStore 4 | { 5 | public abstract class ARewriteEvent : IRewriteEventData where T:class 6 | { 7 | protected ARewriteEvent() 8 | { 9 | HandledType = typeof(T); 10 | } 11 | public Type HandledType { get; } 12 | 13 | public object Rewrite(dynamic jsonData, object deserializedEvent, DateTimeOffset commitDate) 14 | => Rewrite(jsonData, deserializedEvent as T, commitDate); 15 | 16 | public abstract T Rewrite(dynamic jsonData, T deserializedEvent, DateTimeOffset commitDate); 17 | } 18 | } -------------------------------------------------------------------------------- /src/DominoEventStore/AppendResult.cs: -------------------------------------------------------------------------------- 1 | namespace DominoEventStore 2 | { 3 | public class AppendResult 4 | { 5 | public static readonly AppendResult Ok=new AppendResult(); 6 | 7 | private AppendResult() 8 | { 9 | WasSuccessful = true; 10 | } 11 | 12 | public bool WasSuccessful { get; } 13 | 14 | public AppendResult(Commit commit) 15 | { 16 | DuplicateCommit = commit; 17 | } 18 | 19 | public Commit DuplicateCommit { get; } 20 | } 21 | } -------------------------------------------------------------------------------- /src/DominoEventStore/BatchOperation.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DominoEventStore 4 | { 5 | public class BatchOperation : IDisposable 6 | { 7 | private readonly IStoreBatchProgress _store; 8 | private readonly dynamic _config; 9 | private ProcessedCommitsCount _processed; 10 | 11 | public BatchOperation(IStoreBatchProgress store, ReadModelGenerationConfig config) 12 | { 13 | _store = store; 14 | _config = config; 15 | _processed = _store.StartOrContinue(config.Name); 16 | EventStore.Logger.Debug($"'{config.Name}' [read model generation] starts/resumes after {_processed.Value} commits"); 17 | } 18 | public BatchOperation(IStoreBatchProgress store, MigrationConfig config) 19 | { 20 | _store = store; 21 | _config = config; 22 | _processed = _store.StartOrContinue(config.Name); 23 | EventStore.Logger.Debug($"'{config.Name}' [migration] starts/resumes after {_processed.Value} commits"); 24 | } 25 | 26 | private bool _hasEnded = false; 27 | private CommittedEvents _commits; 28 | 29 | /// 30 | /// Commits should be ordered ascending by commit date 31 | /// 32 | 33 | /// 34 | public Optional GetNextCommit() 35 | { 36 | start: 37 | if (_commits == null) 38 | { 39 | EventStore.Logger.Debug($"Getting next batch of '{_config.Name}'"); 40 | _commits= _store.GetNextBatch(_config, _processed); 41 | } 42 | if (_commits.IsEmpty) 43 | { 44 | EventStore.Logger.Debug($"No more batches for '{_config.Name}'"); 45 | _hasEnded = true; 46 | return Optional.Empty; 47 | } 48 | 49 | var next= _commits.GetNext(); 50 | if (next.IsEmpty) 51 | { 52 | _commits = null; 53 | goto start; 54 | } 55 | _processed++; 56 | EventStore.Logger.Debug($"{_processed.Value} commits processed as part of '{_config.Name}'"); 57 | return next; 58 | } 59 | 60 | private bool _disposed = false; 61 | public void Dispose() 62 | { 63 | if (_disposed) return; 64 | _disposed = true; 65 | if (_hasEnded) 66 | { 67 | _store.MarkOperationAsEnded(_config.Name); 68 | EventStore.Logger.Debug($"'{_config.Name}' ended"); 69 | return; 70 | } 71 | if(_processed==0) return; 72 | _store.UpdateProgress(_config.Name,_processed.Value-1); 73 | EventStore.Logger.Debug($"'{_config.Name}' saved progress at {_processed.Value-1}"); 74 | } 75 | } 76 | } -------------------------------------------------------------------------------- /src/DominoEventStore/Commit.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DominoEventStore 4 | { 5 | public class Commit:UnversionedCommit 6 | { 7 | protected Commit() 8 | { 9 | 10 | } 11 | 12 | public Commit(int version,UnversionedCommit comm):this(comm.TenantId,comm.EntityId,comm.EventData,comm.CommitId,comm.Timestamp,version) 13 | { 14 | 15 | } 16 | 17 | public Commit(string tenantId, Guid entityId, string eventData, Guid commitId, DateTimeOffset timestamp,int version) : base(tenantId, entityId, eventData, commitId, timestamp) 18 | { 19 | Version = version; 20 | } 21 | 22 | /// 23 | /// Entity version 24 | /// 25 | public int Version { get; private set; } 26 | 27 | } 28 | } -------------------------------------------------------------------------------- /src/DominoEventStore/CommittedEvents.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | 5 | namespace DominoEventStore 6 | { 7 | public class CommittedEvents //: IReadOnlyCollection 8 | { 9 | private readonly Commit[] _commits; 10 | 11 | public Commit this[int i] => _commits[i]; 12 | public CommittedEvents(Commit[] commits) 13 | { 14 | commits.MustNotBeNull(); 15 | _commits = commits; 16 | IsEmpty = commits.Length == 0; 17 | } 18 | 19 | 20 | private int _i = -1; 21 | public bool IsEmpty { get; } 22 | 23 | public Optional GetNext() 24 | { 25 | _i++; 26 | if (_commits.Length<=_i) return Optional.Empty; 27 | return new Optional(_commits[_i]); 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /src/DominoEventStore/ConcurrencyException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DominoEventStore 4 | { 5 | public class ConcurrencyException : Exception 6 | { 7 | 8 | } 9 | } -------------------------------------------------------------------------------- /src/DominoEventStore/DominoEventStore.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netstandard2.0 4 | true 5 | 1.0.0 6 | DominoES 7 | Mihai Mogosanu 8 | https://github.com/sapiens/DominoEventStore/blob/master/LICENSE 9 | https://github.com/sapiens/DominoEventStore 10 | https://github.com/sapiens/DominoEventStore 11 | An alternative to the good but somewhat outdated NEventStore, DominoES is designed to be easy to use, lightweight and versatile. 12 | Featuring: .Net Core support, Multi-tenant support, Bulk read model generation assistance, Easy migrations and event data rewriting support (for those special cases) , Easy to setup/use, Great for small to medium non-distributed apps. 13 | eventsourcing, cqrs, event store 14 | 15 | 16 | bin\Release\netstandard2.0\DominoEventStore.xml 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/DominoEventStore/DuplicateCommitException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace DominoEventStore 5 | { 6 | public class DuplicateCommitException : Exception 7 | { 8 | //public Guid CommitId { get; } 9 | 10 | //public IReadOnlyCollection Events { get; } 11 | 12 | //public DuplicateCommitException(Guid commitId, IEnumerable events) 13 | //{ 14 | // CommitId = commitId; 15 | // Events = new List(events); 16 | //} 17 | } 18 | } -------------------------------------------------------------------------------- /src/DominoEventStore/EntityEvents.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | 5 | namespace DominoEventStore 6 | { 7 | public class EntityEvents:IReadOnlyCollection 8 | { 9 | private readonly IReadOnlyCollection _events; 10 | public Optional LatestSnapshot { get; }=Optional.Empty; 11 | 12 | public static readonly EntityEvents Empty=new EntityEvents(); 13 | 14 | private EntityEvents() 15 | { 16 | _events=new object[0]; 17 | Version = 0; 18 | } 19 | 20 | public EntityEvents(IReadOnlyCollection events,int version,Optional latestSnapshot) 21 | { 22 | _events = events; 23 | LatestSnapshot = latestSnapshot; 24 | Version = version; 25 | } 26 | 27 | public IEnumerator GetEnumerator() 28 | => _events.GetEnumerator(); 29 | 30 | IEnumerator IEnumerable.GetEnumerator() 31 | { 32 | return GetEnumerator(); 33 | } 34 | 35 | public int Count => _events.Count; 36 | 37 | /// 38 | /// Latest commit version 39 | /// 40 | public int Version { get; } 41 | } 42 | } -------------------------------------------------------------------------------- /src/DominoEventStore/EntityStreamData.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace DominoEventStore 6 | { 7 | public class EntityStreamData 8 | { 9 | public Optional LatestSnapshot { get; set; } 10 | public IEnumerable Commits { get; set; }=Enumerable.Empty(); 11 | 12 | public Optional ToOptional() => LatestSnapshot.IsEmpty && Commits.IsNullOrEmpty() 13 | ? Optional.Empty 14 | : new Optional(this); 15 | } 16 | } -------------------------------------------------------------------------------- /src/DominoEventStore/EventStore.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Serilog; 3 | 4 | namespace DominoEventStore 5 | { 6 | public class EventStore 7 | { 8 | public const string DefaultTenant = "_"; 9 | 10 | public static EventStore WithLogger(ILogger logger) 11 | { 12 | logger.MustNotBeNull(); 13 | Logger = logger.ForContext(); 14 | return new EventStore(); 15 | } 16 | 17 | [Obsolete("Will be removed in the next iteration. Use the other overload",false)] 18 | public static EventStore WithLogger(Action cfg, LoggerConfiguration existing = null) 19 | { 20 | existing=existing??new LoggerConfiguration(); 21 | existing.MinimumLevel.Debug(); 22 | cfg(existing); 23 | var es=new EventStore(); 24 | Logger = existing.CreateLogger().ForContext(); 25 | return es; 26 | } 27 | 28 | public static ILogger Logger { get; private set; } 29 | 30 | public IStoreEvents Build(Action cfg) 31 | { 32 | cfg.MustNotBeDefault(); 33 | var settings=new EventStoreSettings(); 34 | cfg(settings); 35 | settings.EnsureIsValid(); 36 | EventStore.Logger.Information("Event Store configured"); 37 | EventStore.Logger.Debug("Making sure the db is initiated"); 38 | settings.Store.InitStorage(); 39 | EventStore.Logger.Debug("Event store ready!"); 40 | return new StoreFacade(settings.Store,settings); 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /src/DominoEventStore/EventStoreSettings.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Serilog; 4 | 5 | namespace DominoEventStore 6 | { 7 | public class EventStoreSettings:IConfigureEventStore 8 | { 9 | Dictionary _eventMappers=new Dictionary(); 10 | 11 | 12 | public IReadOnlyDictionary EventMappers => _eventMappers; 13 | 14 | public ISpecificDbStorage Store { get; private set; } 15 | 16 | public IConfigureEventStore AddMapper(AMapFromEventDataToObject mapper) where T : class 17 | { 18 | _eventMappers.Add(typeof(T),mapper); 19 | EventStore.Logger.Debug($"Added event mapper {typeof(T)}"); 20 | return this; 21 | } 22 | 23 | public IConfigureEventStore WithProvider(ISpecificDbStorage store,string schema="") 24 | { 25 | store.MustNotBeNull(); 26 | EventStore.Logger.Debug($"Using provider {store.GetType()}"); 27 | Store = store; 28 | Store.Schema = schema; 29 | return this; 30 | } 31 | 32 | 33 | public void EnsureIsValid() 34 | { 35 | Store.MustNotBeNull(); 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /src/DominoEventStore/EventsRewriter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace DominoEventStore 6 | { 7 | class EventsRewriter 8 | { 9 | private readonly IReadOnlyDictionary _mapps; 10 | 11 | public EventsRewriter(IEnumerable rewrites, IReadOnlyDictionary mapps) 12 | { 13 | _mapps = CreateMappersFromRewriters(rewrites, mapps); 14 | } 15 | public Commit Rewrite(Commit commit) 16 | { 17 | var evs = Utils.UnpackEvents(commit.Timestamp, commit.EventData, _mapps); 18 | return new Commit(commit.TenantId,commit.EntityId,Utils.PackEvents(evs),commit.CommitId,commit.Timestamp,commit.Version); 19 | } 20 | 21 | Dictionary CreateMappersFromRewriters(IEnumerable rew, IReadOnlyDictionary mapps) 22 | { 23 | var rez = new Dictionary(); 24 | foreach (var r in rew) 25 | { 26 | 27 | if (mapps.ContainsKey(r.HandledType)) 28 | { 29 | rez.Add(r.HandledType,new LambdaMap(r.HandledType,mapps[r.HandledType],r)); 30 | } 31 | else 32 | { 33 | rez.Add(r.HandledType, new LambdaMap(r.HandledType, rew: r)); 34 | } 35 | } 36 | 37 | foreach (var kv in mapps.Where(d => !rez.ContainsKey(d.Key))) 38 | { 39 | rez.Add(kv.Key,kv.Value); 40 | } 41 | return rez; 42 | } 43 | 44 | class LambdaMap : IMapEventDataToObject 45 | { 46 | private readonly Type _type; 47 | private readonly IMapEventDataToObject _mapr; 48 | private readonly IRewriteEventData _rew; 49 | 50 | 51 | public LambdaMap(Type type,IMapEventDataToObject mapr=null,IRewriteEventData rew=null) 52 | { 53 | _type = type; 54 | _mapr = mapr; 55 | _rew = rew; 56 | } 57 | 58 | public bool Handles(Type type) 59 | => type == _type; 60 | 61 | public object Map(IDictionary existingData, object deserializedEvent, 62 | DateTimeOffset commitDate) 63 | { 64 | var rez= _mapr?.Map(existingData, deserializedEvent, commitDate)??deserializedEvent; 65 | return _rew?.Rewrite(existingData, rez, commitDate) ?? rez; 66 | } 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /src/DominoEventStore/IAdvancedFeatures.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace DominoEventStore 7 | { 8 | public interface IAdvancedFeatures 9 | { 10 | /// 11 | /// 12 | /// 13 | /// 14 | /// 15 | /// 16 | /// 17 | void MigrateEventsTo(IStoreEvents newStorage, string name, Action config = null); 18 | void ResetStorage(); 19 | void ImportCommit(Commit commits); 20 | /// 21 | /// You can't delete the default () tenant 22 | /// 23 | /// 24 | /// 25 | void DeleteTenant(string tenantId); 26 | 27 | /// 28 | /// Bulk generate read model using the provided function. 29 | /// 30 | /// 31 | /// 32 | /// By default, all the events will be processed 33 | /// 34 | void GenerateReadModel(string operationName, Action modelUpdater, Action config = null); 35 | } 36 | } -------------------------------------------------------------------------------- /src/DominoEventStore/IConfigMigration.cs: -------------------------------------------------------------------------------- 1 | namespace DominoEventStore 2 | { 3 | public interface IConfigMigration 4 | { 5 | /// 6 | /// 7 | /// 8 | /// At least 100 9 | /// 10 | IConfigMigration BatchSize(int size); 11 | /// 12 | /// By default all commits from the store are migrated, 13 | /// Specify here if you want only one specific tenant to be migrated 14 | /// 15 | /// 16 | /// 17 | IConfigMigration OnlyTenant(string tenantId); 18 | /// 19 | /// For event rewriting 20 | /// 21 | /// 22 | /// 23 | IConfigMigration AddConverters(params IRewriteEventData[] converters); 24 | } 25 | } -------------------------------------------------------------------------------- /src/DominoEventStore/IConfigReadModelGeneration.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DominoEventStore 4 | { 5 | public interface IConfigReadModelGeneration 6 | { 7 | IConfigReadModelGeneration ForTheDefaultTenant(); 8 | IConfigReadModelGeneration ForTenant(string id); 9 | IConfigReadModelGeneration ForEntity(Guid id); 10 | IConfigReadModelGeneration StartingWithDate(DateTimeOffset start); 11 | 12 | 13 | } 14 | } -------------------------------------------------------------------------------- /src/DominoEventStore/IConfigureEventStore.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace DominoEventStore 3 | { 4 | public interface IConfigureEventStore 5 | { 6 | /// 7 | /// Register mappers for existing data to events. Use it when the event structure changes. 8 | /// 9 | /// 10 | /// 11 | /// 12 | IConfigureEventStore AddMapper(AMapFromEventDataToObject mapper) where T : class; 13 | IConfigureEventStore WithProvider(ISpecificDbStorage store,string schema=null); 14 | } 15 | } -------------------------------------------------------------------------------- /src/DominoEventStore/IConfigureQuery.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DominoEventStore 4 | { 5 | public interface IConfigureQuery 6 | { 7 | /// 8 | /// Ignored if entity is not set 9 | /// 10 | /// 11 | void FromBeginningUntilVersion(int commitVersion); 12 | /// 13 | /// Default is 14 | /// 15 | /// 16 | /// 17 | IConfigureQuery OfTenant(string tenantId); 18 | 19 | IConfigureQuery OfEntity(Guid entityId); 20 | IConfigureQueryByDate WithCommitDate { get; } 21 | 22 | /// 23 | /// Ignored if no entity is specified 24 | /// 25 | /// 26 | /// 27 | IConfigureQuery IncludeSnapshots(bool include); 28 | } 29 | } -------------------------------------------------------------------------------- /src/DominoEventStore/IConfigureQueryByDate.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DominoEventStore 4 | { 5 | public interface IConfigureQueryByDate 6 | { 7 | IConfigureQuery OlderThan(DateTimeOffset date); 8 | IConfigureQuery NewerThan(DateTimeOffset date); 9 | IConfigureQuery Between(DateTimeOffset start,DateTimeOffset end); 10 | } 11 | } -------------------------------------------------------------------------------- /src/DominoEventStore/IMapEventDataToObject.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace DominoEventStore 5 | { 6 | public interface IMapEventDataToObject 7 | { 8 | bool Handles(Type type); 9 | object Map(IDictionary existingData, object deserializedEvent, DateTimeOffset commitDate); 10 | 11 | } 12 | 13 | public interface IMapEventDataToObject:IMapEventDataToObject where T : class 14 | { 15 | /// 16 | /// Allows you to change the values of the event 17 | /// 18 | /// Stored event data 19 | /// Event instance containing values automatically deserialized from existing data 20 | /// 21 | /// 22 | T Map(IDictionary existingData, T deserializedEvent, DateTimeOffset commitDate); 23 | } 24 | } -------------------------------------------------------------------------------- /src/DominoEventStore/IMapEventVersionAction.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DominoEventStore 4 | { 5 | public interface IMapEventVersionAction 6 | { 7 | IMapEventVersionCondition Use(R value); 8 | IMapEventVersionCondition Use(Func valueAction); 9 | } 10 | } -------------------------------------------------------------------------------- /src/DominoEventStore/IMapEventVersionCondition.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq.Expressions; 3 | 4 | namespace DominoEventStore 5 | { 6 | public interface IMapEventVersionCondition 7 | { 8 | IMapEventVersionAction WhenMissingColumn(Expression> column); 9 | } 10 | } -------------------------------------------------------------------------------- /src/DominoEventStore/IRewriteEventData.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DominoEventStore 4 | { 5 | public interface IRewriteEventData 6 | { 7 | Type HandledType { get; } 8 | object Rewrite(dynamic jsonData, object deserializedEvent, DateTimeOffset commitDate); 9 | } 10 | } -------------------------------------------------------------------------------- /src/DominoEventStore/ISpecificDbStorage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace DominoEventStore 8 | { 9 | public interface ISpecificDbStorage : IStoreBatchProgress 10 | { 11 | // /// 12 | // /// Should calculate the version of the entity and use that to detect concurrency problems. 13 | // /// If commit id exists, the existing commit will be returned 14 | // /// 15 | // /// 16 | // /// 17 | ///// 18 | // Task Append(UnversionedCommit commit); 19 | /// 20 | /// Should calculate the versions of each entity and use that to detect concurrency problems. 21 | /// 22 | /// 23 | /// 24 | /// 25 | /// 26 | Task Append(params UnversionedCommit[] commits); 27 | /// 28 | /// Adds the commit as is. Duplicates should be ignored 29 | /// 30 | /// 31 | /// 32 | void Import(Commit commit); 33 | 34 | Task> GetData(QueryConfig cfg, CancellationToken token); 35 | 36 | /// 37 | /// Creates the tables in the specified/default schema. If they already exist, ignore them 38 | /// 39 | void InitStorage(); 40 | 41 | /// 42 | /// Empty means default schema 43 | /// 44 | string Schema { get; set; } 45 | 46 | void ResetStorage(); 47 | void DeleteTenant(string tenantId); 48 | 49 | Task Store(Snapshot snapshot); 50 | /// 51 | /// Delete one or all stored snapshots 52 | /// 53 | /// 54 | /// 55 | /// If missing, it deletes all stored snapshots 56 | /// 57 | Task DeleteSnapshot(Guid entityId, string tenantId, int? entityVersion=null); 58 | } 59 | 60 | public interface IUnitOfWork : IDisposable 61 | { 62 | void Commit(); 63 | } 64 | } -------------------------------------------------------------------------------- /src/DominoEventStore/IStoreBatchProgress.cs: -------------------------------------------------------------------------------- 1 | namespace DominoEventStore 2 | { 3 | public interface IStoreBatchProgress 4 | { 5 | /// 6 | /// 7 | /// 8 | /// 9 | /// 10 | ProcessedCommitsCount StartOrContinue(string name); 11 | 12 | /// 13 | /// 14 | /// 15 | /// 16 | /// 17 | 18 | /// 19 | CommittedEvents GetNextBatch(ReadModelGenerationConfig config,ProcessedCommitsCount count); 20 | CommittedEvents GetNextBatch(MigrationConfig config,ProcessedCommitsCount count); 21 | 22 | void UpdateProgress(string name, ProcessedCommitsCount processedCommits); 23 | void MarkOperationAsEnded(string name); 24 | } 25 | } -------------------------------------------------------------------------------- /src/DominoEventStore/IStoreEvents.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace DominoEventStore 6 | { 7 | /// 8 | /// This should be treated as a singleton 9 | /// 10 | public interface IStoreEvents 11 | { 12 | /// 13 | /// 14 | /// 15 | /// 16 | /// 17 | /// 18 | 19 | /// 20 | Task Append(Guid entityId,Guid commitId,params object[] events); 21 | Task Append(string tenantId,Guid entityId,Guid commitId,params object[] events); 22 | 23 | IStoreUnitOfWork StartCommit(Guid id); 24 | 25 | /// 26 | /// Gets all the events from the beginning until present unless overriden. 27 | /// For performance reasons, by default it doesn't check for snapshots. 28 | /// You have to enable those explicitly 29 | /// 30 | /// 31 | /// 32 | /// 33 | /// 34 | /// 35 | Task> GetEvents(Guid entityId, string tenantId = EventStore.DefaultTenant, CancellationToken? token = null, bool includeSnapshots = false); 36 | /// 37 | /// Customize your query a bit more, if you need more finesse 38 | /// 39 | /// 40 | /// 41 | /// 42 | Task> GetEvents(Action advancedConfig, CancellationToken? token = null); 43 | 44 | IWorkWithSnapshots Snapshots { get; } 45 | IAdvancedFeatures Advanced { get; } 46 | } 47 | } -------------------------------------------------------------------------------- /src/DominoEventStore/IStoreUnitOfWork.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace DominoEventStore 5 | { 6 | public interface IStoreUnitOfWork:IDisposable 7 | { 8 | void Append(string tenantId, Guid entityId, params object[] events); 9 | void Append(Guid entityId, params object[] events); 10 | Task Commit(); 11 | } 12 | } -------------------------------------------------------------------------------- /src/DominoEventStore/IWorkWithSnapshots.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace DominoEventStore 5 | { 6 | public interface IWorkWithSnapshots 7 | { 8 | /// 9 | /// If a snapshot for the same version exists, it will be replaced 10 | /// 11 | /// Snapshot represents this version of the entity state 12 | /// 13 | /// 14 | /// 15 | /// 16 | Task Store(int entityVersion, Guid entityId, object memento, string tenantId = EventStore.DefaultTenant); 17 | 18 | Task Delete(Guid entityId, int entityVersion, string tenantId = EventStore.DefaultTenant); 19 | Task DeleteAll(Guid entityId, string tenantId = EventStore.DefaultTenant); 20 | } 21 | } -------------------------------------------------------------------------------- /src/DominoEventStore/JsonedEvent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DominoEventStore 4 | { 5 | public class JsonedEvent 6 | { 7 | /// 8 | /// Type name including namespace 9 | /// 10 | public string Type { get; set; } 11 | public string EventData { get; set; } 12 | public DateTimeOffset CommitDate { get; set; } 13 | 14 | } 15 | } -------------------------------------------------------------------------------- /src/DominoEventStore/MigrationConfig.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace DominoEventStore 5 | { 6 | public class MigrationConfig:IConfigMigration 7 | { 8 | public MigrationConfig(string name) 9 | { 10 | Name = name; 11 | } 12 | 13 | /// 14 | /// How many commits to process per batch. Default is 1000 15 | /// 16 | public int BatchSize { get; set; } = 1000; 17 | 18 | public string TenantId { get; set; } 19 | 20 | public string Name { get; private set; } 21 | 22 | public List Converters { get; }=new List(); 23 | IConfigMigration IConfigMigration.BatchSize(int size) 24 | { 25 | //size.Must(d=>d>100); 26 | BatchSize = size; 27 | return this; 28 | } 29 | 30 | public IConfigMigration OnlyTenant(string tenantId) 31 | { 32 | TenantId = tenantId; 33 | return this; 34 | } 35 | 36 | public IConfigMigration AddConverters(params IRewriteEventData[] converters) 37 | { 38 | Converters.AddRange(converters); 39 | return this; 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /src/DominoEventStore/ProcessedCommitsCount.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DominoEventStore 4 | { 5 | public struct ProcessedCommitsCount 6 | { 7 | public ProcessedCommitsCount(int value) 8 | { 9 | value.Must(d=>d>=0); 10 | Value = value; 11 | } 12 | 13 | public int Value { get; } 14 | 15 | public static implicit operator int(ProcessedCommitsCount d) => d.Value; 16 | public static implicit operator ProcessedCommitsCount(int d) => new ProcessedCommitsCount(d); 17 | } 18 | } -------------------------------------------------------------------------------- /src/DominoEventStore/ProviderExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Data.Common; 3 | using DominoEventStore.Providers; 4 | using SqlFu; 5 | using SqlFu.Configuration; 6 | using SqlFu.Providers.SqlServer; 7 | 8 | namespace DominoEventStore 9 | { 10 | public static class ProviderExtensions 11 | { 12 | public static IConfigureEventStore UseMSSql(this IConfigureEventStore store,Func factory, string cnx,string schema = null) 13 | { 14 | SqlFuManager.Configure(d=> 15 | { 16 | d.AddProfile(new SqlServer2012Provider(factory), cnx, "es_mssql"); 17 | RegisterSqlFuConfig(d, schema); 18 | }); 19 | 20 | var provider=new SqlServerProvider(SqlFuManager.GetDbFactory("es_mssql")); 21 | store.WithProvider(provider); 22 | return store; 23 | } 24 | public static IConfigureEventStore UseSqlite(this IConfigureEventStore store,Func factory, string cnx,string schema = null) 25 | { 26 | SqlFuManager.Configure(d=> 27 | { 28 | d.AddProfile(new SqlFu.Providers.Sqlite.SqliteProvider(factory), cnx, "es_sqlite"); 29 | RegisterSqlFuConfig(d, schema); 30 | }); 31 | 32 | var provider=new SqliteProvider(SqlFuManager.GetDbFactory("es_sqlite")); 33 | store.WithProvider(provider); 34 | return store; 35 | } 36 | 37 | 38 | 39 | 40 | public static void RegisterSqlFuConfig(SqlFuConfig config,string schema=null) 41 | { 42 | config.ConfigureTableForPoco(d => 43 | { 44 | d.TableName = new TableName(ASqlDbProvider.CommitsTable, schema); 45 | }); 46 | config.ConfigureTableForPoco(d => 47 | { 48 | d.TableName = new TableName(ASqlDbProvider.SnapshotsTable, schema); 49 | }); 50 | config.ConfigureTableForPoco(d =>d.TableName = new TableName(ASqlDbProvider.BatchTable, schema)); 51 | 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /src/DominoEventStore/Providers/ASqlDbProvider.cs: -------------------------------------------------------------------------------- 1 | using SqlFu; 2 | using SqlFu.Configuration; 3 | using System; 4 | using System.Data.Common; 5 | using System.Linq; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace DominoEventStore.Providers 10 | { 11 | public abstract class ASqlDbProvider:ISpecificDbStorage 12 | { 13 | private readonly IDbFactory _db; 14 | 15 | public const string CommitsTable = "ES_Commits"; 16 | public const string SnapshotsTable = "ES_Snapshots"; 17 | public const string BatchTable = "ES_Batch"; 18 | 19 | protected ASqlDbProvider(IDbFactory db) 20 | { 21 | _db = db; 22 | 23 | db.Configuration.OnCommand = cmd => EventStore.Logger.Debug(cmd.FormatCommand()); 24 | db.Configuration.OnException = (cmd,ex) => EventStore.Logger.Debug(ex,cmd.FormatCommand()); 25 | } 26 | 27 | 28 | 29 | public ProcessedCommitsCount StartOrContinue(string name) 30 | { 31 | using (var db = _db.Create()) 32 | { 33 | var skip = db.QueryValue(q => 34 | q.From().Where(d => d.Name == name).Select(d => d.Skip).MapTo()); 35 | if (skip == null) 36 | { 37 | skip = 0; 38 | db.Insert(new BatchProgress() { Name = name }); 39 | } 40 | return new ProcessedCommitsCount((int)skip.Value); 41 | } 42 | } 43 | 44 | public CommittedEvents GetNextBatch(ReadModelGenerationConfig config, ProcessedCommitsCount count) 45 | { 46 | using (var db = _db.Create()) 47 | { 48 | var all = db.QueryAs(q => q 49 | .FromAnonymous(new {Id = 1, TenantId = "", EntityId = Guid.Empty}, 50 | new TableName(CommitsTable, Schema)).Where(d => true) 51 | .AndIf(() => config.EntityId.HasValue, d => d.EntityId == config.EntityId.Value) 52 | .AndIf(() => !config.TenantId.IsNullOrEmpty(), d => d.TenantId == config.TenantId) 53 | .OrderBy(d => d.Id) 54 | .Limit(config.BatchSize, count.Value) 55 | .SelectAll(useAsterisk: true).MapTo() 56 | ); 57 | return new CommittedEvents(all.ToArray()); 58 | } 59 | } 60 | 61 | public CommittedEvents GetNextBatch(MigrationConfig config, ProcessedCommitsCount count) 62 | { 63 | var all = _db.QueryOver(new {Id = 1, TenantId = "", EntityId = Guid.Empty}, 64 | new TableName(CommitsTable, Schema)).Build(q => 65 | q.Where(d => true) 66 | .AndIf(() => !config.TenantId.IsNullOrEmpty(), d => d.TenantId == config.TenantId) 67 | .OrderBy(d => d.Id) 68 | .Limit(config.BatchSize, count.Value) 69 | .SelectAll(useAsterisk: true).MapTo()) 70 | .GetRows(); 71 | return new CommittedEvents(all.ToArray()); 72 | 73 | } 74 | 75 | public void UpdateProgress(string name, ProcessedCommitsCount processedCommits) 76 | { 77 | using (var db = _db.Create()) 78 | { 79 | db.Update().Set(d => d.Skip, processedCommits.Value) 80 | .Where(d => d.Name == name).Execute(); 81 | } 82 | } 83 | 84 | 85 | public void MarkOperationAsEnded(string name) 86 | { 87 | using (var db = _db.Create()) 88 | { 89 | db.DeleteFrom(d => d.Name == name); 90 | } 91 | } 92 | 93 | public async Task Append(params UnversionedCommit[] commits) 94 | { 95 | using (var db = await _db.CreateAsync(CancellationToken.None)) 96 | { 97 | try 98 | { 99 | using (var t = db.BeginTransaction()) 100 | { 101 | 102 | foreach (var commit in commits) 103 | { 104 | var max = await db.WithSql(q => 105 | q.From() 106 | .Where(d => d.EntityId == commit.EntityId && d.TenantId == commit.TenantId) 107 | .Select(d => d.Max(d.Version)) 108 | .MapTo()) 109 | .GetValueAsync() 110 | .ConfigureFalse() ?? 0; 111 | 112 | var com=new Commit(max+1,commit); 113 | await db.InsertAsync(com, CancellationToken.None).ConfigureFalse(); 114 | } 115 | 116 | t.Commit(); 117 | } 118 | } 119 | 120 | catch (DbException ex) 121 | { 122 | if (ex.Message.Contains(DuplicateCommmitMessage)) 123 | { 124 | throw new DuplicateCommitException(); 125 | 126 | } 127 | 128 | if (ex.Message.Contains(DuplicateVersion)) 129 | { 130 | throw new ConcurrencyException(); 131 | } 132 | throw; 133 | } 134 | 135 | } 136 | } 137 | 138 | protected virtual string DuplicateVersion => "Ver"; 139 | 140 | protected virtual string DuplicateCommmitMessage { get; } = "Cid"; 141 | public void Import(Commit commit) 142 | { 143 | using (var db = _db.Create()) 144 | { 145 | try 146 | { 147 | db.Insert(commit); 148 | } 149 | catch (DbException ex) when (_db.Provider.IsUniqueViolation(ex)) 150 | { 151 | //ignore duplicates 152 | } 153 | } 154 | } 155 | 156 | public async Task> GetData(QueryConfig cfg, CancellationToken token) 157 | { 158 | using (var db = await _db.CreateAsync(token).ConfigureFalse()) 159 | { 160 | var snapshot = Optional.Empty; 161 | if (!cfg.IgnoreSnapshots) snapshot=await GetSnapshot(db,cfg).ConfigureFalse(); 162 | var vers = snapshot.IsEmpty ? 1:snapshot.Value.Version+1; 163 | token.ThrowIfCancellationRequested(); 164 | var all=await db.QueryAsAsync( 165 | q => q.From() 166 | .Where(d => d.TenantId == cfg.TenantId) 167 | .AndIf(() => cfg.EntityId.HasValue, d => d.EntityId == cfg.EntityId.Value) 168 | .And(d=>d.Version>=Math.Max(cfg.VersionStart,vers)) 169 | .AndIf(()=>cfg.DateEnd.HasValue,d=>d.Timestamp<=cfg.DateEnd) 170 | .AndIf(()=>cfg.DateStart.HasValue,d=>d.Timestamp>=cfg.DateStart) 171 | .AndIf(()=>cfg.VersionEnd.HasValue,d=>d.Version<=cfg.VersionEnd) 172 | .OrderBy(d=>d.Version) 173 | .SelectAll(useAsterisk:true) 174 | , token).ConfigureFalse(); 175 | var rez=new EntityStreamData(); 176 | rez.LatestSnapshot = snapshot; 177 | rez.Commits = all; 178 | return rez.ToOptional(); 179 | } 180 | 181 | } 182 | 183 | private async Task> GetSnapshot(DbConnection db, QueryConfig cfg) 184 | { 185 | if(cfg.EntityId==null) return Optional.Empty; 186 | var row = await db.QueryRowAsync( 187 | q => q.From() 188 | .Where(d => d.EntityId == cfg.EntityId && d.TenantId == cfg.TenantId) 189 | .OrderByDescending(d => d.Version) 190 | .Limit(1) 191 | .SelectAll(useAsterisk: true), CancellationToken.None).ConfigureFalse(); 192 | return row == null ? Optional.Empty :new Optional(row); 193 | } 194 | 195 | public void InitStorage() 196 | { 197 | using (var db=_db.Create()) 198 | { 199 | db.Execute(GetInitStorageSql(Schema)); 200 | } 201 | } 202 | 203 | protected abstract string GetInitStorageSql(string schema); 204 | 205 | public string Schema { get; set; } 206 | public void ResetStorage() 207 | { 208 | 209 | using (var db = _db.Create()) 210 | { 211 | db.DeleteFrom(); 212 | db.DeleteFrom(); 213 | } 214 | } 215 | 216 | public void DeleteTenant(string tenantId) 217 | { 218 | tenantId.MustNotBeEmpty(); 219 | using (var db = _db.Create()) 220 | { 221 | db.DeleteFrom(d => d.TenantId == tenantId); 222 | } 223 | } 224 | 225 | public async Task Store(Snapshot snapshot) 226 | { 227 | using (var db = await _db.CreateAsync(CancellationToken.None)) 228 | { 229 | var rez = await db.Update().Set(d => d.SerializedData, snapshot.SerializedData) 230 | .Set(d => d.Version, snapshot.Version) 231 | .Where(d => d.EntityId == snapshot.EntityId && d.TenantId == snapshot.TenantId) 232 | .ExecuteAsync(CancellationToken.None).ConfigureFalse(); 233 | if (rez == 0) 234 | { 235 | await db.InsertAsync(snapshot, CancellationToken.None); 236 | } 237 | 238 | } 239 | } 240 | 241 | public async Task DeleteSnapshot(Guid entityId, string tenantId, int? entityVersion = null) 242 | { 243 | using (var db = await _db.CreateAsync(CancellationToken.None)) 244 | { 245 | if (entityVersion==null) 246 | await db.DeleteFromAsync(CancellationToken.None, 247 | d => d.EntityId == entityId && d.TenantId == tenantId).ConfigureFalse(); 248 | else 249 | await db.DeleteFromAsync(CancellationToken.None, 250 | d => d.EntityId == entityId && d.TenantId == tenantId && d.Version== entityVersion).ConfigureFalse(); 251 | } 252 | } 253 | } 254 | } -------------------------------------------------------------------------------- /src/DominoEventStore/Providers/BatchProgress.cs: -------------------------------------------------------------------------------- 1 | namespace DominoEventStore.Providers 2 | { 3 | public class BatchProgress 4 | { 5 | public string Name { get; set; } 6 | public long Skip { get; set; } 7 | } 8 | } -------------------------------------------------------------------------------- /src/DominoEventStore/Providers/InMemory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace DominoEventStore.Providers 8 | { 9 | public class InMemory:ISpecificDbStorage 10 | { 11 | object _sync=new object(); 12 | 13 | Dictionary _batch=new Dictionary(); 14 | 15 | public ProcessedCommitsCount StartOrContinue(string name) 16 | { 17 | return _batch.GetValueOrCreate(name,()=>0); 18 | } 19 | 20 | public CommittedEvents GetNextBatch(ReadModelGenerationConfig config, ProcessedCommitsCount count) 21 | { 22 | IEnumerable all=_commits; 23 | if (!config.TenantId.IsNullOrEmpty()) all = all.Where(d => d.TenantId == config.TenantId); 24 | if (config.EntityId.HasValue) all = all.Where(d => d.EntityId == config.EntityId.Value); 25 | all = all.OrderBy(d => d.Timestamp); 26 | return new CommittedEvents(all.ToArray()); 27 | } 28 | 29 | public string Schema { get; set; } = ""; 30 | 31 | public CommittedEvents GetNextBatch(MigrationConfig config, ProcessedCommitsCount count) 32 | { 33 | IEnumerable all = _commits; 34 | if (!config.TenantId.IsNullOrEmpty()) all = all.Where(d => d.TenantId == config.TenantId); 35 | all = all.OrderBy(d => d.Timestamp); 36 | return new CommittedEvents(all.ToArray()); 37 | } 38 | 39 | public void UpdateProgress(string name, ProcessedCommitsCount processedCommits) 40 | { 41 | _batch[name] = processedCommits; 42 | } 43 | 44 | public void MarkOperationAsEnded(string name) 45 | { 46 | _batch.Remove(name); 47 | } 48 | 49 | 50 | List _commits=new List(); 51 | 52 | public Task Append(params UnversionedCommit[] commits) 53 | { 54 | lock (_sync) 55 | { 56 | foreach (var commit in commits) 57 | { 58 | var all = _commits.Where(d => d.TenantId == commit.TenantId && d.EntityId == commit.EntityId); 59 | var dup = all.FirstOrDefault(d => d.CommitId == commit.CommitId); 60 | if (dup != null) 61 | { 62 | return Task.FromResult(new AppendResult(dup)); 63 | } 64 | var max=!all.Any()?0:all.Max(d => d.Version); 65 | var c=new Commit(max+1,commit); 66 | _commits.Add(c); 67 | } 68 | 69 | } 70 | 71 | return Task.CompletedTask; 72 | } 73 | 74 | public void Import(Commit commit) 75 | { 76 | var all = _commits.Where(d => d.TenantId == commit.TenantId && d.EntityId == commit.EntityId); 77 | var dup = all.FirstOrDefault(d => d.CommitId == commit.CommitId); 78 | if (dup!=null) return; 79 | _commits.Add(commit); 80 | } 81 | 82 | public Task> GetData(QueryConfig cfg, CancellationToken token) 83 | { 84 | var esd=new EntityStreamData(); 85 | if (!cfg.IgnoreSnapshots) esd.LatestSnapshot = GetSnapshot(cfg); 86 | var ver = esd.LatestSnapshot.IsEmpty ? 1 : esd.LatestSnapshot.Value.Version + 1; 87 | cfg.VersionStart = Math.Max(cfg.VersionStart, ver); 88 | cfg.VersionEnd = cfg.VersionEnd ?? int.MaxValue; 89 | lock (_sync) 90 | { 91 | esd.Commits = 92 | _commits.Where(d => d.TenantId == cfg.TenantId).Where(d => 93 | { 94 | 95 | if (cfg.EntityId.HasValue) return d.EntityId == cfg.EntityId; 96 | return true; 97 | }).Where(d => d.Version >= cfg.VersionStart && d.Version <= cfg.VersionEnd) 98 | .Where(d => d.Timestamp >= (cfg.DateStart ?? DateTimeOffset.MinValue)) 99 | .Where(d => d.Timestamp <= (cfg.DateEnd ?? DateTimeOffset.MaxValue)) 100 | .OrderBy(d=>d.Version) 101 | .ToArray(); 102 | } 103 | 104 | return Task.FromResult(esd.ToOptional()); 105 | } 106 | 107 | private Optional GetSnapshot(QueryConfig cfg) 108 | { 109 | cfg.EntityId.MustNotBeDefault(); 110 | lock (_sync) 111 | { 112 | var all = _snapshots.GetValueOrDefault(cfg.EntityId.Value,new List()).ToArray(); 113 | if (!all.Any()) return Optional.Empty; 114 | var snapshot = all.OrderByDescending(d => d.Version).FirstOrDefault(); 115 | return snapshot==null?Optional.Empty:new Optional(snapshot); 116 | } 117 | } 118 | 119 | public void InitStorage() 120 | { 121 | 122 | } 123 | 124 | public void ResetStorage() 125 | { 126 | _commits.Clear(); 127 | _snapshots.Clear(); 128 | } 129 | 130 | public void DeleteTenant(string tenantId) 131 | { 132 | lock (_sync) 133 | { 134 | _commits.RemoveAll(d => d.TenantId == tenantId); 135 | } 136 | } 137 | Dictionary> _snapshots=new Dictionary>(); 138 | public Task Store(Snapshot snapshot) 139 | { 140 | lock (_sync) 141 | { 142 | var arr = _snapshots.GetValueOrCreate(snapshot.EntityId, () => new List()); 143 | if (arr.Any(d => d.Version == snapshot.Version)) 144 | { 145 | arr.RemoveAll(d => d.Version == snapshot.Version); 146 | } 147 | arr.Add(snapshot); 148 | } 149 | 150 | return Task.CompletedTask; 151 | } 152 | 153 | public Task DeleteSnapshot(Guid entityId, string tenantId, int? entityVersion = null) 154 | { 155 | lock (_sync) 156 | { 157 | 158 | if (entityVersion == null) 159 | { 160 | _snapshots.Remove(entityId); 161 | 162 | } 163 | else 164 | { 165 | var snaps = _snapshots.GetValueOrDefault(entityId, new List()); 166 | snaps.RemoveAll(d => d.Version == entityVersion); 167 | } 168 | } 169 | 170 | return Task.CompletedTask; 171 | } 172 | } 173 | } -------------------------------------------------------------------------------- /src/DominoEventStore/Providers/SqlServerProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using SqlFu; 3 | 4 | namespace DominoEventStore.Providers 5 | { 6 | public class SqlServerProvider : ASqlDbProvider 7 | { 8 | public SqlServerProvider(IDbFactory db) : base(db) 9 | { 10 | } 11 | 12 | 13 | protected override string GetInitStorageSql(string schema) 14 | { 15 | schema = schema ?? "dbo"; 16 | 17 | 18 | return $@" 19 | IF not EXISTS(SELECT 1 FROM sys.Objects WHERE Object_id = OBJECT_ID(N'{schema}.{CommitsTable}') or Object_id =OBJECT_ID(N'{schema}.{SnapshotsTable}') AND Type = N'U') 20 | begin 21 | CREATE TABLE [{schema}].[{CommitsTable}]( 22 | [Id][int] IDENTITY(1,1) NOT NULL, 23 | 24 | [TenantId] [varchar] (75) NOT NULL, 25 | 26 | [EntityId] [uniqueidentifier] 27 | NOT NULL, 28 | 29 | [CommitId] [uniqueidentifier] 30 | NOT NULL, 31 | 32 | [EventData] [nvarchar] (max) NOT NULL, 33 | [Timestamp] [datetimeoffset] (7) NOT NULL, 34 | 35 | [Version] [int] NOT NULL, 36 | CONSTRAINT[PK_Commits] PRIMARY KEY CLUSTERED 37 | ( 38 | [Id] ASC 39 | )WITH(PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON[PRIMARY], 40 | CONSTRAINT[IX_Commits_Cid] UNIQUE NONCLUSTERED 41 | ( 42 | [EntityId] Asc, 43 | [CommitId] ASC 44 | )WITH(PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON[PRIMARY], 45 | CONSTRAINT[IX_Commits_Ver] UNIQUE NONCLUSTERED 46 | ( 47 | [EntityId] ASC, 48 | [Version] ASC 49 | )WITH(PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON[PRIMARY] 50 | ) ON[PRIMARY] TEXTIMAGE_ON[PRIMARY]; 51 | CREATE TABLE [{schema}].[{SnapshotsTable}]( 52 | [Id] [int] IDENTITY(1,1) NOT NULL, 53 | [TenantId] [varchar](75) NOT NULL, 54 | [EntityId] [uniqueidentifier] NOT NULL, 55 | [Version] [int] NOT NULL, 56 | [SerializedData] [nvarchar](max) NOT NULL, 57 | [SnapshotDate] [datetimeoffset](7) NOT NULL, 58 | CONSTRAINT [PK_SNapshots] PRIMARY KEY CLUSTERED 59 | ( 60 | [Id] ASC 61 | )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] 62 | ) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]; 63 | CREATE UNIQUE NONCLUSTERED INDEX [IX_SNapshots_Ver] ON [{schema}].[{SnapshotsTable}] 64 | ( 65 | [EntityId] ASC, 66 | [Version] ASC 67 | ) 68 | ; 69 | CREATE TABLE [{schema}].[{BatchTable}]( 70 | [Name] [varchar](250) NOT NULL, 71 | [Skip] [bigint] NOT NULL 72 | ) ON [PRIMARY] 73 | end 74 | " 75 | ; 76 | } 77 | } 78 | 79 | 80 | 81 | } -------------------------------------------------------------------------------- /src/DominoEventStore/Providers/SqliteProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using SqlFu; 3 | 4 | namespace DominoEventStore.Providers 5 | { 6 | public class SqliteProvider : ASqlDbProvider 7 | { 8 | protected override string DuplicateCommmitMessage { get; } = "CommitId"; 9 | 10 | protected override string DuplicateVersion { get; } = "Version"; 11 | 12 | protected override string GetInitStorageSql(string schema) 13 | { 14 | 15 | return $@" 16 | 17 | CREATE TABLE if not exists `{CommitsTable}`( 18 | `Id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT 19 | , `TenantId` TEXT NOT NULL 20 | , `EntityId` TEXT NOT NULL 21 | , `CommitId` TEXT NOT NULL 22 | , `EventData` TEXT NOT NULL 23 | , `Timestamp` TEXT NOT NULL 24 | , `Version` INTEGER NOT NULL 25 | ); 26 | CREATE unique INDEX if not exists `IX_Commits_Cid` ON `{CommitsTable}` (`EntityId`,`CommitId` ASC); 27 | CREATE unique INDEX if not exists `IX_Commits_Ver` ON `{CommitsTable}` (`EntityId` ,`Version` ); 28 | 29 | CREATE TABLE if not exists `{SnapshotsTable}` 30 | ( 31 | `Id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, `TenantId` TEXT NOT NULL, `EntityId` TEXT NOT NULL, `Version` INTEGER NOT NULL, `SerializedData` TEXT NOT NULL, `SnapshotDate` TEXT NOT NULL 32 | ); 33 | CREATE INDEX if not exists `IX_SNapshots_Ver` ON `{SnapshotsTable}` (`EntityId` ,`Version` ); 34 | CREATE TABLE if not exists `{BatchTable}` ( `Name` TEXT NOT NULL, `Skip` INTEGER NOT NULL ); 35 | " 36 | ; 37 | } 38 | 39 | public SqliteProvider(IDbFactory db) : base(db) 40 | { 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /src/DominoEventStore/QueryConfig.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DominoEventStore 4 | { 5 | public class QueryConfig:IConfigureQuery,IConfigureQueryByDate 6 | { 7 | public string TenantId { get; set; } = EventStore.DefaultTenant; 8 | public Guid? EntityId { get; set; } 9 | 10 | /// 11 | /// Commit version 12 | /// 13 | public int VersionStart { get; set; } = 1; 14 | public int? VersionEnd { get; set; } 15 | public bool IgnoreSnapshots { get; set; } = true; 16 | public DateTimeOffset? DateEnd { get; set; } 17 | public DateTimeOffset? DateStart { get; set; } 18 | 19 | public IConfigureQueryByDate WithCommitDate => this; 20 | 21 | public void FromBeginningUntilVersion(int commitVersion) 22 | { 23 | commitVersion.Must(d=>d>0); 24 | VersionEnd = commitVersion; 25 | } 26 | 27 | 28 | IConfigureQuery IConfigureQuery.IncludeSnapshots(bool include) 29 | { 30 | IgnoreSnapshots = !include; 31 | return this; 32 | } 33 | 34 | public IConfigureQuery OfTenant(string tenantId) 35 | { 36 | tenantId.MustNotBeEmpty(); 37 | TenantId = tenantId; 38 | return this; 39 | } 40 | 41 | public IConfigureQuery OfEntity(Guid entityId) 42 | { 43 | entityId.MustNotBeDefault(); 44 | EntityId = entityId; 45 | return this; 46 | } 47 | 48 | public IConfigureQuery OlderThan(DateTimeOffset date) 49 | { 50 | DateEnd = date; 51 | return this; 52 | } 53 | 54 | public IConfigureQuery NewerThan(DateTimeOffset date) 55 | { 56 | DateStart = date; 57 | return this; 58 | } 59 | 60 | public IConfigureQuery Between(DateTimeOffset start, DateTimeOffset end) 61 | { 62 | this.DateStart = start; 63 | DateEnd = end; 64 | return this; 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/DominoEventStore/ReadModelGenerationConfig.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DominoEventStore 4 | { 5 | public class ReadModelGenerationConfig:IConfigReadModelGeneration 6 | { 7 | public string Name { get; } 8 | 9 | /// 10 | /// How many commits to process per batch. Default is 1000 11 | /// 12 | public int BatchSize { get; set; } = 1000; 13 | 14 | /// 15 | /// Can be empty 16 | /// 17 | public string TenantId { get; set; } 18 | 19 | public Guid? EntityId { get; set; } 20 | 21 | public ReadModelGenerationConfig(string name) 22 | { 23 | Name = name; 24 | } 25 | 26 | public IConfigReadModelGeneration ForTheDefaultTenant() 27 | => ForTenant(EventStore.DefaultTenant); 28 | 29 | public IConfigReadModelGeneration ForTenant(string id) 30 | { 31 | TenantId = id; 32 | return this; 33 | } 34 | 35 | public IConfigReadModelGeneration ForEntity(Guid id) 36 | { 37 | id.MustNotBeDefault(); 38 | EntityId = id; 39 | return this; 40 | } 41 | 42 | public IConfigReadModelGeneration StartingWithDate(DateTimeOffset start) 43 | { 44 | StartDate = start; 45 | return this; 46 | } 47 | 48 | public DateTimeOffset? StartDate { get; set; } 49 | 50 | 51 | } 52 | } -------------------------------------------------------------------------------- /src/DominoEventStore/Snapshot.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DominoEventStore 4 | { 5 | public class Snapshot 6 | { 7 | protected Snapshot() { } 8 | public Snapshot(int version, Guid entityId, string tenantId, string serializedData, DateTimeOffset snapshotDate) 9 | { 10 | Version = version; 11 | EntityId = entityId; 12 | TenantId = tenantId; 13 | SerializedData = serializedData; 14 | SnapshotDate = snapshotDate; 15 | } 16 | 17 | public int Version { get; private set; } 18 | public Guid EntityId { get; private set; } 19 | public string TenantId { get; private set; } 20 | public string SerializedData { get; private set; } 21 | public DateTimeOffset SnapshotDate { get; private set; } 22 | } 23 | } -------------------------------------------------------------------------------- /src/DominoEventStore/SomeData.cs: -------------------------------------------------------------------------------- 1 | namespace DominoEventStore 2 | { 3 | public class SomeData 4 | { 5 | public string Name { get; set; } 6 | } 7 | } -------------------------------------------------------------------------------- /src/DominoEventStore/SomeDataMapper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace DominoEventStore 5 | { 6 | public class SomeDataMapper : AMapFromEventDataToObject 7 | { 8 | public override SomeData Map(IDictionary existingData, SomeData deserializedEvent, 9 | DateTimeOffset commitDate) 10 | { 11 | throw new NotImplementedException(); 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /src/DominoEventStore/StoreFacade.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | 9 | namespace DominoEventStore 10 | { 11 | public class StoreFacade:IStoreEvents,IWorkWithSnapshots, IAdvancedFeatures 12 | { 13 | private readonly ISpecificDbStorage _store; 14 | 15 | public void ImportCommit(Commit commit) 16 | => _store.Import(commit); 17 | 18 | private readonly EventStoreSettings _settings; 19 | 20 | public StoreFacade(ISpecificDbStorage store,EventStoreSettings settings) 21 | { 22 | _store = store; 23 | _settings = settings; 24 | } 25 | 26 | public Task Append(Guid entityId, Guid commitId, params object[] events) 27 | => Append(EventStore.DefaultTenant, entityId, commitId, events); 28 | 29 | public async Task Append(string tenantId, Guid entityId, Guid commitId, params object[] events) 30 | { 31 | tenantId.MustNotBeEmpty(); 32 | entityId.MustNotBeDefault(); 33 | commitId.MustNotBeDefault(); 34 | if (events.IsNullOrEmpty()) return ; 35 | 36 | using (var t=StartCommit(commitId)) 37 | { 38 | t.Append(tenantId,entityId,events); 39 | await t.Commit().ConfigureFalse(); 40 | } 41 | 42 | // var commit=new UnversionedCommit(tenantId,entityId,Utils.PackEvents(events),commitId,DateTimeOffset.Now); 43 | // var dbgInfo = new{tenantId,entityId,commitId}; 44 | //EventStore.Logger.Debug("Appending {@commit} with events {@events}",dbgInfo,events); 45 | // var rez= await _store.Append(commit); 46 | // if (rez.WasSuccessful) 47 | // { 48 | // EventStore.Logger.Debug("Append successful for commit {@commit}",dbgInfo); 49 | // return; 50 | // } 51 | // throw new DuplicateCommitException(commitId,Utils.UnpackEvents(commit.Timestamp,commit.EventData,_settings.EventMappers)); 52 | } 53 | 54 | public IStoreUnitOfWork StartCommit(Guid id) 55 | { 56 | return new UnitOfWork(id,_store); 57 | } 58 | 59 | class UnitOfWork : IStoreUnitOfWork 60 | { 61 | private readonly Guid _commitId; 62 | private readonly ISpecificDbStorage _store; 63 | List _commits= new List(); 64 | 65 | public UnitOfWork(Guid commitId,ISpecificDbStorage store) 66 | { 67 | _commitId = commitId; 68 | _store = store; 69 | } 70 | public void Dispose() 71 | { 72 | 73 | } 74 | 75 | public void Append(string tenantId, Guid entityId, params object[] events) 76 | { 77 | _commits.Add(new UnversionedCommit(tenantId,entityId,Utils.PackEvents(events),_commitId, DateTimeOffset.Now)); 78 | } 79 | 80 | public void Append(Guid entityId, params object[] events) 81 | { 82 | Append(EventStore.DefaultTenant,entityId,events); 83 | } 84 | 85 | public Task Commit() 86 | { 87 | return _store.Append(_commits.ToArray()); 88 | } 89 | } 90 | 91 | public Task> GetEvents(Guid entityId, string tenantId = EventStore.DefaultTenant, 92 | CancellationToken? token = null, bool includeSnapshots = false) 93 | => GetEvents(g => g.OfTenant(tenantId).OfEntity(entityId).IncludeSnapshots(includeSnapshots), token); 94 | 95 | private EntityEvents ConvertToEntityEvents(EntityStreamData raw) 96 | { 97 | Optional GetSnapshot(string sData) 98 | { 99 | if (sData.IsNullOrEmpty()) return Optional.Empty; 100 | return new Optional(Utils.UnpackSnapshot(sData)); 101 | } 102 | 103 | IReadOnlyCollection GetEvents(IEnumerable commits) 104 | => 105 | commits.SelectMany(d => 106 | Utils.UnpackEvents(d.Timestamp, d.EventData, _settings.EventMappers)).ToArray(); 107 | return new EntityEvents(GetEvents(raw.Commits),raw.Commits.Max(d=>d.Version),GetSnapshot(raw.LatestSnapshot.ValueOr(null)?.SerializedData)); 108 | 109 | } 110 | 111 | 112 | public IWorkWithSnapshots Snapshots => this; 113 | public IAdvancedFeatures Advanced => this; 114 | 115 | 116 | public async Task Store(int entityVersion, Guid entityId, object memento, string tenantId = EventStore.DefaultTenant) 117 | { 118 | var snapshot=new Snapshot(entityVersion,entityId,tenantId,Utils.PackSnapshot(memento),DateTimeOffset.Now); 119 | EventStore.Logger.Debug("Storing snapshot {@snapshot}",snapshot); 120 | await _store.Store(snapshot).ConfigureFalse(); 121 | 122 | EventStore.Logger.Debug("Snapshot for {@entity} stored successfully",new{entityId,tenantId,entityVersion}); 123 | } 124 | 125 | public Task Delete(Guid entityId, int entityVersion, string tenantId = EventStore.DefaultTenant) 126 | => _store.DeleteSnapshot(entityId,tenantId,entityVersion); 127 | 128 | /// 129 | /// Deletes all stored snapshots for entity 130 | /// 131 | /// 132 | /// 133 | /// 134 | public Task DeleteAll(Guid entityId, string tenantId = EventStore.DefaultTenant) 135 | => _store.DeleteSnapshot(entityId, tenantId); 136 | 137 | 138 | public async Task> GetEvents(Action advancedConfig, CancellationToken? token = null) 139 | { 140 | var config = new QueryConfig(); 141 | advancedConfig(config); 142 | EventStore.Logger.Debug("Getting events with query {@query}",new{config.TenantId,config.EntityId,config.IgnoreSnapshots,config.DateStart,config.DateEnd}); 143 | var raw = await _store.GetData(config, token ?? CancellationToken.None).ConfigureFalse(); 144 | var dbg = new{config.TenantId,config.EntityId}; 145 | if (raw.HasValue) 146 | { 147 | var events = ConvertToEntityEvents(raw.Value); 148 | EventStore.Logger.Debug("Query for {@entity} returned "+events.Count+" events",dbg); 149 | 150 | return new Optional(events); 151 | } 152 | else 153 | { 154 | EventStore.Logger.Debug("Query for {@entity} returned empty",dbg); 155 | return Optional.Empty; 156 | } 157 | } 158 | 159 | 160 | public void MigrateEventsTo(IStoreEvents newStorage, string name, Action config = null) 161 | { 162 | name.MustNotBeEmpty(); 163 | var conf = new MigrationConfig(name); 164 | config?.Invoke(conf); 165 | var l =EventStore.Logger; 166 | 167 | var rew=new EventsRewriter(conf.Converters,_settings.EventMappers); 168 | l.Debug("Starting store migration with batch operation: {name}",name); 169 | using (var operation = new BatchOperation(_store, conf)) 170 | { 171 | Optional commit; 172 | do 173 | { 174 | commit = operation.GetNextCommit(); 175 | if (commit.HasValue) 176 | { 177 | l.Debug("Importing commit {commit}",commit.Value.CommitId); 178 | newStorage.Advanced.ImportCommit(rew.Rewrite(commit.Value)); 179 | } 180 | 181 | } while (commit.HasValue); 182 | 183 | 184 | } 185 | l.Debug("Migration {name} completed",name); 186 | 187 | } 188 | 189 | public void ResetStorage() 190 | { 191 | EventStore.Logger.Debug("Resetting events store"); 192 | _store.ResetStorage(); 193 | } 194 | 195 | public void DeleteTenant(string tenantId) 196 | { 197 | tenantId.MustNotBe(EventStore.DefaultTenant); 198 | _store.DeleteTenant(tenantId); 199 | EventStore.Logger.Debug("Tenant {tenant} deleted",tenantId); 200 | } 201 | 202 | public void GenerateReadModel(string operationName, Action modelUpdater, Action config = null) 203 | { 204 | operationName.MustNotBeEmpty(); 205 | var conf=new ReadModelGenerationConfig(operationName); 206 | config?.Invoke(conf); 207 | 208 | void HandleCommit(Commit commit,Action updater) 209 | { 210 | var evs = Utils.UnpackEvents(commit.Timestamp, commit.EventData, _settings.EventMappers); 211 | foreach (var ev in evs) 212 | { 213 | EventStore.Logger.Debug("Updating readmodel from {@event}",ev); 214 | updater((dynamic) ev); 215 | } 216 | } 217 | 218 | 219 | using (var operation = new BatchOperation(_store,conf)) 220 | { 221 | Optional commit; 222 | do 223 | { 224 | commit = operation.GetNextCommit(); 225 | if (commit.HasValue) 226 | { 227 | HandleCommit(commit.Value, modelUpdater); 228 | } 229 | } while (commit.HasValue); 230 | 231 | 232 | } 233 | 234 | } 235 | } 236 | } -------------------------------------------------------------------------------- /src/DominoEventStore/UnversionedCommit.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DominoEventStore 4 | { 5 | public class UnversionedCommit 6 | { 7 | public UnversionedCommit(string tenantId, Guid entityId, string eventData, Guid commitId, DateTimeOffset timestamp) 8 | { 9 | tenantId.MustNotBeEmpty(); 10 | entityId.MustNotBeDefault(); 11 | eventData.MustNotBeEmpty(); 12 | commitId.MustNotBeDefault(); 13 | timestamp.MustNotBeDefault(); 14 | 15 | 16 | TenantId = tenantId; 17 | EntityId = entityId; 18 | EventData = eventData; 19 | CommitId = commitId; 20 | Timestamp = timestamp; 21 | } 22 | 23 | protected UnversionedCommit() 24 | { 25 | 26 | } 27 | 28 | public Guid CommitId { get; protected set; } 29 | public DateTimeOffset Timestamp { get; protected set; } 30 | public string TenantId { get; protected set; } 31 | public Guid EntityId { get; protected set; } 32 | public string EventData { get; protected set; } 33 | } 34 | } -------------------------------------------------------------------------------- /src/DominoEventStore/Utils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace DominoEventStore 6 | { 7 | public static class Utils 8 | { 9 | public class JsonWrap 10 | { 11 | public string Type { get; set; } 12 | public dynamic Data { get; set; } 13 | 14 | public JsonWrap() 15 | { 16 | 17 | } 18 | 19 | public JsonWrap(object data) 20 | { 21 | data.MustNotBeNull(); 22 | Type = data.GetType().AssemblyQualifiedName; 23 | Data = data; 24 | } 25 | } 26 | 27 | 28 | public static string PackEvents(IEnumerable events) 29 | => ToJson(events.Select(d => new JsonWrap(d))); 30 | 31 | public static IReadOnlyCollection UnpackEvents(DateTimeOffset commitDate, string data, IReadOnlyDictionary upcasters) 32 | { 33 | var d = Utf8Json.JsonSerializer.Deserialize(data); 34 | return d.Select(c => 35 | { 36 | var des = DynamicToObject(c); 37 | var type = des.GetType(); 38 | 39 | upcasters.TryGetValue(type, out var upcast); 40 | return upcast?.Map(c.Data, des, commitDate) ?? des; 41 | }).ToArray(); 42 | 43 | } 44 | 45 | 46 | 47 | static object DynamicToObject(JsonWrap w) => 48 | Utf8Json.JsonSerializer.NonGeneric.Deserialize(Type.GetType(w.Type), Utf8Json.JsonSerializer.NonGeneric.Serialize(w.Data)); 49 | 50 | static string ToJson(object o) => Utf8Json.JsonSerializer.PrettyPrint(Utf8Json.JsonSerializer.Serialize(o)); 51 | 52 | public static string PackSnapshot(object memento) 53 | { 54 | memento.MustNotBeNull(); 55 | return ToJson(new JsonWrap(memento)); 56 | } 57 | 58 | public static object UnpackSnapshot(string snapData) 59 | { 60 | var wrap = Utf8Json.JsonSerializer.Deserialize(snapData); 61 | return DynamicToObject(wrap); 62 | } 63 | 64 | } 65 | } -------------------------------------------------------------------------------- /src/Tests/BatchOperationsTests.cs: -------------------------------------------------------------------------------- 1 |  2 | using FluentAssertions; 3 | using Xunit; 4 | using System; 5 | using System.Linq; 6 | using DominoEventStore; 7 | using NSubstitute; 8 | using Xunit.Abstractions; 9 | 10 | 11 | namespace Tests 12 | { 13 | public class BatchOperationsTests 14 | { 15 | private BatchOperation _sut; 16 | private IStoreBatchProgress _store; 17 | 18 | public BatchOperationsTests(ITestOutputHelper h) 19 | { 20 | Setup.Logger(h); 21 | _store = Substitute.For(); 22 | _sut = new BatchOperation(_store,CreateReadModelConfig()); 23 | ConfigureStore(); 24 | } 25 | 26 | private void ConfigureStore() 27 | { 28 | _store.StartOrContinue("test").Returns(new ProcessedCommitsCount(0)); 29 | } 30 | 31 | public static ReadModelGenerationConfig CreateReadModelConfig() 32 | => new ReadModelGenerationConfig("test"); 33 | 34 | [Fact] 35 | public void no_commits_returned_on_dispose_marks_operation_end() 36 | { 37 | NoCommitsStoreSetup(); 38 | _sut.GetNextCommit(); 39 | _sut.Dispose(); 40 | _store.Received(1).MarkOperationAsEnded("test"); 41 | } 42 | 43 | private void NoCommitsStoreSetup() 44 | { 45 | _store.GetNextBatch(Arg.Any(), Arg.Any()) 46 | .Returns(new CommittedEvents(new Commit[0])); 47 | } 48 | 49 | void StoreSetupWithCommits() 50 | { 51 | _store.GetNextBatch(Arg.Any(), Arg.Any()) 52 | .Returns( new CommittedEvents(Setup.Commits(4).ToArray())); 53 | } 54 | 55 | [Fact] 56 | public void with_commits_dispose_saves_progress() 57 | { 58 | StoreSetupWithCommits(); 59 | _sut.GetNextCommit().HasValue.Should().BeTrue(); 60 | _sut.Dispose(); 61 | _store.Received(1).UpdateProgress("test",0); 62 | } 63 | 64 | [Fact] 65 | public void no_commits_returned_on_get_next_returns_empty_commit() 66 | { 67 | NoCommitsStoreSetup(); 68 | _sut.GetNextCommit().IsEmpty.Should().BeTrue(); 69 | } 70 | 71 | 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Tests/Event1.cs: -------------------------------------------------------------------------------- 1 | namespace Tests 2 | { 3 | public class Event1 4 | { 5 | public int Nr { get; set; } 6 | public string Name { get; set; } 7 | } 8 | 9 | 10 | } -------------------------------------------------------------------------------- /src/Tests/Event2.cs: -------------------------------------------------------------------------------- 1 | namespace Tests 2 | { 3 | public class Event2 4 | { 5 | 6 | } 7 | } -------------------------------------------------------------------------------- /src/Tests/GetEventsAndSnapshotTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using AutoFixture; 7 | using DominoEventStore; 8 | using Xunit; 9 | using FluentAssertions; 10 | using NSubstitute; 11 | using Xunit.Abstractions; 12 | 13 | namespace Tests 14 | { 15 | public class GetEventsAndSnapshotTests 16 | { 17 | private StoreFacade _sut; 18 | private ISpecificDbStorage _storage; 19 | Fixture _fixture=new Fixture(); 20 | 21 | public GetEventsAndSnapshotTests(ITestOutputHelper h) 22 | { 23 | _storage = Substitute.For(); 24 | 25 | _sut = new StoreFacade(_storage,Setup.EventStoreSettings(h)); 26 | 27 | } 28 | 29 | [Fact] 30 | public async Task events_for_non_existing_entity_returns_empty() 31 | { 32 | var rez = await _sut.GetEvents(Guid.NewGuid()); 33 | rez.IsEmpty.Should().BeTrue(); 34 | } 35 | 36 | [Fact] 37 | public async Task four_events_without_snapshot() 38 | { 39 | var data=new EntityStreamData() 40 | { 41 | Commits = SetupCommits(2) 42 | }; 43 | _storage.GetData(Arg.Any(), CancellationToken.None) 44 | .Returns(new Optional(data)); 45 | var raw = await _sut.GetEvents(Setup.EntityId); 46 | raw.Value.LatestSnapshot.IsEmpty.Should().BeTrue(); 47 | var evs = raw.Value; 48 | evs.Count.Should().Be(4); 49 | evs.Version.Should().Be(2); 50 | } 51 | 52 | [Fact] 53 | public async Task four_events_with_snapshot() 54 | { 55 | var data=new EntityStreamData() 56 | { 57 | Commits = SetupCommits(2),LatestSnapshot = new Optional(CreateSnapshot()) 58 | }; 59 | _storage.GetData(Arg.Any(), CancellationToken.None) 60 | .Returns(new Optional(data)); 61 | var raw = await _sut.GetEvents(Setup.EntityId); 62 | var mem = raw.Value.LatestSnapshot.Value.CastAs(); 63 | mem.Should().NotBeNull(); 64 | mem.Name.Should().NotBeNullOrEmpty(); 65 | mem.CreatedOn.Should().NotBe(new DateTimeOffset()); 66 | var evs = raw.Value; 67 | evs.Count.Should().Be(4); 68 | evs.Version.Should().Be(2); 69 | } 70 | 71 | private Snapshot CreateSnapshot() 72 | => new Snapshot(2, Guid.NewGuid(), "_", Utils.PackSnapshot(_fixture.Create()), DateTimeOffset.Now); 73 | 74 | IEnumerable SetupCommits(int count) 75 | { 76 | return Enumerable.Range(1, 2) 77 | .Select(i => new Commit("_", Setup.EntityId,Utils.PackEvents(new []{_fixture.Create(), _fixture.Create()}), Guid.NewGuid(),DateTimeOffset.Now, i)); 78 | } 79 | 80 | } 81 | } -------------------------------------------------------------------------------- /src/Tests/IntegrationTests.cs: -------------------------------------------------------------------------------- 1 |  2 | using FluentAssertions; 3 | using Xunit; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Data.SqlClient; 7 | using System.Data.SQLite; 8 | using System.Diagnostics; 9 | using System.Linq; 10 | using System.Threading.Tasks; 11 | using AutoFixture; 12 | using CavemanTools.Logging; 13 | using DominoEventStore; 14 | using NSubstitute.Exceptions; 15 | using Serilog; 16 | using Serilog.Core; 17 | using SqlFu; 18 | using SqlFu.Providers.SqlServer; 19 | using Xunit.Abstractions; 20 | 21 | 22 | namespace Tests 23 | { 24 | public class IntegrationTests:IDisposable 25 | { 26 | private static int MaxEntities = Setup.IsAppVeyor?10:10; 27 | private IStoreEvents _dest; 28 | private IStoreEvents _src; 29 | Fixture _fixture=new Fixture(); 30 | private List _events=new List(); 31 | private List _entities; 32 | 33 | 34 | public IntegrationTests(ITestOutputHelper h) 35 | { 36 | 37 | 38 | _dest = EventStore.WithLogger(Logger.None).Build(c => 39 | { 40 | 41 | c.UseMSSql(SqlClientFactory.Instance.CreateConnection, SqlServerTests.ConnectionString); 42 | 43 | }); 44 | 45 | 46 | _src = EventStore.WithLogger(Logger.None).Build(c => 47 | { 48 | c.UseSqlite(SQLiteFactory.Instance.CreateConnection, SqliteTests.ConnectionString); 49 | 50 | }); 51 | 52 | } 53 | 54 | 55 | async Task SetupSrc() 56 | { 57 | _entities=new List(); 58 | for (var i = 0; i < MaxEntities;i++) 59 | { 60 | var id = Guid.NewGuid(); 61 | _entities.Add(id); 62 | var @event = i%2==0?(object)_fixture.Create():_fixture.Create(); 63 | _events.Add(@event); 64 | await _src.Append(id, Guid.NewGuid(),@event); 65 | } 66 | 67 | } 68 | 69 | 70 | public class RewriteEvent : ARewriteEvent 71 | { 72 | public override Event1 Rewrite(dynamic jsonData, Event1 deserializedEvent, DateTimeOffset commitDate) 73 | { 74 | deserializedEvent.Nr += 60; 75 | return deserializedEvent; 76 | } 77 | } 78 | 79 | [Fact] 80 | public async Task migrate_from_sqlite_to_sqlserver() 81 | { 82 | await SetupSrc(); 83 | loop: 84 | var i = new Random().Next(0,MaxEntities); 85 | 86 | _src.Advanced.MigrateEventsTo(_dest, "bubu",c=>c.BatchSize(50).AddConverters(new RewriteEvent())); 87 | var evs = await _dest.GetEvents(_entities[i]); 88 | evs.Value.Count.Should().Be(1); 89 | var orig = _events[i].CastAs(); 90 | if (orig==null) goto loop; 91 | 92 | var ev = evs.Value.First() as Event1; 93 | ev.Nr.Should().Be(60+orig.Nr); 94 | } 95 | 96 | [Fact] 97 | public async Task regenerate_read_model() 98 | { 99 | await SetupSrc(); 100 | var count = 0; 101 | _src.Advanced.GenerateReadModel("gen", ev => 102 | { 103 | count++; 104 | }); 105 | count.Should().Be(MaxEntities); 106 | } 107 | 108 | public void Dispose() 109 | { 110 | _src.Advanced.ResetStorage(); 111 | _dest.Advanced.ResetStorage(); 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/Tests/JsonStuff.cs: -------------------------------------------------------------------------------- 1 | using DominoEventStore; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Diagnostics; 5 | using System.Linq; 6 | using FluentAssertions; 7 | using Utf8Json; 8 | using Xunit; 9 | using Xunit.Abstractions; 10 | 11 | namespace Tests 12 | { 13 | public class JsonStuff 14 | { 15 | private readonly ITestOutputHelper _w; 16 | private static MyEvent[] _myEvents = new[] { new MyEvent(), new MyEvent() { Name = "Strula" } }; 17 | 18 | public JsonStuff(ITestOutputHelper w) 19 | { 20 | _w = w; 21 | } 22 | [Fact] 23 | public void pack_unpack_events() 24 | { 25 | 26 | var ts = new Stopwatch(); 27 | ts.Start(); 28 | var ser = PackEvents(); 29 | _w.WriteLine($"Packing in {ts.Elapsed}"); 30 | ts.Restart(); 31 | var events = Utils.UnpackEvents(DateTimeOffset.Now, ser, new Dictionary()); 32 | _w.WriteLine($"Unpacking in {ts.Elapsed}"); 33 | ts.Stop(); 34 | events.Count.Should().Be(2); 35 | var last = events.Skip(1).First().CastAs(); 36 | last.Name.Should().Be("Strula"); 37 | last.Enum.Should().Be(MyEnum.First); 38 | last.Age.Should().Be(23); 39 | } 40 | 41 | private static string PackEvents() 42 | { 43 | var ser = Utils.PackEvents(_myEvents); 44 | return ser; 45 | } 46 | 47 | 48 | 49 | [Fact] 50 | public void pack_unpack_events_with_upcasting() 51 | { 52 | var ser = Utils.PackEvents(new[] { new MyEvent() { Age = 23 }, new MyEvent() { Name = "Strula", Age = 15 } }); 53 | var events = Utils.UnpackEvents(DateTimeOffset.Now, ser, new Dictionary() 54 | { 55 | { typeof(MyEvent),new MyEventUpcase()} 56 | }); 57 | var last = events.Skip(1).First().CastAs(); 58 | var first = events.First().CastAs(); 59 | first.Age.Should().Be(33); 60 | last.Name.Should().Be("Strula"); 61 | last.Age.Should().Be(25); 62 | } 63 | 64 | 65 | [Fact] 66 | public void pack_unpack_memento() 67 | { 68 | var memento = new MyMemento() { Age = 23, Data = "Hi" }; 69 | var data = Utils.PackSnapshot(memento); 70 | var unpackSnapshot = Utils.UnpackSnapshot(data); 71 | var des = unpackSnapshot as MyMemento; 72 | des.Should().BeEquivalentTo(memento); 73 | } 74 | 75 | public class MyMemento 76 | { 77 | public int Age { get; set; } 78 | public string Data { get; set; } 79 | public DateTimeOffset Date { get; set; } = DateTimeOffset.Now; 80 | 81 | } 82 | 83 | public enum MyEnum 84 | { 85 | None, 86 | First, Second 87 | } 88 | 89 | public class MyEvent 90 | { 91 | public string Name { get; set; } = "Bula"; 92 | public MyEnum Enum { get; set; } = MyEnum.First; 93 | public Guid SomeId { get; set; } = Guid.NewGuid(); 94 | public int Age { get; set; } = 23; 95 | public DateTime Date { get; set; } = DateTime.Now; 96 | } 97 | 98 | public class MyEventUpcase : AMapFromEventDataToObject 99 | { 100 | 101 | public override MyEvent Map(IDictionary existingData, MyEvent deserializedEvent, 102 | DateTimeOffset commitDate) 103 | { 104 | var age = Convert.ToInt32(existingData["Age"]); 105 | deserializedEvent.Age = age + 10; 106 | return deserializedEvent; 107 | } 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/Tests/MigratingEventsFacadeStoreTests.cs: -------------------------------------------------------------------------------- 1 |  2 | using FluentAssertions; 3 | using Xunit; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Collections.ObjectModel; 7 | using System.Linq; 8 | using DominoEventStore; 9 | using NSubstitute; 10 | using Xunit.Abstractions; 11 | 12 | 13 | namespace Tests 14 | { 15 | public class MigratingEventsFacadeStoreTests 16 | { 17 | private ISpecificDbStorage _store; 18 | private StoreFacade _sut; 19 | private IStoreEvents _dest; 20 | // private EventStoreSettings _settings= new EventStoreSettings(); 21 | private FakeImport _importer; 22 | private List _events; 23 | private CommittedEvents _commits; 24 | private EventStoreSettings _settings; 25 | 26 | public MigratingEventsFacadeStoreTests(ITestOutputHelper h) 27 | { 28 | _store = Substitute.For(); 29 | _settings = Setup.EventStoreSettings(h); 30 | _sut = new StoreFacade(_store, _settings); 31 | _dest = Substitute.For(); 32 | _importer=new FakeImport(); 33 | _dest.Advanced.Returns(_importer); 34 | } 35 | 36 | void SetupStore() 37 | { 38 | _store.StartOrContinue("m").Returns(new ProcessedCommitsCount(0)); 39 | 40 | _store.GetNextBatch(Arg.Any(), new ProcessedCommitsCount(3)).Returns(new CommittedEvents(new Commit[0])); 41 | CreateEvents(); 42 | _commits=new CommittedEvents( 43 | new []{ 44 | Setup.Commit(_events[0],_events[1]), 45 | Setup.Commit(_events[2]), 46 | Setup.Commit(_events[3]) } 47 | ); 48 | _store.GetNextBatch(Arg.Any(), new ProcessedCommitsCount(0)).Returns(_commits); 49 | } 50 | 51 | 52 | private void CreateEvents() 53 | { 54 | _events = Setup.Events(); 55 | } 56 | 57 | 58 | 59 | private static MigrationConfig GetMigrationConfig() 60 | { 61 | return new MigrationConfig("m"); 62 | } 63 | 64 | [Fact] 65 | public void simple_import_no_rewriting_no_casting() 66 | { 67 | SetupStore(); 68 | _sut.Advanced.MigrateEventsTo(_dest,"m"); 69 | _importer.Commits.Count.Should().Be(3); 70 | _importer.Commits[0].Should().BeEquivalentTo(_commits[0]); 71 | _importer.Commits[1].Should().BeEquivalentTo(_commits[1]); 72 | _importer.Commits[2].Should().BeEquivalentTo(_commits[2]); 73 | 74 | } 75 | 76 | [Fact] 77 | public void import_no_rewriting_with_casting() 78 | { 79 | _settings.AddMapper(new UpcastEvent1()); 80 | SetupStore(); 81 | _sut.Advanced.MigrateEventsTo(_dest,"m"); 82 | _importer.Commits.Count.Should().Be(3); 83 | //we avoid double upcasting 84 | var evs = _importer.Commits[0].GetEvents(new Dictionary()); 85 | evs.First().CastAs().Nr.Should().Be(_events[0].CastAs().Nr + 10); 86 | 87 | } 88 | 89 | [Fact] 90 | public void import_rewriting_with_casting() 91 | { 92 | _settings.AddMapper(new UpcastEvent1()); 93 | SetupStore(); 94 | _sut.Advanced.MigrateEventsTo(_dest,"m", c => 95 | { 96 | c.AddConverters(new RewriteEvent1()); 97 | }); 98 | _importer.Commits.Count.Should().Be(3); 99 | //we avoid double upcasting 100 | var evs = _importer.Commits[0].GetEvents(new Dictionary()); 101 | var event1 = evs.First().CastAs(); 102 | event1.Nr.Should().Be(_events[0].CastAs().Nr+10); 103 | event1.Name.Should().Be("rewritten"); 104 | } 105 | 106 | [Fact] 107 | public void import_rewriting_with_no_casting() 108 | { 109 | SetupStore(); 110 | _sut.Advanced.MigrateEventsTo(_dest,"m", c => 111 | { 112 | c.AddConverters(new RewriteEvent1()); 113 | }); 114 | _importer.Commits.Count.Should().Be(3); 115 | //we avoid double upcasting 116 | var evs = _importer.Commits[0].GetEvents(new Dictionary()); 117 | var event1 = evs.First().CastAs(); 118 | event1.Nr.Should().Be(_events[0].CastAs().Nr); 119 | event1.Name.Should().Be("rewritten"); 120 | } 121 | 122 | 123 | 124 | class FakeImport : IAdvancedFeatures 125 | { 126 | public void MigrateEventsTo(IStoreEvents newStorage, string name, Action config = null) 127 | { 128 | throw new NotImplementedException(); 129 | } 130 | 131 | public void ResetStorage() 132 | { 133 | throw new NotImplementedException(); 134 | } 135 | 136 | public List Commits { get; private set; }= new List(); 137 | 138 | public void ImportCommit(Commit commits) 139 | { 140 | Commits.Add(commits); 141 | } 142 | 143 | public void DeleteTenant(string tenantId) 144 | { 145 | throw new NotImplementedException(); 146 | } 147 | 148 | public void GenerateReadModel(string operationName, Action modelUpdater, Action config = null) 149 | { 150 | throw new NotImplementedException(); 151 | } 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/Tests/Providers/ASpecificStorageTests.cs: -------------------------------------------------------------------------------- 1 | using DominoEventStore; 2 | using FluentAssertions; 3 | using SqlFu; 4 | using System; 5 | using System.Diagnostics; 6 | using System.Linq; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | using CavemanTools.Logging; 10 | using Xunit; 11 | using Xunit.Abstractions; 12 | using Utils = DominoEventStore.Utils; 13 | 14 | namespace Tests 15 | { 16 | public abstract class ASpecificStorageTests:IDisposable 17 | { 18 | private readonly ITestOutputHelper _t; 19 | private readonly ISpecificDbStorage _store; 20 | private CancellationToken _cancellationToken; 21 | private CancellationTokenSource _src; 22 | 23 | 24 | protected ASpecificStorageTests(ITestOutputHelper t) 25 | { 26 | 27 | _t = t; 28 | _store = Setup.GetDbStorage(GetFactory(),t); 29 | _src=new CancellationTokenSource(); 30 | _cancellationToken = _src.Token; 31 | _store.InitStorage(); 32 | } 33 | 34 | protected abstract IDbFactory GetFactory(); 35 | protected virtual void DisposeOther() 36 | { 37 | 38 | } 39 | 40 | //[Fact] 41 | //public async Task mini_benchmark() 42 | //{ 43 | // LogManager.OutputTo(f => { }); 44 | // var s = new Stopwatch(); 45 | // var arr = Enumerable.Range(1, 1000).Select(d => Setup.UnversionedCommit()).ToArray(); 46 | 47 | 48 | // s.Start(); 49 | // //Parallel.For(1, 1000, async (i, state) => 50 | // //{ 51 | // // await _store.Append(arr[i - 1]); 52 | // //}); 53 | // foreach (var g in arr) await _store.Append(g); 54 | // s.Stop(); 55 | // _t.WriteLine($"1000 commits in {s.ElapsedMilliseconds}ms"); 56 | //} 57 | 58 | [Fact] 59 | public async Task append_then_get_events_no_snapshot() 60 | { 61 | var commit1 = Setup.UnversionedCommit(); 62 | var commit2 = Setup.UnversionedCommit(entityId:commit1.EntityId); 63 | var commit3 = Setup.UnversionedCommit(); 64 | await _store.Append(commit1); 65 | await _store.Append(commit2); 66 | await _store.Append(commit3); 67 | var data = await _store.GetData(Config(c=>c.OfEntity(commit1.EntityId)), _cancellationToken); 68 | var commits = data.Value.Commits.ToArray(); 69 | commits.Length.Should().Be(2); 70 | commits[0].Version.Should().Be(1); 71 | commits[0].EventData.Should().Be(commit1.EventData); 72 | commits[1].Version.Should().Be(2); 73 | commits[1].EventData.Should().Be(commit2.EventData); 74 | } 75 | 76 | [Fact] 77 | public async Task append_then_get_events_with_snapshot() 78 | { 79 | var commit1 = Setup.UnversionedCommit(); 80 | var commit2 = Setup.UnversionedCommit(entityId: commit1.EntityId); 81 | var commit3 = Setup.UnversionedCommit(entityId: commit1.EntityId); 82 | var snapshot = Setup.Snapshot(2, commit1.EntityId); 83 | 84 | await _store.Append(commit1); 85 | await _store.Append(commit2); 86 | await _store.Store(snapshot); 87 | await _store.Append(commit3); 88 | 89 | var data = await _store.GetData(Config(c => c.OfEntity(commit1.EntityId).IncludeSnapshots(true)), _cancellationToken); 90 | var commits = data.Value.Commits.ToArray(); 91 | data.Value.LatestSnapshot.HasValue.Should().BeTrue(); 92 | data.Value.LatestSnapshot.Value.Should().BeEquivalentTo(snapshot,i=>i.Excluding(d=>d.SnapshotDate)); 93 | commits.Length.Should().Be(1); 94 | commits[0].Version.Should().Be(3); 95 | } 96 | 97 | 98 | [Fact] 99 | public async Task append_with_snapshot_0_events_after() 100 | { 101 | var commit1 = Setup.UnversionedCommit(); 102 | var commit2 = Setup.UnversionedCommit(entityId: commit1.EntityId); 103 | 104 | var snapshot = Setup.Snapshot(2, commit1.EntityId); 105 | await _store.Append(commit1); 106 | await _store.Append(commit2); 107 | await _store.Store(snapshot); 108 | 109 | 110 | var data = await _store.GetData(Config(c => c.OfEntity(commit1.EntityId).IncludeSnapshots(true)), _cancellationToken); 111 | var commits = data.Value.Commits.ToArray(); 112 | data.Value.LatestSnapshot.HasValue.Should().BeTrue(); 113 | data.Value.LatestSnapshot.Value.Should().BeEquivalentTo(snapshot,i=>i.Excluding(d=>d.SnapshotDate)); 114 | commits.Length.Should().Be(0); 115 | } 116 | 117 | 118 | #if !IN_MEMORY 119 | [Fact(Skip = "Needs to be rewritten")] 120 | public void concurrency_Exception_when_trying_to_commit_with_an_existing_version() 121 | { 122 | 123 | //// if(Setup.IsAppVeyor) return; 124 | // var commit = Setup.UnversionedCommit(); 125 | // var comm2 = Setup.UnversionedCommit(entityId: commit.EntityId); 126 | // await _store.Append(commit); 127 | // _store.Import(new Commit(2, comm2)); 128 | // try 129 | // { 130 | // await _store.Append() 131 | // throw new Exception("Shouldn't get here"); 132 | // } 133 | // catch (ConcurrencyException ex) 134 | // { 135 | 136 | // } 137 | // catch 138 | // { 139 | // throw new Exception(); 140 | // } 141 | 142 | } 143 | #endif 144 | 145 | 146 | QueryConfig Config(Action cfg) 147 | { 148 | var c=new QueryConfig(); 149 | cfg(c); 150 | return c; 151 | } 152 | 153 | [Fact] 154 | public async Task existing_snapshot_with_same_entity_version_is_replaced() 155 | { 156 | var snap = Setup.Snapshot(3, Guid.NewGuid()); 157 | await _store.Store(snap); 158 | 159 | var snap1 = Setup.Snapshot(3, snap.EntityId); 160 | await _store.Store(snap1); 161 | 162 | var get = await _store.GetData(Config(c => c.OfEntity(snap.EntityId).IncludeSnapshots(true)), 163 | _cancellationToken); 164 | var mem2 = Utils.UnpackSnapshot(get.Value.LatestSnapshot.Value.SerializedData) as SomeMemento; 165 | var mem1 = Utils.UnpackSnapshot(snap1.SerializedData) as SomeMemento; 166 | mem2.Name.Should().Be(mem1.Name); 167 | mem2.IsOpen.Should().Be(mem1.IsOpen); 168 | 169 | 170 | } 171 | 172 | [Fact] 173 | public async Task only_most_recent_snapshot_is_used() 174 | { 175 | var snap = Setup.Snapshot(3, Guid.NewGuid()); 176 | await _store.Store(snap); 177 | 178 | var snap1 = Setup.Snapshot(4, snap.EntityId); 179 | await _store.Store(snap1); 180 | 181 | var get = await _store.GetData(Config(c => c.OfEntity(snap.EntityId).IncludeSnapshots(true)), 182 | _cancellationToken); 183 | var mem2 = Utils.UnpackSnapshot(get.Value.LatestSnapshot.Value.SerializedData) as SomeMemento; 184 | var mem1 = Utils.UnpackSnapshot(snap1.SerializedData) as SomeMemento; 185 | mem2.Name.Should().Be(mem1.Name); 186 | mem2.IsOpen.Should().Be(mem1.IsOpen); 187 | } 188 | 189 | [Fact] 190 | public async Task delete_snapshot() 191 | { 192 | var snap = Setup.Snapshot(3, Guid.NewGuid()); 193 | await _store.Append(Setup.UnversionedCommit(snap.TenantId, snap.EntityId)); 194 | await _store.Store(snap); 195 | var get = await _store.GetData(Config(c => c.OfEntity(snap.EntityId).IncludeSnapshots(true)), 196 | _cancellationToken); 197 | get.Value.LatestSnapshot.Value.Should().BeEquivalentTo(snap,i=>i.Excluding(d=>d.SnapshotDate)); 198 | await _store.DeleteSnapshot(snap.EntityId, snap.TenantId); 199 | get = await _store.GetData(Config(c => c.OfEntity(snap.EntityId).IncludeSnapshots(true)), 200 | _cancellationToken); 201 | get.Value.LatestSnapshot.IsEmpty.Should().BeTrue(); 202 | } 203 | 204 | [Fact] 205 | public async Task delete_specific_snapshot() 206 | { 207 | var snap = Setup.Snapshot(2, Guid.NewGuid()); 208 | await _store.Append(Setup.UnversionedCommit(snap.TenantId, snap.EntityId)); 209 | await _store.Store(snap); 210 | var snap1 = Setup.Snapshot(3, snap.EntityId, snap.TenantId); 211 | await _store.Store(snap1); 212 | 213 | await _store.DeleteSnapshot(snap.EntityId, snap.TenantId,snap.Version); 214 | var get = await _store.GetData(Config(c => c.OfEntity(snap.EntityId).IncludeSnapshots(true)), 215 | _cancellationToken); 216 | get.Value.LatestSnapshot.Value.Should().BeEquivalentTo(snap1,c=>c.Excluding(d=>d.SnapshotDate)); 217 | 218 | await _store.DeleteSnapshot(snap.EntityId, snap.TenantId, snap1.Version); 219 | get = await _store.GetData(Config(c => c.OfEntity(snap.EntityId).IncludeSnapshots(true)), 220 | _cancellationToken); 221 | get.Value.LatestSnapshot.IsEmpty.Should().BeTrue(); 222 | } 223 | 224 | [Fact] 225 | public void batch_start_returns_0() 226 | { 227 | _store.StartOrContinue(Guid.NewGuid().ToString()).Value.Should().Be(0); 228 | } 229 | 230 | [Fact] 231 | public void batch_continue_returns_savepoint() 232 | { 233 | var test =Guid.NewGuid().ToString(); 234 | _store.StartOrContinue(test); 235 | _store.UpdateProgress(test,5); 236 | _store.StartOrContinue(test).Value.Should().Be(5); 237 | } 238 | 239 | [Fact] 240 | public void batch_ends() 241 | { 242 | var test = Guid.NewGuid().ToString(); 243 | _store.StartOrContinue(test); 244 | _store.UpdateProgress(test, 5); 245 | _store.MarkOperationAsEnded(test); 246 | _store.StartOrContinue(test).Value.Should().Be(0); 247 | } 248 | 249 | [Fact] 250 | public async Task batch_get_next_for_read_model_all_events() 251 | { 252 | var entity = Guid.NewGuid(); 253 | await _store.Append(Setup.UnversionedCommit(entityId: entity)); 254 | await _store.Append(Setup.UnversionedCommit(entityId: entity)); 255 | await _store.Append(Setup.UnversionedCommit("1")); 256 | 257 | var rm=new ReadModelGenerationConfig("test"); 258 | 259 | var rez=_store.GetNextBatch(rm, 0); 260 | rez.IsEmpty.Should().BeFalse(); 261 | var first = rez.GetNext().Value; 262 | first.EntityId.Should().Be(entity); 263 | first.TenantId.Should().Be("_"); 264 | first.Version.Should().Be(1); 265 | 266 | var second = rez.GetNext().Value; 267 | second.EntityId.Should().Be(entity); 268 | second.TenantId.Should().Be("_"); 269 | second.Version.Should().Be(2); 270 | 271 | var third = rez.GetNext().Value; 272 | third.EntityId.Should().NotBe(entity); 273 | third.TenantId.Should().Be("1"); 274 | third.Version.Should().Be(1); 275 | 276 | rez.GetNext().IsEmpty.Should().BeTrue(); 277 | } 278 | 279 | [Fact] 280 | public async Task batch_get_next_for_migration_all_events() 281 | { 282 | LogManager.OutputToDebug(); 283 | var entity = Guid.NewGuid(); 284 | await _store.Append(Setup.UnversionedCommit(entityId: entity)); 285 | await _store.Append(Setup.UnversionedCommit(entityId: entity)); 286 | await _store.Append(Setup.UnversionedCommit("1")); 287 | 288 | var rm=new MigrationConfig("test"); 289 | 290 | var rez=_store.GetNextBatch(rm, 0); 291 | rez.IsEmpty.Should().BeFalse(); 292 | var first = rez.GetNext().Value; 293 | first.EntityId.Should().Be(entity); 294 | first.TenantId.Should().Be("_"); 295 | first.Version.Should().Be(1); 296 | 297 | var second = rez.GetNext().Value; 298 | second.EntityId.Should().Be(entity); 299 | second.TenantId.Should().Be("_"); 300 | second.Version.Should().Be(2); 301 | 302 | var third = rez.GetNext().Value; 303 | third.EntityId.Should().NotBe(entity); 304 | third.TenantId.Should().Be("1"); 305 | third.Version.Should().Be(1); 306 | 307 | rez.GetNext().IsEmpty.Should().BeTrue(); 308 | } 309 | 310 | public void Dispose() 311 | { 312 | _src.Cancel(true); 313 | _store.ResetStorage(); 314 | DisposeOther(); 315 | } 316 | } 317 | } -------------------------------------------------------------------------------- /src/Tests/Providers/InMemoryStoreTests.cs: -------------------------------------------------------------------------------- 1 | using DominoEventStore.Providers; 2 | using SqlFu; 3 | using Xunit.Abstractions; 4 | 5 | namespace Tests 6 | { 7 | public class InMemoryStoreTests : ASpecificStorageTests 8 | { 9 | public InMemoryStoreTests(ITestOutputHelper t) : base(t) 10 | { 11 | } 12 | 13 | protected override IDbFactory GetFactory() 14 | => null; 15 | } 16 | } -------------------------------------------------------------------------------- /src/Tests/Providers/SqlServerTests.cs: -------------------------------------------------------------------------------- 1 | using System.Data.SqlClient; 2 | using System.Data.SQLite; 3 | using DominoEventStore; 4 | using DominoEventStore.Providers; 5 | using SqlFu; 6 | using SqlFu.Providers.SqlServer; 7 | using Xunit; 8 | using Xunit.Abstractions; 9 | 10 | namespace Tests 11 | { 12 | [Collection("Sql server")] 13 | public class SqlServerTests : ASpecificStorageTests 14 | { 15 | public static string ConnectionString => 16 | Setup.IsAppVeyor 17 | ? @"Server=(local)\SQL2016;Database=tempdb;User ID=sa;Password=Password12!" 18 | : @"Data Source=(localdb)\ProjectsV13;Initial Catalog=tempdb;Integrated Security=True;Connect Timeout=30;Encrypt=False;TrustServerCertificate=True;ApplicationIntent=ReadWrite;MultiSubnetFailover=False"; 19 | //: @"Data Source=.\SQLExpress;Initial Catalog=tempdb;Integrated Security=True;MultipleActiveResultSets=True"; 20 | public SqlServerTests(ITestOutputHelper t) : base(t) 21 | { 22 | 23 | } 24 | protected override IDbFactory GetFactory() 25 | => new SqlFuConfig().CreateFactoryForTesting(new SqlServer2012Provider(SqlClientFactory.Instance.CreateConnection), ConnectionString); 26 | } 27 | } -------------------------------------------------------------------------------- /src/Tests/Providers/SqliteTests.cs: -------------------------------------------------------------------------------- 1 | using System.Data.SQLite; 2 | using SqlFu; 3 | using Xunit; 4 | using Xunit.Abstractions; 5 | 6 | namespace Tests 7 | { 8 | [Collection("sqlite")] 9 | 10 | public class SqliteTests : ASpecificStorageTests 11 | { 12 | public static string ConnectionString { get; } = "Data Source=test.db;Version=3;New=True;BinaryGUID=False"; 13 | 14 | 15 | public SqliteTests(ITestOutputHelper t) : base(t) 16 | { 17 | 18 | } 19 | 20 | protected override IDbFactory GetFactory() 21 | => new SqlFuConfig().CreateFactoryForTesting(new SqlFu.Providers.Sqlite.SqliteProvider(SQLiteFactory.Instance.CreateConnection), ConnectionString); 22 | 23 | protected override void DisposeOther() 24 | { 25 | 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /src/Tests/ReadModelGenFacadeTests.cs: -------------------------------------------------------------------------------- 1 |  2 | using FluentAssertions; 3 | using Xunit; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using DominoEventStore; 8 | using NSubstitute; 9 | using Xunit.Abstractions; 10 | 11 | 12 | namespace Tests 13 | { 14 | public class ReadModelGenFacadeTests 15 | { 16 | private readonly ITestOutputHelper _h; 17 | private ISpecificDbStorage _storage; 18 | private StoreFacade _sut; 19 | private ReadModelGenerationConfig _config; 20 | 21 | public ReadModelGenFacadeTests(ITestOutputHelper h) 22 | { 23 | _h = h; 24 | _storage = Substitute.For(); 25 | _sut = new StoreFacade(_storage, Setup.EventStoreSettings(h)); 26 | _config=new ReadModelGenerationConfig("test"); 27 | } 28 | 29 | void SetupCommits() 30 | { 31 | _storage.StartOrContinue("test").Returns(new ProcessedCommitsCount(0)); 32 | 33 | _storage.GetNextBatch(Arg.Any(), 2) 34 | .Returns(new CommittedEvents(new Commit[0])); 35 | _storage.GetNextBatch(Arg.Any(), 0) 36 | .Returns(Setup.CommittedEvents(2)); 37 | } 38 | 39 | [Fact] 40 | public void read_model_function_is_invoked_for_each_event() 41 | { 42 | var r=new List(); 43 | SetupCommits(); 44 | _sut.GenerateReadModel("test",e=>{ r.Add(e.GetType());}); 45 | 46 | r.Count(d => d == typeof(Event1)).Should().Be(2); 47 | r.Count(d => d == typeof(Event2)).Should().Be(2); 48 | } 49 | 50 | 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Tests/RewriteEvent1.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using DominoEventStore; 3 | 4 | namespace Tests 5 | { 6 | public class RewriteEvent1:ARewriteEvent 7 | { 8 | public override Event1 Rewrite(dynamic jsonData, Event1 deserializedEvent, DateTimeOffset commitDate) 9 | { 10 | deserializedEvent.Name = "rewritten"; 11 | return deserializedEvent; 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /src/Tests/Setup.cs: -------------------------------------------------------------------------------- 1 | using DominoEventStore; 2 | using DominoEventStore.Providers; 3 | using SqlFu; 4 | using SqlFu.Providers.SqlServer; 5 | using System; 6 | using System.Collections.Generic; 7 | 8 | using System.Linq; 9 | using AutoFixture; 10 | using CavemanTools.Logging; 11 | using Serilog; 12 | using Xunit; 13 | using Xunit.Abstractions; 14 | using Utils = DominoEventStore.Utils; 15 | [assembly: CollectionBehavior(DisableTestParallelization = true, MaxParallelThreads = 1)] 16 | namespace Tests 17 | { 18 | public static class Setup 19 | { 20 | public static EventStoreSettings EventStoreSettings(ITestOutputHelper h) 21 | { 22 | Logger(h); 23 | 24 | var f = new EventStoreSettings(); 25 | 26 | return f; 27 | } 28 | 29 | public static void Logger(ITestOutputHelper h) 30 | { 31 | var l = new LoggerConfiguration().MinimumLevel.Information().WriteTo.TestOutput(h).CreateLogger(); 32 | EventStore.WithLogger(l); 33 | } 34 | 35 | public const string TestSchema = ""; 36 | 37 | public static ISpecificDbStorage GetDbStorage(IDbFactory f,ITestOutputHelper t) 38 | { 39 | if (f==null) return new InMemory(); 40 | var provs=new Dictionary() 41 | { 42 | { 43 | SqlServer2012Provider.Id 44 | ,new SqlServerProvider(f) 45 | }, 46 | { 47 | SqlFu.Providers.Sqlite.SqliteProvider.Id 48 | ,new SqliteProvider(f) 49 | } 50 | }; 51 | ProviderExtensions.RegisterSqlFuConfig(f.Configuration); 52 | 53 | f.Configuration.UseLogManager(); 54 | Log.Logger=new LoggerConfiguration().MinimumLevel.Error().WriteTo.TestOutput(t).CreateLogger(); 55 | // LogManager.OutputTo(t.WriteLine); 56 | return provs[f.Provider.ProviderId]; 57 | } 58 | 59 | public static readonly bool IsAppVeyor = Environment.GetEnvironmentVariable("Appveyor")?.ToUpperInvariant() == "TRUE"; 60 | 61 | public static readonly Guid EntityId = Guid.NewGuid(); 62 | 63 | public static IEnumerable Commits(int count) => Commits(count); 64 | public static Commit Commit(params object[] events) => new Commit("_",Guid.NewGuid(), Utils.PackEvents(events),Guid.NewGuid(), DateTimeOffset.Now, 1); 65 | 66 | public static IEnumerable Commits(int count) where T : class, new() where V : class, new() 67 | { 68 | return Enumerable.Range(1, count) 69 | .Select(i => new Commit("_", Setup.EntityId, Utils.PackEvents(new Object[]{new T(), new V()}), Guid.NewGuid(), DateTimeOffset.Now, i)); 70 | } 71 | 72 | public static CommittedEvents CommittedEvents(int count) where T : class, new() where V : class, new() 73 | => new CommittedEvents(Setup.Commits(count).ToArray()); 74 | 75 | public static IEnumerable GetEvents(this Commit commit, IReadOnlyDictionary upc) 76 | { 77 | return Utils.UnpackEvents(commit.Timestamp, commit.EventData, upc); 78 | } 79 | 80 | public static UnversionedCommit UnversionedCommit(string tenantId = "_", Guid? entityId = null,Guid? commit=null) 81 | => 82 | new UnversionedCommit(tenantId, entityId ?? Guid.NewGuid(), Utils.PackEvents(Events(1)), commit??Guid.NewGuid(), 83 | DateTimeOffset.Now); 84 | 85 | 86 | public static List Events(int count=4) 87 | { 88 | var f = new Fixture(); 89 | return Enumerable.Range(1, count) 90 | .Select(i => i % 2 == 1 ? (object)f.Create() : f.Create()) 91 | .ToList(); 92 | } 93 | 94 | public static Snapshot Snapshot(int ver,Guid entity,string tenant="_") 95 | => new Snapshot(ver,entity,tenant,Utils.PackSnapshot(new Fixture().Create()),DateTimeOffset.Now); 96 | 97 | public static Func> EventDeserializerWIthoutUpcasting() 98 | { 99 | return c=>c.GetEvents(new Dictionary()); 100 | } 101 | } 102 | 103 | 104 | 105 | } -------------------------------------------------------------------------------- /src/Tests/SomeEvent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Tests 4 | { 5 | public class SomeEvent 6 | { 7 | public int Id { get; set; } 8 | public string Name { get; set; } 9 | public DateTime CreatedOn { get; set; } 10 | public string Email { get; set; } 11 | } 12 | } -------------------------------------------------------------------------------- /src/Tests/SomeMemento.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Tests 4 | { 5 | public class SomeMemento 6 | { 7 | public bool IsOpen { get; set; } 8 | public DateTimeOffset CreatedOn { get; set; } 9 | public string Name { get; set; } 10 | } 11 | } -------------------------------------------------------------------------------- /src/Tests/Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netcoreapp2.1;net472 4 | false 5 | 6 | Library 7 | 8 | 9 | 10 | TRACE;DEBUG;NETCOREAPP2_1; 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | all 23 | runtime; build; native; contentfiles; analyzers 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/Tests/UpcastEvent1.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using DominoEventStore; 4 | 5 | namespace Tests 6 | { 7 | public class UpcastEvent1 : AMapFromEventDataToObject 8 | { 9 | public override Event1 Map(IDictionary existingData, Event1 deserializedEvent, 10 | DateTimeOffset commitDate) 11 | { 12 | deserializedEvent.Nr += 10; 13 | return deserializedEvent; 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /src/domino.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26730.16 5 | MinimumVisualStudioVersion = 15.0.26124.0 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DominoEventStore", "DominoEventStore\DominoEventStore.csproj", "{72FD2C75-51A2-4F54-A02D-1359A494C528}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tests", "Tests\Tests.csproj", "{DE05653A-DCBE-4763-B9F0-6DEC9A6FB726}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Extra", "Extra", "{B7297E9B-66B0-40D7-B894-22C93F8B33D6}" 11 | ProjectSection(SolutionItems) = preProject 12 | ..\appveyor.yml = ..\appveyor.yml 13 | ..\README.md = ..\README.md 14 | EndProjectSection 15 | EndProject 16 | Global 17 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 18 | Debug|Any CPU = Debug|Any CPU 19 | Debug|x64 = Debug|x64 20 | Debug|x86 = Debug|x86 21 | Release|Any CPU = Release|Any CPU 22 | Release|x64 = Release|x64 23 | Release|x86 = Release|x86 24 | EndGlobalSection 25 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 26 | {72FD2C75-51A2-4F54-A02D-1359A494C528}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {72FD2C75-51A2-4F54-A02D-1359A494C528}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {72FD2C75-51A2-4F54-A02D-1359A494C528}.Debug|x64.ActiveCfg = Debug|Any CPU 29 | {72FD2C75-51A2-4F54-A02D-1359A494C528}.Debug|x64.Build.0 = Debug|Any CPU 30 | {72FD2C75-51A2-4F54-A02D-1359A494C528}.Debug|x86.ActiveCfg = Debug|Any CPU 31 | {72FD2C75-51A2-4F54-A02D-1359A494C528}.Debug|x86.Build.0 = Debug|Any CPU 32 | {72FD2C75-51A2-4F54-A02D-1359A494C528}.Release|Any CPU.ActiveCfg = Release|Any CPU 33 | {72FD2C75-51A2-4F54-A02D-1359A494C528}.Release|Any CPU.Build.0 = Release|Any CPU 34 | {72FD2C75-51A2-4F54-A02D-1359A494C528}.Release|x64.ActiveCfg = Release|Any CPU 35 | {72FD2C75-51A2-4F54-A02D-1359A494C528}.Release|x64.Build.0 = Release|Any CPU 36 | {72FD2C75-51A2-4F54-A02D-1359A494C528}.Release|x86.ActiveCfg = Release|Any CPU 37 | {72FD2C75-51A2-4F54-A02D-1359A494C528}.Release|x86.Build.0 = Release|Any CPU 38 | {DE05653A-DCBE-4763-B9F0-6DEC9A6FB726}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 39 | {DE05653A-DCBE-4763-B9F0-6DEC9A6FB726}.Debug|Any CPU.Build.0 = Debug|Any CPU 40 | {DE05653A-DCBE-4763-B9F0-6DEC9A6FB726}.Debug|x64.ActiveCfg = Debug|Any CPU 41 | {DE05653A-DCBE-4763-B9F0-6DEC9A6FB726}.Debug|x64.Build.0 = Debug|Any CPU 42 | {DE05653A-DCBE-4763-B9F0-6DEC9A6FB726}.Debug|x86.ActiveCfg = Debug|Any CPU 43 | {DE05653A-DCBE-4763-B9F0-6DEC9A6FB726}.Debug|x86.Build.0 = Debug|Any CPU 44 | {DE05653A-DCBE-4763-B9F0-6DEC9A6FB726}.Release|Any CPU.ActiveCfg = Release|Any CPU 45 | {DE05653A-DCBE-4763-B9F0-6DEC9A6FB726}.Release|Any CPU.Build.0 = Release|Any CPU 46 | {DE05653A-DCBE-4763-B9F0-6DEC9A6FB726}.Release|x64.ActiveCfg = Release|Any CPU 47 | {DE05653A-DCBE-4763-B9F0-6DEC9A6FB726}.Release|x64.Build.0 = Release|Any CPU 48 | {DE05653A-DCBE-4763-B9F0-6DEC9A6FB726}.Release|x86.ActiveCfg = Release|Any CPU 49 | {DE05653A-DCBE-4763-B9F0-6DEC9A6FB726}.Release|x86.Build.0 = Release|Any CPU 50 | EndGlobalSection 51 | GlobalSection(SolutionProperties) = preSolution 52 | HideSolutionNode = FALSE 53 | EndGlobalSection 54 | GlobalSection(ExtensibilityGlobals) = postSolution 55 | SolutionGuid = {DA391DD0-BC1C-4A4E-AB0A-B4322B42D136} 56 | EndGlobalSection 57 | EndGlobal 58 | --------------------------------------------------------------------------------