├── .github └── workflows │ └── build.yml ├── .gitignore ├── LICENSE ├── README.md ├── azure-pipelines.yml ├── docs ├── cqrs-clean.png ├── cqrs-clean.xml ├── cqrs_layer_diagram.png ├── cqrs_layer_diagram.xml ├── create_card_interaction.png ├── create_card_interaction.xml ├── end-points.drawio ├── get_card_list_interaction.png ├── get_card_list_interaction.xml ├── guaraci-icon.png ├── guaraci-icon.xcf └── sync_write_read.jpg └── src ├── .dockerignore ├── Ametista.Api ├── Ametista.Api.csproj ├── Dockerfile ├── Endpoints │ ├── CreateCard │ │ ├── CreateCardEndpoint.cs │ │ ├── CreateCardRequest.cs │ │ └── CreateCardResponse.cs │ ├── CreateTransaction │ │ ├── CreateTransactionEndpoint.cs │ │ ├── CreateTransactionRequest.cs │ │ └── CreateTransactionResponse.cs │ ├── GetCardList │ │ ├── GetCardListEndpoint.cs │ │ ├── GetCardListRequest.cs │ │ └── GetCardListResponse.cs │ ├── GetCardView │ │ ├── GetCardViewEndpoint.cs │ │ └── GetCardViewResponse.cs │ └── GetTransactions │ │ ├── GetTransactionsEndpoint.cs │ │ ├── GetTransactionsRequest.cs │ │ └── GetTransactionsResponse.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── Startup.cs ├── appsettings.Development.json ├── appsettings.Docker.json └── appsettings.json ├── Ametista.Command ├── Abstractions │ ├── CommandResult.cs │ ├── ICommand.cs │ ├── ICommandDispatcher.cs │ ├── ICommandHandler.cs │ └── ICommandResult.cs ├── Ametista.Command.csproj ├── CreateCard │ ├── CreateCardCommand.cs │ ├── CreateCardCommandHandler.cs │ └── CreateCardCommandResult.cs └── CreateTransaction │ ├── CreateTransactionCommand.cs │ ├── CreateTransactionCommandHandler.cs │ └── CreateTransactionCommandResult.cs ├── Ametista.Core ├── Ametista.Core.csproj ├── AmetistaConfiguration.cs ├── Cards │ ├── BillingCycle.cs │ ├── Card.cs │ ├── CardCreatedEvent.cs │ ├── CardValidator.cs │ ├── HasValidNumberSpec.cs │ └── ICardWriteOnlyRepository.cs ├── Shared │ ├── Event.cs │ ├── IAggregateRoot.cs │ ├── IBuilder.cs │ ├── ICache.cs │ ├── IDatabase.cs │ ├── IEntity.cs │ ├── IEvent.cs │ ├── IEventBus.cs │ ├── IEventDispatcher.cs │ ├── IEventHandler.cs │ ├── IPersistentConnection.cs │ ├── ISpecification.cs │ ├── IWriteOnlyRepository.cs │ ├── Money.cs │ ├── ValidationNotification.cs │ ├── ValidationNotificationHandler.cs │ └── Validator.cs └── Transactions │ ├── ITransactionWriteOnlyRepository.cs │ ├── Transaction.cs │ └── TransactionCreatedEvent.cs ├── Ametista.Infrastructure ├── Ametista.Infrastructure.csproj ├── Bus │ ├── RabbitMQEventBus.cs │ └── RabbitMQPersistentConnection.cs ├── Cache │ └── RedisCache.cs ├── Dispatchers │ ├── CommandDispatcher.cs │ ├── EventDispatcher.cs │ └── QueryDispatcher.cs ├── IoC │ ├── CommandModule.cs │ ├── EventModule.cs │ ├── InfrastructureModule.cs │ └── QueryModule.cs ├── Migrations │ ├── 20180828224253_InitialCreate.Designer.cs │ ├── 20180828224253_InitialCreate.cs │ └── WriteDbContextModelSnapshot.cs └── Persistence │ ├── Repository │ ├── CardWriteOnlyRepository.cs │ └── TransactionWriteOnlyRepository.cs │ └── WriteDbContext.cs ├── Ametista.Query ├── Abstractions │ ├── IMaterializer.cs │ ├── IQuery.cs │ ├── IQueryDispatcher.cs │ ├── IQueryHandler.cs │ ├── IQueryModel.cs │ └── IQueryParameters.cs ├── Ametista.Query.csproj ├── EventHandlers │ ├── CardCreatedEventHandler.cs │ └── TransactionCreatedEventHandler.cs ├── LinqExtensions.cs ├── Materializers │ ├── CardListQueryModelMaterializer.cs │ └── TransactionListQueryModelMaterializer.cs ├── Queries │ ├── Cards │ │ ├── GetCardByIdQuery.cs │ │ ├── GetCardByIdQueryHandler.cs │ │ ├── GetCardListQuery.cs │ │ └── GetCardListQueryHandler.cs │ └── Transactions │ │ ├── GetTransactionListQuery.cs │ │ └── GetTransactionListQueryHandler.cs ├── QueryModel │ ├── CardListQueryModel.cs │ ├── CardViewQueryModel.cs │ └── TransactionListQueryModel.cs └── ReadDbContext.cs ├── Ametista.UnitTest ├── Ametista.UnitTest.csproj ├── Api │ ├── ApiApplicationFactory.cs │ └── Endpoints │ │ └── CreateCard │ │ ├── CreateCardApiTests.cs │ │ └── CreateCardEndpointTests.cs ├── Builders │ └── TransactionBuilder.cs ├── Command │ ├── CreateCardCommandHandlerTests.cs │ └── CreateTransactionCommandHandlerTests.cs ├── Core │ └── Cards │ │ ├── CardSpecificationTests.cs │ │ └── CardValidatorTests.cs ├── Fakes │ ├── FakeCache.cs │ └── FakeEventBus.cs ├── Mothers │ └── TransactionMother.cs └── Query │ └── TransactionListQueryModelMaterializerUnitTest.cs ├── Ametista.sln ├── conf └── rabbitmq.conf └── docker-compose.yml /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: .NET 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Setup .NET 16 | uses: actions/setup-dotnet@v1 17 | with: 18 | dotnet-version: 6.0.x 19 | - name: Restore dependencies 20 | run: dotnet restore ./src/Ametista.sln 21 | - name: Build 22 | run: dotnet build ./src/Ametista.sln --no-restore 23 | - name: Test 24 | run: dotnet test ./src/Ametista.sln --no-build --verbosity normal 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/aspnetcore 3 | 4 | ### ASPNETCore ### 5 | ## Ignore Visual Studio temporary files, build results, and 6 | ## files generated by popular Visual Studio add-ons. 7 | 8 | # User-specific files 9 | *.suo 10 | *.user 11 | *.userosscache 12 | *.sln.docstates 13 | 14 | # User-specific files (MonoDevelop/Xamarin Studio) 15 | *.userprefs 16 | 17 | # Build results 18 | [Dd]ebug/ 19 | [Dd]ebugPublic/ 20 | [Rr]elease/ 21 | [Rr]eleases/ 22 | x64/ 23 | x86/ 24 | bld/ 25 | [Bb]in/ 26 | [Oo]bj/ 27 | [Ll]og/ 28 | 29 | # Visual Studio 2015 cache/options directory 30 | .vs/ 31 | # Uncomment if you have tasks that create the project's static files in wwwroot 32 | #wwwroot/ 33 | 34 | # MSTest test Results 35 | [Tt]est[Rr]esult*/ 36 | [Bb]uild[Ll]og.* 37 | 38 | # NUNIT 39 | *.VisualState.xml 40 | TestResult.xml 41 | 42 | # Build Results of an ATL Project 43 | [Dd]ebugPS/ 44 | [Rr]eleasePS/ 45 | dlldata.c 46 | 47 | # DNX 48 | project.lock.json 49 | project.fragment.lock.json 50 | artifacts/ 51 | 52 | *_i.c 53 | *_p.c 54 | *_i.h 55 | *.ilk 56 | *.meta 57 | *.obj 58 | *.pch 59 | *.pdb 60 | *.pgc 61 | *.pgd 62 | *.rsp 63 | *.sbr 64 | *.tlb 65 | *.tli 66 | *.tlh 67 | *.tmp 68 | *.tmp_proj 69 | *.log 70 | *.vspscc 71 | *.vssscc 72 | .builds 73 | *.pidb 74 | *.svclog 75 | *.scc 76 | 77 | # Chutzpah Test files 78 | _Chutzpah* 79 | 80 | # Visual C++ cache files 81 | ipch/ 82 | *.aps 83 | *.ncb 84 | *.opendb 85 | *.opensdf 86 | *.sdf 87 | *.cachefile 88 | *.VC.db 89 | *.VC.VC.opendb 90 | 91 | # Visual Studio profiler 92 | *.psess 93 | *.vsp 94 | *.vspx 95 | *.sap 96 | 97 | # TFS 2012 Local Workspace 98 | $tf/ 99 | 100 | # Guidance Automation Toolkit 101 | *.gpState 102 | 103 | # ReSharper is a .NET coding add-in 104 | _ReSharper*/ 105 | *.[Rr]e[Ss]harper 106 | *.DotSettings.user 107 | 108 | # JustCode is a .NET coding add-in 109 | .JustCode 110 | 111 | # TeamCity is a build add-in 112 | _TeamCity* 113 | 114 | # DotCover is a Code Coverage Tool 115 | *.dotCover 116 | 117 | # Visual Studio code coverage results 118 | *.coverage 119 | *.coveragexml 120 | 121 | # NCrunch 122 | _NCrunch_* 123 | .*crunch*.local.xml 124 | nCrunchTemp_* 125 | 126 | # MightyMoose 127 | *.mm.* 128 | AutoTest.Net/ 129 | 130 | # Web workbench (sass) 131 | .sass-cache/ 132 | 133 | # Installshield output folder 134 | [Ee]xpress/ 135 | 136 | # DocProject is a documentation generator add-in 137 | DocProject/buildhelp/ 138 | DocProject/Help/*.HxT 139 | DocProject/Help/*.HxC 140 | DocProject/Help/*.hhc 141 | DocProject/Help/*.hhk 142 | DocProject/Help/*.hhp 143 | DocProject/Help/Html2 144 | DocProject/Help/html 145 | 146 | # Click-Once directory 147 | publish/ 148 | 149 | # Publish Web Output 150 | *.[Pp]ublish.xml 151 | *.azurePubxml 152 | # TODO: Comment the next line if you want to checkin your web deploy settings 153 | # but database connection strings (with potential passwords) will be unencrypted 154 | *.pubxml 155 | *.publishproj 156 | 157 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 158 | # checkin your Azure Web App publish settings, but sensitive information contained 159 | # in these scripts will be unencrypted 160 | PublishScripts/ 161 | 162 | # NuGet Packages 163 | *.nupkg 164 | # The packages folder can be ignored because of Package Restore 165 | **/packages/* 166 | # except build/, which is used as an MSBuild target. 167 | !**/packages/build/ 168 | # Uncomment if necessary however generally it will be regenerated when needed 169 | #!**/packages/repositories.config 170 | # NuGet v3's project.json files produces more ignoreable files 171 | *.nuget.props 172 | *.nuget.targets 173 | 174 | # Microsoft Azure Build Output 175 | csx/ 176 | *.build.csdef 177 | 178 | # Microsoft Azure Emulator 179 | ecf/ 180 | rcf/ 181 | 182 | # Windows Store app package directories and files 183 | AppPackages/ 184 | BundleArtifacts/ 185 | Package.StoreAssociation.xml 186 | _pkginfo.txt 187 | 188 | # Visual Studio cache files 189 | # files ending in .cache can be ignored 190 | *.[Cc]ache 191 | # but keep track of directories ending in .cache 192 | !*.[Cc]ache/ 193 | 194 | # Others 195 | ClientBin/ 196 | ~$* 197 | *~ 198 | *.dbmdl 199 | *.dbproj.schemaview 200 | *.jfm 201 | *.pfx 202 | *.publishsettings 203 | node_modules/ 204 | orleans.codegen.cs 205 | 206 | # Since there are multiple workflows, uncomment next line to ignore bower_components 207 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 208 | #bower_components/ 209 | 210 | # RIA/Silverlight projects 211 | Generated_Code/ 212 | 213 | # Backup & report files from converting an old project file 214 | # to a newer Visual Studio version. Backup files are not needed, 215 | # because we have git ;-) 216 | _UpgradeReport_Files/ 217 | Backup*/ 218 | UpgradeLog*.XML 219 | UpgradeLog*.htm 220 | 221 | # SQL Server files 222 | *.mdf 223 | *.ldf 224 | 225 | # Business Intelligence projects 226 | *.rdl.data 227 | *.bim.layout 228 | *.bim_*.settings 229 | 230 | # Microsoft Fakes 231 | FakesAssemblies/ 232 | 233 | # GhostDoc plugin setting file 234 | *.GhostDoc.xml 235 | 236 | # Node.js Tools for Visual Studio 237 | .ntvs_analysis.dat 238 | 239 | # Visual Studio 6 build log 240 | *.plg 241 | 242 | # Visual Studio 6 workspace options file 243 | *.opt 244 | 245 | # Visual Studio LightSwitch build output 246 | **/*.HTMLClient/GeneratedArtifacts 247 | **/*.DesktopClient/GeneratedArtifacts 248 | **/*.DesktopClient/ModelManifest.xml 249 | **/*.Server/GeneratedArtifacts 250 | **/*.Server/ModelManifest.xml 251 | _Pvt_Extensions 252 | 253 | # Paket dependency manager 254 | .paket/paket.exe 255 | paket-files/ 256 | 257 | # FAKE - F# Make 258 | .fake/ 259 | 260 | # JetBrains Rider 261 | .idea/ 262 | *.sln.iml 263 | 264 | # CodeRush 265 | .cr/ 266 | 267 | # Python Tools for Visual Studio (PTVS) 268 | __pycache__/ 269 | *.pyc 270 | 271 | # Cake - Uncomment if you are using it 272 | # tools/ 273 | 274 | 275 | # End of https://www.gitignore.io/api/aspnetcore -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Filipe Augusto Lima de Souza 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 | # ![Guaraci](docs/guaraci-icon.png) Clean Architecture CQRS with Derived Data 2 | 3 | CQRS, using Clean Architecture, multiple databases, and Eventual Consistency 4 | 5 | ## :bookmark_tabs: Detailed information 6 | 7 | I also keep more detailed information in my blog 8 | 9 | - [X] [CQRS Translated to Clean Architecture](https://blog.fals.io/2018-09-19-cqrs-clean-architecture/) 10 | - [X] CQRS Deep dive into Commands 11 | - [X] CQRS Queries and Materialization 12 | - [X] CQRS Consensus and Consistency 13 | - [X] CQRS Distributed chaos, CAP Theorem 14 | 15 | 16 | ## :floppy_disk: How do I use it? 17 | 18 | You're going to need the following tools: 19 | 20 | * Docker 21 | * Visual Studio 2019+ or Visual Studio Code 22 | * .Net 6 23 | 24 | Execute in your favorite command line from the `/src` folder 25 | 26 | ```bash 27 | $ cd src 28 | $ docker compose build 29 | $ docker compose up 30 | ``` 31 | 32 | Open your browser and hit the URL to see the OpenApi 33 | 34 | `http://localhost:5000/swagger/index.html` 35 | 36 | If you can't run you can hit the health checks to see if a component is down. 37 | 38 | `http://localhost:5000/healthcheck` 39 | 40 | If you still have issues, don't run as detached and look the error on the console after executing the docker command. 41 | 42 | ## :dart: Clean Architecture 43 | 44 | Here's the basic architecture of this microservice template: 45 | 46 | * Respecting policy rules, with dependencies always pointing inward 47 | * Separation of technology details from the rest of the system 48 | * SOLID 49 | * Single responsibility of each layer 50 | 51 | 52 | ![cqrs-clean](docs/cqrs-clean.png) 53 | 54 | ## :scissors: CQRS 55 | 56 | Segregation between Commands and Queries, with isolated databases and different models 57 | 58 | ![](docs/cqrs_layer_diagram.png) 59 | 60 | ### :arrow_down: Command Stack 61 | 62 | Has direct access to business rules and is responsible for only writing in the application. 63 | 64 | Below you can find a basic interaction between components in the **Command Stack**: 65 | 66 | ![](docs/create_card_interaction.png) 67 | 68 | ### :arrow_up: Query Stack 69 | 70 | Responsible for providing data to consumers of your application, containing a simplified and more suitable model for reading, with calculated data, aggregated values and materialized structures. 71 | 72 | The image contains the basic interaction between components in the **Query Stack**: 73 | 74 | 75 | 76 | ![](docs/get_card_list_interaction.png) 77 | 78 | ## :books: DDD 79 | 80 | This example contains a simplified Domain Model, with entities, aggregate roots, value objects and events, which are essential to synchronize the writing with reading database. 81 | 82 | ## :heavy_check_mark: TDD 83 | 84 | The project contains a well-defined IoC structure that allows you unit test almost every part of this service template, besides technology dependencies. 85 | 86 | Inside the main layer you're going to find Interfaces that are essential for the application, but with their implementations inside their own layers, what allow Mocking, Stubbing, using test doubles. 87 | 88 | There's a simple example using Mother Object Pattern and Builder to simplify unit tests and keep it maintainable and clean. 89 | 90 | ## :bar_chart: Data Intensive Microservice 91 | 92 | This microservice template comes with SRP and SOC in mind. Given the own nature of CQRS, you can easily scale this application tuning each stack separately. 93 | 94 | ## :page_facing_up: Derived Data 95 | 96 | Having multiple data stores makes this system a Derived Data system, which means, you never lose data, you can always rebuild one store from another, for example, if you lose an event which syncs data between the write and read database you can always get this data back from the write database and rebuild the read store. 97 | 98 | *Domain Model* is materialized to *Query Models* using view materializer. Keeping this as separed component in the query stack allows fully control to mapped properties and fully testable. 99 | 100 | ## :envelope: Message Broker 101 | 102 | Given the physical isolation of data stores, **Command Stack** and **Query Stack** must communicate to synchronize data. This is done here using a Message Broker. 103 | 104 | ![](docs/sync_write_read.jpg) 105 | 106 | Every successful handled command creates an event, which is published into a Message Broker. A synchronization background process subscribes to those events and is responsible for updating the reading database. 107 | 108 | ## :clock2: Eventual Consistency 109 | 110 | Everything comes with some trade offs. The case of CQRS with multiple databases, to maintain high availability and scalability we create inconsistencies between databases. 111 | 112 | More specifically, replicating data between two databases creates an eventual consistency, which in a specific moment in time, given the replication lag they are different, although is a temporary state and it eventually resolves itself. 113 | 114 | ## :clipboard: References 115 | 116 | Here's a list of reliable information used to bring this project to life. 117 | 118 | * Designing Data Intensive Applications 119 | 120 | * Clean Architecture, Robert C. Martin 121 | 122 | * Cloud Application Architecture Guide 123 | 124 | * Microsoft .NET - Architecting Applications for the Enterprise, 2nd Edition 125 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | # ASP.NET Core 2 | # Build and test ASP.NET Core web applications targeting .NET Core. 3 | # Add steps that run tests, create a NuGet package, deploy, and more: 4 | # https://docs.microsoft.com/vsts/pipelines/languages/dotnet-core 5 | 6 | trigger: 7 | - master 8 | 9 | pool: 10 | vmImage: 'ubuntu-latest' 11 | 12 | variables: 13 | buildConfiguration: 'Release' 14 | 15 | steps: 16 | - script: | 17 | dotnet build src/Ametista.sln --configuration $(buildConfiguration) 18 | dotnet test src/Ametista.sln 19 | -------------------------------------------------------------------------------- /docs/cqrs-clean.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fals/cqrs-clean-eventual-consistency/e4e1be8e737f1e9b4de172169a7f333aeec86f8d/docs/cqrs-clean.png -------------------------------------------------------------------------------- /docs/cqrs-clean.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /docs/cqrs_layer_diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fals/cqrs-clean-eventual-consistency/e4e1be8e737f1e9b4de172169a7f333aeec86f8d/docs/cqrs_layer_diagram.png -------------------------------------------------------------------------------- /docs/cqrs_layer_diagram.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /docs/create_card_interaction.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fals/cqrs-clean-eventual-consistency/e4e1be8e737f1e9b4de172169a7f333aeec86f8d/docs/create_card_interaction.png -------------------------------------------------------------------------------- /docs/create_card_interaction.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /docs/end-points.drawio: -------------------------------------------------------------------------------- 1 | 7V1bd5s4EP41fuweJO6PjZvupqfdS3t6tn3ERrbZEuQFuUn216+wBQakOEpiGGE7LwEBsphP30iaGQ0Te3p7/2serVefaEzSCbbi+4n9boIxdj2X/ytLHnYlyAlEyTJPYlG2L/iS/EdEoSVKN0lMitaNjNKUJet24ZxmGZmzVlmU5/SufduCpu1fXUdLIhV8mUepXPp3ErPVrjRwrX35byRZrqpfRpa4chtVN4uCYhXF9K5RZF9P7GlOKdsd3d5PSVpKr5LL7rn3j1ytG5aTjOk8kLxxQ/fuCv3+IZ5/9j/YN4tvf72p8PkZpRvxxqK17KESQU43WUzKWqyJfXW3Shj5so7m5dU7jjovW7HblJ8hfrhI0nRKU5pvn7VnKI4X5WMFy+kP0riCLN8OeR1XogEkZ+T+0VdDtcB4VyP0lrD8gd8iHnACf/eI6GV2KGR+t4cMV5CtGnBhRxRGopss67r3kuQHQpjPEazTr2Bja04IVgmWcw75PQkWOeCC9ZEkRxJzyopTmrMVXdIsSq/3pVdtSe/v+UjpWsj3H8LYg9A/0YbRtvTJfcK+NY6/l1X94oqzd/ei5u3JgzjZtbNs3GHp83ehm3xODrx0KDRflC8JO3Afwmo4c5JGLPnZbsjxoZG7OBg0+DA2HIH84dsexPL0e/Pa/rHtGSSmj1B0GExDSY1NcxIxMo3y+GvB/xXklXqNZkxgiyxZzy0iL7CUA8jUt93tlbKG5hPbv+PoP9fFLf2HbVn/IazQf15v6s+SxD0gxywY9YeqmZ/Z+q9qZnPMn+LJ26skYyRflAzgp1dcMl7K3+JqlvOjJdvKaldSrKOsWzarCm5Kzn0ma1okjJbN3V3nTZ11n+FlyqoaP9jpRM9hZBzGroqREXGQjY/DvLDq6NWUzlMwDymY5/bFPIRlVcgBeY0kF3P+p5Kk7dmhHR9HkgHGhknScUF1WK23vjeu9K/DAk0dFoKqsEBWYQrdtJ8D3GTrDXtU7bxGyfiRkhozx3Mt51jDe2d5A08NzxxqWAep8dgMGg03g64Um9mUqlqpTak/NuyEOGUjG5hT2FYA0JFisYrW5WGaZD/a3FAwR8mwrvB1+7ksyIagVKaVqky794pf+JPyaWjDtIPdFk6e4/7itivZEVA817QsdqpClvdkXTuOSnVtAa3f/RXLIoOmFNp60wdTnL6m4gS2JwEPhv6pogpqUaqa2VlHNRe2Lx7i5jGZBbPeV1Rue23q+dDTxsosdRnimuOSHwadYakjf90BrlsRsjpt6Xl0s2XD0k22yCPeyzdztsllEyzv3KwNcpsRGc1Ihz6iKEqTZcZP56Q0WvGCkirJPErfigu3SRxvNa3KsNvWvo0Owydhx5leWk9zz1E5pHqjnjy9f7tOeEFMCi4wLovSEV2KpTwoUUqyJZcftqZcOjlNUy5l8+Hzq3PRSHQELO1qtfvQpn1TO2BnSCyHXik0WGGuGnXsNuV89EI12q1ocDUq++S3VDWefEfSnbWnqpJ/5bBo9KNwUN0pR59M6TmNZoEfPolIzZJBIHEDSfojWHNBLbkcXcekA7qQdkNYUPGJgmpDglr5KowAdTA3mjY4rgsKDmiUWkeNGgiOBwqOPKs3P06tqQwbutEgdQjqZfNsczD1TwdTJ4AEFdkGoXpCTEXAqDoXVHtAFYOuMZAN6oI9WQ3sWrCogvpgTxdVUK5Wzey4YA84JB5xwSo2JqFZhNQbkyzLu377ftJrwD5Cnegjt45EgYvpkz1E+2Cvz6RY00yxiUJf4vM5cbfy69Xpja22ZLFjgGRlf01Tsv9uSMGMFywyUK6yo+RrsXVZRgUpJImerHne7fR5lXkeq5DpzTzvG7QgetkOuyENuboh98BhbgYth7SnWCMAFTTKzZE3UnxMClYOTMX57Zt0/FA1qA26c9IBdoK92iL/JDWq+YCmVU8L0IEclKr4RXmPRM2gk9p15FhKcgw643OB/VVgDuKnSYVHSyp53XlOG49cD8PTSg5wqnXYSNenTgC/PnW9Q2IdqUHFBMFW4WjjTgdVy6xetivSFtWB48OkLQo0dmiYnw+qK9lAkWhraMGCzlyA8kEFtuakJAANtAnkPm6+oxcsIZQ+qKChbcEho/v5mTbCQFaBwxo2AlD7oQWkAd1xaEBFfs1LSqiXMa+bEgrVsxGw+XIgL0RGmRPKBFH6oFoMJilUoOvMCEC3GgRnnRUqhOeGQa4L47fvhLpeEFhOhXouj1O1ziLLg84LFcpRcJIYzy9pRjcvFLLD4yWGUlXW86bvENQGMbbMUKGjqzxBl1UhaBjU6DJD6aMKalkK5bDDkWeGQg6GnjuGGl/4OL9RTsroVH8y5LW5oQK305i+xzfZBHHJDXWAfYMmhwrlXHePJ4ciWbwue8kYAqz7SAdVw1AxyZLR87xB4VPZPfrUnaPMB4WcrmvjpQmhBtedcrjDWSWEkhhXpa2ASghVfz3tkhHqACTDZoRC2KD9CcN9csbSzaYLa0ZEGDRh19g20z8DVtgcCRjU5D+2fdf6sIaw2+ktRfLr2sJ/LRYAEvJj2X2NO+GBYQAd0IowsKVQ31B4VDpob5LbdUg4PsgrrDHuje8GHFvgHxJCiqWqofEUBoReIiscC2HkJev4Uh5I4wR4hEX9a5eUB20/vWr9OWzKgzrpgglTYeO3xyM0jq0BqPqcpgmomp/14Bmwgrpw63Ze8h5s7+ZjG3jiA4RgNwgAfW8V6e4QEH0WjjIqD/25JD/gbw+f/QChy2dXn8Ms/Q/NATNLtradVYytjeBTICBsELfMN0RofyZczCHguHUgwdVo7BBSfj8D7HZj/JIt3FCkbbeDpotstxt/yhJDBhjQnYNQ8QnVEtL4SVgV42UCPCMIUHgGrrCWnuqjtCbgOoIIhWfgCuthwrInpB6pxh+h0B3BsN3f+MVPc0pZM2Y1j9arTzQm5R3/Aw== -------------------------------------------------------------------------------- /docs/get_card_list_interaction.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fals/cqrs-clean-eventual-consistency/e4e1be8e737f1e9b4de172169a7f333aeec86f8d/docs/get_card_list_interaction.png -------------------------------------------------------------------------------- /docs/get_card_list_interaction.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /docs/guaraci-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fals/cqrs-clean-eventual-consistency/e4e1be8e737f1e9b4de172169a7f333aeec86f8d/docs/guaraci-icon.png -------------------------------------------------------------------------------- /docs/guaraci-icon.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fals/cqrs-clean-eventual-consistency/e4e1be8e737f1e9b4de172169a7f333aeec86f8d/docs/guaraci-icon.xcf -------------------------------------------------------------------------------- /docs/sync_write_read.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fals/cqrs-clean-eventual-consistency/e4e1be8e737f1e9b4de172169a7f333aeec86f8d/docs/sync_write_read.jpg -------------------------------------------------------------------------------- /src/.dockerignore: -------------------------------------------------------------------------------- 1 | .dockerignore 2 | .env 3 | .git 4 | .gitignore 5 | .vs 6 | .vscode 7 | docker-compose.yml 8 | docker-compose.*.yml 9 | */bin 10 | */obj 11 | -------------------------------------------------------------------------------- /src/Ametista.Api/Ametista.Api.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0 5 | Linux 6 | Linux 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/Ametista.Api/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base 2 | WORKDIR /app 3 | EXPOSE 80 4 | EXPOSE 443 5 | 6 | FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build 7 | WORKDIR /src 8 | COPY ["Ametista.Api/Ametista.Api.csproj", "Ametista.Api/"] 9 | COPY ["Ametista.Command/Ametista.Command.csproj", "Ametista.Command/"] 10 | COPY ["Ametista.Core/Ametista.Core.csproj", "Ametista.Core/"] 11 | COPY ["Ametista.Infrastructure/Ametista.Infrastructure.csproj", "Ametista.Infrastructure/"] 12 | COPY ["Ametista.Query/Ametista.Query.csproj", "Ametista.Query/"] 13 | RUN dotnet restore "Ametista.Api/Ametista.Api.csproj" 14 | COPY . . 15 | WORKDIR "/src/Ametista.Api" 16 | RUN dotnet build "Ametista.Api.csproj" -c Release -o /app/build 17 | 18 | FROM build AS publish 19 | RUN dotnet publish "Ametista.Api.csproj" -c Release -o /app/publish 20 | 21 | FROM base AS final 22 | WORKDIR /app 23 | COPY --from=publish /app/publish . 24 | ENTRYPOINT ["dotnet", "Ametista.Api.dll"] -------------------------------------------------------------------------------- /src/Ametista.Api/Endpoints/CreateCard/CreateCardEndpoint.cs: -------------------------------------------------------------------------------- 1 | using Ametista.Command.Abstractions; 2 | using Ametista.Command.CreateCard; 3 | using Microsoft.AspNetCore.Mvc; 4 | using System; 5 | using System.Threading.Tasks; 6 | 7 | namespace Ametista.Api.Endpoints.CreateCard 8 | { 9 | [Route("api/cards")] 10 | [Produces("application/json")] 11 | public class CreateCardEndpoint : Controller 12 | { 13 | private readonly ICommandDispatcher commandDispatcher; 14 | 15 | public CreateCardEndpoint(ICommandDispatcher commandDispatcher) 16 | { 17 | this.commandDispatcher = commandDispatcher ?? throw new ArgumentNullException(nameof(commandDispatcher)); 18 | } 19 | 20 | [HttpPost] 21 | public async Task Post([FromBody] CreateCardRequest request) 22 | { 23 | var command = new CreateCardCommand(request.Number, request.CardHolder, request.ExpirationDate); 24 | var result = await commandDispatcher.Dispatch(command); 25 | 26 | if (result.Success) 27 | { 28 | var response = new CreateCardResponse() 29 | { 30 | Id = result.Id, 31 | Number = result.Number, 32 | CardHolder = result.CardHolder, 33 | ExpirationDate = result.ExpirationDate 34 | }; 35 | 36 | return CreatedAtAction(null, response); 37 | } 38 | 39 | return BadRequest(request); 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /src/Ametista.Api/Endpoints/CreateCard/CreateCardRequest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.DataAnnotations; 3 | 4 | namespace Ametista.Api.Endpoints.CreateCard 5 | { 6 | public class CreateCardRequest 7 | { 8 | [Required] 9 | public string Number { get; set; } 10 | 11 | [Required] 12 | public string CardHolder { get; set; } 13 | 14 | [Required] 15 | public DateTime ExpirationDate { get; set; } 16 | } 17 | } -------------------------------------------------------------------------------- /src/Ametista.Api/Endpoints/CreateCard/CreateCardResponse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Ametista.Api.Endpoints.CreateCard 4 | { 5 | public class CreateCardResponse 6 | { 7 | public Guid Id { get; set; } 8 | public string Number { get; set; } 9 | public string CardHolder { get; set; } 10 | public DateTime ExpirationDate { get; set; } 11 | } 12 | } -------------------------------------------------------------------------------- /src/Ametista.Api/Endpoints/CreateTransaction/CreateTransactionEndpoint.cs: -------------------------------------------------------------------------------- 1 | using Ametista.Command.Abstractions; 2 | using Ametista.Command.CreateTransaction; 3 | using Ametista.Core; 4 | using Microsoft.AspNetCore.Mvc; 5 | using System; 6 | using System.Threading.Tasks; 7 | 8 | namespace Ametista.Api.Endpoints.CreateTransaction 9 | { 10 | [Route("api/transactions")] 11 | [ApiController] 12 | public class CreateTransactionEndpoint : ControllerBase 13 | { 14 | private readonly ICommandDispatcher commandDispatcher; 15 | private readonly ValidationNotificationHandler validationNotificationHandler; 16 | 17 | public CreateTransactionEndpoint(ICommandDispatcher commandDispatcher, ValidationNotificationHandler validationNotificationHandler) 18 | { 19 | this.commandDispatcher = commandDispatcher ?? throw new ArgumentNullException(nameof(commandDispatcher)); 20 | this.validationNotificationHandler = validationNotificationHandler ?? throw new ArgumentNullException(nameof(validationNotificationHandler)); 21 | } 22 | 23 | [HttpPost] 24 | public async Task Post([FromBody] CreateTransactionRequest request) 25 | { 26 | var command = new CreateTransactionCommand(request.Amount, request.CurrencyCode, request.CardId, request.UniqueId, request.ChargeDate); 27 | var result = await commandDispatcher.Dispatch(command); 28 | 29 | if (result.Success) 30 | { 31 | var response = new CreateTransactionResponse() 32 | { 33 | Amount = result.Amount, 34 | CardId = result.CardId, 35 | ChargeDate = result.ChargeDate, 36 | CurrencyCode = result.CurrencyCode, 37 | Id = result.Id, 38 | UniqueId = result.UniqueId 39 | }; 40 | 41 | return CreatedAtAction(null, response); 42 | } 43 | 44 | return BadRequest(validationNotificationHandler.Notifications); 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /src/Ametista.Api/Endpoints/CreateTransaction/CreateTransactionRequest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.DataAnnotations; 3 | 4 | namespace Ametista.Api.Endpoints.CreateTransaction 5 | { 6 | public class CreateTransactionRequest 7 | { 8 | [Required] 9 | public decimal Amount { get; set; } 10 | 11 | [Required] 12 | public string CurrencyCode { get; set; } 13 | 14 | [Required] 15 | public Guid CardId { get; set; } 16 | 17 | [Required] 18 | public string UniqueId { get; set; } 19 | 20 | [Required] 21 | public DateTimeOffset ChargeDate { get; set; } 22 | } 23 | } -------------------------------------------------------------------------------- /src/Ametista.Api/Endpoints/CreateTransaction/CreateTransactionResponse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Ametista.Api.Endpoints.CreateTransaction 4 | { 5 | public class CreateTransactionResponse 6 | { 7 | public Guid Id { get; set; } 8 | public Guid CardId { get; set; } 9 | public DateTimeOffset ChargeDate { get; set; } 10 | public string UniqueId { get; set; } 11 | public decimal Amount { get; set; } 12 | public string CurrencyCode { get; set; } 13 | } 14 | } -------------------------------------------------------------------------------- /src/Ametista.Api/Endpoints/GetCardList/GetCardListEndpoint.cs: -------------------------------------------------------------------------------- 1 | using Ametista.Query.Abstractions; 2 | using Ametista.Query.Queries; 3 | using Microsoft.AspNetCore.Mvc; 4 | using System; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | 8 | namespace Ametista.Api.Endpoints.GetCardList 9 | { 10 | [Route("api/cards")] 11 | [Produces("application/json")] 12 | public class GetCardListEndpoint : Controller 13 | { 14 | private readonly IQueryDispatcher queryDispatcher; 15 | 16 | public GetCardListEndpoint(IQueryDispatcher queryDispatcher) 17 | { 18 | this.queryDispatcher = queryDispatcher ?? throw new ArgumentNullException(nameof(queryDispatcher)); 19 | } 20 | 21 | [HttpGet] 22 | public async Task Get([FromQuery] GetCardListRequest request) 23 | { 24 | var query = new GetCardListQuery() 25 | { 26 | CardHolder = request.CardHolder, 27 | ChargeDate = request.ChargeDate, 28 | Number = request.Number, 29 | Limit = request.Limit, 30 | Offset = request.Offset 31 | }; 32 | 33 | var result = await queryDispatcher.ExecuteAsync(query); 34 | 35 | if (!result.Any()) 36 | { 37 | return NotFound(query); 38 | } 39 | 40 | var respose = result.Select(x => new GetCardListResponse() 41 | { 42 | Id = x.Id, 43 | Number = x.Number, 44 | CardHolder = x.CardHolder, 45 | ExpirationDate = x.ExpirationDate, 46 | HighestTransactionAmount = x.HighestTransactionAmount, 47 | HighestTransactionId = x.HighestTransactionId 48 | }); 49 | 50 | return Ok(respose); 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /src/Ametista.Api/Endpoints/GetCardList/GetCardListRequest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Ametista.Api.Endpoints.GetCardList 4 | { 5 | public class GetCardListRequest 6 | { 7 | public string CardHolder { get; set; } 8 | public string Number { get; set; } 9 | public int Offset { get; set; } = 0; 10 | public int Limit { get; set; } = 1; 11 | public DateTime? ChargeDate { get; internal set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Ametista.Api/Endpoints/GetCardList/GetCardListResponse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Ametista.Api.Endpoints.GetCardList 4 | { 5 | public class GetCardListResponse 6 | { 7 | public string CardHolder { get; set; } 8 | public DateTime ExpirationDate { get; set; } 9 | public DateTime? HighestChargeDate { get; set; } 10 | public decimal? HighestTransactionAmount { get; set; } 11 | public Guid? HighestTransactionId { get; set; } 12 | public Guid Id { get; set; } 13 | public string Number { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Ametista.Api/Endpoints/GetCardView/GetCardViewEndpoint.cs: -------------------------------------------------------------------------------- 1 | using Ametista.Query.Abstractions; 2 | using Ametista.Query.Queries; 3 | using Microsoft.AspNetCore.Mvc; 4 | using System; 5 | using System.Threading.Tasks; 6 | 7 | namespace Ametista.Api.Endpoints.GetCardView 8 | { 9 | [Route("api/cards")] 10 | [Produces("application/json")] 11 | public class GetCardViewEndpoint : Controller 12 | { 13 | private readonly IQueryDispatcher queryDispatcher; 14 | 15 | public GetCardViewEndpoint(IQueryDispatcher queryDispatcher) 16 | { 17 | this.queryDispatcher = queryDispatcher ?? throw new ArgumentNullException(nameof(queryDispatcher)); 18 | } 19 | 20 | [HttpGet("{id}")] 21 | public async Task GetById(Guid id) 22 | { 23 | var query = new GetCardByIdQuery(id); 24 | 25 | var queryResult = await queryDispatcher.ExecuteAsync(query); 26 | 27 | if (queryResult == null) 28 | { 29 | return BadRequest(id); 30 | } 31 | 32 | var response = new GetCardViewResponse() 33 | { 34 | CardHolder = queryResult.CardHolder, 35 | ExpirationDate = queryResult.ExpirationDate, 36 | Id = queryResult.Id, 37 | Number = queryResult.Number 38 | }; 39 | 40 | return Ok(response); 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /src/Ametista.Api/Endpoints/GetCardView/GetCardViewResponse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Ametista.Api.Endpoints.GetCardView 4 | { 5 | public class GetCardViewResponse 6 | { 7 | public string CardHolder { get; set; } 8 | 9 | public DateTime ExpirationDate { get; set; } 10 | 11 | public Guid Id { get; set; } 12 | 13 | public string Number { get; set; } 14 | } 15 | } -------------------------------------------------------------------------------- /src/Ametista.Api/Endpoints/GetTransactions/GetTransactionsEndpoint.cs: -------------------------------------------------------------------------------- 1 | using Ametista.Query.Abstractions; 2 | using Ametista.Query.Queries; 3 | using Microsoft.AspNetCore.Mvc; 4 | using System; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | 8 | namespace Ametista.Api.Endpoints.GetTransactions 9 | { 10 | [Route("api/transactions")] 11 | [ApiController] 12 | public class TransactionController : ControllerBase 13 | { 14 | private readonly IQueryDispatcher queryDispatcher; 15 | 16 | public TransactionController(IQueryDispatcher queryDispatcher) 17 | { 18 | this.queryDispatcher = queryDispatcher ?? throw new ArgumentNullException(nameof(queryDispatcher)); 19 | } 20 | 21 | [HttpGet] 22 | public async Task Get([FromQuery] GetTransactionsRequest request) 23 | { 24 | var query = new GetTransactionListQuery() 25 | { 26 | BetweenAmount = request.BetweenAmount, 27 | CardHolder = request.CardHolder, 28 | CardNumber = request.CardNumber, 29 | ChargeDate = request.ChargeDate, 30 | Limit = request.Limit, 31 | Offset = request.Offset 32 | }; 33 | 34 | var result = await queryDispatcher.ExecuteAsync(query); 35 | 36 | if (!result.Any()) 37 | { 38 | return NotFound(query); 39 | } 40 | 41 | var response = result.Select(x => new GetTransactionsResponse() 42 | { 43 | Amount = x.Amount, 44 | CardHolder = x.CardHolder, 45 | CardNumber = x.CardNumber, 46 | ChargeDate = x.ChargeDate, 47 | CurrencyCode = x.CurrencyCode, 48 | UniqueId = x.UniqueId 49 | }); 50 | 51 | return Ok(response); 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /src/Ametista.Api/Endpoints/GetTransactions/GetTransactionsRequest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Ametista.Api.Endpoints.GetTransactions 4 | { 5 | public class GetTransactionsRequest 6 | { 7 | public decimal? BetweenAmount { get; set; } 8 | public string CardHolder { get; set; } 9 | public string CardNumber { get; set; } 10 | public DateTime? ChargeDate { get; set; } 11 | public int Offset { get; set; } = 0; 12 | public int Limit { get; set; } = 1; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Ametista.Api/Endpoints/GetTransactions/GetTransactionsResponse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Ametista.Api.Endpoints.GetTransactions 4 | { 5 | public class GetTransactionsResponse 6 | { 7 | public decimal Amount { get; set; } 8 | public string CurrencyCode { get; set; } 9 | public string CardNumber { get; set; } 10 | public string CardHolder { get; set; } 11 | public string UniqueId { get; set; } 12 | public DateTimeOffset ChargeDate { get; set; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Ametista.Api/Program.cs: -------------------------------------------------------------------------------- 1 | using Autofac.Extensions.DependencyInjection; 2 | using Microsoft.AspNetCore; 3 | using Microsoft.AspNetCore.Hosting; 4 | using Microsoft.Extensions.Configuration; 5 | using Microsoft.Extensions.Logging; 6 | using System.IO; 7 | 8 | namespace Ametista.Api 9 | { 10 | public class Program 11 | { 12 | public static void Main(string[] args) 13 | { 14 | CreateWebHostBuilder(args).Build().Run(); 15 | } 16 | 17 | public static IWebHostBuilder CreateWebHostBuilder(string[] args) => 18 | WebHost.CreateDefaultBuilder(args) 19 | .ConfigureServices(services => services.AddAutofac()) 20 | .ConfigureAppConfiguration((hostingContext, config) => 21 | { 22 | config.SetBasePath(Directory.GetCurrentDirectory()); 23 | config.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true); 24 | config.AddJsonFile($"appsettings.{hostingContext.HostingEnvironment.EnvironmentName}.json", optional: true, reloadOnChange: true); 25 | }) 26 | .UseKestrel() 27 | .UseContentRoot(Directory.GetCurrentDirectory()) 28 | .ConfigureLogging((hostingContext, logging) => 29 | { 30 | logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging")); 31 | logging.AddConsole(); 32 | logging.AddDebug(); 33 | }) 34 | .UseIIS() 35 | .UseStartup(); 36 | } 37 | } -------------------------------------------------------------------------------- /src/Ametista.Api/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:64675/", 7 | "sslPort": 0 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "launchUrl": "swagger", 15 | "environmentVariables": { 16 | "ASPNETCORE_ENVIRONMENT": "Development" 17 | } 18 | }, 19 | "Ametista.Api": { 20 | "commandName": "Project", 21 | "launchBrowser": true, 22 | "launchUrl": "swagger", 23 | "environmentVariables": { 24 | "ASPNETCORE_ENVIRONMENT": "Development" 25 | }, 26 | "applicationUrl": "http://localhost:5101/" 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /src/Ametista.Api/Startup.cs: -------------------------------------------------------------------------------- 1 | using Ametista.Core; 2 | using Ametista.Core.Cards; 3 | using Ametista.Core.Interfaces; 4 | using Ametista.Core.Transactions; 5 | using Ametista.Infrastructure.IoC; 6 | using Ametista.Infrastructure.Persistence; 7 | using Autofac; 8 | using Microsoft.AspNetCore.Builder; 9 | using Microsoft.AspNetCore.Hosting; 10 | using Microsoft.EntityFrameworkCore; 11 | using Microsoft.Extensions.Configuration; 12 | using Microsoft.Extensions.DependencyInjection; 13 | using Microsoft.Extensions.Hosting; 14 | 15 | namespace Ametista.Api 16 | { 17 | public class Startup 18 | { 19 | public Startup(IConfiguration configuration) 20 | { 21 | Configuration = configuration; 22 | } 23 | 24 | public IConfiguration Configuration { get; } 25 | 26 | // This method gets called by the runtime. Use this method to add services to the container. 27 | public virtual void ConfigureServices(IServiceCollection services) 28 | { 29 | services.AddCors(options => 30 | { 31 | options.AddPolicy("CorsPolicy", 32 | builder => builder.AllowAnyOrigin() 33 | .AllowAnyMethod() 34 | .AllowAnyHeader()); 35 | }); 36 | services.AddScoped(); 37 | services.AddSingleton(Configuration.Get()); 38 | 39 | services.AddControllers(); 40 | 41 | services.AddSwaggerGen(c => 42 | { 43 | c.SwaggerDoc("v1", new Microsoft.OpenApi.Models.OpenApiInfo { Title = "My API", Version = "v1" }); 44 | }); 45 | 46 | var sqlConnString = Configuration.GetConnectionString("SqlServerConnectionString"); 47 | 48 | services 49 | .AddDbContext(options => 50 | options.UseSqlServer(sqlConnString, 51 | b => b.MigrationsAssembly("Ametista.Infrastructure"))); 52 | 53 | var redisConnString = Configuration.GetConnectionString("RedisCache"); 54 | 55 | services 56 | .AddHealthChecks() 57 | .AddSqlServer(sqlConnString) 58 | .AddRedis(redisConnString); 59 | } 60 | 61 | public virtual void ConfigureContainer(ContainerBuilder builder) 62 | { 63 | builder.RegisterModule(new CommandModule()); 64 | builder.RegisterModule(new EventModule()); 65 | builder.RegisterModule(new InfrastructureModule()); 66 | builder.RegisterModule(new QueryModule()); 67 | } 68 | 69 | private void ConfigureEventBus(IApplicationBuilder app) 70 | { 71 | var eventBus = app.ApplicationServices.GetRequiredService(); 72 | 73 | eventBus.Subscribe(); 74 | eventBus.Subscribe(); 75 | } 76 | 77 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 78 | public virtual void Configure(IApplicationBuilder app, IWebHostEnvironment env) 79 | { 80 | app.UseSwagger(); 81 | 82 | // Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.), 83 | // specifying the Swagger JSON endpoint. 84 | app.UseSwaggerUI(c => 85 | { 86 | c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1"); 87 | }); 88 | 89 | if (env.IsDevelopment()) 90 | { 91 | app.UseDeveloperExceptionPage(); 92 | } 93 | 94 | if (env.EnvironmentName == "Docker") 95 | { 96 | using (var serviceScope = app.ApplicationServices.CreateScope()) 97 | { 98 | var context = serviceScope.ServiceProvider.GetService(); 99 | context.Database.Migrate(); 100 | } 101 | } 102 | 103 | app.UseStaticFiles(); 104 | app.UseRouting(); 105 | app.UseCors("CorsPolicy"); 106 | app.UseEndpoints(endpoints => 107 | { 108 | endpoints.MapControllers(); 109 | endpoints.MapHealthChecks("/healthcheck"); 110 | }); 111 | 112 | ConfigureEventBus(app); 113 | } 114 | } 115 | } -------------------------------------------------------------------------------- /src/Ametista.Api/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "IncludeScopes": false, 4 | "LogLevel": { 5 | "Default": "Debug", 6 | "System": "Information", 7 | "Microsoft": "Information" 8 | } 9 | }, 10 | "RetryCount": 5, 11 | "ConnectionStrings": { 12 | "SqlServerConnectionString": "Server=(localdb)\\mssqllocaldb;Database=Ametista;Trusted_Connection=True;MultipleActiveResultSets=true; Integrated Security=SSPI", 13 | "MongoConnectionString": "mongodb://nosql.data", 14 | "MongoDatabase": "AmetistaDb", 15 | "EventBusHostname": "rabbitmq", 16 | "EventBusUsername": null, 17 | "EventBusPassword": null, 18 | "RedisCache": "redis.cache" 19 | } 20 | } -------------------------------------------------------------------------------- /src/Ametista.Api/appsettings.Docker.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "IncludeScopes": false, 4 | "LogLevel": { 5 | "Default": "Debug", 6 | "System": "Information", 7 | "Microsoft": "Information" 8 | } 9 | }, 10 | "RetryCount": 5, 11 | "ConnectionStrings": { 12 | "SqlServerConnectionString": "Server=mssql;Database=AmetistaDb;User=sa;Password=MyPassword123456;", 13 | "MongoConnectionString": "mongodb://nosql.data", 14 | "MongoDatabase": "AmetistaDb", 15 | "EventBusHostname": "rabbitmq", 16 | "EventBusUsername": null, 17 | "EventBusPassword": null, 18 | "RedisCache": "redis.cache" 19 | } 20 | } -------------------------------------------------------------------------------- /src/Ametista.Api/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "IncludeScopes": false, 4 | "Debug": { 5 | "LogLevel": { 6 | "Default": "Warning" 7 | } 8 | }, 9 | "Console": { 10 | "LogLevel": { 11 | "Default": "Warning" 12 | } 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /src/Ametista.Command/Abstractions/CommandResult.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Ametista.Command.Abstractions 4 | { 5 | public abstract class CommandResult : ICommandResult 6 | { 7 | protected CommandResult() 8 | { 9 | Success = false; 10 | Executed = DateTime.Now; 11 | } 12 | protected CommandResult(bool success) 13 | { 14 | Success = success; 15 | Executed = DateTime.Now; 16 | } 17 | 18 | public bool Success { get; set; } 19 | 20 | public DateTime Executed { get; set; } 21 | 22 | public static TCommandResul CreateFailResult() where TCommandResul : CommandResult, new() 23 | { 24 | return new TCommandResul(); 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /src/Ametista.Command/Abstractions/ICommand.cs: -------------------------------------------------------------------------------- 1 | namespace Ametista.Command.Abstractions 2 | { 3 | public interface ICommand 4 | { } 5 | 6 | public interface ICommand : ICommand where TResult : ICommandResult 7 | { } 8 | } -------------------------------------------------------------------------------- /src/Ametista.Command/Abstractions/ICommandDispatcher.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Ametista.Command.Abstractions 4 | { 5 | public interface ICommandDispatcher 6 | { 7 | Task Dispatch(ICommand command) where TResult : ICommandResult; 8 | 9 | Task DispatchNonResult(ICommand command); 10 | } 11 | } -------------------------------------------------------------------------------- /src/Ametista.Command/Abstractions/ICommandHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Ametista.Command.Abstractions 4 | { 5 | public interface ICommandHandler { } 6 | 7 | public interface ICommandHandler : ICommandHandler 8 | where TCommand : ICommand where TResult : ICommandResult 9 | { 10 | Task Handle(TCommand command); 11 | } 12 | 13 | public interface ICommandHandler : ICommandHandler 14 | where TCommand : ICommand 15 | { 16 | Task HandleNonResult(TCommand command); 17 | } 18 | } -------------------------------------------------------------------------------- /src/Ametista.Command/Abstractions/ICommandResult.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Ametista.Command.Abstractions 4 | { 5 | public interface ICommandResult 6 | { 7 | bool Success { get; } 8 | DateTime Executed { get; } 9 | } 10 | } -------------------------------------------------------------------------------- /src/Ametista.Command/Ametista.Command.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/Ametista.Command/CreateCard/CreateCardCommand.cs: -------------------------------------------------------------------------------- 1 | using Ametista.Command.Abstractions; 2 | using System; 3 | 4 | namespace Ametista.Command.CreateCard 5 | { 6 | public class CreateCardCommand : ICommand 7 | { 8 | public CreateCardCommand(string number, string cardHolder, DateTime expirationDate) 9 | { 10 | Number = number ?? throw new ArgumentNullException(nameof(number)); 11 | CardHolder = cardHolder ?? throw new ArgumentNullException(nameof(cardHolder)); 12 | ExpirationDate = expirationDate; 13 | } 14 | 15 | public string Number { get; set; } 16 | public string CardHolder { get; set; } 17 | public DateTime ExpirationDate { get; set; } 18 | } 19 | } -------------------------------------------------------------------------------- /src/Ametista.Command/CreateCard/CreateCardCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using Ametista.Command.Abstractions; 2 | using Ametista.Core; 3 | using Ametista.Core.Cards; 4 | using Ametista.Core.Interfaces; 5 | using System; 6 | using System.Threading.Tasks; 7 | 8 | namespace Ametista.Command.CreateCard 9 | { 10 | public class CreateCardCommandHandler : ICommandHandler 11 | { 12 | private readonly IEventBus eventBus; 13 | private readonly ICardWriteOnlyRepository cardRepository; 14 | private readonly ValidationNotificationHandler notificationHandler; 15 | 16 | public CreateCardCommandHandler(IEventBus eventBus, ICardWriteOnlyRepository cardRepository, ValidationNotificationHandler notificationHandler) 17 | { 18 | this.eventBus = eventBus ?? throw new ArgumentNullException(nameof(eventBus)); 19 | this.cardRepository = cardRepository ?? throw new ArgumentNullException(nameof(cardRepository)); 20 | this.notificationHandler = notificationHandler ?? throw new ArgumentNullException(nameof(notificationHandler)); ; 21 | } 22 | 23 | public async Task Handle(CreateCardCommand command) 24 | { 25 | if (cardRepository.IsDuplicatedCardNumber(command.Number)) 26 | { 27 | notificationHandler.AddNotification(nameof(CreateCardCommand.Number), $"Card number already exists {command.Number}"); 28 | } 29 | 30 | var newCard = Card.CreateNewCard(command.Number, command.CardHolder, command.ExpirationDate); 31 | newCard.Validate(notificationHandler); 32 | 33 | if (newCard.Valid) 34 | { 35 | var success = await cardRepository.Add(newCard); 36 | 37 | if (success) 38 | { 39 | var cardCreatedEvent = new CardCreatedEvent(newCard); 40 | 41 | eventBus.Publish(cardCreatedEvent); 42 | } 43 | 44 | return new CreateCardCommandResult(newCard.Id, newCard.Number, newCard.CardHolder, newCard.ExpirationDate, success); 45 | } 46 | 47 | return CommandResult.CreateFailResult(); 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /src/Ametista.Command/CreateCard/CreateCardCommandResult.cs: -------------------------------------------------------------------------------- 1 | using Ametista.Command.Abstractions; 2 | using System; 3 | 4 | namespace Ametista.Command.CreateCard 5 | { 6 | public class CreateCardCommandResult : CommandResult 7 | { 8 | public CreateCardCommandResult() 9 | { 10 | Success = false; 11 | } 12 | 13 | public CreateCardCommandResult(Guid id, string number, string cardHolder, DateTime expirationDate, bool success) 14 | { 15 | Id = id; 16 | Number = number ?? throw new ArgumentNullException(nameof(number)); 17 | CardHolder = cardHolder ?? throw new ArgumentNullException(nameof(cardHolder)); 18 | ExpirationDate = expirationDate; 19 | Success = success; 20 | } 21 | 22 | public Guid Id { get; set; } 23 | public string Number { get; set; } 24 | public string CardHolder { get; set; } 25 | public DateTime ExpirationDate { get; set; } 26 | } 27 | } -------------------------------------------------------------------------------- /src/Ametista.Command/CreateTransaction/CreateTransactionCommand.cs: -------------------------------------------------------------------------------- 1 | using Ametista.Command.Abstractions; 2 | using System; 3 | 4 | namespace Ametista.Command.CreateTransaction 5 | { 6 | public class CreateTransactionCommand : ICommand 7 | { 8 | public CreateTransactionCommand(decimal amount, string currencyCode, Guid cardId, string uniqueId, DateTimeOffset chargeDate) 9 | { 10 | Amount = amount; 11 | CurrencyCode = currencyCode ?? throw new ArgumentNullException(nameof(currencyCode)); 12 | CardId = cardId; 13 | UniqueId = uniqueId ?? throw new ArgumentNullException(nameof(uniqueId)); 14 | ChargeDate = chargeDate; 15 | } 16 | 17 | public decimal Amount { get; internal set; } 18 | public string CurrencyCode { get; internal set; } 19 | public Guid CardId { get; internal set; } 20 | public string UniqueId { get; internal set; } 21 | public DateTimeOffset ChargeDate { get; internal set; } 22 | } 23 | } -------------------------------------------------------------------------------- /src/Ametista.Command/CreateTransaction/CreateTransactionCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using Ametista.Command.Abstractions; 2 | using Ametista.Core; 3 | using Ametista.Core.Interfaces; 4 | using Ametista.Core.Transactions; 5 | using System; 6 | using System.Threading.Tasks; 7 | 8 | namespace Ametista.Command.CreateTransaction 9 | { 10 | public class CreateTransactionCommandHandler : ICommandHandler 11 | { 12 | private readonly IEventBus eventBus; 13 | private readonly ITransactionWriteOnlyRepository transactionRepository; 14 | 15 | public CreateTransactionCommandHandler(IEventBus eventBus, ITransactionWriteOnlyRepository transactionRepository) 16 | { 17 | this.eventBus = eventBus ?? throw new ArgumentNullException(nameof(eventBus)); 18 | this.transactionRepository = transactionRepository ?? throw new ArgumentNullException(nameof(transactionRepository)); 19 | } 20 | 21 | public async Task Handle(CreateTransactionCommand command) 22 | { 23 | var charge = new Money(command.Amount, command.CurrencyCode); 24 | var newTransaction = Transaction.CreateTransactionForCard(command.CardId, command.UniqueId, command.ChargeDate, charge); 25 | 26 | var success = await transactionRepository.Add(newTransaction); 27 | 28 | if (success) 29 | { 30 | var transactionCreatedEvent = new TransactionCreatedEvent(newTransaction); 31 | 32 | eventBus.Publish(transactionCreatedEvent); 33 | } 34 | 35 | return new CreateTransactionCommandResult( 36 | newTransaction.Id, 37 | newTransaction.CardId, 38 | newTransaction.ChargeDate, 39 | newTransaction.UniqueId, 40 | newTransaction.Charge.Amount, 41 | newTransaction.Charge.CurrencyCode, 42 | success); 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /src/Ametista.Command/CreateTransaction/CreateTransactionCommandResult.cs: -------------------------------------------------------------------------------- 1 | using Ametista.Command.Abstractions; 2 | using System; 3 | 4 | namespace Ametista.Command.CreateTransaction 5 | { 6 | public class CreateTransactionCommandResult : CommandResult 7 | { 8 | public Guid Id { get; set; } 9 | public Guid CardId { get; set; } 10 | public DateTimeOffset ChargeDate { get; set; } 11 | public string UniqueId { get; set; } 12 | public decimal Amount { get; set; } 13 | public string CurrencyCode { get; set; } 14 | 15 | public CreateTransactionCommandResult(Guid id, Guid cardId, DateTimeOffset chargeDate, string uniqueId, decimal amount, string currencyCode, bool success) 16 | { 17 | Id = id; 18 | CardId = cardId; 19 | ChargeDate = chargeDate; 20 | UniqueId = uniqueId; 21 | Amount = amount; 22 | CurrencyCode = currencyCode; 23 | Success = success; 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /src/Ametista.Core/Ametista.Core.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/Ametista.Core/AmetistaConfiguration.cs: -------------------------------------------------------------------------------- 1 | namespace Ametista.Core 2 | { 3 | public class AmetistaConfiguration 4 | { 5 | public ConnectionStrings ConnectionStrings { get; set; } 6 | public int? RetryCount { get; set; } 7 | } 8 | 9 | public class ConnectionStrings 10 | { 11 | public string SqlServerConnectionString { get; set; } 12 | public string MongoConnectionString { get; set; } 13 | public string MongoDatabase { get; set; } 14 | public string EventBusHostname { get; set; } 15 | public string EventBusUsername { get; set; } 16 | public string EventBusPassword { get; set; } 17 | public string RedisCache { get; set; } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Ametista.Core/Cards/BillingCycle.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Ametista.Core.Cards 4 | { 5 | public sealed class BillingCycle 6 | { 7 | public int DueDay { get; private set; } 8 | public int Range { get; private set; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/Ametista.Core/Cards/Card.cs: -------------------------------------------------------------------------------- 1 | using Ametista.Core.Interfaces; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | namespace Ametista.Core.Cards 7 | { 8 | public class Card : IAggregateRoot 9 | { 10 | protected Card() 11 | { 12 | } 13 | 14 | private Card(string number, string cardHolder, DateTime expirationDate) 15 | { 16 | Id = Guid.NewGuid(); 17 | Number = number; 18 | CardHolder = cardHolder; 19 | ExpirationDate = expirationDate; 20 | } 21 | 22 | public string CardHolder { get; private set; } 23 | 24 | public DateTime ExpirationDate { get; private set; } 25 | 26 | public Guid Id { get; private set; } 27 | 28 | public string Number { get; private set; } 29 | 30 | public bool Valid { get; private set; } 31 | 32 | public static Card CreateNewCard(string number, string cardHolder, DateTime expirationDate) 33 | { 34 | return new Card(number, cardHolder, expirationDate); 35 | } 36 | 37 | public override bool Equals(object obj) 38 | { 39 | var card = obj as Card; 40 | return card != null && 41 | Number == card.Number; 42 | } 43 | 44 | public override int GetHashCode() 45 | { 46 | var hashCode = 1924120557; 47 | hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(Id); 48 | hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(Number); 49 | hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(CardHolder); 50 | hashCode = hashCode * -1521134295 + ExpirationDate.GetHashCode(); 51 | return hashCode; 52 | } 53 | 54 | public void Validate(ValidationNotificationHandler notificationHandler) 55 | { 56 | var validator = new CardValidator(notificationHandler); 57 | 58 | validator.Validate(this); 59 | 60 | Valid = !notificationHandler.Notifications.Any(); 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /src/Ametista.Core/Cards/CardCreatedEvent.cs: -------------------------------------------------------------------------------- 1 | namespace Ametista.Core.Cards 2 | { 3 | public class CardCreatedEvent : Event 4 | { 5 | public Card Data { get; set; } 6 | public CardCreatedEvent(Card card) 7 | { 8 | Data = card; 9 | Name = (nameof(CardCreatedEvent)); 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /src/Ametista.Core/Cards/CardValidator.cs: -------------------------------------------------------------------------------- 1 | namespace Ametista.Core.Cards 2 | { 3 | public class CardValidator : Validator 4 | { 5 | public CardValidator(ValidationNotificationHandler notificationHandler) : base(notificationHandler) 6 | { 7 | } 8 | 9 | public override void Validate(Card entity) 10 | { 11 | CheckRule(entity, nameof(Card.Number), $"Ivalid Card Number {entity.Number}"); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Ametista.Core/Cards/HasValidNumberSpec.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | 3 | namespace Ametista.Core.Cards 4 | { 5 | public class HasValidNumberSpec : CompositeSpecification 6 | { 7 | public HasValidNumberSpec() 8 | { 9 | 10 | } 11 | 12 | public override bool IsSatisfiedBy(Card candidate) 13 | { 14 | if (string.IsNullOrEmpty(candidate.Number) || candidate.Number.Length < 13) 15 | { 16 | return false; 17 | } 18 | 19 | // 20 | // https://stackoverflow.com/questions/21249670/implementing-luhn-algorithm-using-c-sharp 21 | // https://en.wikipedia.org/wiki/Luhn_algorithm 22 | // 23 | var digits = candidate.Number; 24 | 25 | return digits.All(char.IsDigit) && digits.Reverse() 26 | .Select(c => c - 48) 27 | .Select((thisNum, i) => i % 2 == 0 28 | ? thisNum 29 | : ((thisNum *= 2) > 9 ? thisNum - 9 : thisNum) 30 | ).Sum() % 10 == 0; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Ametista.Core/Cards/ICardWriteOnlyRepository.cs: -------------------------------------------------------------------------------- 1 |  2 | using Ametista.Core.Interfaces; 3 | 4 | namespace Ametista.Core.Cards 5 | { 6 | public interface ICardWriteOnlyRepository : IWriteOnlyRepository 7 | { 8 | bool IsDuplicatedCardNumber(string cardNamber); 9 | } 10 | } -------------------------------------------------------------------------------- /src/Ametista.Core/Shared/Event.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Ametista.Core 4 | { 5 | public class Event : IEvent 6 | { 7 | public Event() 8 | { 9 | Id = Guid.NewGuid(); 10 | OccurredOn = DateTime.Now; 11 | } 12 | 13 | public Guid Id { get; set; } 14 | public string Name { get; set; } 15 | public DateTime OccurredOn { get; set; } 16 | } 17 | } -------------------------------------------------------------------------------- /src/Ametista.Core/Shared/IAggregateRoot.cs: -------------------------------------------------------------------------------- 1 | namespace Ametista.Core.Interfaces 2 | { 3 | public interface IAggregateRoot : IEntity 4 | { 5 | } 6 | } -------------------------------------------------------------------------------- /src/Ametista.Core/Shared/IBuilder.cs: -------------------------------------------------------------------------------- 1 | namespace Ametista.Core.Shared 2 | { 3 | public interface IBuilder 4 | { 5 | T Build(); 6 | } 7 | } -------------------------------------------------------------------------------- /src/Ametista.Core/Shared/ICache.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Threading.Tasks; 5 | 6 | namespace Ametista.Core.Interfaces 7 | { 8 | public interface ICache 9 | { 10 | Task Store(string key, T value, params string[] @params); 11 | Task Get(string key); 12 | Task Delete(string key); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Ametista.Core/Shared/IDatabase.cs: -------------------------------------------------------------------------------- 1 | //using System.Linq; 2 | //using System.Threading.Tasks; 3 | 4 | //namespace FastUI.Core 5 | //{ 6 | // public interface IDatabase 7 | // { 8 | // Task InsertAsync(TEntity entity) where TEntity : IEntity; 9 | 10 | // Task UpdateAsync(TEntity entity) where TEntity : IEntity; 11 | 12 | // Task DeleteAsync(TEntity entity) where TEntity : IEntity; 13 | 14 | // IQueryable Query() where TEntity : IEntity; 15 | // } 16 | //} -------------------------------------------------------------------------------- /src/Ametista.Core/Shared/IEntity.cs: -------------------------------------------------------------------------------- 1 | namespace Ametista.Core 2 | { 3 | public interface IEntity 4 | { 5 | bool Valid { get; } 6 | void Validate(ValidationNotificationHandler notificationHandler); 7 | } 8 | 9 | public interface IEntity : IEntity 10 | { 11 | T Id { get; } 12 | } 13 | } -------------------------------------------------------------------------------- /src/Ametista.Core/Shared/IEvent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Ametista.Core 4 | { 5 | public interface IEvent 6 | { 7 | Guid Id { get; set; } 8 | string Name { get; set; } 9 | DateTime OccurredOn { get; set; } 10 | } 11 | } -------------------------------------------------------------------------------- /src/Ametista.Core/Shared/IEventBus.cs: -------------------------------------------------------------------------------- 1 | namespace Ametista.Core.Interfaces 2 | { 3 | public interface IEventBus 4 | { 5 | void Publish(IEvent @event); 6 | 7 | void Subscribe() where T : IEvent; 8 | } 9 | } -------------------------------------------------------------------------------- /src/Ametista.Core/Shared/IEventDispatcher.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Ametista.Core.Interfaces 4 | { 5 | public interface IEventDispatcher 6 | { 7 | Task Dispatch(TEvent e) where TEvent : IEvent; 8 | } 9 | } -------------------------------------------------------------------------------- /src/Ametista.Core/Shared/IEventHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Ametista.Core.Interfaces 4 | { 5 | public interface IEventHandler where TEvent : IEvent 6 | { 7 | Task Handle(TEvent e); 8 | } 9 | } -------------------------------------------------------------------------------- /src/Ametista.Core/Shared/IPersistentConnection.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Ametista.Core.Interfaces 4 | { 5 | public interface IPersistentConnection : IDisposable 6 | { 7 | bool IsConnected { get; } 8 | 9 | bool TryConnect(); 10 | 11 | T CreateModel(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Ametista.Core/Shared/ISpecification.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq.Expressions; 3 | 4 | namespace Ametista.Core 5 | { 6 | public interface ISpecification 7 | { 8 | bool IsSatisfiedBy(T candidate); 9 | ISpecification And(ISpecification other); 10 | ISpecification AndNot(ISpecification other); 11 | ISpecification Or(ISpecification other); 12 | ISpecification OrNot(ISpecification other); 13 | ISpecification Not(); 14 | } 15 | 16 | public abstract class LinqSpecification : CompositeSpecification 17 | { 18 | public abstract Expression> AsExpression(); 19 | public override bool IsSatisfiedBy(T candidate) => AsExpression().Compile()(candidate); 20 | } 21 | 22 | public abstract class CompositeSpecification : ISpecification 23 | { 24 | public abstract bool IsSatisfiedBy(T candidate); 25 | public ISpecification And(ISpecification other) => new AndSpecification(this, other); 26 | public ISpecification AndNot(ISpecification other) => new AndNotSpecification(this, other); 27 | public ISpecification Or(ISpecification other) => new OrSpecification(this, other); 28 | public ISpecification OrNot(ISpecification other) => new OrNotSpecification(this, other); 29 | public ISpecification Not() => new NotSpecification(this); 30 | } 31 | 32 | public class AndSpecification : CompositeSpecification 33 | { 34 | private readonly ISpecification left; 35 | private readonly ISpecification right; 36 | 37 | public AndSpecification(ISpecification left, ISpecification right) 38 | { 39 | this.left = left; 40 | this.right = right; 41 | } 42 | 43 | public override bool IsSatisfiedBy(T candidate) => left.IsSatisfiedBy(candidate) && right.IsSatisfiedBy(candidate); 44 | } 45 | 46 | public class AndNotSpecification : CompositeSpecification 47 | { 48 | private readonly ISpecification left; 49 | private readonly ISpecification right; 50 | 51 | public AndNotSpecification(ISpecification left, ISpecification right) 52 | { 53 | this.left = left; 54 | this.right = right; 55 | } 56 | 57 | public override bool IsSatisfiedBy(T candidate) => left.IsSatisfiedBy(candidate) && right.IsSatisfiedBy(candidate) != true; 58 | } 59 | 60 | public class OrSpecification : CompositeSpecification 61 | { 62 | private readonly ISpecification left; 63 | private readonly ISpecification right; 64 | 65 | public OrSpecification(ISpecification left, ISpecification right) 66 | { 67 | this.left = left; 68 | this.right = right; 69 | } 70 | 71 | public override bool IsSatisfiedBy(T candidate) => left.IsSatisfiedBy(candidate) || right.IsSatisfiedBy(candidate); 72 | } 73 | public class OrNotSpecification : CompositeSpecification 74 | { 75 | private readonly ISpecification left; 76 | private readonly ISpecification right; 77 | 78 | public OrNotSpecification(ISpecification left, ISpecification right) 79 | { 80 | this.left = left; 81 | this.right = right; 82 | } 83 | 84 | public override bool IsSatisfiedBy(T candidate) => left.IsSatisfiedBy(candidate) || !right.IsSatisfiedBy(candidate); 85 | } 86 | 87 | public class NotSpecification : CompositeSpecification 88 | { 89 | private readonly ISpecification other; 90 | public NotSpecification(ISpecification other) => this.other = other; 91 | public override bool IsSatisfiedBy(T candidate) => !other.IsSatisfiedBy(candidate); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/Ametista.Core/Shared/IWriteOnlyRepository.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | 5 | namespace Ametista.Core.Interfaces 6 | { 7 | public interface IWriteOnlyRepository where TEntity : IAggregateRoot 8 | { 9 | Task FindAsync(Guid id); // only allowed find the entity for update or delete 10 | 11 | Task Add(TEntity entity); 12 | 13 | Task Update(TEntity entity); 14 | 15 | Task Delete(TEntity entity); 16 | } 17 | } -------------------------------------------------------------------------------- /src/Ametista.Core/Shared/Money.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Ametista.Core 5 | { 6 | public sealed class Money 7 | { 8 | public Money(decimal amount, string currencyCode) 9 | { 10 | Amount = amount; 11 | CurrencyCode = currencyCode ?? throw new ArgumentNullException(nameof(currencyCode)); 12 | } 13 | 14 | public decimal Amount { get; private set; } 15 | public string CurrencyCode { get; private set; } 16 | 17 | public override bool Equals(object obj) 18 | { 19 | var currency = obj as Money; 20 | return currency != null && 21 | Amount == currency.Amount && 22 | CurrencyCode == currency.CurrencyCode; 23 | } 24 | 25 | public override int GetHashCode() 26 | { 27 | var hashCode = -1731499236; 28 | hashCode = hashCode * -1521134295 + Amount.GetHashCode(); 29 | hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(CurrencyCode); 30 | return hashCode; 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /src/Ametista.Core/Shared/ValidationNotification.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Ametista.Core 5 | { 6 | public class ValidationNotification : IEquatable 7 | { 8 | public ValidationNotification(string code, string message) 9 | { 10 | Code = code; 11 | Message = message ?? throw new ArgumentNullException(nameof(message)); 12 | } 13 | 14 | public string Code { get; private set; } 15 | public string Message { get; private set; } 16 | 17 | public override bool Equals(object obj) 18 | { 19 | return Equals(obj as ValidationNotification); 20 | } 21 | 22 | public bool Equals(ValidationNotification other) 23 | { 24 | return other != null && 25 | Code.Equals(other.Code) && 26 | Message == other.Message; 27 | } 28 | 29 | public override int GetHashCode() 30 | { 31 | var hashCode = -1809243720; 32 | hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(Code); 33 | hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(Message); 34 | return hashCode; 35 | } 36 | 37 | public static bool operator ==(ValidationNotification notification1, ValidationNotification notification2) 38 | { 39 | return EqualityComparer.Default.Equals(notification1, notification2); 40 | } 41 | 42 | public static bool operator !=(ValidationNotification notification1, ValidationNotification notification2) 43 | { 44 | return !(notification1 == notification2); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Ametista.Core/Shared/ValidationNotificationHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Ametista.Core 4 | { 5 | public class ValidationNotificationHandler 6 | { 7 | public virtual IReadOnlyCollection Notifications => _notifications.AsReadOnly(); 8 | 9 | private readonly List _notifications = new List(); 10 | 11 | public void AddNotification(string code, string message) 12 | { 13 | var notification = new ValidationNotification(code, message); 14 | 15 | _notifications.Add(notification); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Ametista.Core/Shared/Validator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Ametista.Core 4 | { 5 | public abstract class Validator 6 | { 7 | private readonly ValidationNotificationHandler notificationHandler; 8 | 9 | protected Validator(ValidationNotificationHandler notificationHandler) 10 | { 11 | this.notificationHandler = notificationHandler ?? throw new ArgumentNullException(nameof(notificationHandler)); 12 | } 13 | 14 | public abstract void Validate(TEntity entity); 15 | 16 | protected void CheckRule(TEntity entity, ISpecification specification, string code, string message) 17 | { 18 | var isSatisfied = specification.IsSatisfiedBy(entity); 19 | 20 | if (!isSatisfied) 21 | { 22 | notificationHandler.AddNotification(code, message); 23 | } 24 | } 25 | 26 | protected void CheckRule(TEntity entity, string code, string message) where TSpecification : CompositeSpecification, new() 27 | { 28 | var spec = new TSpecification(); 29 | 30 | CheckRule(entity, spec, code, message); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Ametista.Core/Transactions/ITransactionWriteOnlyRepository.cs: -------------------------------------------------------------------------------- 1 | using Ametista.Core.Interfaces; 2 | 3 | namespace Ametista.Core.Transactions 4 | { 5 | public interface ITransactionWriteOnlyRepository : IWriteOnlyRepository 6 | { 7 | } 8 | } -------------------------------------------------------------------------------- /src/Ametista.Core/Transactions/Transaction.cs: -------------------------------------------------------------------------------- 1 | using Ametista.Core.Interfaces; 2 | using System; 3 | using System.Collections.Generic; 4 | 5 | namespace Ametista.Core.Transactions 6 | { 7 | public class Transaction : IAggregateRoot 8 | { 9 | protected Transaction() 10 | { 11 | } 12 | 13 | private Transaction(Guid cardGuid, string uniqueId, DateTimeOffset chargeDate, Money charge) 14 | { 15 | Id = Guid.NewGuid(); 16 | CardId = cardGuid; 17 | UniqueId = uniqueId ?? throw new ArgumentNullException(nameof(uniqueId)); 18 | ChargeDate = chargeDate; 19 | Charge = charge ?? throw new ArgumentNullException(nameof(charge)); 20 | } 21 | 22 | public Guid CardId { get; private set; } 23 | 24 | public Money Charge { get; private set; } 25 | 26 | public DateTimeOffset ChargeDate { get; private set; } 27 | 28 | public Guid Id { get; private set; } 29 | 30 | public string UniqueId { get; private set; } 31 | 32 | public bool Valid { get; private set; } 33 | 34 | public static Transaction CreateTransactionForCard(Guid cardGuid, string uniqueId, DateTimeOffset chargeDate, Money charge) 35 | { 36 | return new Transaction(cardGuid, uniqueId, chargeDate, charge); 37 | } 38 | 39 | public override bool Equals(object obj) 40 | { 41 | var transaction = obj as Transaction; 42 | return transaction != null && 43 | UniqueId == transaction.UniqueId; 44 | } 45 | 46 | public override int GetHashCode() 47 | { 48 | return -401120461 + EqualityComparer.Default.GetHashCode(UniqueId); 49 | } 50 | 51 | public void Validate(ValidationNotificationHandler notificationHandler) 52 | { 53 | throw new NotImplementedException(); 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /src/Ametista.Core/Transactions/TransactionCreatedEvent.cs: -------------------------------------------------------------------------------- 1 | namespace Ametista.Core.Transactions 2 | { 3 | public class TransactionCreatedEvent : Event 4 | { 5 | public Transaction Data { get; set; } 6 | 7 | public TransactionCreatedEvent(Transaction transaction) 8 | { 9 | Data = transaction; 10 | Name = (nameof(TransactionCreatedEvent)); 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /src/Ametista.Infrastructure/Ametista.Infrastructure.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | all 12 | runtime; build; native; contentfiles; analyzers; buildtransitive 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/Ametista.Infrastructure/Bus/RabbitMQEventBus.cs: -------------------------------------------------------------------------------- 1 | using Ametista.Core; 2 | using Ametista.Core.Interfaces; 3 | using Microsoft.Extensions.Logging; 4 | using Newtonsoft.Json; 5 | using Newtonsoft.Json.Serialization; 6 | using Polly; 7 | using Polly.Retry; 8 | using RabbitMQ.Client; 9 | using RabbitMQ.Client.Events; 10 | using RabbitMQ.Client.Exceptions; 11 | using System; 12 | using System.Collections.Generic; 13 | using System.Net.Sockets; 14 | using System.Reflection; 15 | using System.Text; 16 | using System.Threading.Tasks; 17 | 18 | namespace Ametista.Infrastructure.Bus 19 | { 20 | public class RabbitMQEventBus : IEventBus, IDisposable 21 | { 22 | private readonly string QUEUE_NAME = "ametista_events"; 23 | private readonly string BROKER_NAME = "ametista_events"; 24 | 25 | private readonly IEventDispatcher _eventDispatcher; 26 | private readonly IPersistentConnection _persistentConnection; 27 | private readonly ILogger _logger; 28 | private readonly static Dictionary _subsManager = new(); 29 | private readonly int _retryCount; 30 | 31 | private IModel _consumerChannel; 32 | 33 | public RabbitMQEventBus(IEventDispatcher eventDispatcher, 34 | IPersistentConnection persistentConnection, 35 | ILogger logger, 36 | int retryCount = 5) 37 | { 38 | _eventDispatcher = eventDispatcher ?? throw new ArgumentNullException(nameof(eventDispatcher)); 39 | _persistentConnection = persistentConnection ?? throw new ArgumentNullException(nameof(persistentConnection)); 40 | _logger = logger ?? throw new ArgumentNullException(nameof(logger)); 41 | _retryCount = retryCount; 42 | _consumerChannel = CreateConsumerChannel(); 43 | 44 | JsonConvert.DefaultSettings = () => new JsonSerializerSettings 45 | { 46 | ContractResolver = new NonPublicPropertiesResolver() 47 | }; 48 | } 49 | 50 | public void Publish(IEvent @event) 51 | { 52 | if (!_persistentConnection.IsConnected) 53 | { 54 | _persistentConnection.TryConnect(); 55 | } 56 | 57 | var policy = RetryPolicy.Handle() 58 | .Or() 59 | .WaitAndRetry(_retryCount, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), (ex, time) => 60 | { 61 | _logger.LogWarning("Retrying message because of error {0}", ex.ToString()); 62 | }); 63 | 64 | using var channel = _persistentConnection.CreateModel(); 65 | var eventName = @event.GetType().Name; 66 | 67 | channel.ExchangeDeclare(exchange: BROKER_NAME, 68 | type: "direct"); 69 | 70 | var message = JsonConvert.SerializeObject(@event); 71 | var body = Encoding.UTF8.GetBytes(message); 72 | 73 | policy.Execute(() => 74 | { 75 | var properties = channel.CreateBasicProperties(); 76 | properties.DeliveryMode = 2; // persistent 77 | 78 | channel.BasicPublish(exchange: BROKER_NAME, 79 | routingKey: eventName, 80 | mandatory: true, 81 | basicProperties: properties, 82 | body: body); 83 | }); 84 | } 85 | 86 | public class NonPublicPropertiesResolver : DefaultContractResolver 87 | { 88 | protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) 89 | { 90 | var prop = base.CreateProperty(member, memberSerialization); 91 | if (member is PropertyInfo pi) 92 | { 93 | prop.Readable = (pi.GetMethod != null); 94 | prop.Writable = (pi.SetMethod != null); 95 | } 96 | return prop; 97 | } 98 | } 99 | 100 | public void Subscribe() where T : IEvent 101 | { 102 | var eventName = typeof(T).Name; 103 | var containsKey = _subsManager.ContainsKey(eventName); 104 | if (!containsKey) 105 | { 106 | _subsManager.Add(eventName, typeof(T)); 107 | } 108 | 109 | if (!_persistentConnection.IsConnected) 110 | { 111 | _persistentConnection.TryConnect(); 112 | } 113 | 114 | using var channel = _persistentConnection.CreateModel(); 115 | channel.QueueBind(queue: QUEUE_NAME, 116 | exchange: BROKER_NAME, 117 | routingKey: eventName); 118 | } 119 | 120 | private IModel CreateConsumerChannel() 121 | { 122 | if (!_persistentConnection.IsConnected) 123 | { 124 | _persistentConnection.TryConnect(); 125 | } 126 | 127 | var channel = _persistentConnection.CreateModel(); 128 | 129 | channel.ExchangeDeclare(exchange: BROKER_NAME, 130 | type: "direct"); 131 | 132 | channel.QueueDeclare(queue: QUEUE_NAME, 133 | durable: true, 134 | exclusive: false, 135 | autoDelete: false, 136 | arguments: null); 137 | 138 | var consumer = new EventingBasicConsumer(channel); 139 | consumer.Received += async (model, ea) => 140 | { 141 | var eventName = ea.RoutingKey; 142 | var message = Encoding.UTF8.GetString(ea.Body.ToArray()); 143 | 144 | await ProcessEvent(eventName, message); 145 | 146 | channel.BasicAck(ea.DeliveryTag, multiple: false); 147 | }; 148 | 149 | channel.BasicConsume(queue: QUEUE_NAME, 150 | autoAck: false, 151 | consumer: consumer); 152 | 153 | channel.CallbackException += (sender, ea) => 154 | { 155 | _consumerChannel.Dispose(); 156 | _consumerChannel = CreateConsumerChannel(); 157 | }; 158 | 159 | return channel; 160 | } 161 | 162 | private async Task ProcessEvent(string eventName, string message) 163 | { 164 | if (_subsManager.ContainsKey(eventName)) 165 | { 166 | var @type = _subsManager[eventName]; 167 | var @event = JsonConvert.DeserializeObject(message, @type) as IEvent; 168 | 169 | await _eventDispatcher.Dispatch(@event); 170 | } 171 | } 172 | 173 | private bool disposedValue = false; // To detect redundant calls 174 | 175 | protected virtual void Dispose(bool disposing) 176 | { 177 | if (!disposedValue) 178 | { 179 | if (disposing) 180 | { 181 | if (_consumerChannel != null) 182 | { 183 | _consumerChannel.Dispose(); 184 | } 185 | 186 | _subsManager.Clear(); 187 | } 188 | disposedValue = true; 189 | } 190 | } 191 | 192 | void IDisposable.Dispose() 193 | { 194 | Dispose(true); 195 | } 196 | } 197 | } -------------------------------------------------------------------------------- /src/Ametista.Infrastructure/Bus/RabbitMQPersistentConnection.cs: -------------------------------------------------------------------------------- 1 | using Ametista.Core; 2 | using Ametista.Core.Interfaces; 3 | using Microsoft.Extensions.Logging; 4 | using Polly; 5 | using Polly.Retry; 6 | using RabbitMQ.Client; 7 | using RabbitMQ.Client.Events; 8 | using RabbitMQ.Client.Exceptions; 9 | using System; 10 | using System.IO; 11 | using System.Net.Sockets; 12 | 13 | namespace Ametista.Infrastructure.Bus 14 | { 15 | public class RabbitMQPersistentConnection : IPersistentConnection 16 | { 17 | private readonly IConnectionFactory _connectionFactory; 18 | private readonly ILogger _logger; 19 | private readonly int _retryCount; 20 | private readonly object sync_root = new object(); 21 | private IConnection _connection; 22 | private bool _disposed; 23 | 24 | public RabbitMQPersistentConnection(AmetistaConfiguration configuration, ILogger logger) 25 | { 26 | _connectionFactory = CreateFactory(configuration); 27 | _logger = logger ?? throw new ArgumentNullException(nameof(logger)); 28 | _retryCount = configuration.RetryCount ?? 5; 29 | } 30 | 31 | public bool IsConnected 32 | { 33 | get 34 | { 35 | return _connection != null && _connection.IsOpen && !_disposed; 36 | } 37 | } 38 | 39 | public IModel CreateModel() 40 | { 41 | if (!IsConnected) 42 | { 43 | throw new InvalidOperationException("No RabbitMQ connections are available to perform this action"); 44 | } 45 | 46 | return _connection.CreateModel(); 47 | } 48 | 49 | public void Dispose() 50 | { 51 | if (_disposed) return; 52 | 53 | _disposed = true; 54 | 55 | try 56 | { 57 | _connection.Dispose(); 58 | } 59 | catch (IOException ex) 60 | { 61 | _logger.LogCritical(ex.ToString()); 62 | } 63 | } 64 | 65 | public bool TryConnect() 66 | { 67 | _logger.LogInformation("RabbitMQ Client is trying to connect"); 68 | 69 | lock (sync_root) 70 | { 71 | var policy = RetryPolicy.Handle() 72 | .Or() 73 | .WaitAndRetry(_retryCount, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), (ex, time) => 74 | { 75 | _logger.LogWarning(ex.ToString()); 76 | } 77 | ); 78 | 79 | policy.Execute(() => 80 | { 81 | _connection = _connectionFactory 82 | .CreateConnection(); 83 | }); 84 | 85 | if (IsConnected) 86 | { 87 | _connection.ConnectionShutdown += OnConnectionShutdown; 88 | _connection.CallbackException += OnCallbackException; 89 | _connection.ConnectionBlocked += OnConnectionBlocked; 90 | 91 | _logger.LogInformation($"RabbitMQ persistent connection acquired a connection {_connection.Endpoint.HostName} and is subscribed to failure events"); 92 | 93 | return true; 94 | } 95 | else 96 | { 97 | _logger.LogCritical("FATAL ERROR: RabbitMQ connections could not be created and opened"); 98 | 99 | return false; 100 | } 101 | } 102 | } 103 | 104 | private IConnectionFactory CreateFactory(AmetistaConfiguration configuration) 105 | { 106 | var factory = new ConnectionFactory() 107 | { 108 | HostName = configuration.ConnectionStrings.EventBusHostname, 109 | }; 110 | 111 | if (!string.IsNullOrEmpty(configuration.ConnectionStrings.EventBusUsername)) 112 | { 113 | factory.UserName = configuration.ConnectionStrings.EventBusUsername; 114 | } 115 | 116 | if (!string.IsNullOrEmpty(configuration.ConnectionStrings.EventBusPassword)) 117 | { 118 | factory.Password = configuration.ConnectionStrings.EventBusPassword; 119 | } 120 | 121 | return factory; 122 | } 123 | private void OnCallbackException(object sender, CallbackExceptionEventArgs e) 124 | { 125 | if (_disposed) return; 126 | 127 | _logger.LogWarning("A RabbitMQ connection throw exception. Trying to re-connect..."); 128 | 129 | TryConnect(); 130 | } 131 | 132 | private void OnConnectionBlocked(object sender, ConnectionBlockedEventArgs e) 133 | { 134 | if (_disposed) return; 135 | 136 | _logger.LogWarning("A RabbitMQ connection is shutdown. Trying to re-connect..."); 137 | 138 | TryConnect(); 139 | } 140 | private void OnConnectionShutdown(object sender, ShutdownEventArgs reason) 141 | { 142 | if (_disposed) return; 143 | 144 | _logger.LogWarning("A RabbitMQ connection is on shutdown. Trying to re-connect..."); 145 | 146 | TryConnect(); 147 | } 148 | } 149 | } -------------------------------------------------------------------------------- /src/Ametista.Infrastructure/Cache/RedisCache.cs: -------------------------------------------------------------------------------- 1 | using Ametista.Core; 2 | using Ametista.Core.Interfaces; 3 | using Microsoft.Extensions.Logging; 4 | using Newtonsoft.Json; 5 | using StackExchange.Redis; 6 | using System; 7 | using System.Linq; 8 | using System.Threading.Tasks; 9 | 10 | namespace Ametista.Infrastructure.Cache 11 | { 12 | public class RedisCache : ICache 13 | { 14 | private readonly ConnectionMultiplexer redis; 15 | private readonly IDatabase db; 16 | private readonly ILogger _logger; 17 | 18 | public RedisCache(AmetistaConfiguration configuration, ILogger logger) 19 | { 20 | redis = ConnectionMultiplexer.Connect(configuration.ConnectionStrings.RedisCache); 21 | db = redis.GetDatabase(); 22 | _logger = logger; 23 | } 24 | 25 | public async Task Delete(string key) 26 | { 27 | try 28 | { 29 | foreach (var ep in redis.GetEndPoints()) 30 | { 31 | var server = redis.GetServer(ep); 32 | var keys = server.Keys(database: 0, pattern: key + "*").ToArray(); 33 | await db.KeyDeleteAsync(keys); 34 | } 35 | 36 | return await Task.FromResult(true); 37 | } 38 | catch (Exception ex) 39 | { 40 | _logger.LogError(ex, "Erro when deleting redis cache key"); 41 | 42 | throw; 43 | } 44 | } 45 | 46 | public async Task Get(string key) 47 | { 48 | var value = await db.StringGetAsync(key); 49 | 50 | if (!value.HasValue) 51 | { 52 | return default(T); 53 | } 54 | 55 | var result = JsonConvert.DeserializeObject(value); 56 | 57 | return await Task.FromResult(result); 58 | } 59 | 60 | public async Task Store(string key, T value, params string[] @params) 61 | { 62 | var complexKey = GenerateKeyWithParams(key, @params); 63 | var cache = JsonConvert.SerializeObject(value); 64 | 65 | await db.StringSetAsync(complexKey, cache); 66 | } 67 | 68 | private string GenerateKeyWithParams(string key, string[] @params) 69 | { 70 | if (@params == null) 71 | { 72 | return key; 73 | } 74 | 75 | var complexKey = key; 76 | 77 | foreach (var param in @params) 78 | { 79 | complexKey += $"&{param}"; 80 | } 81 | 82 | return complexKey; 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Ametista.Infrastructure/Dispatchers/CommandDispatcher.cs: -------------------------------------------------------------------------------- 1 | using Ametista.Command.Abstractions; 2 | using Autofac; 3 | using System.Threading.Tasks; 4 | 5 | namespace Ametista.Infrastructure 6 | { 7 | public class CommandDispatcher : ICommandDispatcher 8 | { 9 | private readonly IComponentContext componentContext; 10 | 11 | public CommandDispatcher(IComponentContext componentContext) 12 | { 13 | this.componentContext = componentContext; 14 | } 15 | 16 | public Task Dispatch(ICommand command) where TResult : ICommandResult 17 | { 18 | if (command == null) 19 | { 20 | throw new System.ArgumentNullException(nameof(command)); 21 | } 22 | 23 | var commandHandlerType = typeof(ICommandHandler<,>).MakeGenericType(command.GetType(), typeof(TResult)); 24 | 25 | dynamic handler = componentContext.Resolve(commandHandlerType); 26 | 27 | return (Task)commandHandlerType 28 | .GetMethod("Handle") 29 | .Invoke(handler, new object[] { command }); 30 | } 31 | 32 | public Task DispatchNonResult(ICommand command) 33 | { 34 | var commandHandlerType = typeof(ICommandHandler<>).MakeGenericType(command.GetType()); 35 | 36 | dynamic handler = componentContext.Resolve(commandHandlerType); 37 | 38 | return (Task)commandHandlerType 39 | .GetMethod("HandleNonResult") 40 | .Invoke(handler, new object[] { command }); 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /src/Ametista.Infrastructure/Dispatchers/EventDispatcher.cs: -------------------------------------------------------------------------------- 1 | using Ametista.Core; 2 | using Ametista.Core.Interfaces; 3 | using Autofac; 4 | using System.Threading.Tasks; 5 | 6 | namespace Ametista.Infrastructure 7 | { 8 | public class EventDispatcher : IEventDispatcher 9 | { 10 | private readonly IComponentContext componentContext; 11 | 12 | public EventDispatcher(IComponentContext componentContext) 13 | { 14 | this.componentContext = componentContext; 15 | } 16 | 17 | public Task Dispatch(TEvent e) where TEvent : IEvent 18 | { 19 | if (e == null) 20 | { 21 | throw new System.ArgumentNullException(nameof(e)); 22 | } 23 | 24 | var eventType = typeof(IEventHandler<>).MakeGenericType(e.GetType()); 25 | 26 | dynamic handler = componentContext.Resolve(eventType); 27 | 28 | return (Task)eventType 29 | .GetMethod("Handle") 30 | .Invoke(handler, new object[] { e }); 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /src/Ametista.Infrastructure/Dispatchers/QueryDispatcher.cs: -------------------------------------------------------------------------------- 1 | using Ametista.Query.Abstractions; 2 | using Autofac; 3 | using System.Threading.Tasks; 4 | 5 | namespace Ametista.Infrastructure 6 | { 7 | public class QueryDispatcher : IQueryDispatcher 8 | { 9 | private readonly IComponentContext componentContext; 10 | 11 | public QueryDispatcher(IComponentContext componentContext) 12 | { 13 | this.componentContext = componentContext; 14 | } 15 | 16 | public Task ExecuteAsync(IQuery query) 17 | { 18 | var queryHandlerType = typeof(IQueryHandler<,>).MakeGenericType(query.GetType(), typeof(TModel)); 19 | 20 | var handler = componentContext.Resolve(queryHandlerType); 21 | 22 | return (Task)queryHandlerType 23 | .GetMethod("HandleAsync") 24 | .Invoke(handler, new object[] { query }); 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /src/Ametista.Infrastructure/IoC/CommandModule.cs: -------------------------------------------------------------------------------- 1 | using Ametista.Command.Abstractions; 2 | using Ametista.Core.Cards; 3 | using Ametista.Core.Transactions; 4 | using Ametista.Infrastructure.Persistence.Repository; 5 | using Autofac; 6 | using System.Reflection; 7 | 8 | namespace Ametista.Infrastructure.IoC 9 | { 10 | public class CommandModule : Autofac.Module 11 | { 12 | protected override void Load(ContainerBuilder builder) 13 | { 14 | builder 15 | .RegisterType() 16 | .As() 17 | .InstancePerLifetimeScope(); 18 | 19 | builder 20 | .RegisterType() 21 | .As() 22 | .InstancePerLifetimeScope(); 23 | 24 | builder 25 | .RegisterAssemblyTypes(typeof(ICommandHandler<,>).GetTypeInfo().Assembly) 26 | .AsClosedTypesOf(typeof(ICommandHandler<,>)) 27 | .InstancePerLifetimeScope(); 28 | 29 | builder 30 | .RegisterAssemblyTypes(typeof(ICommandHandler<>).GetTypeInfo().Assembly) 31 | .AsClosedTypesOf(typeof(ICommandHandler<>)) 32 | .InstancePerLifetimeScope(); 33 | 34 | builder 35 | .RegisterType() 36 | .As() 37 | .SingleInstance(); 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /src/Ametista.Infrastructure/IoC/EventModule.cs: -------------------------------------------------------------------------------- 1 | using Ametista.Core.Cards; 2 | using Ametista.Core.Transactions; 3 | using Ametista.Core.Interfaces; 4 | using Ametista.Query; 5 | using Ametista.Query.EventHandlers; 6 | using Autofac; 7 | using System.Reflection; 8 | 9 | namespace Ametista.Infrastructure.IoC 10 | { 11 | public class EventModule : Autofac.Module 12 | { 13 | protected override void Load(ContainerBuilder builder) 14 | { 15 | builder 16 | .RegisterAssemblyTypes(typeof(IEventHandler<>).GetTypeInfo().Assembly) 17 | .AsClosedTypesOf(typeof(IEventHandler<>)) 18 | .InstancePerLifetimeScope(); 19 | 20 | builder 21 | .RegisterType() 22 | .As>() 23 | .InstancePerLifetimeScope(); 24 | 25 | builder 26 | .RegisterType() 27 | .As>() 28 | .InstancePerLifetimeScope(); 29 | 30 | builder 31 | .RegisterType() 32 | .AsSelf() 33 | .InstancePerLifetimeScope(); 34 | 35 | builder 36 | .RegisterType() 37 | .As() 38 | .SingleInstance(); 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /src/Ametista.Infrastructure/IoC/InfrastructureModule.cs: -------------------------------------------------------------------------------- 1 | using Ametista.Core.Interfaces; 2 | using Ametista.Infrastructure.Bus; 3 | using Ametista.Infrastructure.Cache; 4 | using Autofac; 5 | using RabbitMQ.Client; 6 | 7 | namespace Ametista.Infrastructure.IoC 8 | { 9 | public class InfrastructureModule : Autofac.Module 10 | { 11 | protected override void Load(ContainerBuilder builder) 12 | { 13 | builder 14 | .RegisterType() 15 | .As>() 16 | .SingleInstance(); 17 | 18 | builder 19 | .RegisterType() 20 | .As() 21 | .SingleInstance(); 22 | 23 | builder 24 | .RegisterType() 25 | .As() 26 | .SingleInstance(); 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /src/Ametista.Infrastructure/IoC/QueryModule.cs: -------------------------------------------------------------------------------- 1 | using Ametista.Query; 2 | using Ametista.Query.Abstractions; 3 | using Ametista.Query.Materializers; 4 | using Autofac; 5 | using System.Reflection; 6 | 7 | namespace Ametista.Infrastructure.IoC 8 | { 9 | public class QueryModule : Autofac.Module 10 | { 11 | protected override void Load(ContainerBuilder builder) 12 | { 13 | builder.RegisterAssemblyTypes(typeof(IQueryHandler<,>).GetTypeInfo().Assembly) 14 | .AsClosedTypesOf(typeof(IQueryHandler<,>)) 15 | .InstancePerLifetimeScope(); 16 | 17 | builder 18 | .RegisterType() 19 | .AsSelf() 20 | .InstancePerLifetimeScope(); 21 | 22 | builder 23 | .RegisterType() 24 | .As() 25 | .SingleInstance(); 26 | 27 | builder 28 | .RegisterType() 29 | .As() 30 | .SingleInstance(); 31 | 32 | builder 33 | .RegisterType() 34 | .As() 35 | .SingleInstance(); 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /src/Ametista.Infrastructure/Migrations/20180828224253_InitialCreate.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using Ametista.Infrastructure.Persistence; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Infrastructure; 6 | using Microsoft.EntityFrameworkCore.Metadata; 7 | using Microsoft.EntityFrameworkCore.Migrations; 8 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 9 | 10 | namespace Ametista.Infrastructure.Migrations 11 | { 12 | [DbContext(typeof(WriteDbContext))] 13 | [Migration("20180828224253_InitialCreate")] 14 | partial class InitialCreate 15 | { 16 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 17 | { 18 | #pragma warning disable 612, 618 19 | modelBuilder 20 | .HasAnnotation("ProductVersion", "2.1.2-rtm-30932") 21 | .HasAnnotation("Relational:MaxIdentifierLength", 128) 22 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 23 | 24 | modelBuilder.Entity("Ametista.Core.Entity.Card", b => 25 | { 26 | b.Property("Id") 27 | .ValueGeneratedOnAdd(); 28 | 29 | b.Property("CardHolder"); 30 | 31 | b.Property("ExpirationDate"); 32 | 33 | b.Property("Number"); 34 | 35 | b.HasKey("Id"); 36 | 37 | b.ToTable("Cards"); 38 | }); 39 | 40 | modelBuilder.Entity("Ametista.Core.Entity.Transaction", b => 41 | { 42 | b.Property("Id") 43 | .ValueGeneratedOnAdd(); 44 | 45 | b.Property("CardId"); 46 | 47 | b.Property("ChargeDate"); 48 | 49 | b.Property("UniqueId"); 50 | 51 | b.HasKey("Id"); 52 | 53 | b.ToTable("Transactions"); 54 | }); 55 | 56 | modelBuilder.Entity("Ametista.Core.Entity.Transaction", b => 57 | { 58 | b.OwnsOne("Ametista.Core.ValueObjects.Money", "Charge", b1 => 59 | { 60 | b1.Property("TransactionId"); 61 | 62 | b1.Property("Amount") 63 | .HasColumnName("Amount"); 64 | 65 | b1.Property("CurrencyCode") 66 | .HasColumnName("CurrencyCode"); 67 | 68 | b1.ToTable("Transactions"); 69 | 70 | b1.HasOne("Ametista.Core.Entity.Transaction") 71 | .WithOne("Charge") 72 | .HasForeignKey("Ametista.Core.ValueObjects.Money", "TransactionId") 73 | .OnDelete(DeleteBehavior.Cascade); 74 | }); 75 | }); 76 | #pragma warning restore 612, 618 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Ametista.Infrastructure/Migrations/20180828224253_InitialCreate.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | using System; 3 | 4 | namespace Ametista.Infrastructure.Migrations 5 | { 6 | public partial class InitialCreate : Migration 7 | { 8 | protected override void Up(MigrationBuilder migrationBuilder) 9 | { 10 | migrationBuilder.CreateTable( 11 | name: "Cards", 12 | columns: table => new 13 | { 14 | CardHolder = table.Column(nullable: true), 15 | ExpirationDate = table.Column(nullable: false), 16 | Id = table.Column(nullable: false), 17 | Number = table.Column(nullable: true) 18 | }, 19 | constraints: table => 20 | { 21 | table.PrimaryKey("PK_Cards", x => x.Id); 22 | }); 23 | 24 | migrationBuilder.CreateTable( 25 | name: "Transactions", 26 | columns: table => new 27 | { 28 | CardId = table.Column(nullable: false), 29 | Amount = table.Column(nullable: false), 30 | CurrencyCode = table.Column(nullable: true), 31 | Id = table.Column(nullable: false), 32 | ChargeDate = table.Column(nullable: false), 33 | UniqueId = table.Column(nullable: true) 34 | }, 35 | constraints: table => 36 | { 37 | table.PrimaryKey("PK_Transactions", x => x.Id); 38 | }); 39 | } 40 | 41 | protected override void Down(MigrationBuilder migrationBuilder) 42 | { 43 | migrationBuilder.DropTable( 44 | name: "Cards"); 45 | 46 | migrationBuilder.DropTable( 47 | name: "Transactions"); 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /src/Ametista.Infrastructure/Migrations/WriteDbContextModelSnapshot.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using Ametista.Infrastructure.Persistence; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Infrastructure; 6 | using Microsoft.EntityFrameworkCore.Metadata; 7 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 8 | 9 | namespace Ametista.Infrastructure.Migrations 10 | { 11 | [DbContext(typeof(WriteDbContext))] 12 | partial class WriteDbContextModelSnapshot : ModelSnapshot 13 | { 14 | protected override void BuildModel(ModelBuilder modelBuilder) 15 | { 16 | #pragma warning disable 612, 618 17 | modelBuilder 18 | .HasAnnotation("ProductVersion", "2.1.2-rtm-30932") 19 | .HasAnnotation("Relational:MaxIdentifierLength", 128) 20 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 21 | 22 | modelBuilder.Entity("Ametista.Core.Entity.Card", b => 23 | { 24 | b.Property("Id") 25 | .ValueGeneratedOnAdd(); 26 | 27 | b.Property("CardHolder"); 28 | 29 | b.Property("ExpirationDate"); 30 | 31 | b.Property("Number"); 32 | 33 | b.HasKey("Id"); 34 | 35 | b.ToTable("Cards"); 36 | }); 37 | 38 | modelBuilder.Entity("Ametista.Core.Entity.Transaction", b => 39 | { 40 | b.Property("Id") 41 | .ValueGeneratedOnAdd(); 42 | 43 | b.Property("CardId"); 44 | 45 | b.Property("ChargeDate"); 46 | 47 | b.Property("UniqueId"); 48 | 49 | b.HasKey("Id"); 50 | 51 | b.ToTable("Transactions"); 52 | }); 53 | 54 | modelBuilder.Entity("Ametista.Core.Entity.Transaction", b => 55 | { 56 | b.OwnsOne("Ametista.Core.ValueObjects.Money", "Charge", b1 => 57 | { 58 | b1.Property("TransactionId"); 59 | 60 | b1.Property("Amount") 61 | .HasColumnName("Amount"); 62 | 63 | b1.Property("CurrencyCode") 64 | .HasColumnName("CurrencyCode"); 65 | 66 | b1.ToTable("Transactions"); 67 | 68 | b1.HasOne("Ametista.Core.Entity.Transaction") 69 | .WithOne("Charge") 70 | .HasForeignKey("Ametista.Core.ValueObjects.Money", "TransactionId") 71 | .OnDelete(DeleteBehavior.Cascade); 72 | }); 73 | }); 74 | #pragma warning restore 612, 618 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Ametista.Infrastructure/Persistence/Repository/CardWriteOnlyRepository.cs: -------------------------------------------------------------------------------- 1 | using Ametista.Core.Cards; 2 | using System; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace Ametista.Infrastructure.Persistence.Repository 7 | { 8 | public class CardWriteOnlyRepository : ICardWriteOnlyRepository 9 | { 10 | private readonly WriteDbContext writeDbContext; 11 | 12 | public CardWriteOnlyRepository(WriteDbContext writeDbContext) 13 | { 14 | this.writeDbContext = writeDbContext ?? throw new ArgumentNullException(nameof(writeDbContext)); 15 | } 16 | 17 | public async Task Add(Card entity) 18 | { 19 | writeDbContext.Cards.Add(entity); 20 | return await writeDbContext.SaveChangesAsync() > 0; 21 | } 22 | 23 | public async Task Delete(Card entity) 24 | { 25 | writeDbContext.Cards.Remove(entity); 26 | return await writeDbContext.SaveChangesAsync() > 0; 27 | } 28 | 29 | public IQueryable FindAll() 30 | { 31 | return writeDbContext.Cards; 32 | } 33 | 34 | public async Task FindAsync(Guid id) 35 | { 36 | return await writeDbContext.Cards.FindAsync(id); 37 | } 38 | 39 | public bool IsDuplicatedCardNumber(string cardNamber) 40 | { 41 | try 42 | { 43 | return writeDbContext.Cards.Any(x => x.Number == cardNamber); 44 | } 45 | catch (Exception ex) 46 | { 47 | throw; 48 | } 49 | 50 | } 51 | 52 | public async Task Update(Card entity) 53 | { 54 | writeDbContext.Cards.Update(entity); 55 | return await writeDbContext.SaveChangesAsync() > 0; 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /src/Ametista.Infrastructure/Persistence/Repository/TransactionWriteOnlyRepository.cs: -------------------------------------------------------------------------------- 1 | using Ametista.Core.Transactions; 2 | using System; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace Ametista.Infrastructure.Persistence.Repository 7 | { 8 | public class TransactionWriteOnlyRepository : ITransactionWriteOnlyRepository 9 | { 10 | private readonly WriteDbContext writeDbContext; 11 | 12 | public TransactionWriteOnlyRepository(WriteDbContext writeDbContext) 13 | { 14 | this.writeDbContext = writeDbContext ?? throw new ArgumentNullException(nameof(writeDbContext)); 15 | } 16 | 17 | public async Task Add(Transaction entity) 18 | { 19 | writeDbContext.Transactions.Add(entity); 20 | return await writeDbContext.SaveChangesAsync() > 0; 21 | } 22 | 23 | public async Task Delete(Transaction entity) 24 | { 25 | writeDbContext.Transactions.Remove(entity); 26 | return await writeDbContext.SaveChangesAsync() > 0; 27 | } 28 | 29 | public IQueryable FindAll() 30 | { 31 | return writeDbContext.Transactions; 32 | } 33 | 34 | public async Task FindAsync(Guid id) 35 | { 36 | return await writeDbContext.Transactions.FindAsync(id); 37 | } 38 | 39 | public async Task Update(Transaction entity) 40 | { 41 | writeDbContext.Transactions.Update(entity); 42 | return await writeDbContext.SaveChangesAsync() > 0; 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /src/Ametista.Infrastructure/Persistence/WriteDbContext.cs: -------------------------------------------------------------------------------- 1 | using Ametista.Core.Cards; 2 | using Ametista.Core.Transactions; 3 | using Microsoft.EntityFrameworkCore; 4 | 5 | namespace Ametista.Infrastructure.Persistence 6 | { 7 | public class WriteDbContext : DbContext 8 | { 9 | public DbSet Cards { get; set; } 10 | public DbSet Transactions { get; set; } 11 | 12 | public WriteDbContext(DbContextOptions options) 13 | : base(options) 14 | { 15 | } 16 | 17 | protected override void OnModelCreating(ModelBuilder modelBuilder) 18 | { 19 | modelBuilder.Entity() 20 | .ToTable("Cards"); 21 | 22 | modelBuilder.Entity() 23 | .Ignore(x => x.Valid); 24 | 25 | modelBuilder.Entity() 26 | .ToTable("Transactions"); 27 | 28 | modelBuilder.Entity() 29 | .Ignore(x => x.Valid); 30 | 31 | modelBuilder 32 | .Entity() 33 | .OwnsOne(p => p.Charge) 34 | .Property(p => p.CurrencyCode).HasColumnName("CurrencyCode"); 35 | 36 | modelBuilder 37 | .Entity() 38 | .OwnsOne(p => p.Charge) 39 | .Property(p => p.Amount).HasColumnName("Amount"); 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /src/Ametista.Query/Abstractions/IMaterializer.cs: -------------------------------------------------------------------------------- 1 | namespace Ametista.Query.Abstractions 2 | { 3 | public interface IMaterializer where TQueryModel : IQueryModel 4 | { 5 | TQueryModel Materialize(TSource source); 6 | } 7 | 8 | public interface IMaterializer where TQueryModel : IQueryModel 9 | { 10 | TQueryModel Materialize(TSource1 source1, TSource2 source2); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Ametista.Query/Abstractions/IQuery.cs: -------------------------------------------------------------------------------- 1 | namespace Ametista.Query.Abstractions 2 | { 3 | public interface IQuery 4 | { } 5 | 6 | public interface IQuery : IQuery 7 | { } 8 | } -------------------------------------------------------------------------------- /src/Ametista.Query/Abstractions/IQueryDispatcher.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Ametista.Query.Abstractions 4 | { 5 | public interface IQueryDispatcher 6 | { 7 | Task ExecuteAsync(IQuery query); 8 | } 9 | } -------------------------------------------------------------------------------- /src/Ametista.Query/Abstractions/IQueryHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Ametista.Query.Abstractions 4 | { 5 | public interface IQueryHandler { } 6 | 7 | public interface IQueryHandler : IQueryHandler 8 | where TQuery : IQuery 9 | { 10 | Task HandleAsync(TQuery query); 11 | } 12 | } -------------------------------------------------------------------------------- /src/Ametista.Query/Abstractions/IQueryModel.cs: -------------------------------------------------------------------------------- 1 | namespace Ametista.Query.Abstractions 2 | { 3 | public interface IQueryModel 4 | { 5 | } 6 | } -------------------------------------------------------------------------------- /src/Ametista.Query/Abstractions/IQueryParameters.cs: -------------------------------------------------------------------------------- 1 | namespace Ametista.Query.Abstractions 2 | { 3 | public interface IQueryParameters where TQuery : IQuery 4 | { 5 | T GetParameters(TQuery model); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/Ametista.Query/Ametista.Query.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/Ametista.Query/EventHandlers/CardCreatedEventHandler.cs: -------------------------------------------------------------------------------- 1 | using Ametista.Core.Cards; 2 | using Ametista.Core.Interfaces; 3 | using Ametista.Query.QueryModel; 4 | using System; 5 | using System.Threading.Tasks; 6 | 7 | namespace Ametista.Query.EventHandlers 8 | { 9 | public class MaterializeCardEventHandler : IEventHandler 10 | { 11 | private readonly ReadDbContext readDbContext; 12 | private readonly ICache cache; 13 | 14 | public MaterializeCardEventHandler(ReadDbContext readDbContext, ICache cache) 15 | { 16 | this.readDbContext = readDbContext ?? throw new ArgumentNullException(nameof(readDbContext)); 17 | this.cache = cache ?? throw new ArgumentNullException(nameof(cache)); 18 | } 19 | 20 | public async Task Handle(CardCreatedEvent e) 21 | { 22 | var cardView = new CardViewQueryModel() 23 | { 24 | CardHolder = e.Data.CardHolder, 25 | ExpirationDate = e.Data.ExpirationDate, 26 | Id = e.Data.Id, 27 | Number = e.Data.Number 28 | }; 29 | 30 | var cardList = new CardListQueryModel() 31 | { 32 | Id = e.Data.Id, 33 | Number = e.Data.Number, 34 | CardHolder = e.Data.CardHolder, 35 | ExpirationDate = e.Data.ExpirationDate, 36 | HighestChargeDate = null, 37 | HighestTransactionAmount = null, 38 | HighestTransactionId = null, 39 | TransactionCount = 0 40 | }; 41 | 42 | await readDbContext.CardViewMaterializedView.InsertOneAsync(cardView); 43 | await readDbContext.CardListMaterializedView.InsertOneAsync(cardList); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Ametista.Query/EventHandlers/TransactionCreatedEventHandler.cs: -------------------------------------------------------------------------------- 1 | using Ametista.Core.Transactions; 2 | using Ametista.Core.Interfaces; 3 | using Ametista.Query.QueryModel; 4 | using MongoDB.Driver; 5 | using System.Threading.Tasks; 6 | using Ametista.Query.Materializers; 7 | using System; 8 | 9 | namespace Ametista.Query.EventHandlers 10 | { 11 | public class TransactionCreatedEventHandler : IEventHandler 12 | { 13 | private readonly ReadDbContext readDbContext; 14 | private readonly ICache cache; 15 | private readonly ITransactionListQueryModelMaterializer transactionMaterializer; 16 | private readonly ICardListQueryModelMaterializer cardListMaterializer; 17 | 18 | public TransactionCreatedEventHandler(ReadDbContext readDbContext, ICache cache, 19 | ITransactionListQueryModelMaterializer transactionMaterializer, 20 | ICardListQueryModelMaterializer cardListMaterializer) 21 | { 22 | this.readDbContext = readDbContext ?? throw new ArgumentNullException(nameof(readDbContext)); 23 | this.cache = cache ?? throw new ArgumentNullException(nameof(cache)); 24 | this.transactionMaterializer = transactionMaterializer ?? throw new ArgumentNullException(nameof(transactionMaterializer)); 25 | this.cardListMaterializer = cardListMaterializer ?? throw new ArgumentNullException(nameof(cardListMaterializer)); 26 | } 27 | 28 | public async Task Handle(Core.Transactions.TransactionCreatedEvent e) 29 | { 30 | FilterDefinition filter = Builders.Filter.Eq("Id", e.Data.CardId); 31 | var cardList = await readDbContext.CardListMaterializedView 32 | .Find(filter) 33 | .FirstOrDefaultAsync(); 34 | 35 | var transactionList = transactionMaterializer.Materialize(e.Data, cardList); 36 | cardList = cardListMaterializer.Materialize(e.Data, cardList); 37 | 38 | await cache.Delete(nameof(CardListQueryModel)); 39 | await readDbContext.TransactionListMaterializedView.InsertOneAsync(transactionList); 40 | await readDbContext.CardListMaterializedView.ReplaceOneAsync(filter, cardList, new UpdateOptions { IsUpsert = true }); 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /src/Ametista.Query/LinqExtensions.cs: -------------------------------------------------------------------------------- 1 | using MongoDB.Driver.Linq; 2 | using System; 3 | using System.Linq; 4 | using System.Linq.Expressions; 5 | 6 | namespace Ametista.Query 7 | { 8 | public static class LinqExtensions 9 | { 10 | public static IMongoQueryable WhereIf(this IMongoQueryable source, bool condition, Expression> predicate) 11 | { 12 | if (condition) 13 | return (IMongoQueryable)Queryable.Where(source, predicate); 14 | else 15 | return source; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Ametista.Query/Materializers/CardListQueryModelMaterializer.cs: -------------------------------------------------------------------------------- 1 | using Ametista.Core.Transactions; 2 | using Ametista.Query.Abstractions; 3 | using Ametista.Query.QueryModel; 4 | 5 | namespace Ametista.Query.Materializers 6 | { 7 | public interface ICardListQueryModelMaterializer : IMaterializer 8 | { 9 | 10 | } 11 | 12 | public class CardListQueryModelMaterializer : ICardListQueryModelMaterializer 13 | { 14 | public CardListQueryModel Materialize(Transaction source1, CardListQueryModel source2) 15 | { 16 | if (!source2.HighestTransactionAmount.HasValue || source1.Charge.Amount > source2.HighestTransactionAmount) 17 | { 18 | source2.HighestChargeDate = source1.ChargeDate; 19 | source2.HighestTransactionId = source1.Id; 20 | source2.HighestTransactionAmount = source1.Charge.Amount; 21 | } 22 | 23 | source2.TransactionCount += 1; 24 | 25 | return source2; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Ametista.Query/Materializers/TransactionListQueryModelMaterializer.cs: -------------------------------------------------------------------------------- 1 | using Ametista.Core.Transactions; 2 | using Ametista.Query.Abstractions; 3 | using Ametista.Query.QueryModel; 4 | 5 | namespace Ametista.Query.Materializers 6 | { 7 | public interface ITransactionListQueryModelMaterializer : IMaterializer 8 | { 9 | 10 | } 11 | 12 | public class TransactionListQueryModelMaterializer : ITransactionListQueryModelMaterializer 13 | { 14 | public TransactionListQueryModel Materialize(Transaction source1, CardListQueryModel source2) 15 | { 16 | return new TransactionListQueryModel() 17 | { 18 | Id = source1.Id, 19 | Amount = source1.Charge.Amount, 20 | ChargeDate = source1.ChargeDate, 21 | CardHolder = source2.CardHolder, 22 | CardNumber = source2.Number, 23 | CurrencyCode = source1.Charge.CurrencyCode, 24 | UniqueId = source1.UniqueId 25 | }; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Ametista.Query/Queries/Cards/GetCardByIdQuery.cs: -------------------------------------------------------------------------------- 1 | using Ametista.Query.Abstractions; 2 | using Ametista.Query.QueryModel; 3 | using System; 4 | 5 | namespace Ametista.Query.Queries 6 | { 7 | public class GetCardByIdQuery : IQuery 8 | { 9 | public GetCardByIdQuery(Guid id) 10 | { 11 | Id = id; 12 | } 13 | 14 | public Guid Id { get; set; } 15 | } 16 | } -------------------------------------------------------------------------------- /src/Ametista.Query/Queries/Cards/GetCardByIdQueryHandler.cs: -------------------------------------------------------------------------------- 1 | using Ametista.Query.Abstractions; 2 | using Ametista.Query.QueryModel; 3 | using MongoDB.Driver; 4 | using System; 5 | using System.Threading.Tasks; 6 | 7 | namespace Ametista.Query.Queries 8 | { 9 | public class GetCardByIdQueryHandler : IQueryHandler 10 | { 11 | private readonly ReadDbContext readDbContext; 12 | 13 | public GetCardByIdQueryHandler(ReadDbContext readDbContext) 14 | { 15 | this.readDbContext = readDbContext ?? throw new ArgumentNullException(nameof(readDbContext)); 16 | } 17 | 18 | public async Task HandleAsync(GetCardByIdQuery query) 19 | { 20 | try 21 | { 22 | FilterDefinition filter = Builders.Filter.Eq("Id", query.Id); 23 | var result = await readDbContext.CardViewMaterializedView.FindAsync(filter); 24 | 25 | return await result.FirstOrDefaultAsync(); 26 | } 27 | catch (Exception) 28 | { 29 | throw; 30 | } 31 | 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /src/Ametista.Query/Queries/Cards/GetCardListQuery.cs: -------------------------------------------------------------------------------- 1 | using Ametista.Query.Abstractions; 2 | using Ametista.Query.QueryModel; 3 | using System; 4 | using System.Collections.Generic; 5 | 6 | namespace Ametista.Query.Queries 7 | { 8 | public class GetCardListQuery : IQuery> 9 | { 10 | public string CardHolder { get; set; } 11 | public DateTime? ChargeDate { get; set; } 12 | public string Number { get; set; } 13 | public int Offset { get; set; } = 0; 14 | public int Limit { get; set; } = 1; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Ametista.Query/Queries/Cards/GetCardListQueryHandler.cs: -------------------------------------------------------------------------------- 1 | using Ametista.Core.Interfaces; 2 | using Ametista.Query.Abstractions; 3 | using Ametista.Query.QueryModel; 4 | using MongoDB.Driver; 5 | using MongoDB.Driver.Linq; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Linq; 9 | using System.Threading.Tasks; 10 | 11 | namespace Ametista.Query.Queries 12 | { 13 | public class GetCardListQueryHandler : IQueryHandler> 14 | { 15 | private readonly ReadDbContext readDbContext; 16 | private readonly ICache cache; 17 | 18 | public GetCardListQueryHandler(ReadDbContext readDbContext, ICache cache) 19 | { 20 | this.readDbContext = readDbContext ?? throw new ArgumentNullException(nameof(readDbContext)); 21 | this.cache = cache ?? throw new ArgumentNullException(nameof(cache)); 22 | } 23 | 24 | public async Task> HandleAsync(GetCardListQuery query) 25 | { 26 | try 27 | { 28 | var cached = await cache.Get>(nameof(CardListQueryModel)); 29 | 30 | if (cached != null && cached.Any()) 31 | { 32 | return cached; 33 | } 34 | 35 | var result = readDbContext 36 | .CardListMaterializedView 37 | .AsQueryable() 38 | .WhereIf(!string.IsNullOrEmpty(query.Number), x => x.Number.Contains(query.Number)) 39 | .WhereIf(!string.IsNullOrEmpty(query.CardHolder), x => x.CardHolder.Contains(query.CardHolder)) 40 | .WhereIf(query.ChargeDate.HasValue, x => x.ExpirationDate == query.ChargeDate); 41 | 42 | var itemsTask = await result 43 | .Skip(query.Offset) 44 | .Take(query.Limit) 45 | .ToListAsync(); 46 | 47 | await cache.Store>(nameof(GetCardListQuery), itemsTask, null); 48 | 49 | return itemsTask; 50 | } 51 | catch (Exception) 52 | { 53 | throw; 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Ametista.Query/Queries/Transactions/GetTransactionListQuery.cs: -------------------------------------------------------------------------------- 1 | using Ametista.Query.Abstractions; 2 | using Ametista.Query.QueryModel; 3 | using System; 4 | using System.Collections.Generic; 5 | 6 | namespace Ametista.Query.Queries 7 | { 8 | public class GetTransactionListQuery : IQuery> 9 | { 10 | public decimal? BetweenAmount { get; set; } 11 | public string CardHolder { get; set; } 12 | public string CardNumber { get; set; } 13 | public DateTimeOffset? ChargeDate { get; set; } 14 | public int Offset { get; set; } 15 | public int Limit { get; set; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Ametista.Query/Queries/Transactions/GetTransactionListQueryHandler.cs: -------------------------------------------------------------------------------- 1 | using Ametista.Query.Abstractions; 2 | using Ametista.Query.QueryModel; 3 | using MongoDB.Driver; 4 | using MongoDB.Driver.Linq; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Threading.Tasks; 8 | 9 | namespace Ametista.Query.Queries 10 | { 11 | public class GetTransactionListQueryHandler : IQueryHandler> 12 | { 13 | private readonly ReadDbContext readDbContext; 14 | 15 | public GetTransactionListQueryHandler(ReadDbContext readDbContext) 16 | { 17 | this.readDbContext = readDbContext ?? throw new ArgumentNullException(nameof(readDbContext)); 18 | } 19 | 20 | public async Task> HandleAsync(GetTransactionListQuery query) 21 | { 22 | try 23 | { 24 | var result = readDbContext 25 | .TransactionListMaterializedView 26 | .AsQueryable() 27 | .WhereIf(!string.IsNullOrEmpty(query.CardNumber), x => x.CardNumber == query.CardNumber) 28 | .WhereIf(!string.IsNullOrEmpty(query.CardHolder), x => x.CardHolder.Contains(query.CardNumber)) 29 | .WhereIf(query.ChargeDate.HasValue, x => x.ChargeDate == query.ChargeDate) 30 | .WhereIf(query.BetweenAmount.HasValue, x => x.Amount >= query.BetweenAmount && x.Amount <= query.BetweenAmount); 31 | 32 | var itemsTask = await result 33 | .Skip(query.Offset) 34 | .Take(query.Limit) 35 | .ToListAsync(); 36 | 37 | return itemsTask; 38 | } 39 | catch (Exception) 40 | { 41 | throw; 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Ametista.Query/QueryModel/CardListQueryModel.cs: -------------------------------------------------------------------------------- 1 | using Ametista.Query.Abstractions; 2 | using System; 3 | 4 | namespace Ametista.Query.QueryModel 5 | { 6 | public class CardListQueryModel : IQueryModel 7 | { 8 | public string CardHolder { get; set; } 9 | public DateTime ExpirationDate { get; set; } 10 | public DateTimeOffset? HighestChargeDate { get; set; } 11 | public decimal? HighestTransactionAmount { get; set; } 12 | public Guid? HighestTransactionId { get; set; } 13 | public Guid Id { get; set; } 14 | public string Number { get; set; } 15 | public int TransactionCount { get; set; } 16 | } 17 | } -------------------------------------------------------------------------------- /src/Ametista.Query/QueryModel/CardViewQueryModel.cs: -------------------------------------------------------------------------------- 1 | using Ametista.Query.Abstractions; 2 | using System; 3 | 4 | namespace Ametista.Query.QueryModel 5 | { 6 | public class CardViewQueryModel : IQueryModel 7 | { 8 | public string CardHolder { get; set; } 9 | 10 | public DateTime ExpirationDate { get; set; } 11 | 12 | public Guid Id { get; set; } 13 | 14 | public string Number { get; set; } 15 | } 16 | } -------------------------------------------------------------------------------- /src/Ametista.Query/QueryModel/TransactionListQueryModel.cs: -------------------------------------------------------------------------------- 1 | using Ametista.Query.Abstractions; 2 | using System; 3 | 4 | namespace Ametista.Query.QueryModel 5 | { 6 | public class TransactionListQueryModel : IQueryModel 7 | { 8 | public object Id { get; set; } 9 | public decimal Amount { get; set; } 10 | public string CurrencyCode { get; set; } 11 | public string CardNumber { get; set; } 12 | public string CardHolder { get; set; } 13 | public string UniqueId { get; set; } 14 | public DateTimeOffset ChargeDate { get; set; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Ametista.Query/ReadDbContext.cs: -------------------------------------------------------------------------------- 1 | using Ametista.Core; 2 | using Ametista.Query.QueryModel; 3 | using MongoDB.Bson.Serialization; 4 | using MongoDB.Driver; 5 | 6 | namespace Ametista.Query 7 | { 8 | public class ReadDbContext 9 | { 10 | private readonly MongoClient _mongoClient; 11 | private readonly IMongoDatabase _database; 12 | 13 | public ReadDbContext(AmetistaConfiguration ametistaConfiguration) 14 | { 15 | _mongoClient = new MongoClient(ametistaConfiguration.ConnectionStrings.MongoConnectionString); 16 | _database = _mongoClient.GetDatabase(ametistaConfiguration.ConnectionStrings.MongoDatabase); 17 | Map(); 18 | } 19 | 20 | internal IMongoCollection CardViewMaterializedView 21 | { 22 | get 23 | { 24 | return _database.GetCollection("CardViewMaterializedView"); 25 | } 26 | } 27 | 28 | internal IMongoCollection CardListMaterializedView 29 | { 30 | get 31 | { 32 | return _database.GetCollection("CardListMaterializedView"); 33 | } 34 | } 35 | 36 | internal IMongoCollection TransactionListMaterializedView 37 | { 38 | get 39 | { 40 | return _database.GetCollection("TransactionListMaterializedView"); 41 | } 42 | } 43 | 44 | private void Map() 45 | { 46 | BsonClassMap.RegisterClassMap(cm => 47 | { 48 | cm.AutoMap(); 49 | }); 50 | 51 | BsonClassMap.RegisterClassMap(cm => 52 | { 53 | cm.AutoMap(); 54 | }); 55 | 56 | BsonClassMap.RegisterClassMap(cm => 57 | { 58 | cm.AutoMap(); 59 | }); 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /src/Ametista.UnitTest/Ametista.UnitTest.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0 5 | false 6 | Full 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | all 19 | runtime; build; native; contentfiles; analyzers 20 | 21 | 22 | all 23 | runtime; build; native; contentfiles; analyzers 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/Ametista.UnitTest/Api/ApiApplicationFactory.cs: -------------------------------------------------------------------------------- 1 | using Ametista.Core.Interfaces; 2 | using Ametista.Infrastructure.Persistence; 3 | using Autofac; 4 | using Microsoft.AspNetCore.Hosting; 5 | using Microsoft.AspNetCore.Mvc.Testing; 6 | using Microsoft.AspNetCore.TestHost; 7 | using Microsoft.Data.Sqlite; 8 | using Microsoft.EntityFrameworkCore; 9 | using Microsoft.Extensions.DependencyInjection; 10 | using Microsoft.Extensions.Logging; 11 | using System.Linq; 12 | 13 | namespace Ametista.UnitTest.Api 14 | { 15 | public class ApiApplicationFactory : WebApplicationFactory 16 | { 17 | protected override void ConfigureWebHost(IWebHostBuilder builder) 18 | { 19 | builder.ConfigureServices(services => 20 | { 21 | var descriptor = services.SingleOrDefault( 22 | d => d.ServiceType == 23 | typeof(DbContextOptions)); 24 | 25 | services.Remove(descriptor); 26 | 27 | var _connection = new SqliteConnection("datasource=:memory:"); 28 | _connection.Open(); 29 | 30 | services.AddDbContext(options => 31 | { 32 | options.UseSqlite(_connection); 33 | }); 34 | 35 | var sp = services.BuildServiceProvider(); 36 | 37 | using (var scope = sp.CreateScope()) 38 | { 39 | var scopedServices = scope.ServiceProvider; 40 | var db = scopedServices.GetRequiredService(); 41 | var logger = scopedServices 42 | .GetRequiredService>(); 43 | 44 | db.Database.EnsureCreated(); 45 | } 46 | }).ConfigureTestContainer(builder => 47 | { 48 | builder 49 | .RegisterType() 50 | .As() 51 | .SingleInstance(); 52 | 53 | builder 54 | .RegisterType() 55 | .As() 56 | .SingleInstance(); 57 | }); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Ametista.UnitTest/Api/Endpoints/CreateCard/CreateCardApiTests.cs: -------------------------------------------------------------------------------- 1 | using Ametista.Api.Endpoints.CreateCard; 2 | using System; 3 | using System.Net; 4 | using System.Net.Http; 5 | using System.Text.Json; 6 | using System.Threading.Tasks; 7 | using Xunit; 8 | 9 | namespace Ametista.UnitTest.Api.Endpoints.CreateCard 10 | { 11 | public class CreateCardApiTests : IClassFixture 12 | { 13 | private readonly ApiApplicationFactory _applicationFactory; 14 | 15 | public CreateCardApiTests(ApiApplicationFactory applicationFactory) 16 | { 17 | _applicationFactory = applicationFactory; 18 | } 19 | 20 | [Fact] 21 | public async Task Should_Return_Created_201() 22 | { 23 | // Arrange 24 | var client = _applicationFactory.CreateClient(); 25 | 26 | var request = new CreateCardRequest 27 | { 28 | Number = "4694437484189508", 29 | CardHolder = "Filipe A. L. Souza", 30 | ExpirationDate = new DateTime(2022, 1, 12), 31 | }; 32 | 33 | var requestContent = new StringContent(JsonSerializer.Serialize(request), System.Text.Encoding.UTF8, "application/json"); 34 | 35 | // Act 36 | var result = await client.PostAsync("api/cards", requestContent); 37 | 38 | // Assert 39 | result.EnsureSuccessStatusCode(); 40 | Assert.Equal(HttpStatusCode.Created, result.StatusCode); 41 | } 42 | 43 | [Fact] 44 | public async Task Should_Return_BadRequest() 45 | { 46 | // Arrange 47 | var client = _applicationFactory.CreateClient(); 48 | 49 | var request = new CreateCardRequest 50 | { 51 | Number = "123456", 52 | CardHolder = "Filipe A. L. Souza", 53 | ExpirationDate = new DateTime(2022, 1, 12), 54 | }; 55 | 56 | var requestContent = new StringContent(JsonSerializer.Serialize(request), System.Text.Encoding.UTF8, "application/json"); 57 | 58 | // Act 59 | var result = await client.PostAsync("api/cards", requestContent); 60 | 61 | // Assert 62 | Assert.Equal(HttpStatusCode.BadRequest, result.StatusCode); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Ametista.UnitTest/Api/Endpoints/CreateCard/CreateCardEndpointTests.cs: -------------------------------------------------------------------------------- 1 | using Ametista.Api.Endpoints.CreateCard; 2 | using Ametista.Command.Abstractions; 3 | using Ametista.Command.CreateCard; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Moq; 6 | using System; 7 | using System.Threading.Tasks; 8 | using Xunit; 9 | 10 | namespace Ametista.UnitTest.Api.Endpoints.CreateCard 11 | { 12 | public class CreateCardEndpointTests 13 | { 14 | private readonly CreateCardEndpoint sut; 15 | private readonly Mock commandDispatcherMock; 16 | 17 | public CreateCardEndpointTests() 18 | { 19 | commandDispatcherMock = new Mock(); 20 | sut = new CreateCardEndpoint(commandDispatcherMock.Object); 21 | } 22 | 23 | [Fact] 24 | public async Task Should_Return_CreatedResult_With_Correct_Data() 25 | { 26 | // Arrange 27 | var request = new CreateCardRequest 28 | { 29 | Number = "xxxx-xxxx-xxxx-xxx", 30 | CardHolder = "Filipe A. L. Souza", 31 | ExpirationDate = new DateTime(2022, 1, 12), 32 | }; 33 | 34 | var commandResult = new CreateCardCommandResult( 35 | id: Guid.NewGuid(), 36 | number: "xxxx-xxxx-xxxx-xxx", 37 | cardHolder: "Filipe A. L. Souza", 38 | expirationDate: new DateTime(2022, 1, 12), 39 | success: true 40 | ); 41 | 42 | commandDispatcherMock 43 | .Setup(x => x.Dispatch(It.IsAny())) 44 | .ReturnsAsync(commandResult); 45 | 46 | // Act 47 | var actionResult = await sut.Post(request); 48 | 49 | // Assert 50 | var createdResult = Assert.IsType(actionResult); 51 | var response = Assert.IsAssignableFrom(createdResult.Value); 52 | AssertResponse(commandResult, response); 53 | } 54 | 55 | private void AssertResponse(CreateCardCommandResult commandResult, CreateCardResponse response) 56 | { 57 | Assert.Equal(commandResult.Id, response.Id); 58 | Assert.Equal(commandResult.Number, response.Number); 59 | Assert.Equal(commandResult.CardHolder, response.CardHolder); 60 | Assert.Equal(commandResult.ExpirationDate, response.ExpirationDate); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Ametista.UnitTest/Builders/TransactionBuilder.cs: -------------------------------------------------------------------------------- 1 | using Ametista.Core; 2 | using Ametista.Core.Shared; 3 | using Ametista.Core.Transactions; 4 | using System; 5 | 6 | namespace Ametista.UnitTest.Builders 7 | { 8 | public class TransactionBuilder : IBuilder 9 | { 10 | private Guid cardId; 11 | 12 | private Money charge; 13 | 14 | private DateTimeOffset chargeDate; 15 | 16 | private string uniqueId; 17 | 18 | public TransactionBuilder ForCard(Guid id) 19 | { 20 | cardId = id; 21 | 22 | return this; 23 | } 24 | 25 | public TransactionBuilder ContainingChargeAmount(Money charge) 26 | { 27 | this.charge = charge; 28 | 29 | return this; 30 | } 31 | 32 | public TransactionBuilder ChargedAt(DateTimeOffset date) 33 | { 34 | this.chargeDate = date; 35 | 36 | return this; 37 | } 38 | 39 | public TransactionBuilder HavingUniqueId(string uniqueId) 40 | { 41 | this.uniqueId = uniqueId; 42 | 43 | return this; 44 | } 45 | 46 | public Transaction Build() 47 | { 48 | return Transaction.CreateTransactionForCard(cardId, uniqueId, chargeDate, charge); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Ametista.UnitTest/Command/CreateCardCommandHandlerTests.cs: -------------------------------------------------------------------------------- 1 | using Ametista.Command.CreateCard; 2 | using Ametista.Core.Cards; 3 | using Ametista.Core.Interfaces; 4 | using Moq; 5 | using System; 6 | using System.Threading.Tasks; 7 | using Xunit; 8 | 9 | namespace Ametista.UnitTest.Command 10 | { 11 | public class CreateCardCommandHandlerTests 12 | { 13 | private readonly CreateCardCommandHandler sut; 14 | private readonly Mock eventBusMock; 15 | private readonly Mock cardRepositoryMock; 16 | 17 | public CreateCardCommandHandlerTests() 18 | { 19 | eventBusMock = new Mock(); 20 | cardRepositoryMock = new Mock(); 21 | cardRepositoryMock.Setup(x => x.Add(It.IsAny())) 22 | .ReturnsAsync(true); 23 | 24 | sut = new CreateCardCommandHandler(eventBusMock.Object, cardRepositoryMock.Object, new Ametista.Core.ValidationNotificationHandler()); 25 | } 26 | 27 | [Fact] 28 | [Trait("Card", nameof(CreateCardCommandHandler))] 29 | public async Task Return_Success_Result() 30 | { 31 | // Arrange 32 | var command = CreateCardCommand(); 33 | 34 | // Act 35 | var result = await sut.Handle(command); 36 | 37 | // Assert 38 | Assert.True(result.Success); 39 | } 40 | 41 | [Fact] 42 | [Trait("Card", nameof(CreateCardCommandHandler))] 43 | public async Task Return_CardHolder_Within_Result() 44 | { 45 | // Arrange 46 | var command = CreateCardCommand(); 47 | 48 | // Act 49 | var result = await sut.Handle(command); 50 | 51 | // Assert 52 | Assert.Equal(command.CardHolder, result.CardHolder); 53 | } 54 | 55 | [Fact] 56 | [Trait("Card", nameof(CreateCardCommandHandler))] 57 | public async Task Return_Number_Within_Result() 58 | { 59 | // Arrange 60 | var command = CreateCardCommand(); 61 | 62 | // Act 63 | var result = await sut.Handle(command); 64 | 65 | // Assert 66 | Assert.Equal(command.Number, result.Number); 67 | } 68 | 69 | [Fact] 70 | [Trait("Card", nameof(CreateCardCommandHandler))] 71 | public async Task Return_ExpirationDate_Within_Result() 72 | { 73 | // Arrange 74 | var command = CreateCardCommand(); 75 | 76 | // Act 77 | var result = await sut.Handle(command); 78 | 79 | // Assert 80 | Assert.Equal(command.ExpirationDate, result.ExpirationDate); 81 | } 82 | 83 | private CreateCardCommand CreateCardCommand() 84 | { 85 | return new CreateCardCommand("371449635398431", "MR FILIPE LIMA", DateTime.Now.Date); 86 | } 87 | } 88 | } -------------------------------------------------------------------------------- /src/Ametista.UnitTest/Command/CreateTransactionCommandHandlerTests.cs: -------------------------------------------------------------------------------- 1 | using Ametista.Command.CreateTransaction; 2 | using Ametista.Core.Interfaces; 3 | using Ametista.Core.Transactions; 4 | using Moq; 5 | using System; 6 | using System.Threading.Tasks; 7 | using Xunit; 8 | 9 | namespace Ametista.UnitTest.Command 10 | { 11 | public class CreateTransactionCommandHandlerTests 12 | { 13 | private readonly CreateTransactionCommandHandler sut; 14 | private readonly Mock eventBusMock; 15 | private readonly Mock transactionRepositoryMock; 16 | 17 | public CreateTransactionCommandHandlerTests() 18 | { 19 | eventBusMock = new Mock(); 20 | transactionRepositoryMock = new Mock(); 21 | transactionRepositoryMock.Setup(x => x.Add(It.IsAny())) 22 | .ReturnsAsync(true); 23 | 24 | sut = new CreateTransactionCommandHandler(eventBusMock.Object, transactionRepositoryMock.Object); 25 | } 26 | 27 | [Fact] 28 | [Trait("Transaction", nameof(CreateTransactionCommandHandler))] 29 | public async Task Return_Success_Result() 30 | { 31 | // Arrange 32 | var command = CreateTransactionCommand(); 33 | 34 | // Act 35 | var result = await sut.Handle(command); 36 | 37 | // Assert 38 | Assert.True(result.Success); 39 | } 40 | 41 | [Fact] 42 | [Trait("Transaction", nameof(CreateTransactionCommandHandler))] 43 | public async Task Return_Id_Within_Result() 44 | { 45 | // Arrange 46 | var command = CreateTransactionCommand(); 47 | 48 | // Act 49 | var result = await sut.Handle(command); 50 | 51 | // Assert 52 | Assert.NotEqual(Guid.Empty, result.Id); 53 | } 54 | 55 | [Fact] 56 | [Trait("Transaction", nameof(CreateTransactionCommandHandler))] 57 | public async Task Return_UniqueId_Within_Result() 58 | { 59 | // Arrange 60 | var command = CreateTransactionCommand(); 61 | 62 | // Act 63 | var result = await sut.Handle(command); 64 | 65 | // Assert 66 | Assert.Equal(command.UniqueId, result.UniqueId); 67 | } 68 | 69 | [Fact] 70 | [Trait("Transaction", nameof(CreateTransactionCommandHandler))] 71 | public async Task Return_ChargeDate_Within_Result() 72 | { 73 | // Arrange 74 | var command = CreateTransactionCommand(); 75 | 76 | // Act 77 | var result = await sut.Handle(command); 78 | 79 | // Assert 80 | Assert.Equal(command.ChargeDate, result.ChargeDate); 81 | } 82 | 83 | [Fact] 84 | [Trait("Transaction", nameof(CreateTransactionCommandHandler))] 85 | public async Task Return_Amount_Within_Result() 86 | { 87 | // Arrange 88 | var command = CreateTransactionCommand(); 89 | 90 | // Act 91 | var result = await sut.Handle(command); 92 | 93 | // Assert 94 | Assert.Equal(command.Amount, result.Amount); 95 | } 96 | 97 | [Fact] 98 | [Trait("Transaction", nameof(CreateTransactionCommandHandler))] 99 | public async Task Return_CurrencyCode_Within_Result() 100 | { 101 | // Arrange 102 | var command = CreateTransactionCommand(); 103 | 104 | // Act 105 | var result = await sut.Handle(command); 106 | 107 | // Assert 108 | Assert.Equal(command.CurrencyCode, result.CurrencyCode); 109 | } 110 | 111 | public CreateTransactionCommand CreateTransactionCommand() 112 | { 113 | return new CreateTransactionCommand(100M, "BRA", Guid.NewGuid(), Guid.NewGuid().ToString(), DateTimeOffset.Now); 114 | } 115 | } 116 | } -------------------------------------------------------------------------------- /src/Ametista.UnitTest/Core/Cards/CardSpecificationTests.cs: -------------------------------------------------------------------------------- 1 | using Ametista.Core.Cards; 2 | using System; 3 | using Xunit; 4 | 5 | namespace Ametista.UnitTest.Core.Cards 6 | { 7 | public class CardSpecificationTests 8 | { 9 | public CardSpecificationTests() 10 | { 11 | 12 | } 13 | 14 | [Theory] 15 | [InlineData(null, false)] 16 | [InlineData("", false)] 17 | [InlineData("371449635398431", true)] 18 | [InlineData("30569309025904", true)] 19 | [InlineData("5555555555554444", true)] 20 | [InlineData("4111111111111111", true)] 21 | [InlineData("AAAAAAAAAAAAAAAA", false)] 22 | public void Card_Has_Valid_Card_Number_Spec(string number, bool valid) 23 | { 24 | // Arrange 25 | var card = Card.CreateNewCard(number, null, DateTime.Now); 26 | var spec = new HasValidNumberSpec(); 27 | 28 | // Act 29 | var isSatisfiedBy = spec.IsSatisfiedBy(card); 30 | 31 | // Assert 32 | Assert.Equal(valid, isSatisfiedBy); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Ametista.UnitTest/Core/Cards/CardValidatorTests.cs: -------------------------------------------------------------------------------- 1 | namespace Ametista.UnitTest.Core.Cards 2 | { 3 | public class CardValidatorTests 4 | { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/Ametista.UnitTest/Fakes/FakeCache.cs: -------------------------------------------------------------------------------- 1 | using Ametista.Core.Interfaces; 2 | using System.Threading.Tasks; 3 | 4 | namespace Ametista.UnitTest.Fakes 5 | { 6 | internal class FakeCache : ICache 7 | { 8 | public Task Delete(string key) 9 | { 10 | return Task.FromResult(true); 11 | } 12 | 13 | public Task Get(string key) 14 | { 15 | return null; 16 | } 17 | 18 | public Task Store(string key, T value, params string[] @params) 19 | { 20 | return Task.CompletedTask; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Ametista.UnitTest/Fakes/FakeEventBus.cs: -------------------------------------------------------------------------------- 1 | using Ametista.Core; 2 | using Ametista.Core.Interfaces; 3 | 4 | namespace Ametista.UnitTest.Fakes 5 | { 6 | internal class FakeEventBus : IEventBus 7 | { 8 | public void Publish(IEvent @event) 9 | { 10 | 11 | } 12 | 13 | public void Subscribe() where T : IEvent 14 | { 15 | 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Ametista.UnitTest/Mothers/TransactionMother.cs: -------------------------------------------------------------------------------- 1 | using Ametista.Core.Transactions; 2 | using Ametista.UnitTest.Builders; 3 | using System; 4 | 5 | namespace Ametista.UnitTest.Mothers 6 | { 7 | public static class TransactionMother 8 | { 9 | public static Transaction CreateSimpleTransaction() 10 | { 11 | return new TransactionBuilder() 12 | .ForCard(Guid.NewGuid()) 13 | .HavingUniqueId(Guid.NewGuid().ToString("N")) 14 | .ChargedAt(DateTimeOffset.Now) 15 | .ContainingChargeAmount(new Ametista.Core.Money(100, "EUR")) 16 | .Build(); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Ametista.UnitTest/Query/TransactionListQueryModelMaterializerUnitTest.cs: -------------------------------------------------------------------------------- 1 | using Ametista.Core.Transactions; 2 | using Ametista.Query.Materializers; 3 | using Ametista.Query.QueryModel; 4 | using Ametista.UnitTest.Mothers; 5 | using Xunit; 6 | 7 | namespace Ametista.UnitTest.Query 8 | { 9 | public class TransactionListQueryModelMaterializerUnitTest 10 | { 11 | private readonly ITransactionListQueryModelMaterializer materializer; 12 | 13 | public TransactionListQueryModelMaterializerUnitTest() 14 | { 15 | materializer = new TransactionListQueryModelMaterializer(); 16 | } 17 | 18 | [Fact] 19 | public void Should_Create_Model_With_Right_Data() 20 | { 21 | // Arrange 22 | var transaction = TransactionMother.CreateSimpleTransaction(); 23 | var cardList = new CardListQueryModel() 24 | { 25 | CardHolder = "Name", 26 | Number = "12345678910" 27 | }; 28 | 29 | // Act 30 | 31 | var queryModel = materializer.Materialize(transaction, cardList); 32 | 33 | // Assert 34 | AssertProperties(queryModel, transaction, cardList); 35 | } 36 | 37 | private void AssertProperties(TransactionListQueryModel queryModel, Transaction transaction, CardListQueryModel cardList) 38 | { 39 | Assert.Equal(queryModel.Id, transaction.Id); 40 | Assert.Equal(queryModel.Amount, transaction.Charge.Amount); 41 | Assert.Equal(queryModel.CurrencyCode, transaction.Charge.CurrencyCode); 42 | Assert.Equal(queryModel.ChargeDate, transaction.ChargeDate); 43 | Assert.Equal(queryModel.UniqueId, transaction.UniqueId); 44 | Assert.Equal(queryModel.CardHolder, cardList.CardHolder); 45 | Assert.Equal(queryModel.CardNumber, cardList.Number); 46 | 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Ametista.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31903.59 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ametista.Core", "Ametista.Core\Ametista.Core.csproj", "{37C6046C-D32F-4819-930B-333C635FCE53}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ametista.Infrastructure", "Ametista.Infrastructure\Ametista.Infrastructure.csproj", "{E09F4EE6-FC5D-4DA0-B00E-8BA43CA4F5BD}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ametista.UnitTest", "Ametista.UnitTest\Ametista.UnitTest.csproj", "{3DDBA5F3-B1AF-4FFF-8738-E9A65FF9A343}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ametista.Query", "Ametista.Query\Ametista.Query.csproj", "{952E7FC7-DC35-4093-9395-BE43B9DD814A}" 13 | EndProject 14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ametista.Command", "Ametista.Command\Ametista.Command.csproj", "{3F46E2CF-F965-4785-A02C-FD14DFD1FC7E}" 15 | EndProject 16 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ametista.Api", "Ametista.Api\Ametista.Api.csproj", "{8C3E9371-487F-4BCF-AA05-DC7586C5B206}" 17 | EndProject 18 | Global 19 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 20 | Debug|Any CPU = Debug|Any CPU 21 | Release|Any CPU = Release|Any CPU 22 | EndGlobalSection 23 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 24 | {37C6046C-D32F-4819-930B-333C635FCE53}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {37C6046C-D32F-4819-930B-333C635FCE53}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {37C6046C-D32F-4819-930B-333C635FCE53}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {37C6046C-D32F-4819-930B-333C635FCE53}.Release|Any CPU.Build.0 = Release|Any CPU 28 | {E09F4EE6-FC5D-4DA0-B00E-8BA43CA4F5BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {E09F4EE6-FC5D-4DA0-B00E-8BA43CA4F5BD}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {E09F4EE6-FC5D-4DA0-B00E-8BA43CA4F5BD}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {E09F4EE6-FC5D-4DA0-B00E-8BA43CA4F5BD}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {3DDBA5F3-B1AF-4FFF-8738-E9A65FF9A343}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {3DDBA5F3-B1AF-4FFF-8738-E9A65FF9A343}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {3DDBA5F3-B1AF-4FFF-8738-E9A65FF9A343}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {3DDBA5F3-B1AF-4FFF-8738-E9A65FF9A343}.Release|Any CPU.Build.0 = Release|Any CPU 36 | {952E7FC7-DC35-4093-9395-BE43B9DD814A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 37 | {952E7FC7-DC35-4093-9395-BE43B9DD814A}.Debug|Any CPU.Build.0 = Debug|Any CPU 38 | {952E7FC7-DC35-4093-9395-BE43B9DD814A}.Release|Any CPU.ActiveCfg = Release|Any CPU 39 | {952E7FC7-DC35-4093-9395-BE43B9DD814A}.Release|Any CPU.Build.0 = Release|Any CPU 40 | {3F46E2CF-F965-4785-A02C-FD14DFD1FC7E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 41 | {3F46E2CF-F965-4785-A02C-FD14DFD1FC7E}.Debug|Any CPU.Build.0 = Debug|Any CPU 42 | {3F46E2CF-F965-4785-A02C-FD14DFD1FC7E}.Release|Any CPU.ActiveCfg = Release|Any CPU 43 | {3F46E2CF-F965-4785-A02C-FD14DFD1FC7E}.Release|Any CPU.Build.0 = Release|Any CPU 44 | {8C3E9371-487F-4BCF-AA05-DC7586C5B206}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 45 | {8C3E9371-487F-4BCF-AA05-DC7586C5B206}.Debug|Any CPU.Build.0 = Debug|Any CPU 46 | {8C3E9371-487F-4BCF-AA05-DC7586C5B206}.Release|Any CPU.ActiveCfg = Release|Any CPU 47 | {8C3E9371-487F-4BCF-AA05-DC7586C5B206}.Release|Any CPU.Build.0 = Release|Any CPU 48 | EndGlobalSection 49 | GlobalSection(SolutionProperties) = preSolution 50 | HideSolutionNode = FALSE 51 | EndGlobalSection 52 | GlobalSection(ExtensibilityGlobals) = postSolution 53 | SolutionGuid = {C456BE88-D66C-4D9A-8786-ED184F8C9341} 54 | EndGlobalSection 55 | EndGlobal 56 | -------------------------------------------------------------------------------- /src/conf/rabbitmq.conf: -------------------------------------------------------------------------------- 1 | loopback_users.guest = false 2 | listeners.tcp.default = 5672 3 | hipe_compile = false 4 | -------------------------------------------------------------------------------- /src/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | services: 4 | mssql: 5 | image: mcr.microsoft.com/mssql/server:2019-latest 6 | container_name: ametista-mssql 7 | environment: 8 | ACCEPT_EULA: Y 9 | MSSQL_PID: Express 10 | SA_PASSWORD: MyPassword123456 11 | ports: 12 | - "1433:1433" 13 | 14 | nosql.data: 15 | container_name: ametista-mongodb 16 | image: mongo:latest 17 | ports: 18 | - "27017:27017" # Important: In a production environment your should remove the external port 19 | 20 | rabbitmq: 21 | container_name: ametista-rabbitmq 22 | image: rabbitmq:3.7.7-management-alpine 23 | ports: 24 | - "15672:15672" # Important: In a production environment your should remove the external port 25 | - "5672:5672" # Important: In a production environment your should remove the external port 26 | 27 | redis.cache: 28 | container_name: ametista-redis 29 | image: redis:alpine 30 | ports: 31 | - "6379:6379" 32 | 33 | ametista.api: 34 | container_name: ametista-api 35 | restart: always 36 | image: ametistaapi 37 | build: 38 | context: . 39 | dockerfile: Ametista.Api/Dockerfile 40 | depends_on: 41 | - mssql 42 | - rabbitmq 43 | - nosql.data 44 | - redis.cache 45 | ports: 46 | - "5000:80" 47 | - "5443:443" 48 | environment: 49 | - ASPNETCORE_ENVIRONMENT=Docker 50 | --------------------------------------------------------------------------------