├── .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 | #  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 | 
53 |
54 | ## :scissors: CQRS
55 |
56 | Segregation between Commands and Queries, with isolated databases and different models
57 |
58 | 
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 | 
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 | 
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 | 
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