├── .gitignore ├── Directory.Build.props ├── LICENSE ├── README.md ├── UserManagement.sln ├── docker-compose.yml ├── specs └── images │ ├── actors-diagram.drawio │ └── actors-diagram.png ├── src ├── UserManagement.Actors │ ├── Chaos │ │ ├── ChaosHelper.cs │ │ └── Headers.cs │ ├── Configuration │ │ └── ActorSettings.cs │ ├── Constants │ │ └── ActorNames.cs │ ├── Managers │ │ ├── ActorManager.cs │ │ └── IActorManager.cs │ ├── RequestActor.cs │ ├── UserActor.cs │ └── UserManagement.Actors.csproj ├── UserManagement.Api │ ├── Configuration │ │ └── ApiSettings.cs │ ├── Constants │ │ └── ExternalErrorReason.cs │ ├── Controllers │ │ ├── BaseController.cs │ │ └── UsersController.cs │ ├── Dockerfile │ ├── Logging │ │ └── RequestLoggingMiddleware.cs │ ├── Models │ │ ├── Requests │ │ │ ├── CreateUserRequest.cs │ │ │ └── GetItemsFilterRequest.cs │ │ └── Responses │ │ │ ├── ErrorResponse.cs │ │ │ ├── UserDetailsResponse.cs │ │ │ └── UsersListResponse.cs │ ├── Monitoring │ │ ├── ChaosMiddleware.cs │ │ └── RequestMonitoringMiddleware.cs │ ├── Program.cs │ ├── Startup.cs │ ├── UserManagement.Api.csproj │ └── appsettings.json ├── UserManagement.Commands │ ├── CreateUser.cs │ ├── DeleteUser.cs │ ├── GetUser.cs │ ├── GetUsers.cs │ ├── UserManagement.Commands.csproj │ └── UserMessage.cs ├── UserManagement.Domain │ ├── UserManagement.Domain.csproj │ ├── Users.cs │ └── ValueObjects │ │ ├── User.cs │ │ └── ValueObject.cs ├── UserManagement.Events │ ├── UnexpectedErrorOcurred.cs │ ├── UserCreated.cs │ ├── UserDeleted.cs │ ├── UserEvent.cs │ ├── UserManagement.Events.csproj │ ├── UserNotFound.cs │ ├── UserRetrieved.cs │ └── UsersRetrieved.cs └── UserManagement.Persistence │ ├── InMemoryProvider.cs │ └── UserManagement.Persistence.csproj └── test └── UserManagement.Domain.Tests ├── UserManagement.Domain.Tests.csproj ├── UsersTests.cs └── ValueObjects └── UserTests.cs /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015/2017 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # Visual Studio 2017 auto generated files 33 | Generated\ Files/ 34 | 35 | # MSTest test Results 36 | [Tt]est[Rr]esult*/ 37 | [Bb]uild[Ll]og.* 38 | 39 | # NUNIT 40 | *.VisualState.xml 41 | TestResult.xml 42 | 43 | # Build Results of an ATL Project 44 | [Dd]ebugPS/ 45 | [Rr]eleasePS/ 46 | dlldata.c 47 | 48 | # Benchmark Results 49 | BenchmarkDotNet.Artifacts/ 50 | 51 | # .NET Core 52 | project.lock.json 53 | project.fragment.lock.json 54 | artifacts/ 55 | **/Properties/launchSettings.json 56 | 57 | # StyleCop 58 | StyleCopReport.xml 59 | 60 | # Files built by Visual Studio 61 | *_i.c 62 | *_p.c 63 | *_i.h 64 | *.ilk 65 | *.meta 66 | *.obj 67 | *.iobj 68 | *.pch 69 | *.pdb 70 | *.ipdb 71 | *.pgc 72 | *.pgd 73 | *.rsp 74 | *.sbr 75 | *.tlb 76 | *.tli 77 | *.tlh 78 | *.tmp 79 | *.tmp_proj 80 | *.log 81 | *.vspscc 82 | *.vssscc 83 | .builds 84 | *.pidb 85 | *.svclog 86 | *.scc 87 | 88 | # Chutzpah Test files 89 | _Chutzpah* 90 | 91 | # Visual C++ cache files 92 | ipch/ 93 | *.aps 94 | *.ncb 95 | *.opendb 96 | *.opensdf 97 | *.sdf 98 | *.cachefile 99 | *.VC.db 100 | *.VC.VC.opendb 101 | 102 | # Visual Studio profiler 103 | *.psess 104 | *.vsp 105 | *.vspx 106 | *.sap 107 | 108 | # Visual Studio Trace Files 109 | *.e2e 110 | 111 | # TFS 2012 Local Workspace 112 | $tf/ 113 | 114 | # Guidance Automation Toolkit 115 | *.gpState 116 | 117 | # ReSharper is a .NET coding add-in 118 | _ReSharper*/ 119 | *.[Rr]e[Ss]harper 120 | *.DotSettings.user 121 | 122 | # JustCode is a .NET coding add-in 123 | .JustCode 124 | 125 | # TeamCity is a build add-in 126 | _TeamCity* 127 | 128 | # DotCover is a Code Coverage Tool 129 | *.dotCover 130 | 131 | # AxoCover is a Code Coverage Tool 132 | .axoCover/* 133 | !.axoCover/settings.json 134 | 135 | # Visual Studio code coverage results 136 | *.coverage 137 | *.coveragexml 138 | 139 | # NCrunch 140 | _NCrunch_* 141 | .*crunch*.local.xml 142 | nCrunchTemp_* 143 | 144 | # MightyMoose 145 | *.mm.* 146 | AutoTest.Net/ 147 | 148 | # Web workbench (sass) 149 | .sass-cache/ 150 | 151 | # Installshield output folder 152 | [Ee]xpress/ 153 | 154 | # DocProject is a documentation generator add-in 155 | DocProject/buildhelp/ 156 | DocProject/Help/*.HxT 157 | DocProject/Help/*.HxC 158 | DocProject/Help/*.hhc 159 | DocProject/Help/*.hhk 160 | DocProject/Help/*.hhp 161 | DocProject/Help/Html2 162 | DocProject/Help/html 163 | 164 | # Click-Once directory 165 | publish/ 166 | 167 | # Publish Web Output 168 | *.[Pp]ublish.xml 169 | *.azurePubxml 170 | # Note: Comment the next line if you want to checkin your web deploy settings, 171 | # but database connection strings (with potential passwords) will be unencrypted 172 | *.pubxml 173 | *.publishproj 174 | 175 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 176 | # checkin your Azure Web App publish settings, but sensitive information contained 177 | # in these scripts will be unencrypted 178 | PublishScripts/ 179 | 180 | # NuGet Packages 181 | *.nupkg 182 | # The packages folder can be ignored because of Package Restore 183 | **/[Pp]ackages/* 184 | # except build/, which is used as an MSBuild target. 185 | !**/[Pp]ackages/build/ 186 | # Uncomment if necessary however generally it will be regenerated when needed 187 | #!**/[Pp]ackages/repositories.config 188 | # NuGet v3's project.json files produces more ignorable files 189 | *.nuget.props 190 | *.nuget.targets 191 | 192 | # Microsoft Azure Build Output 193 | csx/ 194 | *.build.csdef 195 | 196 | # Microsoft Azure Emulator 197 | ecf/ 198 | rcf/ 199 | 200 | # Windows Store app package directories and files 201 | AppPackages/ 202 | BundleArtifacts/ 203 | Package.StoreAssociation.xml 204 | _pkginfo.txt 205 | *.appx 206 | 207 | # Visual Studio cache files 208 | # files ending in .cache can be ignored 209 | *.[Cc]ache 210 | # but keep track of directories ending in .cache 211 | !*.[Cc]ache/ 212 | 213 | # Others 214 | ClientBin/ 215 | ~$* 216 | *~ 217 | *.dbmdl 218 | *.dbproj.schemaview 219 | *.jfm 220 | *.pfx 221 | *.publishsettings 222 | orleans.codegen.cs 223 | 224 | # Including strong name files can present a security risk 225 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 226 | #*.snk 227 | 228 | # Since there are multiple workflows, uncomment next line to ignore bower_components 229 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 230 | #bower_components/ 231 | 232 | # RIA/Silverlight projects 233 | Generated_Code/ 234 | 235 | # Backup & report files from converting an old project file 236 | # to a newer Visual Studio version. Backup files are not needed, 237 | # because we have git ;-) 238 | _UpgradeReport_Files/ 239 | Backup*/ 240 | UpgradeLog*.XML 241 | UpgradeLog*.htm 242 | ServiceFabricBackup/ 243 | *.rptproj.bak 244 | 245 | # SQL Server files 246 | *.mdf 247 | *.ldf 248 | *.ndf 249 | 250 | # Business Intelligence projects 251 | *.rdl.data 252 | *.bim.layout 253 | *.bim_*.settings 254 | *.rptproj.rsuser 255 | 256 | # Microsoft Fakes 257 | FakesAssemblies/ 258 | 259 | # GhostDoc plugin setting file 260 | *.GhostDoc.xml 261 | 262 | # Node.js Tools for Visual Studio 263 | .ntvs_analysis.dat 264 | node_modules/ 265 | 266 | # Visual Studio 6 build log 267 | *.plg 268 | 269 | # Visual Studio 6 workspace options file 270 | *.opt 271 | 272 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 273 | *.vbw 274 | 275 | # Visual Studio LightSwitch build output 276 | **/*.HTMLClient/GeneratedArtifacts 277 | **/*.DesktopClient/GeneratedArtifacts 278 | **/*.DesktopClient/ModelManifest.xml 279 | **/*.Server/GeneratedArtifacts 280 | **/*.Server/ModelManifest.xml 281 | _Pvt_Extensions 282 | 283 | # Paket dependency manager 284 | .paket/paket.exe 285 | paket-files/ 286 | 287 | # FAKE - F# Make 288 | .fake/ 289 | 290 | # JetBrains Rider 291 | .idea/ 292 | *.sln.iml 293 | 294 | # CodeRush 295 | .cr/ 296 | 297 | # Python Tools for Visual Studio (PTVS) 298 | __pycache__/ 299 | *.pyc 300 | 301 | # Cake - Uncomment if you are using it 302 | # tools/** 303 | # !tools/packages.config 304 | 305 | # Tabs Studio 306 | *.tss 307 | 308 | # Telerik's JustMock configuration file 309 | *.jmconfig 310 | 311 | # BizTalk build output 312 | *.btp.cs 313 | *.btm.cs 314 | *.odx.cs 315 | *.xsd.cs 316 | 317 | # OpenCover UI analysis results 318 | OpenCover/ 319 | 320 | # Azure Stream Analytics local run output 321 | ASALocalRun/ 322 | 323 | # MSBuild Binary and Structured Log 324 | *.binlog 325 | 326 | # NVidia Nsight GPU debugger configuration file 327 | *.nvuser 328 | 329 | # MFractors (Xamarin productivity tool) working folder 330 | .mfractor/ 331 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | true 4 | true 5 | latest 6 | 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Ignas 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Proto.Actor UserManagement API 2 | The User Management API built using Proto.Actor and ASP.NET Core 2.1 frameworks. The API, Jaeger, and SEQ are dockerized, hence to run the whole solution simply run the following command: 3 | ``` 4 | docker-compose up --build 5 | ``` -------------------------------------------------------------------------------- /UserManagement.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.28307.421 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UserManagement.Api", "src\UserManagement.Api\UserManagement.Api.csproj", "{0AECE3B6-2559-41E8-A079-894A6E5A96A8}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UserManagement.Persistence", "src\UserManagement.Persistence\UserManagement.Persistence.csproj", "{9E461649-F980-42C3-9BF8-8FCC17007F8E}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UserManagement.Domain", "src\UserManagement.Domain\UserManagement.Domain.csproj", "{ED56B142-EB22-4910-8303-3BEE96F7189E}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UserManagement.Actors", "src\UserManagement.Actors\UserManagement.Actors.csproj", "{88D799FE-3C62-4413-A9E9-AA14C71F76B9}" 13 | EndProject 14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UserManagement.Commands", "src\UserManagement.Commands\UserManagement.Commands.csproj", "{1CCB0905-158A-44B3-804E-BD050C9060CE}" 15 | EndProject 16 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{F081986E-3EA1-4971-BF10-487D6C4EA633}" 17 | EndProject 18 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{F2B40294-B5AA-41F7-AA5C-5E2A67921476}" 19 | EndProject 20 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UserManagement.Domain.Tests", "test\UserManagement.Domain.Tests\UserManagement.Domain.Tests.csproj", "{EBEBDE20-349D-41EB-8865-7773965AA348}" 21 | EndProject 22 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UserManagement.Events", "src\UserManagement.Events\UserManagement.Events.csproj", "{DC8601C0-320C-4015-9945-50AE53B4D2B2}" 23 | EndProject 24 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{0D16C1F6-61F5-46EB-BCC2-706F89C72D8F}" 25 | ProjectSection(SolutionItems) = preProject 26 | docker-compose.yml = docker-compose.yml 27 | EndProjectSection 28 | EndProject 29 | Global 30 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 31 | Debug|Any CPU = Debug|Any CPU 32 | Release|Any CPU = Release|Any CPU 33 | EndGlobalSection 34 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 35 | {0AECE3B6-2559-41E8-A079-894A6E5A96A8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 36 | {0AECE3B6-2559-41E8-A079-894A6E5A96A8}.Debug|Any CPU.Build.0 = Debug|Any CPU 37 | {0AECE3B6-2559-41E8-A079-894A6E5A96A8}.Release|Any CPU.ActiveCfg = Release|Any CPU 38 | {0AECE3B6-2559-41E8-A079-894A6E5A96A8}.Release|Any CPU.Build.0 = Release|Any CPU 39 | {9E461649-F980-42C3-9BF8-8FCC17007F8E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 40 | {9E461649-F980-42C3-9BF8-8FCC17007F8E}.Debug|Any CPU.Build.0 = Debug|Any CPU 41 | {9E461649-F980-42C3-9BF8-8FCC17007F8E}.Release|Any CPU.ActiveCfg = Release|Any CPU 42 | {9E461649-F980-42C3-9BF8-8FCC17007F8E}.Release|Any CPU.Build.0 = Release|Any CPU 43 | {ED56B142-EB22-4910-8303-3BEE96F7189E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 44 | {ED56B142-EB22-4910-8303-3BEE96F7189E}.Debug|Any CPU.Build.0 = Debug|Any CPU 45 | {ED56B142-EB22-4910-8303-3BEE96F7189E}.Release|Any CPU.ActiveCfg = Release|Any CPU 46 | {ED56B142-EB22-4910-8303-3BEE96F7189E}.Release|Any CPU.Build.0 = Release|Any CPU 47 | {88D799FE-3C62-4413-A9E9-AA14C71F76B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 48 | {88D799FE-3C62-4413-A9E9-AA14C71F76B9}.Debug|Any CPU.Build.0 = Debug|Any CPU 49 | {88D799FE-3C62-4413-A9E9-AA14C71F76B9}.Release|Any CPU.ActiveCfg = Release|Any CPU 50 | {88D799FE-3C62-4413-A9E9-AA14C71F76B9}.Release|Any CPU.Build.0 = Release|Any CPU 51 | {1CCB0905-158A-44B3-804E-BD050C9060CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 52 | {1CCB0905-158A-44B3-804E-BD050C9060CE}.Debug|Any CPU.Build.0 = Debug|Any CPU 53 | {1CCB0905-158A-44B3-804E-BD050C9060CE}.Release|Any CPU.ActiveCfg = Release|Any CPU 54 | {1CCB0905-158A-44B3-804E-BD050C9060CE}.Release|Any CPU.Build.0 = Release|Any CPU 55 | {EBEBDE20-349D-41EB-8865-7773965AA348}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 56 | {EBEBDE20-349D-41EB-8865-7773965AA348}.Debug|Any CPU.Build.0 = Debug|Any CPU 57 | {EBEBDE20-349D-41EB-8865-7773965AA348}.Release|Any CPU.ActiveCfg = Release|Any CPU 58 | {EBEBDE20-349D-41EB-8865-7773965AA348}.Release|Any CPU.Build.0 = Release|Any CPU 59 | {DC8601C0-320C-4015-9945-50AE53B4D2B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 60 | {DC8601C0-320C-4015-9945-50AE53B4D2B2}.Debug|Any CPU.Build.0 = Debug|Any CPU 61 | {DC8601C0-320C-4015-9945-50AE53B4D2B2}.Release|Any CPU.ActiveCfg = Release|Any CPU 62 | {DC8601C0-320C-4015-9945-50AE53B4D2B2}.Release|Any CPU.Build.0 = Release|Any CPU 63 | EndGlobalSection 64 | GlobalSection(SolutionProperties) = preSolution 65 | HideSolutionNode = FALSE 66 | EndGlobalSection 67 | GlobalSection(NestedProjects) = preSolution 68 | {0AECE3B6-2559-41E8-A079-894A6E5A96A8} = {F081986E-3EA1-4971-BF10-487D6C4EA633} 69 | {9E461649-F980-42C3-9BF8-8FCC17007F8E} = {F081986E-3EA1-4971-BF10-487D6C4EA633} 70 | {ED56B142-EB22-4910-8303-3BEE96F7189E} = {F081986E-3EA1-4971-BF10-487D6C4EA633} 71 | {88D799FE-3C62-4413-A9E9-AA14C71F76B9} = {F081986E-3EA1-4971-BF10-487D6C4EA633} 72 | {1CCB0905-158A-44B3-804E-BD050C9060CE} = {F081986E-3EA1-4971-BF10-487D6C4EA633} 73 | {EBEBDE20-349D-41EB-8865-7773965AA348} = {F2B40294-B5AA-41F7-AA5C-5E2A67921476} 74 | {DC8601C0-320C-4015-9945-50AE53B4D2B2} = {F081986E-3EA1-4971-BF10-487D6C4EA633} 75 | EndGlobalSection 76 | GlobalSection(ExtensibilityGlobals) = postSolution 77 | SolutionGuid = {1E5B6B3B-00B2-4FBF-B094-313087AF3699} 78 | EndGlobalSection 79 | EndGlobal 80 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.5' 2 | 3 | services: 4 | 5 | users-jaegertracing: 6 | image: jaegertracing/all-in-one:latest 7 | ports: 8 | - "5775:5775/udp" 9 | - "6831:6831/udp" 10 | - "6832:6832/udp" 11 | - "5778:5778" 12 | - "16686:16686" 13 | - "14268:14268" 14 | - "9411:9411" 15 | 16 | users-seq: 17 | image: datalust/seq:latest 18 | environment: 19 | ACCEPT_EULA: Y 20 | ports: 21 | - "5341:80" 22 | 23 | users-api: 24 | build: 25 | context: . 26 | dockerfile: ./src/UserManagement.Api/Dockerfile 27 | depends_on: 28 | - users-seq 29 | - users-jaegertracing 30 | environment: 31 | - ASPNETCORE_URLS=http://*:5000 32 | - ASPNETCORE_ENVIRONMENT=Development 33 | # Override local config values 34 | - Seq:ServerUrl=http://users-seq:5341 35 | - Jaeger:JAEGER_AGENT_HOST=users-jaegertracing 36 | ports: 37 | - "5000:5000" 38 | -------------------------------------------------------------------------------- /specs/images/actors-diagram.drawio: -------------------------------------------------------------------------------- 1 | vZXfj9MwDMf/mkrwcGhrt2p75PaDOwkkxMQBj7nGbcNldUndteOvJ1mdtdW4CSSOp9Yf20nsb9wG0WrfvjOizD+gBB2EE9kG0ToIw2m0nNmHI8eOLGaLDmRGSQ7qwU79BIYTprWSUI0CCVGTKscwwaKAhEZMGIPNOCxFPd61FBlcgF0i9CX9oiTlTKfxsnfcgcpyv3U854r3wkdzKVUuJDYDFG2CaGUQqXvbtyvQrnu+MV3e9hnv+WQGCvqThMPD9n69TM3Hzc1d/VAuZfl0fxPx2ejoKwZpG8AmGsoxw0LoTU9vDdaFBLfqxFp9zHvE0sKphd+B6MhqiprQopz2mr32wOb4lfNPxjdnvJl7c90OnesjWxUJQ2+dqhYkWlSVSjzeKu2X70pydTzbKUYV1iaBK+3xV06YDOhKXHjW004C4B7ssW2eAS1IHcbnEHwls3NcL5p9Yd3+QkNe9yB0zTt9gh81VOQ2SwiNfb7qqn99KbfWdpacrE2uCHalODWkseM8Fk1UZTdgqWqd+Lep7fgKtV3eLRTJOSzk7CSHwScYeBbhYxTHLgML4ksxjc9CHcAQtNelumwtJ8x4uPznhc1mMKt+/vLhmHr4z8UIL8T4XIEZKpHkSssXFCJN0zBJfieEjB/j+QsJEUb/Twlr9p/Nk2/w94k2vwA= -------------------------------------------------------------------------------- /specs/images/actors-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ignas-sakalauskas/ProtoActorUserManagementApi/5b8915ae0d92e7841e414dc459c271698e16d8ef/specs/images/actors-diagram.png -------------------------------------------------------------------------------- /src/UserManagement.Actors/Chaos/ChaosHelper.cs: -------------------------------------------------------------------------------- 1 | using OpenTracing; 2 | 3 | namespace UserManagement.Actors.Chaos 4 | { 5 | public static class ChaosHelper 6 | { 7 | public static bool ShouldCreateUserCrash(ISpan activeSpan) 8 | { 9 | if (activeSpan == null) 10 | return false; 11 | 12 | return activeSpan.GetBaggageItem(Headers.ChaosType) == Headers.CreateUserDown; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/UserManagement.Actors/Chaos/Headers.cs: -------------------------------------------------------------------------------- 1 | namespace UserManagement.Actors.Chaos 2 | { 3 | public static class Headers 4 | { 5 | public const string CreateUserDown = "create_user_down"; 6 | public const string ChaosType = "chaos_type"; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/UserManagement.Actors/Configuration/ActorSettings.cs: -------------------------------------------------------------------------------- 1 | namespace UserManagement.Actors.Configuration 2 | { 3 | public class ActorSettings 4 | { 5 | public int ChildActorTimeoutInMilliseconds { get; set; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/UserManagement.Actors/Constants/ActorNames.cs: -------------------------------------------------------------------------------- 1 | namespace UserManagement.Actors.Constants 2 | { 3 | public static class ActorNames 4 | { 5 | public static string RequestActor = "RequestActor"; 6 | public static string UserActor = "UserActor"; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/UserManagement.Actors/Managers/ActorManager.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using Microsoft.Extensions.Options; 3 | using OpenTracing; 4 | using Proto; 5 | using Proto.OpenTracing; 6 | using Proto.Persistence; 7 | using System; 8 | using UserManagement.Actors.Configuration; 9 | using UserManagement.Actors.Constants; 10 | 11 | namespace UserManagement.Actors.Managers 12 | { 13 | public class ActorManager : IActorManager 14 | { 15 | public IRootContext Context { get; } 16 | 17 | private readonly IActorFactory _actorFactory; 18 | 19 | public ActorManager(IActorFactory actorFactory, IProvider persistenceProvider, IOptions actorSettings, ITracer tracer, ILoggerFactory loggerFactory) 20 | { 21 | _actorFactory = actorFactory; 22 | var settings = actorSettings.Value; 23 | var logger = loggerFactory.CreateLogger(); 24 | 25 | // Configure OpenTracing 26 | Context = new RootContext(new MessageHeader(), OpenTracingExtensions.OpenTracingSenderMiddleware()) 27 | .WithOpenTracing(); 28 | 29 | _actorFactory.RegisterActor(new RequestActor(this, persistenceProvider, TimeSpan.FromMilliseconds(settings.ChildActorTimeoutInMilliseconds), loggerFactory, tracer), ActorNames.RequestActor); 30 | 31 | EventStream.Instance.Subscribe(dl => 32 | { 33 | logger.LogWarning($"DeadLetter from {dl.Sender} to {dl.Pid} : {dl.Message?.GetType().Name} = '{dl.Message?.ToString()}'"); 34 | }); 35 | } 36 | 37 | public PID GetParentActor() 38 | { 39 | return _actorFactory.GetActor(ActorNames.RequestActor); 40 | } 41 | 42 | public PID GetChildActor(string id, IContext parent) 43 | { 44 | return _actorFactory.GetActor(id, null, parent); 45 | } 46 | 47 | public void RegisterChildActor(T actor, string id, IContext parent) where T : IActor 48 | { 49 | _actorFactory.RegisterActor(actor, id, null, parent); 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /src/UserManagement.Actors/Managers/IActorManager.cs: -------------------------------------------------------------------------------- 1 | using Proto; 2 | 3 | namespace UserManagement.Actors.Managers 4 | { 5 | public interface IActorManager 6 | { 7 | IRootContext Context { get; } 8 | PID GetParentActor(); 9 | PID GetChildActor(string id, IContext parent); 10 | void RegisterChildActor(T actor, string id, IContext parent) where T : IActor; 11 | } 12 | } -------------------------------------------------------------------------------- /src/UserManagement.Actors/RequestActor.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using OpenTracing; 3 | using Proto; 4 | using Proto.Persistence; 5 | using System; 6 | using System.Threading.Tasks; 7 | using UserManagement.Actors.Constants; 8 | using UserManagement.Actors.Managers; 9 | using UserManagement.Commands; 10 | using UserManagement.Events; 11 | 12 | namespace UserManagement.Actors 13 | { 14 | public sealed class RequestActor : IActor 15 | { 16 | private readonly IActorManager _actorManager; 17 | private readonly ILoggerFactory _loggerFactory; 18 | private readonly IProvider _persistenceProvider; 19 | private readonly TimeSpan _childActorTimeout; 20 | private readonly ILogger _logger; 21 | private readonly ITracer _tracer; 22 | 23 | public RequestActor(IActorManager actorManager, IProvider persistenceProvider, TimeSpan childActorTimeout, ILoggerFactory loggerFactory, ITracer tracer) 24 | { 25 | _actorManager = actorManager; 26 | _loggerFactory = loggerFactory; 27 | _tracer = tracer; 28 | _persistenceProvider = persistenceProvider; 29 | _childActorTimeout = childActorTimeout; 30 | _logger = loggerFactory.CreateLogger(); 31 | 32 | _logger.LogInformation($"{nameof(RequestActor)} created"); 33 | } 34 | 35 | public async Task ReceiveAsync(IContext context) 36 | { 37 | if (context.Message is Started) 38 | { 39 | _logger.LogInformation($"{nameof(RequestActor)} ID='{context.Self.Id}' has started."); 40 | _actorManager.RegisterChildActor(new UserActor(_persistenceProvider, ActorNames.UserActor, _tracer, _loggerFactory), ActorNames.UserActor, context); 41 | } 42 | if (context.Message is UserMessage message) 43 | { 44 | var userActor = _actorManager.GetChildActor(ActorNames.UserActor, context); 45 | var userEvent = await context.RequestAsync(userActor, message, _childActorTimeout); 46 | context.Respond(userEvent); 47 | } 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /src/UserManagement.Actors/UserActor.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using Newtonsoft.Json; 3 | using OpenTracing; 4 | using Proto; 5 | using Proto.Persistence; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Threading.Tasks; 9 | using UserManagement.Actors.Chaos; 10 | using UserManagement.Commands; 11 | using UserManagement.Domain; 12 | using UserManagement.Events; 13 | 14 | namespace UserManagement.Actors 15 | { 16 | public class UserActor : IActor 17 | { 18 | private readonly Persistence _persistence; 19 | private readonly ITracer _tracer; 20 | private readonly ILogger _logger; 21 | 22 | private Users _state = new Users(); 23 | 24 | public UserActor(IProvider provider, string actorId, ITracer tracer, ILoggerFactory loggerFactory) 25 | { 26 | _tracer = tracer; 27 | _logger = loggerFactory.CreateLogger(); 28 | _persistence = Persistence.WithEventSourcingAndSnapshotting(provider, provider, actorId, ApplyEvent, ApplySnapshot); 29 | 30 | _logger.LogInformation($"{nameof(UserActor)} ID='{actorId}' created"); 31 | } 32 | 33 | public async Task ReceiveAsync(IContext context) 34 | { 35 | switch (context.Message) 36 | { 37 | case Started _: 38 | { 39 | _logger.LogInformation($"{nameof(UserActor)} ID='{context.Self.Id}' has started"); 40 | using (_tracer.BuildSpan("Recovering actor state").StartActive()) 41 | { 42 | await _persistence.RecoverStateAsync(); 43 | } 44 | } 45 | break; 46 | case GetUsers msg: 47 | { 48 | var @event = _state.GetAllUsers(msg.Limit, msg.Skip); 49 | context.Respond(@event); 50 | } 51 | break; 52 | 53 | case GetUser msg: 54 | { 55 | var @event = _state.GetUserById(msg.Id); 56 | context.Respond(@event); 57 | } 58 | break; 59 | 60 | case CreateUser msg: 61 | { 62 | try 63 | { 64 | using (var scope = _tracer.BuildSpan("Persisting UserCreated event").StartActive()) 65 | { 66 | if (ChaosHelper.ShouldCreateUserCrash(scope.Span)) 67 | throw new Exception("Creating user disabled by Chaos"); 68 | 69 | var @event = _state.CreateUser(msg.Id, msg.Name); 70 | if (@event is UserCreated) 71 | { 72 | 73 | scope.Span.Log(new Dictionary { [nameof(@event)] = JsonConvert.SerializeObject(@event) }); 74 | 75 | await _persistence.PersistEventAsync(@event) 76 | .ContinueWith(t => _persistence.PersistSnapshotAsync(_state)); 77 | } 78 | context.Respond(@event); 79 | } 80 | } 81 | catch (Exception e) 82 | { 83 | _logger.LogError(e, "Error creating user"); 84 | context.Respond(new UnexpectedErrorOcurred(msg.Id)); 85 | } 86 | } 87 | break; 88 | 89 | case DeleteUser msg: 90 | { 91 | try 92 | { 93 | var @event = _state.DeleteUser(msg.Id); 94 | if (@event is UserDeleted) 95 | { 96 | using (var scope = _tracer.BuildSpan("Persisting UserDeleted event").StartActive()) 97 | { 98 | scope.Span.Log(new Dictionary { [nameof(@event)] = JsonConvert.SerializeObject(@event), }); 99 | 100 | await _persistence.PersistEventAsync(@event) 101 | .ContinueWith(t => _persistence.PersistSnapshotAsync(_state)); 102 | } 103 | } 104 | context.Respond(@event); 105 | } 106 | catch (Exception e) 107 | { 108 | _logger.LogError(e, "Error deleting user"); 109 | context.Respond(new UnexpectedErrorOcurred(msg.Id)); 110 | } 111 | } 112 | break; 113 | } 114 | } 115 | 116 | private void ApplyEvent(Event @event) 117 | { 118 | switch (@event.Data) 119 | { 120 | case UserCreated e: 121 | _state.CreateUser(e.Id, e.Name, e.CreatedOn); 122 | break; 123 | 124 | case UserDeleted e: 125 | _state.DeleteUser(e.Id); 126 | break; 127 | } 128 | } 129 | 130 | private void ApplySnapshot(Snapshot snapshot) 131 | { 132 | try 133 | { 134 | _state = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(snapshot.State)); 135 | } 136 | catch (Exception e) 137 | { 138 | _logger.LogError(e, "Error retrieving or deserializing state from a snapshot"); 139 | } 140 | } 141 | } 142 | } -------------------------------------------------------------------------------- /src/UserManagement.Actors/UserManagement.Actors.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/UserManagement.Api/Configuration/ApiSettings.cs: -------------------------------------------------------------------------------- 1 | namespace UserManagement.Api.Configuration 2 | { 3 | public class ApiSettings 4 | { 5 | public int RequestTimeoutInMilliseconds { get; set; } 6 | public int DefaultNumberOfResults { get; set; } 7 | public int DefaultNumberOfSkip { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/UserManagement.Api/Constants/ExternalErrorReason.cs: -------------------------------------------------------------------------------- 1 | namespace UserManagement.Api.Constants 2 | { 3 | public static class ExternalErrorReason 4 | { 5 | public const string UserNotFound = "User not found"; 6 | public const string UnexpectedError = "Unexpected error"; 7 | public const string ResourceNotFound = "Resource not found"; 8 | public const string RequestInvalid = "Request invalid"; 9 | public const string RequestTimedOut = "Request timed out"; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/UserManagement.Api/Controllers/BaseController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Microsoft.Extensions.Primitives; 3 | using UserManagement.Api.Constants; 4 | using UserManagement.Api.Models.Responses; 5 | 6 | namespace UserManagement.Api.Controllers 7 | { 8 | public abstract class BaseController : ControllerBase 9 | { 10 | [NonAction] 11 | public NotFoundObjectResult UserNotFound() 12 | { 13 | var errorResponse = new ErrorResponse( 14 | HttpContext.TraceIdentifier, 15 | ExternalErrorReason.ResourceNotFound, 16 | new[] { ExternalErrorReason.UserNotFound }); 17 | 18 | return NotFound(errorResponse); 19 | } 20 | 21 | [NonAction] 22 | public ObjectResult GenericInvalidOperation(int statusCode, StringValues errorCodes) 23 | { 24 | var errorResponse = new ErrorResponse( 25 | HttpContext.TraceIdentifier, 26 | ExternalErrorReason.RequestInvalid, 27 | errorCodes); 28 | 29 | return new ObjectResult(errorResponse) 30 | { 31 | StatusCode = statusCode 32 | }; 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /src/UserManagement.Api/Controllers/UsersController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.Extensions.Options; 4 | using Newtonsoft.Json; 5 | using OpenTracing; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Linq; 9 | using System.Threading.Tasks; 10 | using UserManagement.Actors.Managers; 11 | using UserManagement.Api.Configuration; 12 | using UserManagement.Api.Constants; 13 | using UserManagement.Api.Models.Requests; 14 | using UserManagement.Api.Models.Responses; 15 | using UserManagement.Commands; 16 | using UserManagement.Events; 17 | 18 | namespace UserManagement.Api.Controllers 19 | { 20 | [Route("api/[controller]")] 21 | [ApiController] 22 | public class UsersController : BaseController 23 | { 24 | private readonly IActorManager _actorManager; 25 | private readonly ApiSettings _apiSettings; 26 | private readonly ITracer _tracer; 27 | 28 | public UsersController(IActorManager actorManager, IOptions apiSettings, ITracer tracer) 29 | { 30 | _actorManager = actorManager; 31 | _tracer = tracer; 32 | _apiSettings = apiSettings.Value; 33 | } 34 | 35 | [HttpGet] 36 | public async Task Get([FromQuery] GetItemsFilterRequest filterRequest) 37 | { 38 | if (filterRequest == null) 39 | return GenericInvalidOperation(StatusCodes.Status422UnprocessableEntity, ExternalErrorReason.UnexpectedError); 40 | 41 | try 42 | { 43 | var limit = filterRequest.Limit ?? _apiSettings.DefaultNumberOfResults; 44 | var skip = filterRequest.Skip ?? _apiSettings.DefaultNumberOfSkip; 45 | 46 | _tracer.ActiveSpan?.Log(new Dictionary { [nameof(limit)] = limit, [nameof(skip)] = skip, }); 47 | 48 | var @event = await _actorManager.Context.RequestAsync( 49 | _actorManager.GetParentActor(), 50 | new GetUsers(limit, skip), 51 | TimeSpan.FromMilliseconds(_apiSettings.RequestTimeoutInMilliseconds)); 52 | 53 | switch (@event) 54 | { 55 | case UsersRetrieved e: return Ok(new UsersListResponse(e.TotalCount, e.Users.Select(u => new UserDetailsResponse(u.Id, u.Name, u.CreatedOn)))); 56 | default: return GenericInvalidOperation(StatusCodes.Status400BadRequest, ExternalErrorReason.UnexpectedError); 57 | } 58 | } 59 | catch (TimeoutException) 60 | { 61 | return GenericInvalidOperation(StatusCodes.Status504GatewayTimeout, ExternalErrorReason.RequestTimedOut); 62 | } 63 | } 64 | 65 | [HttpGet("{id:guid}")] 66 | public async Task Get(Guid id) 67 | { 68 | try 69 | { 70 | _tracer.ActiveSpan?.Log(new Dictionary { [nameof(id)] = id }); 71 | 72 | var @event = await _actorManager.Context.RequestAsync( 73 | _actorManager.GetParentActor(), 74 | new GetUser(id), 75 | TimeSpan.FromMilliseconds(_apiSettings.RequestTimeoutInMilliseconds)); 76 | 77 | switch (@event) 78 | { 79 | case UserRetrieved e: return Ok(new UserDetailsResponse(e.Id, e.Name, e.CreatedOn)); 80 | case UserNotFound _: return UserNotFound(); 81 | default: return GenericInvalidOperation(StatusCodes.Status400BadRequest, ExternalErrorReason.UnexpectedError); 82 | } 83 | } 84 | catch (TimeoutException) 85 | { 86 | return GenericInvalidOperation(StatusCodes.Status504GatewayTimeout, ExternalErrorReason.RequestTimedOut); 87 | } 88 | } 89 | 90 | [HttpPost] 91 | public async Task Post([FromBody] CreateUserRequest request) 92 | { 93 | if (request == null) 94 | return GenericInvalidOperation(StatusCodes.Status422UnprocessableEntity, ExternalErrorReason.UnexpectedError); 95 | 96 | try 97 | { 98 | _tracer.ActiveSpan?.Log(new Dictionary { [nameof(request)] = JsonConvert.SerializeObject(request), }); 99 | 100 | var @event = await _actorManager.Context.RequestAsync( 101 | _actorManager.GetParentActor(), 102 | new CreateUser(Guid.NewGuid(), request.Name), 103 | TimeSpan.FromMilliseconds(_apiSettings.RequestTimeoutInMilliseconds)); 104 | 105 | switch (@event) 106 | { 107 | case UserCreated e: return Created(string.Empty, new UserDetailsResponse(e.Id, e.Name, e.CreatedOn)); 108 | default: return GenericInvalidOperation(StatusCodes.Status400BadRequest, ExternalErrorReason.UnexpectedError); 109 | } 110 | } 111 | catch (TimeoutException) 112 | { 113 | return GenericInvalidOperation(StatusCodes.Status504GatewayTimeout, ExternalErrorReason.RequestTimedOut); 114 | } 115 | } 116 | 117 | [HttpDelete("{id}")] 118 | public async Task Delete(Guid id) 119 | { 120 | try 121 | { 122 | _tracer.ActiveSpan?.Log(new Dictionary { [nameof(id)] = id }); 123 | 124 | var @event = await _actorManager.Context.RequestAsync( 125 | _actorManager.GetParentActor(), 126 | new DeleteUser(id), 127 | TimeSpan.FromMilliseconds(_apiSettings.RequestTimeoutInMilliseconds)); 128 | 129 | switch (@event) 130 | { 131 | case UserDeleted _: return NoContent(); 132 | case UserNotFound _: return UserNotFound(); 133 | default: return GenericInvalidOperation(StatusCodes.Status400BadRequest, ExternalErrorReason.UnexpectedError); 134 | } 135 | } 136 | catch (TimeoutException) 137 | { 138 | return GenericInvalidOperation(StatusCodes.Status504GatewayTimeout, ExternalErrorReason.RequestTimedOut); 139 | } 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/UserManagement.Api/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/core/sdk:2.1 AS build 2 | 3 | ARG BUILDCONFIG=RELEASE 4 | ARG VERSION=1.0.0 5 | 6 | # copy src 7 | COPY ./src/ ./src/ 8 | WORKDIR /src/UserManagement.Api/ 9 | RUN dotnet publish -c $BUILDCONFIG -o out /p:Version=$VERSION 10 | 11 | # build runtime image 12 | FROM mcr.microsoft.com/dotnet/core/aspnet:2.1 13 | WORKDIR /app 14 | COPY --from=build /src/UserManagement.Api/out ./ 15 | 16 | EXPOSE 5000 17 | ENTRYPOINT ["dotnet", "UserManagement.Api.dll"] -------------------------------------------------------------------------------- /src/UserManagement.Api/Logging/RequestLoggingMiddleware.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using Microsoft.Extensions.Logging; 3 | using System; 4 | using System.Diagnostics; 5 | using System.Threading.Tasks; 6 | 7 | namespace UserManagement.Api.Logging 8 | { 9 | public sealed class RequestLoggingMiddleware 10 | { 11 | private readonly RequestDelegate _next; 12 | private readonly ILogger _logger; 13 | 14 | public RequestLoggingMiddleware(ILoggerFactory loggerFactory, RequestDelegate next) 15 | { 16 | if (loggerFactory == null) throw new ArgumentNullException(nameof(loggerFactory)); 17 | _logger = loggerFactory.CreateLogger(); 18 | 19 | _next = next ?? throw new ArgumentNullException(nameof(next)); 20 | } 21 | 22 | public async Task Invoke(HttpContext httpContext) 23 | { 24 | if (httpContext == null) throw new ArgumentNullException(nameof(httpContext)); 25 | 26 | var start = Stopwatch.GetTimestamp(); 27 | try 28 | { 29 | await _next(httpContext); 30 | 31 | var elapsedMs = GetElapsedMilliseconds(start, Stopwatch.GetTimestamp()); 32 | _logger.LogInformation($"HTTP {httpContext.Request.Method} {httpContext.Request.Path} responded {httpContext.Response?.StatusCode} in {elapsedMs} ms"); 33 | } 34 | catch (Exception ex) 35 | { 36 | _logger.LogError(ex, $"Exception when processing HTTP request - {ex.Message}"); 37 | throw; 38 | } 39 | } 40 | 41 | private static double GetElapsedMilliseconds(long start, long stop) 42 | { 43 | return (stop - start) * 1000 / (double)Stopwatch.Frequency; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/UserManagement.Api/Models/Requests/CreateUserRequest.cs: -------------------------------------------------------------------------------- 1 | namespace UserManagement.Api.Models.Requests 2 | { 3 | public class CreateUserRequest 4 | { 5 | public string Name { get; set; } 6 | } 7 | } -------------------------------------------------------------------------------- /src/UserManagement.Api/Models/Requests/GetItemsFilterRequest.cs: -------------------------------------------------------------------------------- 1 | namespace UserManagement.Api.Models.Requests 2 | { 3 | public class GetItemsFilterRequest 4 | { 5 | public int? Limit { get; set; } 6 | public int? Skip { get; set; } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/UserManagement.Api/Models/Responses/ErrorResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace UserManagement.Api.Models.Responses 4 | { 5 | public class ErrorResponse 6 | { 7 | public ErrorResponse( 8 | string requestId, 9 | string errorType, 10 | IEnumerable errorCodes = null) 11 | { 12 | RequestId = requestId; 13 | ErrorType = errorType; 14 | ErrorCodes = errorCodes; 15 | } 16 | 17 | public string RequestId { get; } 18 | public string ErrorType { get; } 19 | public IEnumerable ErrorCodes { get; } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/UserManagement.Api/Models/Responses/UserDetailsResponse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace UserManagement.Api.Models.Responses 4 | { 5 | public class UserDetailsResponse 6 | { 7 | public UserDetailsResponse(Guid id, string name, DateTimeOffset createdOn) 8 | { 9 | Id = id; 10 | Name = name; 11 | CreatedOn = createdOn; 12 | } 13 | 14 | public Guid Id { get; } 15 | public string Name { get; } 16 | public DateTimeOffset CreatedOn { get; } 17 | } 18 | } -------------------------------------------------------------------------------- /src/UserManagement.Api/Models/Responses/UsersListResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace UserManagement.Api.Models.Responses 4 | { 5 | public class UsersListResponse 6 | { 7 | public UsersListResponse(long totalCount, IEnumerable users) 8 | { 9 | TotalCount = totalCount; 10 | Users = users ?? new List(); 11 | } 12 | 13 | public long TotalCount { get; } 14 | public IEnumerable Users { get; } 15 | } 16 | } -------------------------------------------------------------------------------- /src/UserManagement.Api/Monitoring/ChaosMiddleware.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using Microsoft.Extensions.Logging; 3 | using OpenTracing; 4 | using System; 5 | using System.Threading.Tasks; 6 | using UserManagement.Actors.Chaos; 7 | using UserManagement.Api.Logging; 8 | 9 | namespace UserManagement.Api.Monitoring 10 | { 11 | public sealed class ChaosMiddleware 12 | { 13 | private readonly RequestDelegate _next; 14 | private readonly ITracer _tracer; 15 | private readonly ILogger _logger; 16 | 17 | public ChaosMiddleware(RequestDelegate next, ITracer tracer, ILoggerFactory loggerFactory) 18 | { 19 | _next = next ?? throw new ArgumentNullException(nameof(next)); 20 | _tracer = tracer; 21 | _logger = loggerFactory.CreateLogger(); 22 | } 23 | 24 | public async Task Invoke(HttpContext httpContext) 25 | { 26 | if (httpContext == null) throw new ArgumentNullException(nameof(httpContext)); 27 | 28 | // These headers should not be allowed in public facing API :) 29 | httpContext.Request.Headers.TryGetValue(Headers.ChaosType, out var chaosTypeHeaderValue); 30 | 31 | var chaosTypeValue = chaosTypeHeaderValue.ToString().ToLowerInvariant(); 32 | 33 | if (chaosTypeValue == Headers.CreateUserDown) 34 | { 35 | var span = _tracer.ActiveSpan; 36 | if (span != null) 37 | { 38 | span.SetBaggageItem(Headers.ChaosType, chaosTypeValue); 39 | 40 | _logger.LogWarning($"Attempting to enable Chaos Engineering mode with type '{chaosTypeValue}'"); 41 | } 42 | } 43 | 44 | await _next(httpContext); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/UserManagement.Api/Monitoring/RequestMonitoringMiddleware.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using OpenTracing; 3 | using OpenTracing.Tag; 4 | using System; 5 | using System.Threading.Tasks; 6 | 7 | namespace UserManagement.Api.Monitoring 8 | { 9 | public sealed class RequestMonitoringMiddleware 10 | { 11 | private readonly RequestDelegate _next; 12 | private readonly ITracer _tracer; 13 | 14 | public RequestMonitoringMiddleware(RequestDelegate next, ITracer tracer) 15 | { 16 | _next = next ?? throw new ArgumentNullException(nameof(next)); 17 | _tracer = tracer; 18 | } 19 | 20 | public async Task Invoke(HttpContext httpContext) 21 | { 22 | if (httpContext == null) throw new ArgumentNullException(nameof(httpContext)); 23 | 24 | using (var scope = _tracer.BuildSpan("HttpRequest").StartActive()) 25 | { 26 | try 27 | { 28 | await _next(httpContext); 29 | 30 | scope.Span 31 | .SetTag(Tags.SpanKind, Tags.SpanKindServer) 32 | .SetTag(Tags.HttpMethod, "GET") 33 | .SetTag(Tags.HttpUrl, httpContext.Request.Path) 34 | .SetTag(Tags.HttpStatus, httpContext.Response?.StatusCode ?? 0); 35 | } 36 | catch (Exception) 37 | { 38 | scope.Span.SetTag(Tags.Error, true); 39 | throw; 40 | } 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/UserManagement.Api/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore; 2 | using Microsoft.AspNetCore.Hosting; 3 | 4 | namespace UserManagement.Api 5 | { 6 | public class Program 7 | { 8 | public static void Main(string[] args) 9 | { 10 | CreateWebHostBuilder(args).Build().Run(); 11 | } 12 | 13 | public static IWebHostBuilder CreateWebHostBuilder(string[] args) => 14 | WebHost.CreateDefaultBuilder(args) 15 | .UseStartup(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/UserManagement.Api/Startup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.Extensions.Configuration; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Microsoft.Extensions.Logging; 6 | using OpenTracing; 7 | using OpenTracing.Util; 8 | using Proto; 9 | using Proto.OpenTracing; 10 | using Proto.Persistence; 11 | using UserManagement.Actors; 12 | using UserManagement.Actors.Configuration; 13 | using UserManagement.Actors.Managers; 14 | using UserManagement.Api.Configuration; 15 | using UserManagement.Api.Logging; 16 | using UserManagement.Api.Monitoring; 17 | using UserManagement.Persistence; 18 | 19 | namespace UserManagement.Api 20 | { 21 | public class Startup 22 | { 23 | private readonly IConfiguration _configuration; 24 | private readonly ILoggerFactory _loggerFactory; 25 | 26 | public Startup(IConfiguration configuration, ILoggerFactory loggerFactory) 27 | { 28 | _configuration = configuration; 29 | _loggerFactory = loggerFactory; 30 | } 31 | 32 | public void ConfigureServices(IServiceCollection services) 33 | { 34 | // Configuration 35 | services.AddOptions(); 36 | services.Configure(_configuration.GetSection("Api")); 37 | services.Configure(_configuration.GetSection("Actors")); 38 | 39 | // Logging 40 | services.AddLogging(builder => 41 | { 42 | builder.AddConfiguration(_configuration.GetSection("Logging")); 43 | builder.AddSeq(_configuration.GetSection("Seq")); 44 | builder.AddDebug(); 45 | }); 46 | 47 | // Persistence 48 | services.AddSingleton(); 49 | 50 | // Monitoring 51 | services.AddSingleton(Jaeger.Configuration.FromIConfiguration(_loggerFactory, _configuration.GetSection("Jaeger")).GetTracer()); 52 | 53 | // Actors 54 | services.AddSingleton(); 55 | services.AddProtoActor(props => 56 | { 57 | props.RegisterProps(p => p.WithChildSupervisorStrategy(new AlwaysRestartStrategy()).WithOpenTracing()); 58 | props.RegisterProps(p => p.WithOpenTracing()); 59 | }); 60 | 61 | // API 62 | services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1); 63 | } 64 | 65 | public void Configure(IApplicationBuilder app, ITracer tracer) 66 | { 67 | _loggerFactory.AddDebug(); 68 | Log.SetLoggerFactory(_loggerFactory); 69 | 70 | // Register Jaeger monitoring 71 | GlobalTracer.Register(tracer); 72 | 73 | app.UseForwardedHeaders(); 74 | app.UseMiddleware(); 75 | app.UseMiddleware(); 76 | app.UseMiddleware(); 77 | app.UseMvc(); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/UserManagement.Api/UserManagement.Api.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp2.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | PreserveNewest 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/UserManagement.Api/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Api": { 3 | "RequestTimeoutInMilliseconds": 5000, 4 | "DefaultNumberOfResults": 10, 5 | "DefaultNumberOfSkip": 0 6 | }, 7 | "Actors": { 8 | "ChildActorTimeoutInMilliseconds": 3000 9 | }, 10 | "Logging": { 11 | "LogLevel": { 12 | "Default": "Information", 13 | "System": "Warning", 14 | "Microsoft": "Warning" 15 | }, 16 | "Console": { 17 | "IncludeScopes": false 18 | } 19 | }, 20 | "Seq": { 21 | "ServerUrl": "http://localhost:5341" 22 | }, 23 | "Jaeger": { 24 | "JAEGER_SERVICE_NAME": "UserManagementApi", 25 | "JAEGER_AGENT_HOST": "localhost", 26 | "JAEGER_AGENT_PORT": "6831", 27 | "JAEGER_SAMPLER_TYPE": "const", 28 | "JAEGER_SAMPLER_PARAM": "1", 29 | "JAEGER_SAMPLER_MANAGER_HOST_PORT":"5778" 30 | } 31 | } -------------------------------------------------------------------------------- /src/UserManagement.Commands/CreateUser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace UserManagement.Commands 4 | { 5 | public sealed class CreateUser : UserMessage 6 | { 7 | public CreateUser(Guid id, string name) 8 | { 9 | Id = id; 10 | Name = name; 11 | } 12 | 13 | public Guid Id { get; } 14 | public string Name { get; set; } 15 | } 16 | } -------------------------------------------------------------------------------- /src/UserManagement.Commands/DeleteUser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace UserManagement.Commands 4 | { 5 | public sealed class DeleteUser : UserMessage 6 | { 7 | public DeleteUser(Guid id) 8 | { 9 | Id = id; 10 | } 11 | 12 | public Guid Id { get; } 13 | } 14 | } -------------------------------------------------------------------------------- /src/UserManagement.Commands/GetUser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace UserManagement.Commands 4 | { 5 | public sealed class GetUser : UserMessage 6 | { 7 | public GetUser(Guid id) 8 | { 9 | Id = id; 10 | } 11 | 12 | public Guid Id { get; } 13 | } 14 | } -------------------------------------------------------------------------------- /src/UserManagement.Commands/GetUsers.cs: -------------------------------------------------------------------------------- 1 | namespace UserManagement.Commands 2 | { 3 | public sealed class GetUsers : UserMessage 4 | { 5 | public GetUsers(int limit, int skip) 6 | { 7 | Limit = limit; 8 | Skip = skip; 9 | } 10 | 11 | public int Skip { get; } 12 | 13 | public int Limit { get; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/UserManagement.Commands/UserManagement.Commands.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/UserManagement.Commands/UserMessage.cs: -------------------------------------------------------------------------------- 1 | namespace UserManagement.Commands 2 | { 3 | public abstract class UserMessage 4 | { 5 | } 6 | } -------------------------------------------------------------------------------- /src/UserManagement.Domain/UserManagement.Domain.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/UserManagement.Domain/Users.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Linq; 4 | using UserManagement.Domain.ValueObjects; 5 | using UserManagement.Events; 6 | 7 | namespace UserManagement.Domain 8 | { 9 | public sealed class Users 10 | { 11 | public Users(ConcurrentDictionary state = null) 12 | { 13 | State = state ?? new ConcurrentDictionary(); 14 | } 15 | 16 | public ConcurrentDictionary State { get; } 17 | 18 | public UserEvent GetUserById(Guid id) 19 | { 20 | var user = GetUser(id); 21 | if (user != null) 22 | { 23 | return new UserRetrieved(user.Id, user.Name, user.CreatedOn); 24 | } 25 | 26 | return new UserNotFound(id); 27 | } 28 | 29 | public UserEvent CreateUser(Guid id, string name, DateTimeOffset? createdOn = null) 30 | { 31 | var user = GetUser(id); 32 | if (user != null) 33 | { 34 | return new UserRetrieved(user.Id, user.Name, user.CreatedOn); 35 | } 36 | 37 | var newUser = new User(id, name, createdOn ?? DateTimeOffset.UtcNow); 38 | if (State.TryAdd(id, newUser)) 39 | { 40 | return new UserCreated(newUser.Id, newUser.Name, newUser.CreatedOn); 41 | } 42 | 43 | return new UnexpectedErrorOcurred(id); 44 | } 45 | 46 | public UserEvent DeleteUser(Guid id) 47 | { 48 | if (!State.ContainsKey(id)) 49 | { 50 | return new UserNotFound(id); 51 | } 52 | 53 | if (State.TryRemove(id, out _)) 54 | { 55 | return new UserDeleted(id); 56 | } 57 | 58 | return new UnexpectedErrorOcurred(id); 59 | } 60 | 61 | public UserEvent GetAllUsers(int limit, int skip) 62 | { 63 | var users = State 64 | .OrderByDescending(u => u.Value.CreatedOn) 65 | .Skip(skip) 66 | .Take(limit) 67 | .Select(user => new UserRetrieved(user.Value.Id, user.Value.Name, user.Value.CreatedOn)) 68 | .ToList(); 69 | 70 | return new UsersRetrieved(State.Count, users); 71 | } 72 | 73 | private User GetUser(Guid id) 74 | { 75 | State.TryGetValue(id, out var user); 76 | return user; 77 | } 78 | } 79 | } -------------------------------------------------------------------------------- /src/UserManagement.Domain/ValueObjects/User.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace UserManagement.Domain.ValueObjects 4 | { 5 | public class User : ValueObject 6 | { 7 | public User(Guid id, string name, DateTimeOffset createdOn) 8 | { 9 | if (id == Guid.Empty) 10 | throw new ArgumentException("User ID must not be empty"); 11 | Id = id; 12 | 13 | if (string.IsNullOrEmpty(name)) 14 | throw new ArgumentException("User name must not be empty"); 15 | Name = name; 16 | 17 | if (createdOn == DateTimeOffset.MinValue) 18 | throw new ArgumentException("Created On date must not be empty"); 19 | CreatedOn = createdOn; 20 | } 21 | 22 | public Guid Id { get; } 23 | public string Name { get; } 24 | public DateTimeOffset CreatedOn { get; } 25 | 26 | protected override bool EqualsCore(User other) 27 | { 28 | return Id == other.Id && 29 | Name == other.Name && 30 | CreatedOn == other.CreatedOn; 31 | } 32 | 33 | protected override int GetHashCodeCore() 34 | { 35 | unchecked 36 | { 37 | var hashCode = Id.GetHashCode(); 38 | hashCode = (hashCode * 397) ^ Name.GetHashCode(); 39 | hashCode = (hashCode * 397) ^ CreatedOn.GetHashCode(); 40 | return hashCode; 41 | } 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /src/UserManagement.Domain/ValueObjects/ValueObject.cs: -------------------------------------------------------------------------------- 1 | namespace UserManagement.Domain.ValueObjects 2 | { 3 | public abstract class ValueObject 4 | where T : ValueObject 5 | { 6 | public override bool Equals(object obj) 7 | { 8 | var valueObject = obj as T; 9 | 10 | if (ReferenceEquals(valueObject, null)) 11 | return false; 12 | 13 | if (GetType() != obj.GetType()) 14 | return false; 15 | 16 | return EqualsCore(valueObject); 17 | } 18 | 19 | protected abstract bool EqualsCore(T other); 20 | 21 | public override int GetHashCode() 22 | { 23 | return GetHashCodeCore(); 24 | } 25 | 26 | protected abstract int GetHashCodeCore(); 27 | 28 | public static bool operator ==(ValueObject a, ValueObject b) 29 | { 30 | if (ReferenceEquals(a, null) && ReferenceEquals(b, null)) 31 | return true; 32 | 33 | if (ReferenceEquals(a, null) || ReferenceEquals(b, null)) 34 | return false; 35 | 36 | return a.Equals(b); 37 | } 38 | 39 | public static bool operator !=(ValueObject a, ValueObject b) 40 | { 41 | return !(a == b); 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /src/UserManagement.Events/UnexpectedErrorOcurred.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace UserManagement.Events 4 | { 5 | public sealed class UnexpectedErrorOcurred : UserEvent 6 | { 7 | public UnexpectedErrorOcurred(Guid userId) 8 | { 9 | UserId = userId; 10 | } 11 | 12 | public Guid UserId { get; } 13 | } 14 | } -------------------------------------------------------------------------------- /src/UserManagement.Events/UserCreated.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace UserManagement.Events 4 | { 5 | public sealed class UserCreated : UserEvent 6 | { 7 | public UserCreated(Guid userId, string name, DateTimeOffset createdOn) 8 | { 9 | Id = userId; 10 | Name = name; 11 | CreatedOn = createdOn; 12 | } 13 | 14 | public Guid Id { get; } 15 | public string Name { get; } 16 | public DateTimeOffset CreatedOn { get; } 17 | } 18 | } -------------------------------------------------------------------------------- /src/UserManagement.Events/UserDeleted.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace UserManagement.Events 4 | { 5 | public sealed class UserDeleted : UserEvent 6 | { 7 | public UserDeleted(Guid id) 8 | { 9 | Id = id; 10 | } 11 | 12 | public Guid Id { get; } 13 | } 14 | } -------------------------------------------------------------------------------- /src/UserManagement.Events/UserEvent.cs: -------------------------------------------------------------------------------- 1 | namespace UserManagement.Events 2 | { 3 | public abstract class UserEvent 4 | { 5 | } 6 | } -------------------------------------------------------------------------------- /src/UserManagement.Events/UserManagement.Events.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/UserManagement.Events/UserNotFound.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace UserManagement.Events 4 | { 5 | public sealed class UserNotFound : UserEvent 6 | { 7 | public UserNotFound(Guid id) 8 | { 9 | Id = id; 10 | } 11 | 12 | public Guid Id { get; } 13 | } 14 | } -------------------------------------------------------------------------------- /src/UserManagement.Events/UserRetrieved.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace UserManagement.Events 4 | { 5 | public sealed class UserRetrieved : UserEvent 6 | { 7 | public UserRetrieved(Guid id, string name, DateTimeOffset createdOn) 8 | { 9 | Id = id; 10 | Name = name; 11 | CreatedOn = createdOn; 12 | } 13 | 14 | public Guid Id { get; } 15 | public string Name { get; } 16 | public DateTimeOffset CreatedOn { get; } 17 | } 18 | } -------------------------------------------------------------------------------- /src/UserManagement.Events/UsersRetrieved.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace UserManagement.Events 4 | { 5 | public sealed class UsersRetrieved : UserEvent 6 | { 7 | public UsersRetrieved(long totalCount, IReadOnlyList users) 8 | { 9 | TotalCount = totalCount; 10 | Users = users ?? new List(); 11 | } 12 | 13 | public long TotalCount { get; } 14 | public IReadOnlyList Users { get; } 15 | } 16 | } -------------------------------------------------------------------------------- /src/UserManagement.Persistence/InMemoryProvider.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Proto.Persistence; 3 | using System; 4 | using System.Collections.Concurrent; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Threading.Tasks; 8 | 9 | namespace UserManagement.Persistence 10 | { 11 | 12 | public sealed class InMemoryProvider : IProvider 13 | { 14 | private readonly ConcurrentDictionary> _globalEvents = new ConcurrentDictionary>(); 15 | 16 | private readonly ConcurrentDictionary> _globalSnapshots = new ConcurrentDictionary>(); 17 | 18 | public Task<(object Snapshot, long Index)> GetSnapshotAsync(string actorName) 19 | { 20 | if (!_globalSnapshots.TryGetValue(actorName, out var snapshots)) 21 | return Task.FromResult<(object, long)>((null, 0)); 22 | 23 | var snapshot = snapshots.OrderBy(ss => ss.Key).LastOrDefault(); 24 | return Task.FromResult((snapshot.Value, snapshot.Key)); 25 | } 26 | 27 | public Task GetEventsAsync(string actorName, long indexStart, long indexEnd, Action callback) 28 | { 29 | if (_globalEvents.TryGetValue(actorName, out var events)) 30 | { 31 | foreach (var e in events.Where(e => e.Key >= indexStart && e.Key <= indexEnd)) 32 | { 33 | callback(e.Value); 34 | } 35 | } 36 | 37 | return Task.FromResult(0L); 38 | } 39 | 40 | public Task PersistEventAsync(string actorName, long index, object @event) 41 | { 42 | var events = _globalEvents.GetOrAdd(actorName, new Dictionary()); 43 | 44 | long nextEventIndex = 1; 45 | if (events.Any()) 46 | { 47 | nextEventIndex = events.Last().Key + 1; 48 | } 49 | 50 | events.Add(nextEventIndex, @event); 51 | 52 | return Task.FromResult(0L); 53 | } 54 | 55 | public Task PersistSnapshotAsync(string actorName, long index, object snapshot) 56 | { 57 | var type = snapshot.GetType(); 58 | var snapshots = _globalSnapshots.GetOrAdd(actorName, new Dictionary()); 59 | var copy = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(snapshot), type); 60 | 61 | snapshots.Add(index, copy); 62 | 63 | return Task.CompletedTask; 64 | } 65 | 66 | public Task DeleteEventsAsync(string actorName, long inclusiveToIndex) 67 | { 68 | if (!_globalEvents.TryGetValue(actorName, out var events)) 69 | return Task.CompletedTask; 70 | 71 | var eventsToRemove = events.Where(s => s.Key <= inclusiveToIndex) 72 | .Select(e => e.Key) 73 | .ToList(); 74 | 75 | eventsToRemove.ForEach(key => events.Remove(key)); 76 | 77 | return Task.CompletedTask; 78 | } 79 | 80 | public Task DeleteSnapshotsAsync(string actorName, long inclusiveToIndex) 81 | { 82 | if (!_globalSnapshots.TryGetValue(actorName, out var snapshots)) 83 | return Task.CompletedTask; 84 | 85 | var snapshotsToRemove = snapshots.Where(s => s.Key <= inclusiveToIndex) 86 | .Select(snapshot => snapshot.Key) 87 | .ToList(); 88 | 89 | snapshotsToRemove.ForEach(key => snapshots.Remove(key)); 90 | 91 | return Task.CompletedTask; 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/UserManagement.Persistence/UserManagement.Persistence.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /test/UserManagement.Domain.Tests/UserManagement.Domain.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp2.1 5 | false 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /test/UserManagement.Domain.Tests/UsersTests.cs: -------------------------------------------------------------------------------- 1 | using AutoFixture; 2 | using AutoFixture.Xunit2; 3 | using FluentAssertions; 4 | using System; 5 | using System.Collections.Concurrent; 6 | using System.Collections.Generic; 7 | using System.ComponentModel.DataAnnotations; 8 | using System.Linq; 9 | using UserManagement.Domain.ValueObjects; 10 | using UserManagement.Events; 11 | using Xunit; 12 | 13 | namespace UserManagement.Domain.Tests 14 | { 15 | public sealed class UsersTests 16 | { 17 | private readonly IReadOnlyList _usersStorage = new List 18 | { 19 | new User(Guid.NewGuid(), "user1", DateTimeOffset.UtcNow), 20 | new User(Guid.NewGuid(), "user2", DateTimeOffset.UtcNow), 21 | new User(Guid.NewGuid(), "user3", DateTimeOffset.UtcNow), 22 | }; 23 | 24 | private readonly Users _sut; 25 | 26 | public UsersTests() 27 | { 28 | var state = new ConcurrentDictionary(); 29 | foreach (var user in _usersStorage) 30 | { 31 | state.TryAdd(user.Id, user); 32 | } 33 | 34 | _sut = new Users(state); 35 | } 36 | 37 | [Fact] 38 | public void Given_null_state_when_creating_should_initialize_with_empty_state() 39 | { 40 | // Given 41 | // When 42 | var result = new Users(); 43 | 44 | // Then 45 | result.State.Should().BeEmpty(); 46 | } 47 | 48 | [Theory, AutoData] 49 | public void Given_empty_state_when_getting_user_by_id_should_return_not_found(Guid id) 50 | { 51 | // Given 52 | var users = new Users(new ConcurrentDictionary()); 53 | 54 | // When 55 | var result = users.GetUserById(id); 56 | 57 | // Then 58 | result.Should().BeOfType(); 59 | result.As().Id.Should().Be(id); 60 | } 61 | 62 | [Fact] 63 | public void Given_empty_state_when_getting_all_users_should_return_empty_set() 64 | { 65 | // Given 66 | var users = new Users(new ConcurrentDictionary()); 67 | 68 | // When 69 | var result = users.GetAllUsers(10, 0); 70 | 71 | // Then 72 | result.Should().BeOfType(); 73 | result.As().Users.Should().BeEmpty(); 74 | result.As().TotalCount.Should().Be(0); 75 | } 76 | 77 | [Theory, AutoData] 78 | public void Given_empty_state_when_deleting_user_should_return_not_found(Guid id) 79 | { 80 | // Given 81 | var users = new Users(new ConcurrentDictionary()); 82 | 83 | // When 84 | var result = users.DeleteUser(id); 85 | 86 | // Then 87 | result.Should().BeOfType(); 88 | result.As().Id.Should().Be(id); 89 | } 90 | 91 | [Theory, AutoData] 92 | public void Given_user_not_found_when_getting_user_by_id_should_return_not_found(Guid id) 93 | { 94 | // Given 95 | // When 96 | var result = _sut.GetUserById(id); 97 | 98 | // Then 99 | result.Should().BeOfType(); 100 | result.As().Id.Should().Be(id); 101 | } 102 | 103 | [Fact] 104 | public void Given_user_found_when_getting_user_by_id_should_return_user_found() 105 | { 106 | // Given 107 | var existingUser = _usersStorage.First(); 108 | 109 | // When 110 | var result = _sut.GetUserById(existingUser.Id); 111 | 112 | // Then 113 | result.Should().BeOfType(); 114 | result.As().Id.Should().Be(existingUser.Id); 115 | result.As().Name.Should().Be(existingUser.Name); 116 | result.As().CreatedOn.Should().Be(existingUser.CreatedOn); 117 | } 118 | 119 | [Theory, AutoData] 120 | public void Given_user_not_found_when_deleting_user_should_return_not_found(Guid id) 121 | { 122 | // Given 123 | // When 124 | var result = _sut.GetUserById(id); 125 | 126 | // Then 127 | result.Should().BeOfType(); 128 | result.As().Id.Should().Be(id); 129 | } 130 | 131 | [Fact] 132 | public void Given_user_found_when_deleting_user_should_return_user_deleted() 133 | { 134 | // Given 135 | var existingUser = _usersStorage.First(); 136 | 137 | // When 138 | var result = _sut.DeleteUser(existingUser.Id); 139 | 140 | // Then 141 | result.Should().BeOfType(); 142 | result.As().Id.Should().Be(existingUser.Id); 143 | } 144 | 145 | [Theory, AutoData] 146 | public void Given_existing_user_id_found_when_creating_user_should_return_user_retrieved(string name) 147 | { 148 | // Given 149 | var existingUser = _usersStorage.First(); 150 | 151 | // When 152 | var result = _sut.CreateUser(existingUser.Id, name); 153 | 154 | // Then 155 | result.Should().BeOfType(); 156 | result.As().Id.Should().Be(existingUser.Id); 157 | result.As().Name.Should().Be(existingUser.Name); 158 | result.As().CreatedOn.Should().Be(existingUser.CreatedOn); 159 | } 160 | 161 | [Theory, AutoData] 162 | public void Given_new_user_id_when_creating_user_should_return_user_created(Guid id, string name) 163 | { 164 | // Given 165 | // When 166 | var result = _sut.CreateUser(id, name); 167 | 168 | // Then 169 | result.Should().BeOfType(); 170 | result.As().Id.Should().Be(id); 171 | result.As().Name.Should().Be(name); 172 | result.As().CreatedOn.Should().BeCloseTo(DateTimeOffset.UtcNow, 20_000); 173 | } 174 | 175 | [Theory, AutoData] 176 | public void Given_created_on_date_provided_when_creating_user_should_use_the_provided_date(Guid id, string name, DateTimeOffset createdOn) 177 | { 178 | // Given 179 | // When 180 | var result = _sut.CreateUser(id, name, createdOn); 181 | 182 | // Then 183 | result.Should().BeOfType(); 184 | result.As().Id.Should().Be(id); 185 | result.As().Name.Should().Be(name); 186 | result.As().CreatedOn.Should().Be(createdOn); 187 | } 188 | 189 | [Theory, AutoData] 190 | public void Given_created_on_date_is_null_when_creating_user_should_use_now_utc_date(Guid id, string name) 191 | { 192 | // Given 193 | // When 194 | var result = _sut.CreateUser(id, name); 195 | 196 | // Then 197 | result.Should().BeOfType(); 198 | result.As().Id.Should().Be(id); 199 | result.As().Name.Should().Be(name); 200 | result.As().CreatedOn.Should().BeCloseTo(DateTimeOffset.UtcNow, TimeSpan.FromSeconds(20), "giving enough time for test to run, even if debugging"); 201 | } 202 | 203 | [Theory, AutoData] 204 | public void Given_empty_state_when_getting_all_users_should_return_empty_list(int limit, int skip) 205 | { 206 | // Given 207 | var users = new Users(new ConcurrentDictionary()); 208 | 209 | // When 210 | var result = users.GetAllUsers(limit, skip); 211 | 212 | // Then 213 | result.Should().BeOfType(); 214 | result.As().TotalCount.Should().Be(0); 215 | result.As().Users.Should().BeEmpty(); 216 | } 217 | 218 | [Theory, AutoData] 219 | public void Given_bigger_limit_filter_than_there_are_users_in_state_when_getting_all_users_should_return_request_number_of_users([Range(100, 1000)]int limit) 220 | { 221 | // Given 222 | // When 223 | var result = _sut.GetAllUsers(limit, 0); 224 | 225 | // Then 226 | result.Should().BeOfType(); 227 | result.As().TotalCount.Should().Be(_usersStorage.Count); 228 | result.As().Users.Should().HaveCount(_usersStorage.Count); 229 | result.As().Users.Select(u => u.Id) 230 | .Should().BeEquivalentTo(_usersStorage.Select(u => u.Id)); 231 | } 232 | 233 | [Theory, AutoData] 234 | public void Given_single_user_in_state_when_getting_all_users_should_return_the_user(User user, int limit) 235 | { 236 | // Given 237 | var state = new ConcurrentDictionary(); 238 | state.TryAdd(user.Id, user); 239 | var users = new Users(state); 240 | 241 | // When 242 | var result = users.GetAllUsers(limit, 0); 243 | 244 | // Then 245 | result.Should().BeOfType(); 246 | result.As().TotalCount.Should().Be(1); 247 | result.As().Users.Should().HaveCount(1); 248 | var userRetrieved = result.As().Users.Single(); 249 | userRetrieved.Id.Should().Be(user.Id); 250 | userRetrieved.Name.Should().Be(user.Name); 251 | userRetrieved.CreatedOn.Should().Be(user.CreatedOn); 252 | } 253 | 254 | [Fact] 255 | public void Given_10_users_when_limit_100_but_skipping_5_when_getting_all_users_should_return_5_users() 256 | { 257 | // Given 258 | var fixture = new Fixture(); 259 | var state = new ConcurrentDictionary(); 260 | for (var i = 0; i < 10; i++) 261 | { 262 | var user = fixture.Create(); 263 | state.TryAdd(user.Id, user); 264 | } 265 | 266 | var users = new Users(state); 267 | 268 | // When 269 | var result = users.GetAllUsers(100, 5); 270 | 271 | // Then 272 | result.Should().BeOfType(); 273 | result.As().TotalCount.Should().Be(10); 274 | result.As().Users.Should().HaveCount(5); 275 | } 276 | 277 | [Fact] 278 | public void Given_10_users_when_limit_5_and_skipping_7_when_getting_all_users_should_return_5_users_offset() 279 | { 280 | // Given 281 | var fixture = new Fixture(); 282 | var state = new ConcurrentDictionary(); 283 | for (var i = 0; i < 10; i++) 284 | { 285 | var user = fixture.Create(); 286 | state.TryAdd(user.Id, user); 287 | } 288 | 289 | var users = new Users(state); 290 | 291 | // When 292 | var result = users.GetAllUsers(5, 7); 293 | 294 | // Then 295 | result.Should().BeOfType(); 296 | result.As().TotalCount.Should().Be(10); 297 | result.As().Users.Should().HaveCount(3); 298 | } 299 | } 300 | } 301 | -------------------------------------------------------------------------------- /test/UserManagement.Domain.Tests/ValueObjects/UserTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using AutoFixture.Xunit2; 3 | using FluentAssertions; 4 | using UserManagement.Domain.ValueObjects; 5 | using Xunit; 6 | 7 | namespace UserManagement.Domain.Tests.ValueObjects 8 | { 9 | public class UserTests 10 | { 11 | [Theory, AutoData] 12 | public void Given_empty_user_id_when_creating_should_throw(string name, DateTimeOffset createdOn) 13 | { 14 | // Given 15 | // When 16 | Action action = () => new User(Guid.Empty, name, createdOn); 17 | 18 | // Then 19 | action.Should().ThrowExactly(); 20 | } 21 | 22 | [Theory] 23 | [InlineAutoData(null)] 24 | [InlineAutoData("")] 25 | public void Given_empty_user_name_when_creating_should_throw(string invalidName, Guid id, DateTimeOffset createdOn) 26 | { 27 | // Given 28 | // When 29 | Action action = () => new User(id, invalidName, createdOn); 30 | 31 | // Then 32 | action.Should().ThrowExactly(); 33 | } 34 | [Theory, AutoData] 35 | public void Given_invalid_created_on_date_when_creating_should_throw(string name, Guid id) 36 | { 37 | // Given 38 | // When 39 | Action action = () => new User(id, name, DateTimeOffset.MinValue); 40 | 41 | // Then 42 | action.Should().ThrowExactly(); 43 | } 44 | 45 | [Theory, AutoData] 46 | public void Given_valid_params_when_creating_should_assign_properties(Guid id, string name, DateTimeOffset createdOn) 47 | { 48 | // Given 49 | // When 50 | var result = new User(id, name, createdOn); 51 | 52 | // Then 53 | result.Id.Should().Be(id); 54 | result.Name.Should().Be(name); 55 | } 56 | 57 | [Theory, AutoData] 58 | public void Given_two_instances_with_the_same_properties_when_comparing_should_be_equal(Guid id, string name, DateTimeOffset createdOn) 59 | { 60 | // Given 61 | var user1 = new User(id, name, createdOn); 62 | var user2 = new User(id, name, createdOn); 63 | 64 | // When 65 | var result = user1 == user2; 66 | 67 | // Then 68 | result.Should().BeTrue(); 69 | } 70 | 71 | [Theory, AutoData] 72 | public void Given_user_when_getting_hash_code_should_not_be_zero(Guid id, string name, DateTimeOffset createdOn) 73 | { 74 | // Given 75 | var user = new User(id, name, createdOn); 76 | 77 | // When 78 | var result = user.GetHashCode(); 79 | 80 | // Then 81 | result.Should().NotBe(0); 82 | } 83 | } 84 | } 85 | --------------------------------------------------------------------------------