├── .gitignore
├── .vscode
├── launch.json
└── tasks.json
├── README.md
├── _debezium_connectors
├── configs
│ ├── customer_config.json
│ └── identity_config.json
├── customer.sh
└── identity.sh
├── _img
├── before_user_and_customer_creation_flow.png
└── user_and_customer_creation_flow.png
├── _postman
└── dev_summit_cdc_debezium.postman_collection.json
├── _sample_db_logs
├── after_outbox_transform_create_user_log.json
└── before_outbox_transform_create_user_log.json
├── _scripts
├── run_services.sh
└── stop_services.sh
├── docker-compose.yml
├── microservices-change-data-capture-with-debezium.sln
└── src
├── Services.Customer
├── Commands
│ ├── Handlers
│ │ └── UpdateCustomerCommandHandler.cs
│ └── UpdateCustomerCommand.cs
├── Controllers
│ └── CustomersController.cs
├── Data
│ ├── CustomerDBContext.cs
│ ├── Entity
│ │ ├── Customer.cs
│ │ └── Outbox.cs
│ └── Migrations
│ │ ├── 20201028192204_initial.Designer.cs
│ │ ├── 20201028192204_initial.cs
│ │ └── CustomerDBContextModelSnapshot.cs
├── Events
│ ├── CustomerUpdatedEvent.cs
│ ├── Handlers
│ │ └── UserCreatedEventHandler.cs
│ └── UserCreatedEvent.cs
├── Program.cs
├── Properties
│ └── launchSettings.json
├── README.md
├── Services.Customer.csproj
├── Startup.cs
├── appsettings.Development.json
└── appsettings.json
├── Services.Identity
├── Commands
│ ├── Handlers
│ │ └── RegisterUserCommandHandler.cs
│ └── RegisterUserCommand.cs
├── Controllers
│ └── AccountsController.cs
├── Data
│ ├── Entity
│ │ ├── Outbox.cs
│ │ └── User.cs
│ ├── IdentityDBContext.cs
│ └── Migrations
│ │ ├── 20201028192522_initial.Designer.cs
│ │ ├── 20201028192522_initial.cs
│ │ └── IdentityDBContextModelSnapshot.cs
├── Events
│ ├── CustomerUpdatedEvent.cs
│ ├── Handlers
│ │ └── CustomerUpdatedEventHandler.cs
│ └── UserCreatedEvent.cs
├── Program.cs
├── Properties
│ └── launchSettings.json
├── README.md
├── Services.Identity.csproj
├── Startup.cs
├── appsettings.Development.json
└── appsettings.json
├── Services.Notification
├── Data
│ ├── Entity
│ │ └── Customer.cs
│ ├── Migrations
│ │ ├── 20201028194326_initial.Designer.cs
│ │ ├── 20201028194326_initial.cs
│ │ └── NotificationDBContextModelSnapshot.cs
│ └── NotificationDBContext.cs
├── Events
│ ├── CustomerUpdatedEvent.cs
│ ├── Handlers
│ │ ├── CustomerUpdatedEventHandler.cs
│ │ └── UserCreatedEventHandler.cs
│ └── UserCreatedEvent.cs
├── Program.cs
├── Properties
│ └── launchSettings.json
├── README.md
├── Services.Notification.csproj
├── Startup.cs
├── appsettings.Development.json
└── appsettings.json
└── Shared
├── DbInitilializer.cs
├── Kafka
├── Consumer
│ ├── BackGroundKafkaConsumer.cs
│ ├── IKafkaHandler.cs
│ ├── KafkaConsumerConfig.cs
│ └── KafkaDeserializer.cs
└── RegisterServiceExtensions.cs
└── Shared.csproj
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 |
4 | # User-specific files
5 | *.suo
6 | *.user
7 | *.userosscache
8 | *.sln.docstates
9 |
10 | # User-specific files (MonoDevelop/Xamarin Studio)
11 | *.userprefs
12 |
13 | # Build results
14 | [Dd]ebug/
15 | [Dd]ebugPublic/
16 | [Rr]elease/
17 | [Rr]eleases/
18 | x64/
19 | x86/
20 | bld/
21 | [Bb]in/
22 | [Oo]bj/
23 | [Ll]og/
24 |
25 | # Visual Studio 2015 cache/options directory
26 | .vs/
27 | # Uncomment if you have tasks that create the project's static files in wwwroot
28 | #wwwroot/
29 |
30 | # MSTest test Results
31 | [Tt]est[Rr]esult*/
32 | [Bb]uild[Ll]og.*
33 |
34 | # NUNIT
35 | *.VisualState.xml
36 | TestResult.xml
37 |
38 | # Build Results of an ATL Project
39 | [Dd]ebugPS/
40 | [Rr]eleasePS/
41 | dlldata.c
42 |
43 | # DNX
44 | project.lock.json
45 | project.fragment.lock.json
46 | artifacts/
47 |
48 | *_i.c
49 | *_p.c
50 | *_i.h
51 | *.ilk
52 | *.meta
53 | *.obj
54 | *.pch
55 | *.pdb
56 | *.pgc
57 | *.pgd
58 | *.rsp
59 | *.sbr
60 | *.tlb
61 | *.tli
62 | *.tlh
63 | *.tmp
64 | *.tmp_proj
65 | *.log
66 | *.vspscc
67 | *.vssscc
68 | .builds
69 | *.pidb
70 | *.svclog
71 | *.scc
72 |
73 | # Chutzpah Test files
74 | _Chutzpah*
75 |
76 | # Visual C++ cache files
77 | ipch/
78 | *.aps
79 | *.ncb
80 | *.opendb
81 | *.opensdf
82 | *.sdf
83 | *.cachefile
84 | *.VC.db
85 | *.VC.VC.opendb
86 |
87 | # Visual Studio profiler
88 | *.psess
89 | *.vsp
90 | *.vspx
91 | *.sap
92 |
93 | # TFS 2012 Local Workspace
94 | $tf/
95 |
96 | # Guidance Automation Toolkit
97 | *.gpState
98 |
99 | # ReSharper is a .NET coding add-in
100 | _ReSharper*/
101 | *.[Rr]e[Ss]harper
102 | *.DotSettings.user
103 |
104 | # JustCode is a .NET coding add-in
105 | .JustCode
106 |
107 | # TeamCity is a build add-in
108 | _TeamCity*
109 |
110 | # DotCover is a Code Coverage Tool
111 | *.dotCover
112 |
113 | # Visual Studio code coverage results
114 | *.coverage
115 | *.coveragexml
116 |
117 | # NCrunch
118 | _NCrunch_*
119 | .*crunch*.local.xml
120 | nCrunchTemp_*
121 |
122 | # MightyMoose
123 | *.mm.*
124 | AutoTest.Net/
125 |
126 | # Web workbench (sass)
127 | .sass-cache/
128 |
129 | # Installshield output folder
130 | [Ee]xpress/
131 |
132 | # DocProject is a documentation generator add-in
133 | DocProject/buildhelp/
134 | DocProject/Help/*.HxT
135 | DocProject/Help/*.HxC
136 | DocProject/Help/*.hhc
137 | DocProject/Help/*.hhk
138 | DocProject/Help/*.hhp
139 | DocProject/Help/Html2
140 | DocProject/Help/html
141 |
142 | # Click-Once directory
143 | publish/
144 |
145 | # Publish Web Output
146 | *.[Pp]ublish.xml
147 | *.azurePubxml
148 | # TODO: Comment the next line if you want to checkin your web deploy settings
149 | # but database connection strings (with potential passwords) will be unencrypted
150 | *.pubxml
151 | *.publishproj
152 |
153 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
154 | # checkin your Azure Web App publish settings, but sensitive information contained
155 | # in these scripts will be unencrypted
156 | PublishScripts/
157 |
158 | # NuGet Packages
159 | *.nupkg
160 | # The packages folder can be ignored because of Package Restore
161 | **/packages/*
162 | # except build/, which is used as an MSBuild target.
163 | !**/packages/build/
164 | # Uncomment if necessary however generally it will be regenerated when needed
165 | #!**/packages/repositories.config
166 | # NuGet v3's project.json files produces more ignoreable files
167 | *.nuget.props
168 | *.nuget.targets
169 |
170 | # Microsoft Azure Build Output
171 | csx/
172 | *.build.csdef
173 |
174 | # Microsoft Azure Emulator
175 | ecf/
176 | rcf/
177 |
178 | # Windows Store app package directories and files
179 | AppPackages/
180 | BundleArtifacts/
181 | Package.StoreAssociation.xml
182 | _pkginfo.txt
183 |
184 | # Visual Studio cache files
185 | # files ending in .cache can be ignored
186 | *.[Cc]ache
187 | # but keep track of directories ending in .cache
188 | !*.[Cc]ache/
189 |
190 | # Others
191 | ClientBin/
192 | ~$*
193 | *~
194 | *.dbmdl
195 | *.dbproj.schemaview
196 | *.jfm
197 | *.pfx
198 | *.publishsettings
199 | node_modules/
200 | orleans.codegen.cs
201 |
202 | # Since there are multiple workflows, uncomment next line to ignore bower_components
203 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
204 | #bower_components/
205 |
206 | # RIA/Silverlight projects
207 | Generated_Code/
208 |
209 | # Backup & report files from converting an old project file
210 | # to a newer Visual Studio version. Backup files are not needed,
211 | # because we have git ;-)
212 | _UpgradeReport_Files/
213 | Backup*/
214 | UpgradeLog*.XML
215 | UpgradeLog*.htm
216 |
217 | # SQL Server files
218 | *.mdf
219 | *.ldf
220 |
221 | # Grafana
222 | *.db
223 |
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 | .vscode/*
272 | !.vscode/settings.json
273 | !.vscode/tasks.json
274 | !.vscode/launch.json
275 |
276 | NuGet.config
277 | appsettings.local.json
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "name": "identity",
9 | "type": "coreclr",
10 | "request": "launch",
11 | "preLaunchTask": "build-identity",
12 | "program": "${workspaceFolder}/src/Services.Identity/bin/Debug/net5.0/Services.Identity.dll",
13 | "args": [],
14 | "cwd": "${workspaceFolder}/src/Services.Identity",
15 | "stopAtEntry": false,
16 | "internalConsoleOptions": "openOnSessionStart",
17 | "launchBrowser": {
18 | "enabled": true,
19 | "args": "${auto-detect-url}",
20 | "windows": {
21 | "command": "cmd.exe",
22 | "args": "/C start ${auto-detect-url}"
23 | },
24 | "osx": {
25 | "command": "open"
26 | },
27 | "linux": {
28 | "command": "xdg-open"
29 | }
30 | },
31 | "env": {
32 | "ASPNETCORE_ENVIRONMENT": "Development",
33 | "ASPNETCORE_URLS": "http://localhost:5001"
34 | }
35 | },
36 | {
37 | "name": "customer",
38 | "type": "coreclr",
39 | "request": "launch",
40 | "preLaunchTask": "build-customer",
41 | "program": "${workspaceFolder}/src/Services.Customer/bin/Debug/net5.0/Services.Customer.dll",
42 | "args": [],
43 | "cwd": "${workspaceFolder}/src/Services.Customer",
44 | "stopAtEntry": false,
45 | "internalConsoleOptions": "openOnSessionStart",
46 | "launchBrowser": {
47 | "enabled": true,
48 | "args": "${auto-detect-url}",
49 | "windows": {
50 | "command": "cmd.exe",
51 | "args": "/C start ${auto-detect-url}"
52 | },
53 | "osx": {
54 | "command": "open"
55 | },
56 | "linux": {
57 | "command": "xdg-open"
58 | }
59 | },
60 | "env": {
61 | "ASPNETCORE_ENVIRONMENT": "Development",
62 | "ASPNETCORE_URLS": "http://localhost:5005"
63 | }
64 | },
65 | {
66 | "name": "notification",
67 | "type": "coreclr",
68 | "request": "launch",
69 | "preLaunchTask": "build-notification",
70 | "program": "${workspaceFolder}/src/Services.Notification/bin/Debug/net5.0/Services.Notification.dll",
71 | "args": [],
72 | "cwd": "${workspaceFolder}/src/Services.Notification",
73 | "stopAtEntry": false,
74 | "internalConsoleOptions": "openOnSessionStart",
75 | "launchBrowser": {
76 | "enabled": true,
77 | "args": "${auto-detect-url}",
78 | "windows": {
79 | "command": "cmd.exe",
80 | "args": "/C start ${auto-detect-url}"
81 | },
82 | "osx": {
83 | "command": "open"
84 | },
85 | "linux": {
86 | "command": "xdg-open"
87 | }
88 | },
89 | "env": {
90 | "ASPNETCORE_ENVIRONMENT": "Development",
91 | "ASPNETCORE_URLS": "http://localhost:5010"
92 | }
93 | },
94 | {
95 | "name": ".NET Core Attach",
96 | "type": "coreclr",
97 | "request": "attach",
98 | "processId": "${command:pickProcess}"
99 | }
100 | ],
101 | "compounds": [
102 | {
103 | "name": "All",
104 | "configurations": [
105 | "identity",
106 | "customer",
107 | "notification"
108 | ]
109 | }
110 | ]
111 | }
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0.0",
3 | "tasks": [
4 | {
5 | "label": "build-identity",
6 | "command": "dotnet",
7 | "type": "process",
8 | "args": [
9 | "build",
10 | "${workspaceFolder}/src/Services.Identity/Services.Identity.csproj"
11 | ],
12 | "problemMatcher": "$msCompile"
13 | },
14 | {
15 | "label": "build-customer",
16 | "command": "dotnet",
17 | "type": "process",
18 | "args": [
19 | "build",
20 | "${workspaceFolder}/src/Services.Customer/Services.Customer.csproj",
21 | ],
22 | "problemMatcher": "$msCompile"
23 | },
24 | {
25 | "label": "build-notification",
26 | "command": "dotnet",
27 | "type": "process",
28 | "args": [
29 | "build",
30 | "${workspaceFolder}/src/Services.Notification/Services.Notification.csproj",
31 | ],
32 | "problemMatcher": "$msCompile"
33 | }
34 | ]
35 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | This simple project demonstrates how to manage eventual consistency between microservices with Change Data Capture and Outbox Pattern using Debezium, Kafka, and Kafka Connect.
2 |
3 | ## Prerequisites
4 |
5 | * .NET 5.0 SDK
6 | * Docker Desktop
7 |
8 | ## Run in Debug Mode
9 |
10 | * Run 'docker-compose up' and wait for all infra to up and running.
11 | * Select 'All' debug option and start debugging. (for vs code)
12 | * Wait until all microservices are up and running.
13 |
14 | **Initiating Databases:** Each service will be created its own database while it's starting for the first time.
15 |
16 | ## Register Debezium Postgres Connectors to Kafka Connect
17 |
18 | You need to register two Postgres Connectors. One for Customer Database and the other for Identity Database.
19 |
20 | customer_config.json -> Customer Connector Config.
21 |
22 | identity_config.json -> Identity Connector Config.
23 |
24 |
25 | Use customer.sh and identity.sh to create/update/delete connectors. For instance, to create customer connector;
26 |
27 | ```bash
28 | .\customer.sh create_connector
29 | ```
30 |
31 | **update_connector** function is commented out. If you want to update the connector config, uncomment the function and download jq from here.
32 |
33 | After registeration of two Debezium Connectors, two workers will be created on Kafka Connect which are listening to the outbox tables to push events to Kafka topics.
34 |
35 | Check the connector list via the following endpoint and see the following json result to be sure everything is okay.
36 |
37 | ```bash
38 | curl -X GET http://localhost:8083/connectors
39 | ```
40 |
41 | ```json
42 | ["identity_outbox_connector", "customer_outbox_connector"]
43 | ```
44 |
45 | Now you are ready to test. See sample postman requests here.
46 |
47 | ## Overall Architecture
48 |
49 | When a new user created on Identity Service, eventual consistency will be obtained for Customer and Notification Services as shown following flow.
50 |
51 |
52 |
53 | ## Tool Set
54 |
55 | * Asp.Net 5.0
56 | * Entity Framework Core 5.0
57 | * PostgreSQL - Npgsql
58 | * MediatR
59 | * Kafka - Zookeeper
60 | * Confluent.Kafka
61 | * Kafka Connect
62 | * Debezium
63 | * Kafdrop
64 | * Docker - Docker Compose
65 | * Azure Data Studio
66 | * VS Code
67 |
--------------------------------------------------------------------------------
/_debezium_connectors/configs/customer_config.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "customer_outbox_connector",
3 | "config": {
4 | "connector.class": "io.debezium.connector.postgresql.PostgresConnector",
5 | "tasks.max": "1",
6 | "database.hostname": "postgres",
7 | "database.port": "5432",
8 | "database.user": "admin",
9 | "database.password": "admin",
10 | "database.dbname": "Customer",
11 | "database.server.name": "postgres",
12 | "table.whitelist": "Customer.outbox_events",
13 | "slot.name": "customer_slot",
14 | "transforms": "outbox",
15 | "transforms.outbox.type": "io.debezium.transforms.outbox.EventRouter",
16 | "transforms.outbox.table.field.event.id": "id",
17 | "transforms.outbox.table.field.event.key": "aggregate_id",
18 | "transforms.outbox.table.field.event.type": "type",
19 | "transforms.outbox.table.field.event.payload.id": "aggregate_id",
20 | "transforms.outbox.table.fields.additional.placement": "type:header:eventType",
21 | "transforms.outbox.route.by.field": "aggregate_type",
22 | "transforms.outbox.route.topic.replacement": "customer_events",
23 | "key.converter": "org.apache.kafka.connect.storage.StringConverter",
24 | "key.converter.schemas.enable": "false",
25 | "value.converter": "org.apache.kafka.connect.json.JsonConverter",
26 | "value.converter.schemas.enable": "false",
27 | "include.schema.changes": "false"
28 | }
29 | }
--------------------------------------------------------------------------------
/_debezium_connectors/configs/identity_config.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "identity_outbox_connector",
3 | "config": {
4 | "connector.class": "io.debezium.connector.postgresql.PostgresConnector",
5 | "tasks.max": "1",
6 | "database.hostname": "postgres",
7 | "database.port": "5432",
8 | "database.user": "admin",
9 | "database.password": "admin",
10 | "database.dbname": "Identity",
11 | "database.server.name": "postgres",
12 | "table.whitelist": "Identity.outbox_events",
13 | "slot.name": "identity_slot",
14 | "transforms": "outbox",
15 | "transforms.outbox.type": "io.debezium.transforms.outbox.EventRouter",
16 | "transforms.outbox.table.field.event.id": "id",
17 | "transforms.outbox.table.field.event.key": "aggregate_id",
18 | "transforms.outbox.table.field.event.type": "type",
19 | "transforms.outbox.table.field.event.payload.id": "aggregate_id",
20 | "transforms.outbox.table.fields.additional.placement": "type:header:eventType",
21 | "transforms.outbox.route.by.field": "aggregate_type",
22 | "transforms.outbox.route.topic.replacement": "user_events",
23 | "key.converter": "org.apache.kafka.connect.storage.StringConverter",
24 | "key.converter.schemas.enable": "false",
25 | "value.converter": "org.apache.kafka.connect.json.JsonConverter",
26 | "value.converter.schemas.enable": "false",
27 | "include.schema.changes": "false"
28 | }
29 | }
--------------------------------------------------------------------------------
/_debezium_connectors/customer.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | ## configure debezium connector for customer database
4 |
5 | create_connector () {
6 | curl -X POST http://localhost:8083/connectors -d @configs/customer_config.json \
7 | --header "Content-Type: application/json"
8 | }
9 |
10 | # update_connector () {
11 | # curl -X PUT http://localhost:8083/connectors/customer_outbox_connector/config --data "$(jq '.config' configs/customer_config.json)" \
12 | # --header "Content-Type: application/json"
13 | # }
14 |
15 | delete_connector () {
16 | curl -X DELETE http://localhost:8083/connectors/customer_outbox_connector \
17 | --header "Content-Type: application/json"
18 | }
19 |
20 | stop_connector () {
21 | curl -X PUT http://localhost:8083/connectors/customer_outbox_connector/pause \
22 | --header "Content-Type: application/json"
23 | }
24 |
25 | start_connector () {
26 | curl -X PUT http://localhost:8083/connectors/customer_outbox_connector/resume \
27 | --header "Content-Type: application/json"
28 | }
29 |
30 | "$@"
31 | read
--------------------------------------------------------------------------------
/_debezium_connectors/identity.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | ## configure debezium connector for identity database
4 |
5 | create_connector () {
6 | curl -X POST http://localhost:8083/connectors -d @configs/identity_config.json \
7 | --header "Content-Type: application/json"
8 | }
9 |
10 | # update_connector () {
11 | # curl -X PUT http://localhost:8083/connectors/identity_outbox_connector/config --data "$(jq '.config' configs/identity_config.json)" \
12 | # --header "Content-Type: application/json"
13 | # }
14 |
15 | delete_connector () {
16 | curl -X DELETE http://localhost:8083/connectors/identity_outbox_connector \
17 | --header "Content-Type: application/json"
18 | }
19 |
20 | stop_connector () {
21 | curl -X PUT http://localhost:8083/connectors/identity_outbox_connector/pause \
22 | --header "Content-Type: application/json"
23 | }
24 |
25 | start_connector () {
26 | curl -X PUT http://localhost:8083/connectors/identity_outbox_connector/resume \
27 | --header "Content-Type: application/json"
28 | }
29 |
30 | "$@"
31 | read
--------------------------------------------------------------------------------
/_img/before_user_and_customer_creation_flow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/suadev/microservices-change-data-capture-with-debezium/c89d957d0bf5531b645e4a6a03635bd4f62d8637/_img/before_user_and_customer_creation_flow.png
--------------------------------------------------------------------------------
/_img/user_and_customer_creation_flow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/suadev/microservices-change-data-capture-with-debezium/c89d957d0bf5531b645e4a6a03635bd4f62d8637/_img/user_and_customer_creation_flow.png
--------------------------------------------------------------------------------
/_postman/dev_summit_cdc_debezium.postman_collection.json:
--------------------------------------------------------------------------------
1 | {
2 | "info": {
3 | "_postman_id": "93eb4090-d61f-4879-bc00-d1ffd3e1bd79",
4 | "name": "Dev_Summit_CDC_Debezium",
5 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
6 | },
7 | "item": [
8 | {
9 | "name": "Register User",
10 | "request": {
11 | "auth": {
12 | "type": "bearer",
13 | "bearer": [
14 | {
15 | "key": "token",
16 | "value": "",
17 | "type": "string"
18 | }
19 | ]
20 | },
21 | "method": "POST",
22 | "header": [
23 | {
24 | "key": "Content-Type",
25 | "name": "Content-Type",
26 | "type": "text",
27 | "value": "application/json"
28 | }
29 | ],
30 | "body": {
31 | "mode": "raw",
32 | "raw": " \n \n {\n \"FirstName\" :\"Suat\",\n \"LastName\" :\"Kose\",\n \"Email\" :\"suat@kose.com\",\n \"Password\": \"qweryt\"\n }"
33 | },
34 | "url": {
35 | "raw": "http://localhost:5001/api/users",
36 | "protocol": "http",
37 | "host": [
38 | "localhost"
39 | ],
40 | "port": "5001",
41 | "path": [
42 | "api",
43 | "users"
44 | ]
45 | }
46 | },
47 | "response": []
48 | },
49 | {
50 | "name": "Update Customer",
51 | "request": {
52 | "auth": {
53 | "type": "bearer",
54 | "bearer": [
55 | {
56 | "key": "token",
57 | "value": "",
58 | "type": "string"
59 | }
60 | ]
61 | },
62 | "method": "PUT",
63 | "header": [
64 | {
65 | "key": "Content-Type",
66 | "name": "Content-Type",
67 | "type": "text",
68 | "value": "application/json"
69 | }
70 | ],
71 | "body": {
72 | "mode": "raw",
73 | "raw": "{\n \"FirstName\":\"Suat updated\",\n \"LastName\":\"Kose updated\",\n\t\"Address\": \"Uskudar\",\n\t\"PhoneNumber\":\"5381234567\",\n \"BirthDate\": \"1989-06-09\",\n \"Gender\": \"Male\"\n}"
74 | },
75 | "url": {
76 | "raw": "http://localhost:5005/api/customers/suat@kose.com",
77 | "protocol": "http",
78 | "host": [
79 | "localhost"
80 | ],
81 | "port": "5005",
82 | "path": [
83 | "api",
84 | "customers",
85 | "suat@kose.com"
86 | ]
87 | }
88 | },
89 | "response": []
90 | }
91 | ],
92 | "protocolProfileBehavior": {}
93 | }
--------------------------------------------------------------------------------
/_sample_db_logs/after_outbox_transform_create_user_log.json:
--------------------------------------------------------------------------------
1 | "\"{\\\"Id\\\":\\\"32d4971e-6248-4d56-a763-97ebd53fba82\\\",\\\"Email\\\":\\\"suat@kose.com\\\",\\\"FirstName\\\":\\\"Suat\\\",\\\"LastName\\\":\\\"Kose\\\"}\""
--------------------------------------------------------------------------------
/_sample_db_logs/before_outbox_transform_create_user_log.json:
--------------------------------------------------------------------------------
1 | {
2 | "schema": {
3 | "type": "struct",
4 | "fields": [
5 | {
6 | "type": "struct",
7 | "fields": [
8 | {
9 | "type": "string",
10 | "optional": false,
11 | "name": "io.debezium.data.Uuid",
12 | "version": 1,
13 | "field": "id"
14 | },
15 | {
16 | "type": "string",
17 | "optional": true,
18 | "field": "aggregatetype"
19 | },
20 | {
21 | "type": "string",
22 | "optional": false,
23 | "name": "io.debezium.data.Uuid",
24 | "version": 1,
25 | "field": "aggregateid"
26 | },
27 | {
28 | "type": "string",
29 | "optional": true,
30 | "field": "type"
31 | },
32 | {
33 | "type": "string",
34 | "optional": true,
35 | "field": "payload"
36 | }
37 | ],
38 | "optional": true,
39 | "name": "postgres.identity.outbox_events.Value",
40 | "field": "before"
41 | },
42 | {
43 | "type": "struct",
44 | "fields": [
45 | {
46 | "type": "string",
47 | "optional": false,
48 | "name": "io.debezium.data.Uuid",
49 | "version": 1,
50 | "field": "id"
51 | },
52 | {
53 | "type": "string",
54 | "optional": true,
55 | "field": "aggregatetype"
56 | },
57 | {
58 | "type": "string",
59 | "optional": false,
60 | "name": "io.debezium.data.Uuid",
61 | "version": 1,
62 | "field": "aggregateid"
63 | },
64 | {
65 | "type": "string",
66 | "optional": true,
67 | "field": "type"
68 | },
69 | {
70 | "type": "string",
71 | "optional": true,
72 | "field": "payload"
73 | }
74 | ],
75 | "optional": true,
76 | "name": "postgres.identity.outbox_events.Value",
77 | "field": "after"
78 | },
79 | {
80 | "type": "struct",
81 | "fields": [
82 | {
83 | "type": "string",
84 | "optional": false,
85 | "field": "version"
86 | },
87 | {
88 | "type": "string",
89 | "optional": false,
90 | "field": "connector"
91 | },
92 | {
93 | "type": "string",
94 | "optional": false,
95 | "field": "name"
96 | },
97 | {
98 | "type": "int64",
99 | "optional": false,
100 | "field": "ts_ms"
101 | },
102 | {
103 | "type": "string",
104 | "optional": true,
105 | "name": "io.debezium.data.Enum",
106 | "version": 1,
107 | "parameters": {
108 | "allowed": "true,last,false"
109 | },
110 | "default": "false",
111 | "field": "snapshot"
112 | },
113 | {
114 | "type": "string",
115 | "optional": false,
116 | "field": "db"
117 | },
118 | {
119 | "type": "string",
120 | "optional": false,
121 | "field": "schema"
122 | },
123 | {
124 | "type": "string",
125 | "optional": false,
126 | "field": "table"
127 | },
128 | {
129 | "type": "int64",
130 | "optional": true,
131 | "field": "txId"
132 | },
133 | {
134 | "type": "int64",
135 | "optional": true,
136 | "field": "lsn"
137 | },
138 | {
139 | "type": "int64",
140 | "optional": true,
141 | "field": "xmin"
142 | }
143 | ],
144 | "optional": false,
145 | "name": "io.debezium.connector.postgresql.Source",
146 | "field": "source"
147 | },
148 | {
149 | "type": "string",
150 | "optional": false,
151 | "field": "op"
152 | },
153 | {
154 | "type": "int64",
155 | "optional": true,
156 | "field": "ts_ms"
157 | },
158 | {
159 | "type": "struct",
160 | "fields": [
161 | {
162 | "type": "string",
163 | "optional": false,
164 | "field": "id"
165 | },
166 | {
167 | "type": "int64",
168 | "optional": false,
169 | "field": "total_order"
170 | },
171 | {
172 | "type": "int64",
173 | "optional": false,
174 | "field": "data_collection_order"
175 | }
176 | ],
177 | "optional": true,
178 | "field": "transaction"
179 | }
180 | ],
181 | "optional": false,
182 | "name": "postgres.identity.outbox_events.Envelope"
183 | },
184 | "payload": {
185 | "before": null,
186 | "after": {
187 | "id": "0acce781-c8a2-463d-b373-9f98522aaca8",
188 | "aggregatetype": "User",
189 | "aggregateid": "fc6bc49b-db55-409b-843b-a38a07461ca4",
190 | "type": "UserCreated",
191 | "payload": "{\"Id\":\"fc6bc49b-db55-409b-843b-a38a07461ca4\",\"Email\":\"suat@kose.com\",\"FirstName\":\"Suat\",\"LastName\":\"Kose\"}"
192 | },
193 | "source": {
194 | "version": "1.2.2.Final",
195 | "connector": "postgresql",
196 | "name": "postgres",
197 | "ts_ms": 1603893466085,
198 | "snapshot": "false",
199 | "db": "Identity",
200 | "schema": "identity",
201 | "table": "outbox_events",
202 | "txId": 568,
203 | "lsn": 24229480,
204 | "xmin": null
205 | },
206 | "op": "c",
207 | "ts_ms": 1603893466938,
208 | "transaction": null
209 | }
210 | }
--------------------------------------------------------------------------------
/_scripts/run_services.sh:
--------------------------------------------------------------------------------
1 |
2 | #!/bin/bash
3 |
4 | Services=(Services.Identity Services.Customer Services.Notification)
5 |
6 | cd ../src
7 |
8 | for Service in ${Services[*]}
9 | do
10 | echo ========================================================
11 | echo Running the service: $Service
12 | echo ========================================================
13 | cd $Service
14 | dotnet run &
15 | cd ..
16 | done
17 |
18 | read
--------------------------------------------------------------------------------
/_scripts/stop_services.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | taskkill //F //IM dotnet.exe
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.7'
2 |
3 | services:
4 |
5 | postgres:
6 | image: debezium/postgres
7 | container_name: postgres
8 | networks:
9 | - broker-kafka
10 | environment:
11 | POSTGRES_PASSWORD: admin
12 | POSTGRES_USER: admin
13 | ports:
14 | - 5499:5432
15 |
16 | zookeeper:
17 | image: confluentinc/cp-zookeeper:latest
18 | container_name: zookeeper
19 | networks:
20 | - broker-kafka
21 | ports:
22 | - 2181:2181
23 | environment:
24 | ZOOKEEPER_CLIENT_PORT: 2181
25 | ZOOKEEPER_TICK_TIME: 2000
26 |
27 | kafka:
28 | image: confluentinc/cp-kafka:latest
29 | container_name: kafka
30 | networks:
31 | - broker-kafka
32 | depends_on:
33 | - zookeeper
34 | ports:
35 | - 9092:9092
36 | environment:
37 | KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
38 | KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092
39 | KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT
40 | KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
41 | KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
42 | KAFKA_LOG_CLEANER_DELETE_RETENTION_MS: 5000
43 | KAFKA_BROKER_ID: 1
44 | KAFKA_MIN_INSYNC_REPLICAS: 1
45 |
46 | connector:
47 | image: debezium/connect:latest
48 | container_name: kafka_connect_with_debezium
49 | networks:
50 | - broker-kafka
51 | ports:
52 | - "8083:8083"
53 | environment:
54 | GROUP_ID: 1
55 | CONFIG_STORAGE_TOPIC: my_connect_configs
56 | OFFSET_STORAGE_TOPIC: my_connect_offsets
57 | BOOTSTRAP_SERVERS: kafka:29092
58 | depends_on:
59 | - zookeeper
60 | - kafka
61 |
62 | kafdrop:
63 | image: obsidiandynamics/kafdrop:latest
64 | container_name: kafdrop
65 | networks:
66 | - broker-kafka
67 | depends_on:
68 | - kafka
69 | ports:
70 | - 9000:9000
71 | environment:
72 | KAFKA_BROKERCONNECT: kafka:29092
73 |
74 | networks:
75 | broker-kafka:
76 | driver: bridge
--------------------------------------------------------------------------------
/microservices-change-data-capture-with-debezium.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio 15
4 | VisualStudioVersion = 15.0.26124.0
5 | MinimumVisualStudioVersion = 15.0.26124.0
6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{26EFD2CC-DD20-4083-9A74-A4582AA43ECD}"
7 | EndProject
8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Services.Customer", "src\Services.Customer\Services.Customer.csproj", "{FD43FD79-E6D5-49B5-A9A3-3F0B9A49C11B}"
9 | EndProject
10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Services.Identity", "src\Services.Identity\Services.Identity.csproj", "{52D976B2-BFD6-4746-AA80-ADF03E876255}"
11 | EndProject
12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Shared", "src\Shared\Shared.csproj", "{FD81B39B-C0BE-4668-A78E-B8B5ADDC95D5}"
13 | EndProject
14 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Services.Notification", "src\Services.Notification\Services.Notification.csproj", "{0D7560C4-4F32-4E57-A280-D4F5CC4C6EBD}"
15 | EndProject
16 | Global
17 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
18 | Debug|Any CPU = Debug|Any CPU
19 | Debug|x64 = Debug|x64
20 | Debug|x86 = Debug|x86
21 | Release|Any CPU = Release|Any CPU
22 | Release|x64 = Release|x64
23 | Release|x86 = Release|x86
24 | EndGlobalSection
25 | GlobalSection(SolutionProperties) = preSolution
26 | HideSolutionNode = FALSE
27 | EndGlobalSection
28 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
29 | {FD43FD79-E6D5-49B5-A9A3-3F0B9A49C11B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
30 | {FD43FD79-E6D5-49B5-A9A3-3F0B9A49C11B}.Debug|Any CPU.Build.0 = Debug|Any CPU
31 | {FD43FD79-E6D5-49B5-A9A3-3F0B9A49C11B}.Debug|x64.ActiveCfg = Debug|Any CPU
32 | {FD43FD79-E6D5-49B5-A9A3-3F0B9A49C11B}.Debug|x64.Build.0 = Debug|Any CPU
33 | {FD43FD79-E6D5-49B5-A9A3-3F0B9A49C11B}.Debug|x86.ActiveCfg = Debug|Any CPU
34 | {FD43FD79-E6D5-49B5-A9A3-3F0B9A49C11B}.Debug|x86.Build.0 = Debug|Any CPU
35 | {FD43FD79-E6D5-49B5-A9A3-3F0B9A49C11B}.Release|Any CPU.ActiveCfg = Release|Any CPU
36 | {FD43FD79-E6D5-49B5-A9A3-3F0B9A49C11B}.Release|Any CPU.Build.0 = Release|Any CPU
37 | {FD43FD79-E6D5-49B5-A9A3-3F0B9A49C11B}.Release|x64.ActiveCfg = Release|Any CPU
38 | {FD43FD79-E6D5-49B5-A9A3-3F0B9A49C11B}.Release|x64.Build.0 = Release|Any CPU
39 | {FD43FD79-E6D5-49B5-A9A3-3F0B9A49C11B}.Release|x86.ActiveCfg = Release|Any CPU
40 | {FD43FD79-E6D5-49B5-A9A3-3F0B9A49C11B}.Release|x86.Build.0 = Release|Any CPU
41 | {52D976B2-BFD6-4746-AA80-ADF03E876255}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
42 | {52D976B2-BFD6-4746-AA80-ADF03E876255}.Debug|Any CPU.Build.0 = Debug|Any CPU
43 | {52D976B2-BFD6-4746-AA80-ADF03E876255}.Debug|x64.ActiveCfg = Debug|Any CPU
44 | {52D976B2-BFD6-4746-AA80-ADF03E876255}.Debug|x64.Build.0 = Debug|Any CPU
45 | {52D976B2-BFD6-4746-AA80-ADF03E876255}.Debug|x86.ActiveCfg = Debug|Any CPU
46 | {52D976B2-BFD6-4746-AA80-ADF03E876255}.Debug|x86.Build.0 = Debug|Any CPU
47 | {52D976B2-BFD6-4746-AA80-ADF03E876255}.Release|Any CPU.ActiveCfg = Release|Any CPU
48 | {52D976B2-BFD6-4746-AA80-ADF03E876255}.Release|Any CPU.Build.0 = Release|Any CPU
49 | {52D976B2-BFD6-4746-AA80-ADF03E876255}.Release|x64.ActiveCfg = Release|Any CPU
50 | {52D976B2-BFD6-4746-AA80-ADF03E876255}.Release|x64.Build.0 = Release|Any CPU
51 | {52D976B2-BFD6-4746-AA80-ADF03E876255}.Release|x86.ActiveCfg = Release|Any CPU
52 | {52D976B2-BFD6-4746-AA80-ADF03E876255}.Release|x86.Build.0 = Release|Any CPU
53 | {FD81B39B-C0BE-4668-A78E-B8B5ADDC95D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
54 | {FD81B39B-C0BE-4668-A78E-B8B5ADDC95D5}.Debug|Any CPU.Build.0 = Debug|Any CPU
55 | {FD81B39B-C0BE-4668-A78E-B8B5ADDC95D5}.Debug|x64.ActiveCfg = Debug|Any CPU
56 | {FD81B39B-C0BE-4668-A78E-B8B5ADDC95D5}.Debug|x64.Build.0 = Debug|Any CPU
57 | {FD81B39B-C0BE-4668-A78E-B8B5ADDC95D5}.Debug|x86.ActiveCfg = Debug|Any CPU
58 | {FD81B39B-C0BE-4668-A78E-B8B5ADDC95D5}.Debug|x86.Build.0 = Debug|Any CPU
59 | {FD81B39B-C0BE-4668-A78E-B8B5ADDC95D5}.Release|Any CPU.ActiveCfg = Release|Any CPU
60 | {FD81B39B-C0BE-4668-A78E-B8B5ADDC95D5}.Release|Any CPU.Build.0 = Release|Any CPU
61 | {FD81B39B-C0BE-4668-A78E-B8B5ADDC95D5}.Release|x64.ActiveCfg = Release|Any CPU
62 | {FD81B39B-C0BE-4668-A78E-B8B5ADDC95D5}.Release|x64.Build.0 = Release|Any CPU
63 | {FD81B39B-C0BE-4668-A78E-B8B5ADDC95D5}.Release|x86.ActiveCfg = Release|Any CPU
64 | {FD81B39B-C0BE-4668-A78E-B8B5ADDC95D5}.Release|x86.Build.0 = Release|Any CPU
65 | {0D7560C4-4F32-4E57-A280-D4F5CC4C6EBD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
66 | {0D7560C4-4F32-4E57-A280-D4F5CC4C6EBD}.Debug|Any CPU.Build.0 = Debug|Any CPU
67 | {0D7560C4-4F32-4E57-A280-D4F5CC4C6EBD}.Debug|x64.ActiveCfg = Debug|Any CPU
68 | {0D7560C4-4F32-4E57-A280-D4F5CC4C6EBD}.Debug|x64.Build.0 = Debug|Any CPU
69 | {0D7560C4-4F32-4E57-A280-D4F5CC4C6EBD}.Debug|x86.ActiveCfg = Debug|Any CPU
70 | {0D7560C4-4F32-4E57-A280-D4F5CC4C6EBD}.Debug|x86.Build.0 = Debug|Any CPU
71 | {0D7560C4-4F32-4E57-A280-D4F5CC4C6EBD}.Release|Any CPU.ActiveCfg = Release|Any CPU
72 | {0D7560C4-4F32-4E57-A280-D4F5CC4C6EBD}.Release|Any CPU.Build.0 = Release|Any CPU
73 | {0D7560C4-4F32-4E57-A280-D4F5CC4C6EBD}.Release|x64.ActiveCfg = Release|Any CPU
74 | {0D7560C4-4F32-4E57-A280-D4F5CC4C6EBD}.Release|x64.Build.0 = Release|Any CPU
75 | {0D7560C4-4F32-4E57-A280-D4F5CC4C6EBD}.Release|x86.ActiveCfg = Release|Any CPU
76 | {0D7560C4-4F32-4E57-A280-D4F5CC4C6EBD}.Release|x86.Build.0 = Release|Any CPU
77 | EndGlobalSection
78 | GlobalSection(NestedProjects) = preSolution
79 | {FD43FD79-E6D5-49B5-A9A3-3F0B9A49C11B} = {26EFD2CC-DD20-4083-9A74-A4582AA43ECD}
80 | {52D976B2-BFD6-4746-AA80-ADF03E876255} = {26EFD2CC-DD20-4083-9A74-A4582AA43ECD}
81 | {FD81B39B-C0BE-4668-A78E-B8B5ADDC95D5} = {26EFD2CC-DD20-4083-9A74-A4582AA43ECD}
82 | {0D7560C4-4F32-4E57-A280-D4F5CC4C6EBD} = {26EFD2CC-DD20-4083-9A74-A4582AA43ECD}
83 | EndGlobalSection
84 | EndGlobal
85 |
--------------------------------------------------------------------------------
/src/Services.Customer/Commands/Handlers/UpdateCustomerCommandHandler.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 | using System;
3 | using System.Threading;
4 | using Services.Customer.Data;
5 | using MediatR;
6 | using Microsoft.EntityFrameworkCore;
7 | using System.Text.Json;
8 | using Services.Customer.Events;
9 |
10 | namespace Services.Customer.Commands.Handlers
11 | {
12 | public class UpdateCustomerCommandHandler : AsyncRequestHandler
13 | {
14 | private readonly CustomerDBContext _dbContext;
15 |
16 | public UpdateCustomerCommandHandler(CustomerDBContext dbContext)
17 | {
18 | _dbContext = dbContext;
19 | }
20 |
21 | protected override async Task Handle(UpdateCustomerCommand command, CancellationToken cancellationToken)
22 | {
23 | var customer = await _dbContext.Customers.FirstOrDefaultAsync(s => s.Email == command.Email);
24 | if (customer == null)
25 | throw new ApplicationException("Email is not found.");
26 |
27 | customer.FirstName = command.FirstName;
28 | customer.LastName = command.LastName;
29 | customer.Address = command.Address;
30 | customer.PhoneNumber = command.PhoneNumber;
31 | customer.Gender = command.Gender;
32 | customer.BirthDate = command.BirthDate;
33 |
34 | var outboxEvent = new Outbox
35 | {
36 | Id = Guid.NewGuid(),
37 | AggregateId = customer.Id,
38 | AggregateType = "Customer",
39 | Type = "CustomerUpdated",
40 | Payload = JsonSerializer.Serialize(new CustomerUpdatedEvent
41 | {
42 | Email = customer.Email,
43 | FirstName = command.FirstName,
44 | LastName = command.LastName,
45 | Address = customer.Address,
46 | BirthDate = customer.BirthDate,
47 | PhoneNumber = customer.PhoneNumber,
48 | Gender = customer.Gender
49 | })
50 | };
51 |
52 | _dbContext.Customers.Update(customer);
53 |
54 | _dbContext.OutboxEvents.Add(outboxEvent);
55 |
56 | await _dbContext.SaveChangesAsync();
57 | }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/Services.Customer/Commands/UpdateCustomerCommand.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using MediatR;
3 |
4 | namespace Services.Customer.Commands
5 | {
6 | public class UpdateCustomerCommand : IRequest
7 | {
8 | public string Email { get; private set; }
9 | public string FirstName { get; set; }
10 | public string LastName { get; set; }
11 | public string Address { get; set; }
12 | public string PhoneNumber { get; set; }
13 | public DateTime? BirthDate { get; set; }
14 | public string Gender { get; set; }
15 |
16 | public UpdateCustomerCommand SetEmail(string email)
17 | {
18 | Email = email;
19 | return this;
20 | }
21 | }
22 | }
--------------------------------------------------------------------------------
/src/Services.Customer/Controllers/CustomersController.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 | using MediatR;
3 | using Microsoft.AspNetCore.Mvc;
4 | using Services.Customer.Commands;
5 |
6 | namespace Services.Customer.Controllers
7 | {
8 | [Route("api/customers")]
9 | [ApiController]
10 | public class CustomersController : ControllerBase
11 | {
12 | private readonly IMediator _mediator;
13 | public CustomersController(IMediator mediator)
14 | {
15 | _mediator = mediator;
16 | }
17 |
18 | [HttpPut("{email}")]
19 | public async Task UpdateCustomer(string email, [FromBody] UpdateCustomerCommand command)
20 | => Ok(await _mediator.Send(command.SetEmail(email)));
21 | }
22 | }
--------------------------------------------------------------------------------
/src/Services.Customer/Data/CustomerDBContext.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 |
3 | namespace Services.Customer.Data
4 | {
5 | public class CustomerDBContext : DbContext
6 | {
7 | public CustomerDBContext(DbContextOptions options) : base(options)
8 | {
9 | }
10 | public DbSet Customers { get; set; }
11 | public DbSet OutboxEvents { get; set; }
12 |
13 | protected override void OnModelCreating(ModelBuilder modelBuilder)
14 | {
15 | modelBuilder.HasDefaultSchema("customer");
16 | }
17 | }
18 | }
--------------------------------------------------------------------------------
/src/Services.Customer/Data/Entity/Customer.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Services.Customer.Data
4 | {
5 | public class Customer
6 | {
7 | public Guid Id { get; set; }
8 | public string Email { get; set; }
9 | public string FirstName { get; set; }
10 | public string LastName { get; set; }
11 | public string Address { get; set; }
12 | public string PhoneNumber { get; set; }
13 | public DateTime? BirthDate { get; set; }
14 | public string Gender { get; set; }
15 | }
16 | }
--------------------------------------------------------------------------------
/src/Services.Customer/Data/Entity/Outbox.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Services.Customer.Data
4 | {
5 | public class Outbox
6 | {
7 | public Guid Id { get; set; }
8 | public string AggregateType { get; set; }
9 | public Guid AggregateId { get; set; }
10 | public string Type { get; set; }
11 | public string Payload { get; set; }
12 | }
13 | }
--------------------------------------------------------------------------------
/src/Services.Customer/Data/Migrations/20201028192204_initial.Designer.cs:
--------------------------------------------------------------------------------
1 | //
2 | using System;
3 | using Microsoft.EntityFrameworkCore;
4 | using Microsoft.EntityFrameworkCore.Infrastructure;
5 | using Microsoft.EntityFrameworkCore.Migrations;
6 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
7 | using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
8 | using Services.Customer.Data;
9 |
10 | namespace Services.Customer.Data.Migrations
11 | {
12 | [DbContext(typeof(CustomerDBContext))]
13 | [Migration("20201028192204_initial")]
14 | partial class initial
15 | {
16 | protected override void BuildTargetModel(ModelBuilder modelBuilder)
17 | {
18 | #pragma warning disable 612, 618
19 | modelBuilder
20 | .HasDefaultSchema("customer")
21 | .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn)
22 | .HasAnnotation("ProductVersion", "3.1.2")
23 | .HasAnnotation("Relational:MaxIdentifierLength", 63);
24 |
25 | modelBuilder.Entity("Services.Customer.Data.Customer", b =>
26 | {
27 | b.Property("Id")
28 | .ValueGeneratedOnAdd()
29 | .HasColumnName("id")
30 | .HasColumnType("uuid");
31 |
32 | b.Property("Address")
33 | .HasColumnName("address")
34 | .HasColumnType("text");
35 |
36 | b.Property("BirthDate")
37 | .HasColumnName("birth_date")
38 | .HasColumnType("timestamp without time zone");
39 |
40 | b.Property("Email")
41 | .HasColumnName("email")
42 | .HasColumnType("text");
43 |
44 | b.Property("FirstName")
45 | .HasColumnName("first_name")
46 | .HasColumnType("text");
47 |
48 | b.Property("Gender")
49 | .HasColumnName("gender")
50 | .HasColumnType("text");
51 |
52 | b.Property("LastName")
53 | .HasColumnName("last_name")
54 | .HasColumnType("text");
55 |
56 | b.Property("PhoneNumber")
57 | .HasColumnName("phone_number")
58 | .HasColumnType("text");
59 |
60 | b.HasKey("Id")
61 | .HasName("pk_customers");
62 |
63 | b.ToTable("customers");
64 | });
65 |
66 | modelBuilder.Entity("Services.Customer.Data.Outbox", b =>
67 | {
68 | b.Property("Id")
69 | .ValueGeneratedOnAdd()
70 | .HasColumnName("id")
71 | .HasColumnType("uuid");
72 |
73 | b.Property("AggregateId")
74 | .HasColumnName("aggregate_id")
75 | .HasColumnType("uuid");
76 |
77 | b.Property("AggregateType")
78 | .HasColumnName("aggregate_type")
79 | .HasColumnType("text");
80 |
81 | b.Property("Payload")
82 | .HasColumnName("payload")
83 | .HasColumnType("text");
84 |
85 | b.Property("Type")
86 | .HasColumnName("type")
87 | .HasColumnType("text");
88 |
89 | b.HasKey("Id")
90 | .HasName("pk_outbox_events");
91 |
92 | b.ToTable("outbox_events");
93 | });
94 | #pragma warning restore 612, 618
95 | }
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/src/Services.Customer/Data/Migrations/20201028192204_initial.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Microsoft.EntityFrameworkCore.Migrations;
3 |
4 | namespace Services.Customer.Data.Migrations
5 | {
6 | public partial class initial : Migration
7 | {
8 | protected override void Up(MigrationBuilder migrationBuilder)
9 | {
10 | migrationBuilder.EnsureSchema(
11 | name: "customer");
12 |
13 | migrationBuilder.CreateTable(
14 | name: "customers",
15 | schema: "customer",
16 | columns: table => new
17 | {
18 | id = table.Column(nullable: false),
19 | email = table.Column(nullable: true),
20 | first_name = table.Column(nullable: true),
21 | last_name = table.Column(nullable: true),
22 | address = table.Column(nullable: true),
23 | phone_number = table.Column(nullable: true),
24 | birth_date = table.Column(nullable: true),
25 | gender = table.Column(nullable: true)
26 | },
27 | constraints: table =>
28 | {
29 | table.PrimaryKey("pk_customers", x => x.id);
30 | });
31 |
32 | migrationBuilder.CreateTable(
33 | name: "outbox_events",
34 | schema: "customer",
35 | columns: table => new
36 | {
37 | id = table.Column(nullable: false),
38 | aggregate_type = table.Column(nullable: true),
39 | aggregate_id = table.Column(nullable: false),
40 | type = table.Column(nullable: true),
41 | payload = table.Column(nullable: true)
42 | },
43 | constraints: table =>
44 | {
45 | table.PrimaryKey("pk_outbox_events", x => x.id);
46 | });
47 | }
48 |
49 | protected override void Down(MigrationBuilder migrationBuilder)
50 | {
51 | migrationBuilder.DropTable(
52 | name: "customers",
53 | schema: "customer");
54 |
55 | migrationBuilder.DropTable(
56 | name: "outbox_events",
57 | schema: "customer");
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/Services.Customer/Data/Migrations/CustomerDBContextModelSnapshot.cs:
--------------------------------------------------------------------------------
1 | //
2 | using System;
3 | using Microsoft.EntityFrameworkCore;
4 | using Microsoft.EntityFrameworkCore.Infrastructure;
5 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
6 | using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
7 | using Services.Customer.Data;
8 |
9 | namespace Services.Customer.Data.Migrations
10 | {
11 | [DbContext(typeof(CustomerDBContext))]
12 | partial class CustomerDBContextModelSnapshot : ModelSnapshot
13 | {
14 | protected override void BuildModel(ModelBuilder modelBuilder)
15 | {
16 | #pragma warning disable 612, 618
17 | modelBuilder
18 | .HasDefaultSchema("customer")
19 | .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn)
20 | .HasAnnotation("ProductVersion", "3.1.2")
21 | .HasAnnotation("Relational:MaxIdentifierLength", 63);
22 |
23 | modelBuilder.Entity("Services.Customer.Data.Customer", b =>
24 | {
25 | b.Property("Id")
26 | .ValueGeneratedOnAdd()
27 | .HasColumnName("id")
28 | .HasColumnType("uuid");
29 |
30 | b.Property("Address")
31 | .HasColumnName("address")
32 | .HasColumnType("text");
33 |
34 | b.Property("BirthDate")
35 | .HasColumnName("birth_date")
36 | .HasColumnType("timestamp without time zone");
37 |
38 | b.Property("Email")
39 | .HasColumnName("email")
40 | .HasColumnType("text");
41 |
42 | b.Property("FirstName")
43 | .HasColumnName("first_name")
44 | .HasColumnType("text");
45 |
46 | b.Property("Gender")
47 | .HasColumnName("gender")
48 | .HasColumnType("text");
49 |
50 | b.Property("LastName")
51 | .HasColumnName("last_name")
52 | .HasColumnType("text");
53 |
54 | b.Property("PhoneNumber")
55 | .HasColumnName("phone_number")
56 | .HasColumnType("text");
57 |
58 | b.HasKey("Id")
59 | .HasName("pk_customers");
60 |
61 | b.ToTable("customers");
62 | });
63 |
64 | modelBuilder.Entity("Services.Customer.Data.Outbox", b =>
65 | {
66 | b.Property("Id")
67 | .ValueGeneratedOnAdd()
68 | .HasColumnName("id")
69 | .HasColumnType("uuid");
70 |
71 | b.Property("AggregateId")
72 | .HasColumnName("aggregate_id")
73 | .HasColumnType("uuid");
74 |
75 | b.Property("AggregateType")
76 | .HasColumnName("aggregate_type")
77 | .HasColumnType("text");
78 |
79 | b.Property("Payload")
80 | .HasColumnName("payload")
81 | .HasColumnType("text");
82 |
83 | b.Property("Type")
84 | .HasColumnName("type")
85 | .HasColumnType("text");
86 |
87 | b.HasKey("Id")
88 | .HasName("pk_outbox_events");
89 |
90 | b.ToTable("outbox_events");
91 | });
92 | #pragma warning restore 612, 618
93 | }
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/src/Services.Customer/Events/CustomerUpdatedEvent.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Services.Customer.Events
4 | {
5 | public class CustomerUpdatedEvent
6 | {
7 | public string FirstName { get; set; }
8 | public string LastName { get; set; }
9 | public string Email { get; set; }
10 | public string Address { get; set; }
11 | public string PhoneNumber { get; set; }
12 | public DateTime? BirthDate { get; set; }
13 | public string Gender { get; set; }
14 | }
15 | }
--------------------------------------------------------------------------------
/src/Services.Customer/Events/Handlers/UserCreatedEventHandler.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 | using Services.Customer.Data;
3 | using Services.Customer.Events;
4 | using Shared.Kafka.Consumer;
5 |
6 | namespace Services.Customer.Handlers
7 | {
8 | public class UserCreatedEventHandler : IKafkaHandler
9 | {
10 | private readonly CustomerDBContext _dbContext;
11 |
12 | public UserCreatedEventHandler(CustomerDBContext dbContext)
13 | {
14 | _dbContext = dbContext;
15 | }
16 |
17 | public async Task HandleAsync(string key, UserCreatedEvent @event)
18 | {
19 | _dbContext.Customers.Add(new Data.Customer
20 | {
21 | Id = @event.Id,
22 | Email = @event.Email,
23 | FirstName = @event.FirstName,
24 | LastName = @event.LastName,
25 | });
26 |
27 | await _dbContext.SaveChangesAsync();
28 | }
29 | }
30 | }
--------------------------------------------------------------------------------
/src/Services.Customer/Events/UserCreatedEvent.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Services.Customer.Events
4 | {
5 | public class UserCreatedEvent
6 | {
7 | public Guid Id { get; set; }
8 | public string Email { get; set; }
9 | public string FirstName { get; set; }
10 | public string LastName { get; set; }
11 | }
12 | }
--------------------------------------------------------------------------------
/src/Services.Customer/Program.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Hosting;
2 | using Microsoft.Extensions.Hosting;
3 |
4 | namespace Services.Customer
5 | {
6 | public class Program
7 | {
8 | public static void Main(string[] args)
9 | {
10 | CreateHostBuilder(args).Build().Run();
11 | }
12 |
13 | public static IHostBuilder CreateHostBuilder(string[] args) =>
14 | Host.CreateDefaultBuilder(args)
15 | .ConfigureWebHostDefaults(webBuilder =>
16 | {
17 | webBuilder.UseStartup();
18 | });
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/Services.Customer/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json.schemastore.org/launchsettings.json",
3 | "iisSettings": {
4 | "windowsAuthentication": false,
5 | "anonymousAuthentication": true,
6 | "iisExpress": {
7 | "applicationUrl": "http://localhost:5834",
8 | "sslPort": 44386
9 | }
10 | },
11 | "profiles": {
12 | "IIS Express": {
13 | "commandName": "IISExpress",
14 | "launchBrowser": true,
15 | "environmentVariables": {
16 | "ASPNETCORE_ENVIRONMENT": "Development"
17 | }
18 | },
19 | "Services.Customer": {
20 | "commandName": "Project",
21 | "launchBrowser": true,
22 | "applicationUrl": "http://localhost:5005",
23 | "environmentVariables": {
24 | "ASPNETCORE_ENVIRONMENT": "Development"
25 | }
26 | }
27 | }
28 | }
--------------------------------------------------------------------------------
/src/Services.Customer/README.md:
--------------------------------------------------------------------------------
1 | **Migration**
2 |
3 | 0- cd Services.Customer
4 |
5 | 1- *dotnet ef migrations add "migration_name" -o ./Data/Migrations*
6 |
7 | 2- *dotnet ef database update*
--------------------------------------------------------------------------------
/src/Services.Customer/Services.Customer.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net5.0
5 | InProcess
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/Services.Customer/Startup.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using Microsoft.AspNetCore.Builder;
3 | using Microsoft.AspNetCore.Hosting;
4 | using Microsoft.AspNetCore.Http;
5 | using Microsoft.EntityFrameworkCore;
6 | using Microsoft.Extensions.Configuration;
7 | using Microsoft.Extensions.DependencyInjection;
8 | using Microsoft.Extensions.Hosting;
9 | using Services.Customer.Commands.Handlers;
10 | using Services.Customer.Data;
11 | using Services.Customer.Events;
12 | using Services.Customer.Handlers;
13 | using Shared.Kafka;
14 | using MediatR;
15 | using Shared;
16 |
17 | namespace Services.Customer
18 | {
19 | public class Startup
20 | {
21 | public Startup(IConfiguration configuration)
22 | {
23 | Configuration = configuration;
24 | }
25 |
26 | public IConfiguration Configuration { get; }
27 |
28 | public void ConfigureServices(IServiceCollection services)
29 | {
30 | services.AddDbContext(options => options
31 | .UseNpgsql(Configuration.GetConnectionString("DefaultConnection"))
32 | .UseSnakeCaseNamingConvention()
33 | );
34 |
35 | services.AddMediatR(typeof(UpdateCustomerCommandHandler).GetTypeInfo().Assembly);
36 |
37 | services.AddControllers();
38 |
39 | services.AddKafkaConsumer(p =>
40 | {
41 | p.Topic = "user_events";
42 | p.GroupId = "user_events_customer_group";
43 | p.BootstrapServers = "localhost:9092";
44 | });
45 | }
46 |
47 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
48 | {
49 | if (env.IsDevelopment())
50 | {
51 | app.UseDeveloperExceptionPage();
52 | }
53 |
54 | DbInitilializer.Migrate(app.ApplicationServices);
55 |
56 | app.UseRouting();
57 |
58 | app.UseEndpoints(endpoints =>
59 | {
60 | endpoints.MapControllers();
61 | endpoints.MapGet("", async context => await context.Response.WriteAsync("Customer service is up."));
62 | });
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/Services.Customer/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft": "Warning",
6 | "Microsoft.Hosting.Lifetime": "Information"
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/Services.Customer/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft": "Warning",
6 | "Microsoft.Hosting.Lifetime": "Information"
7 | }
8 | },
9 | "AllowedHosts": "*",
10 | "ConnectionStrings": {
11 | "DefaultConnection": "Host=localhost;Port=5499;Username=admin;Password=admin;Database=Customer;"
12 | }
13 | }
--------------------------------------------------------------------------------
/src/Services.Identity/Commands/Handlers/RegisterUserCommandHandler.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 | using System;
3 | using System.Threading;
4 | using Services.Identity.Data;
5 | using MediatR;
6 | using Microsoft.EntityFrameworkCore;
7 | using System.Text.Json;
8 | using Services.Identity.Events;
9 |
10 | namespace Services.Identity.Commands.Handlers
11 | {
12 | public class RegisterUserCommandHandler : AsyncRequestHandler
13 | {
14 | private readonly IdentityDBContext _dbContext;
15 | public RegisterUserCommandHandler(IdentityDBContext dbContext)
16 | {
17 | _dbContext = dbContext;
18 | }
19 |
20 | protected override async Task Handle(RegisterUserCommand command, CancellationToken cancellationToken)
21 | {
22 | if (await _dbContext.Users.AsNoTracking().AnyAsync(s => s.Email == command.Email))
23 | throw new ApplicationException("Email is already exist.");
24 |
25 | var user = new User
26 | {
27 | Id = command.Id,
28 | Password = command.Password,
29 | Email = command.Email,
30 | FirstName = command.FirstName,
31 | LastName = command.LastName,
32 | };
33 |
34 | var outboxEvent = new Outbox
35 | {
36 | Id = Guid.NewGuid(),
37 | AggregateId = command.Id,
38 | AggregateType = "User",
39 | Type = "UserCreated",
40 | Payload = JsonSerializer.Serialize(new UserCreatedEvent
41 | {
42 | Id = user.Id,
43 | Email = user.Email,
44 | LastName = user.LastName,
45 | FirstName = user.FirstName,
46 | })
47 | };
48 |
49 | _dbContext.Users.Add(user);
50 |
51 | _dbContext.OutboxEvents.Add(outboxEvent);
52 |
53 | await _dbContext.SaveChangesAsync();
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/Services.Identity/Commands/RegisterUserCommand.cs:
--------------------------------------------------------------------------------
1 |
2 | using System;
3 | using MediatR;
4 |
5 | namespace Services.Identity.Commands
6 | {
7 | public class RegisterUserCommand : IRequest
8 | {
9 | public Guid Id { get; set; }
10 | public string Email { get; set; }
11 | public string Password { get; set; }
12 | public string FirstName { get; set; }
13 | public string LastName { get; set; }
14 | public RegisterUserCommand()
15 | {
16 | Id = Guid.NewGuid();
17 | }
18 | }
19 | }
--------------------------------------------------------------------------------
/src/Services.Identity/Controllers/AccountsController.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 | using MediatR;
3 | using Microsoft.AspNetCore.Mvc;
4 | using Services.Identity.Commands;
5 |
6 | namespace Services.Identity.Controllers
7 | {
8 | [Route("api/users")]
9 | [ApiController]
10 | public class AccountsController : ControllerBase
11 | {
12 | private readonly IMediator _mediator;
13 |
14 | public AccountsController(IMediator mediator)
15 | {
16 | _mediator = mediator;
17 | }
18 |
19 | [HttpPost]
20 | public async Task RegisterUser([FromBody] RegisterUserCommand command)
21 | => Ok(await _mediator.Send(command));
22 | }
23 | }
--------------------------------------------------------------------------------
/src/Services.Identity/Data/Entity/Outbox.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Services.Identity.Data
4 | {
5 | public class Outbox
6 | {
7 | public Guid Id { get; set; }
8 | public string AggregateType { get; set; }
9 | public Guid AggregateId { get; set; }
10 | public string Type { get; set; }
11 | public string Payload { get; set; }
12 | }
13 | }
--------------------------------------------------------------------------------
/src/Services.Identity/Data/Entity/User.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Services.Identity.Data
4 | {
5 | public class User
6 | {
7 | public Guid Id { get; set; }
8 | public string Email { get; set; }
9 | public string Password { get; set; }
10 | public string FirstName { get; set; }
11 | public string LastName { get; set; }
12 | }
13 | }
--------------------------------------------------------------------------------
/src/Services.Identity/Data/IdentityDBContext.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 |
3 | namespace Services.Identity.Data
4 | {
5 | public class IdentityDBContext : DbContext
6 | {
7 | public IdentityDBContext(DbContextOptions options) : base(options)
8 | {
9 | }
10 | public DbSet Users { get; set; }
11 | public DbSet OutboxEvents { get; set; }
12 |
13 | protected override void OnModelCreating(ModelBuilder modelBuilder)
14 | {
15 | modelBuilder.HasDefaultSchema("identity");
16 | }
17 | }
18 | }
--------------------------------------------------------------------------------
/src/Services.Identity/Data/Migrations/20201028192522_initial.Designer.cs:
--------------------------------------------------------------------------------
1 | //
2 | using System;
3 | using Microsoft.EntityFrameworkCore;
4 | using Microsoft.EntityFrameworkCore.Infrastructure;
5 | using Microsoft.EntityFrameworkCore.Migrations;
6 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
7 | using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
8 | using Services.Identity.Data;
9 |
10 | namespace Services.Identity.Data.Migrations
11 | {
12 | [DbContext(typeof(IdentityDBContext))]
13 | [Migration("20201028192522_initial")]
14 | partial class initial
15 | {
16 | protected override void BuildTargetModel(ModelBuilder modelBuilder)
17 | {
18 | #pragma warning disable 612, 618
19 | modelBuilder
20 | .HasDefaultSchema("identity")
21 | .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn)
22 | .HasAnnotation("ProductVersion", "3.1.2")
23 | .HasAnnotation("Relational:MaxIdentifierLength", 63);
24 |
25 | modelBuilder.Entity("Services.Identity.Data.Outbox", b =>
26 | {
27 | b.Property("Id")
28 | .ValueGeneratedOnAdd()
29 | .HasColumnName("id")
30 | .HasColumnType("uuid");
31 |
32 | b.Property("AggregateId")
33 | .HasColumnName("aggregate_id")
34 | .HasColumnType("uuid");
35 |
36 | b.Property("AggregateType")
37 | .HasColumnName("aggregate_type")
38 | .HasColumnType("text");
39 |
40 | b.Property("Payload")
41 | .HasColumnName("payload")
42 | .HasColumnType("text");
43 |
44 | b.Property("Type")
45 | .HasColumnName("type")
46 | .HasColumnType("text");
47 |
48 | b.HasKey("Id")
49 | .HasName("pk_outbox_events");
50 |
51 | b.ToTable("outbox_events");
52 | });
53 |
54 | modelBuilder.Entity("Services.Identity.Data.User", b =>
55 | {
56 | b.Property("Id")
57 | .ValueGeneratedOnAdd()
58 | .HasColumnName("id")
59 | .HasColumnType("uuid");
60 |
61 | b.Property("Email")
62 | .HasColumnName("email")
63 | .HasColumnType("text");
64 |
65 | b.Property("FirstName")
66 | .HasColumnName("first_name")
67 | .HasColumnType("text");
68 |
69 | b.Property("LastName")
70 | .HasColumnName("last_name")
71 | .HasColumnType("text");
72 |
73 | b.Property("Password")
74 | .HasColumnName("password")
75 | .HasColumnType("text");
76 |
77 | b.HasKey("Id")
78 | .HasName("pk_users");
79 |
80 | b.ToTable("users");
81 | });
82 | #pragma warning restore 612, 618
83 | }
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/Services.Identity/Data/Migrations/20201028192522_initial.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Microsoft.EntityFrameworkCore.Migrations;
3 |
4 | namespace Services.Identity.Data.Migrations
5 | {
6 | public partial class initial : Migration
7 | {
8 | protected override void Up(MigrationBuilder migrationBuilder)
9 | {
10 | migrationBuilder.EnsureSchema(
11 | name: "identity");
12 |
13 | migrationBuilder.CreateTable(
14 | name: "outbox_events",
15 | schema: "identity",
16 | columns: table => new
17 | {
18 | id = table.Column(nullable: false),
19 | aggregate_type = table.Column(nullable: true),
20 | aggregate_id = table.Column(nullable: false),
21 | type = table.Column(nullable: true),
22 | payload = table.Column(nullable: true)
23 | },
24 | constraints: table =>
25 | {
26 | table.PrimaryKey("pk_outbox_events", x => x.id);
27 | });
28 |
29 | migrationBuilder.CreateTable(
30 | name: "users",
31 | schema: "identity",
32 | columns: table => new
33 | {
34 | id = table.Column(nullable: false),
35 | email = table.Column(nullable: true),
36 | password = table.Column(nullable: true),
37 | first_name = table.Column(nullable: true),
38 | last_name = table.Column(nullable: true)
39 | },
40 | constraints: table =>
41 | {
42 | table.PrimaryKey("pk_users", x => x.id);
43 | });
44 | }
45 |
46 | protected override void Down(MigrationBuilder migrationBuilder)
47 | {
48 | migrationBuilder.DropTable(
49 | name: "outbox_events",
50 | schema: "identity");
51 |
52 | migrationBuilder.DropTable(
53 | name: "users",
54 | schema: "identity");
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/Services.Identity/Data/Migrations/IdentityDBContextModelSnapshot.cs:
--------------------------------------------------------------------------------
1 | //
2 | using System;
3 | using Microsoft.EntityFrameworkCore;
4 | using Microsoft.EntityFrameworkCore.Infrastructure;
5 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
6 | using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
7 | using Services.Identity.Data;
8 |
9 | namespace Services.Identity.Data.Migrations
10 | {
11 | [DbContext(typeof(IdentityDBContext))]
12 | partial class IdentityDBContextModelSnapshot : ModelSnapshot
13 | {
14 | protected override void BuildModel(ModelBuilder modelBuilder)
15 | {
16 | #pragma warning disable 612, 618
17 | modelBuilder
18 | .HasDefaultSchema("identity")
19 | .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn)
20 | .HasAnnotation("ProductVersion", "3.1.2")
21 | .HasAnnotation("Relational:MaxIdentifierLength", 63);
22 |
23 | modelBuilder.Entity("Services.Identity.Data.Outbox", b =>
24 | {
25 | b.Property("Id")
26 | .ValueGeneratedOnAdd()
27 | .HasColumnName("id")
28 | .HasColumnType("uuid");
29 |
30 | b.Property("AggregateId")
31 | .HasColumnName("aggregate_id")
32 | .HasColumnType("uuid");
33 |
34 | b.Property("AggregateType")
35 | .HasColumnName("aggregate_type")
36 | .HasColumnType("text");
37 |
38 | b.Property("Payload")
39 | .HasColumnName("payload")
40 | .HasColumnType("text");
41 |
42 | b.Property("Type")
43 | .HasColumnName("type")
44 | .HasColumnType("text");
45 |
46 | b.HasKey("Id")
47 | .HasName("pk_outbox_events");
48 |
49 | b.ToTable("outbox_events");
50 | });
51 |
52 | modelBuilder.Entity("Services.Identity.Data.User", b =>
53 | {
54 | b.Property("Id")
55 | .ValueGeneratedOnAdd()
56 | .HasColumnName("id")
57 | .HasColumnType("uuid");
58 |
59 | b.Property("Email")
60 | .HasColumnName("email")
61 | .HasColumnType("text");
62 |
63 | b.Property("FirstName")
64 | .HasColumnName("first_name")
65 | .HasColumnType("text");
66 |
67 | b.Property("LastName")
68 | .HasColumnName("last_name")
69 | .HasColumnType("text");
70 |
71 | b.Property("Password")
72 | .HasColumnName("password")
73 | .HasColumnType("text");
74 |
75 | b.HasKey("Id")
76 | .HasName("pk_users");
77 |
78 | b.ToTable("users");
79 | });
80 | #pragma warning restore 612, 618
81 | }
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/src/Services.Identity/Events/CustomerUpdatedEvent.cs:
--------------------------------------------------------------------------------
1 | namespace Services.Identity.Events
2 | {
3 | public class CustomerUpdatedEvent
4 | {
5 | public string FirstName { get; set; }
6 | public string LastName { get; set; }
7 | public string Email { get; set; }
8 | }
9 | }
--------------------------------------------------------------------------------
/src/Services.Identity/Events/Handlers/CustomerUpdatedEventHandler.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading.Tasks;
3 | using Microsoft.EntityFrameworkCore;
4 | using Services.Identity.Data;
5 | using Services.Identity.Events;
6 | using Shared.Kafka.Consumer;
7 |
8 | namespace Services.Identity.Handlers
9 | {
10 | public class CustomerUpdatedEventHandler : IKafkaHandler
11 | {
12 | private readonly IdentityDBContext _dbContext;
13 |
14 | public CustomerUpdatedEventHandler(IdentityDBContext dbContext)
15 | {
16 | _dbContext = dbContext;
17 | }
18 |
19 | public async Task HandleAsync(string key, CustomerUpdatedEvent @event)
20 | {
21 | var user = await _dbContext.Users.FirstOrDefaultAsync(s => s.Email == @event.Email);
22 | if (user == null)
23 | throw new ApplicationException("Email is not found.");
24 |
25 | user.FirstName = @event.FirstName;
26 | user.LastName = @event.LastName;
27 |
28 | await _dbContext.SaveChangesAsync();
29 | }
30 | }
31 | }
--------------------------------------------------------------------------------
/src/Services.Identity/Events/UserCreatedEvent.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Services.Identity.Events
4 | {
5 | public class UserCreatedEvent
6 | {
7 | public Guid Id { get; set; }
8 | public string Email { get; set; }
9 | public string FirstName { get; set; }
10 | public string LastName { get; set; }
11 | }
12 | }
--------------------------------------------------------------------------------
/src/Services.Identity/Program.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Hosting;
2 | using Microsoft.Extensions.Hosting;
3 |
4 | namespace Services.Identity
5 | {
6 | public class Program
7 | {
8 | public static void Main(string[] args)
9 | {
10 | CreateHostBuilder(args).Build().Run();
11 | }
12 |
13 | public static IHostBuilder CreateHostBuilder(string[] args) =>
14 | Host.CreateDefaultBuilder(args)
15 | .ConfigureWebHostDefaults(webBuilder =>
16 | {
17 | webBuilder.UseStartup();
18 | });
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/Services.Identity/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json.schemastore.org/launchsettings.json",
3 | "iisSettings": {
4 | "windowsAuthentication": false,
5 | "anonymousAuthentication": true,
6 | "iisExpress": {
7 | "applicationUrl": "http://localhost:55301",
8 | "sslPort": 44356
9 | }
10 | },
11 | "profiles": {
12 | "IIS Express": {
13 | "commandName": "IISExpress",
14 | "launchBrowser": true,
15 | "environmentVariables": {
16 | "ASPNETCORE_ENVIRONMENT": "Development"
17 | }
18 | },
19 | "Services.Identity": {
20 | "commandName": "Project",
21 | "launchBrowser": true,
22 | "applicationUrl": "http://localhost:5001",
23 | "environmentVariables": {
24 | "ASPNETCORE_ENVIRONMENT": "Development"
25 | }
26 | }
27 | }
28 | }
--------------------------------------------------------------------------------
/src/Services.Identity/README.md:
--------------------------------------------------------------------------------
1 | **Migration**
2 |
3 | 0- cd Services.Identity
4 |
5 | 1- *dotnet ef migrations add "initial" -o ./Data/Migrations*
6 |
7 | 2- *dotnet ef database update*
--------------------------------------------------------------------------------
/src/Services.Identity/Services.Identity.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net5.0
5 | InProcess
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/Services.Identity/Startup.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using MediatR;
3 | using Microsoft.AspNetCore.Builder;
4 | using Microsoft.AspNetCore.Hosting;
5 | using Microsoft.AspNetCore.Http;
6 | using Microsoft.EntityFrameworkCore;
7 | using Microsoft.Extensions.Configuration;
8 | using Microsoft.Extensions.DependencyInjection;
9 | using Microsoft.Extensions.Hosting;
10 | using Services.Identity.Commands.Handlers;
11 | using Services.Identity.Data;
12 | using Services.Identity.Events;
13 | using Services.Identity.Handlers;
14 | using Shared;
15 | using Shared.Kafka;
16 |
17 | namespace Services.Identity
18 | {
19 | public class Startup
20 | {
21 | public Startup(IConfiguration configuration)
22 | {
23 | Configuration = configuration;
24 | }
25 |
26 | public IConfiguration Configuration { get; }
27 |
28 | public void ConfigureServices(IServiceCollection services)
29 | {
30 | services.AddDbContext(options => options
31 | .UseNpgsql(Configuration.GetConnectionString("DefaultConnection"))
32 | .UseSnakeCaseNamingConvention()
33 | );
34 |
35 | services.AddMediatR(typeof(RegisterUserCommandHandler).GetTypeInfo().Assembly);
36 |
37 | services.AddControllers();
38 |
39 | services.AddKafkaConsumer(p =>
40 | {
41 | p.Topic = "customer_events";
42 | p.GroupId = "customer_events_identity_group";
43 | p.BootstrapServers = "localhost:9092";
44 | });
45 | }
46 |
47 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
48 | {
49 | if (env.IsDevelopment())
50 | {
51 | app.UseDeveloperExceptionPage();
52 | }
53 |
54 | DbInitilializer.Migrate(app.ApplicationServices);
55 |
56 | app.UseRouting();
57 | app.UseEndpoints(endpoints =>
58 | {
59 | endpoints.MapControllers();
60 | endpoints.MapGet("", async context => await context.Response.WriteAsync("Identity service is up."));
61 | });
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/Services.Identity/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft": "Warning",
6 | "Microsoft.Hosting.Lifetime": "Information"
7 | }
8 | }
9 | }
--------------------------------------------------------------------------------
/src/Services.Identity/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft": "Warning",
6 | "Microsoft.Hosting.Lifetime": "Information"
7 | }
8 | },
9 | "AllowedHosts": "*",
10 | "ConnectionStrings": {
11 | "DefaultConnection": "Host=localhost;Port=5499;Username=admin;Password=admin;Database=Identity;"
12 | }
13 | }
--------------------------------------------------------------------------------
/src/Services.Notification/Data/Entity/Customer.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Services.Notification.Data
4 | {
5 | public class Customer
6 | {
7 | public Guid Id { get; set; }
8 | public string Email { get; set; }
9 | public string PhoneNumber { get; set; }
10 | public string FirstName { get; set; }
11 | public string LastName { get; set; }
12 | }
13 | }
--------------------------------------------------------------------------------
/src/Services.Notification/Data/Migrations/20201028194326_initial.Designer.cs:
--------------------------------------------------------------------------------
1 | //
2 | using System;
3 | using Microsoft.EntityFrameworkCore;
4 | using Microsoft.EntityFrameworkCore.Infrastructure;
5 | using Microsoft.EntityFrameworkCore.Migrations;
6 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
7 | using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
8 | using Services.Notification.Data;
9 |
10 | namespace Services.Notification.Data.Migrations
11 | {
12 | [DbContext(typeof(NotificationDBContext))]
13 | [Migration("20201028194326_initial")]
14 | partial class initial
15 | {
16 | protected override void BuildTargetModel(ModelBuilder modelBuilder)
17 | {
18 | #pragma warning disable 612, 618
19 | modelBuilder
20 | .HasDefaultSchema("notification")
21 | .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn)
22 | .HasAnnotation("ProductVersion", "3.1.2")
23 | .HasAnnotation("Relational:MaxIdentifierLength", 63);
24 |
25 | modelBuilder.Entity("Services.Notification.Data.Customer", b =>
26 | {
27 | b.Property("Id")
28 | .ValueGeneratedOnAdd()
29 | .HasColumnName("id")
30 | .HasColumnType("uuid");
31 |
32 | b.Property("Email")
33 | .HasColumnName("email")
34 | .HasColumnType("text");
35 |
36 | b.Property("FirstName")
37 | .HasColumnName("first_name")
38 | .HasColumnType("text");
39 |
40 | b.Property("LastName")
41 | .HasColumnName("last_name")
42 | .HasColumnType("text");
43 |
44 | b.Property("PhoneNumber")
45 | .HasColumnName("phone_number")
46 | .HasColumnType("text");
47 |
48 | b.HasKey("Id")
49 | .HasName("pk_customers");
50 |
51 | b.ToTable("customers");
52 | });
53 | #pragma warning restore 612, 618
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/Services.Notification/Data/Migrations/20201028194326_initial.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Microsoft.EntityFrameworkCore.Migrations;
3 |
4 | namespace Services.Notification.Data.Migrations
5 | {
6 | public partial class initial : Migration
7 | {
8 | protected override void Up(MigrationBuilder migrationBuilder)
9 | {
10 | migrationBuilder.EnsureSchema(
11 | name: "notification");
12 |
13 | migrationBuilder.CreateTable(
14 | name: "customers",
15 | schema: "notification",
16 | columns: table => new
17 | {
18 | id = table.Column(nullable: false),
19 | email = table.Column(nullable: true),
20 | phone_number = table.Column(nullable: true),
21 | first_name = table.Column(nullable: true),
22 | last_name = table.Column(nullable: true)
23 | },
24 | constraints: table =>
25 | {
26 | table.PrimaryKey("pk_customers", x => x.id);
27 | });
28 | }
29 |
30 | protected override void Down(MigrationBuilder migrationBuilder)
31 | {
32 | migrationBuilder.DropTable(
33 | name: "customers",
34 | schema: "notification");
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/Services.Notification/Data/Migrations/NotificationDBContextModelSnapshot.cs:
--------------------------------------------------------------------------------
1 | //
2 | using System;
3 | using Microsoft.EntityFrameworkCore;
4 | using Microsoft.EntityFrameworkCore.Infrastructure;
5 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
6 | using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
7 | using Services.Notification.Data;
8 |
9 | namespace Services.Notification.Data.Migrations
10 | {
11 | [DbContext(typeof(NotificationDBContext))]
12 | partial class NotificationDBContextModelSnapshot : ModelSnapshot
13 | {
14 | protected override void BuildModel(ModelBuilder modelBuilder)
15 | {
16 | #pragma warning disable 612, 618
17 | modelBuilder
18 | .HasDefaultSchema("notification")
19 | .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn)
20 | .HasAnnotation("ProductVersion", "3.1.2")
21 | .HasAnnotation("Relational:MaxIdentifierLength", 63);
22 |
23 | modelBuilder.Entity("Services.Notification.Data.Customer", b =>
24 | {
25 | b.Property("Id")
26 | .ValueGeneratedOnAdd()
27 | .HasColumnName("id")
28 | .HasColumnType("uuid");
29 |
30 | b.Property("Email")
31 | .HasColumnName("email")
32 | .HasColumnType("text");
33 |
34 | b.Property("FirstName")
35 | .HasColumnName("first_name")
36 | .HasColumnType("text");
37 |
38 | b.Property("LastName")
39 | .HasColumnName("last_name")
40 | .HasColumnType("text");
41 |
42 | b.Property("PhoneNumber")
43 | .HasColumnName("phone_number")
44 | .HasColumnType("text");
45 |
46 | b.HasKey("Id")
47 | .HasName("pk_customers");
48 |
49 | b.ToTable("customers");
50 | });
51 | #pragma warning restore 612, 618
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/Services.Notification/Data/NotificationDBContext.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 |
3 | namespace Services.Notification.Data
4 | {
5 | public class NotificationDBContext : DbContext
6 | {
7 | public NotificationDBContext(DbContextOptions options) : base(options)
8 | {
9 | }
10 | public DbSet Customers { get; set; }
11 |
12 | protected override void OnModelCreating(ModelBuilder modelBuilder)
13 | {
14 | modelBuilder.HasDefaultSchema("notification");
15 | modelBuilder.Entity().ToTable("customers");
16 | }
17 | }
18 | }
--------------------------------------------------------------------------------
/src/Services.Notification/Events/CustomerUpdatedEvent.cs:
--------------------------------------------------------------------------------
1 | namespace Services.Notification.Events
2 | {
3 | public class CustomerUpdatedEvent
4 | {
5 | public string FirstName { get; set; }
6 | public string LastName { get; set; }
7 | public string Email { get; set; }
8 | public string PhoneNumber { get; set; }
9 | }
10 | }
--------------------------------------------------------------------------------
/src/Services.Notification/Events/Handlers/CustomerUpdatedEventHandler.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading.Tasks;
3 | using Microsoft.EntityFrameworkCore;
4 | using Services.Notification.Data;
5 | using Services.Notification.Events;
6 | using Shared.Kafka.Consumer;
7 |
8 | namespace Services.Notification.Handlers
9 | {
10 | public class CustomerUpdatedEventHandler : IKafkaHandler
11 | {
12 | private readonly NotificationDBContext _dbContext;
13 |
14 | public CustomerUpdatedEventHandler(NotificationDBContext dbContext)
15 | {
16 | _dbContext = dbContext;
17 | }
18 |
19 | public async Task HandleAsync(string key, CustomerUpdatedEvent @event)
20 | {
21 | var user = await _dbContext.Customers.FirstOrDefaultAsync(s => s.Email == @event.Email);
22 | if (user == null)
23 | throw new ApplicationException("Email is not found.");
24 |
25 | user.FirstName = @event.FirstName;
26 | user.LastName = @event.LastName;
27 | user.PhoneNumber = @event.PhoneNumber;
28 |
29 | await _dbContext.SaveChangesAsync();
30 | }
31 | }
32 | }
--------------------------------------------------------------------------------
/src/Services.Notification/Events/Handlers/UserCreatedEventHandler.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 | using Services.Notification.Data;
3 | using Services.Notification.Events;
4 | using Shared.Kafka.Consumer;
5 |
6 | namespace Services.Notification.Handlers
7 | {
8 | public class UserCreatedEventHandler : IKafkaHandler
9 | {
10 | private readonly NotificationDBContext _dbContext;
11 |
12 | public UserCreatedEventHandler(NotificationDBContext dbContext)
13 | {
14 | _dbContext = dbContext;
15 | }
16 |
17 | public async Task HandleAsync(string key, UserCreatedEvent @event)
18 | {
19 | _dbContext.Customers.Add(new Data.Customer
20 | {
21 | Id = @event.Id,
22 | Email = @event.Email,
23 | FirstName = @event.FirstName,
24 | LastName = @event.LastName,
25 | });
26 |
27 | await _dbContext.SaveChangesAsync();
28 | }
29 | }
30 | }
--------------------------------------------------------------------------------
/src/Services.Notification/Events/UserCreatedEvent.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Services.Notification.Events
4 | {
5 | public class UserCreatedEvent
6 | {
7 | public Guid Id { get; set; }
8 | public string Email { get; set; }
9 | public string FirstName { get; set; }
10 | public string LastName { get; set; }
11 | }
12 | }
--------------------------------------------------------------------------------
/src/Services.Notification/Program.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Threading.Tasks;
5 | using Microsoft.AspNetCore.Hosting;
6 | using Microsoft.Extensions.Configuration;
7 | using Microsoft.Extensions.Hosting;
8 | using Microsoft.Extensions.Logging;
9 |
10 | namespace Services.Notification
11 | {
12 | public class Program
13 | {
14 | public static void Main(string[] args)
15 | {
16 | CreateHostBuilder(args).Build().Run();
17 | }
18 |
19 | public static IHostBuilder CreateHostBuilder(string[] args) =>
20 | Host.CreateDefaultBuilder(args)
21 | .ConfigureWebHostDefaults(webBuilder =>
22 | {
23 | webBuilder.UseStartup();
24 | });
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/Services.Notification/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json.schemastore.org/launchsettings.json",
3 | "iisSettings": {
4 | "windowsAuthentication": false,
5 | "anonymousAuthentication": true,
6 | "iisExpress": {
7 | "applicationUrl": "http://localhost:10137",
8 | "sslPort": 44317
9 | }
10 | },
11 | "profiles": {
12 | "IIS Express": {
13 | "commandName": "IISExpress",
14 | "launchBrowser": true,
15 | "launchUrl": "weatherforecast",
16 | "environmentVariables": {
17 | "ASPNETCORE_ENVIRONMENT": "Development"
18 | }
19 | },
20 | "Services.Notification": {
21 | "commandName": "Project",
22 | "launchBrowser": true,
23 | "applicationUrl": "http://localhost:5010",
24 | "environmentVariables": {
25 | "ASPNETCORE_ENVIRONMENT": "Development"
26 | }
27 | }
28 | }
29 | }
--------------------------------------------------------------------------------
/src/Services.Notification/README.md:
--------------------------------------------------------------------------------
1 | **Migration**
2 |
3 | 0- cd Services.Notification
4 |
5 | 1- *dotnet ef migrations add "migration_name" -o ./Data/Migrations*
6 |
7 | 2- *dotnet ef database update*
--------------------------------------------------------------------------------
/src/Services.Notification/Services.Notification.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net5.0
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/src/Services.Notification/Startup.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Builder;
2 | using Microsoft.AspNetCore.Hosting;
3 | using Microsoft.AspNetCore.Http;
4 | using Microsoft.EntityFrameworkCore;
5 | using Microsoft.Extensions.Configuration;
6 | using Microsoft.Extensions.DependencyInjection;
7 | using Microsoft.Extensions.Hosting;
8 | using Services.Notification.Data;
9 | using Services.Notification.Events;
10 | using Services.Notification.Handlers;
11 | using Shared;
12 | using Shared.Kafka;
13 |
14 | namespace Services.Notification
15 | {
16 | public class Startup
17 | {
18 | public Startup(IConfiguration configuration)
19 | {
20 | Configuration = configuration;
21 | }
22 |
23 | public IConfiguration Configuration { get; }
24 |
25 | public void ConfigureServices(IServiceCollection services)
26 | {
27 | services.AddDbContext(options => options
28 | .UseNpgsql(Configuration.GetConnectionString("DefaultConnection"))
29 | .UseSnakeCaseNamingConvention()
30 | );
31 |
32 | services.AddControllers();
33 |
34 | services.AddKafkaConsumer(p =>
35 | {
36 | p.Topic = "user_events";
37 | p.GroupId = "user_events_notification_group";
38 | p.BootstrapServers = "localhost:9092";
39 | });
40 |
41 | services.AddKafkaConsumer(p =>
42 | {
43 | p.Topic = "customer_events";
44 | p.GroupId = "customer_events_notification_group";
45 | p.BootstrapServers = "localhost:9092";
46 | });
47 | }
48 |
49 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
50 | {
51 | if (env.IsDevelopment())
52 | {
53 | app.UseDeveloperExceptionPage();
54 | }
55 |
56 | DbInitilializer.Migrate(app.ApplicationServices);
57 |
58 | app.UseRouting();
59 | app.UseEndpoints(endpoints =>
60 | {
61 | endpoints.MapControllers();
62 | endpoints.MapGet("", async context => await context.Response.WriteAsync("Notification service is up."));
63 | });
64 | }
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/Services.Notification/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft": "Warning",
6 | "Microsoft.Hosting.Lifetime": "Information"
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/Services.Notification/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft": "Warning",
6 | "Microsoft.Hosting.Lifetime": "Information"
7 | }
8 | },
9 | "AllowedHosts": "*",
10 | "ConnectionStrings": {
11 | "DefaultConnection": "Host=localhost;Port=5499;Username=admin;Password=admin;Database=Notification;"
12 | }
13 | }
--------------------------------------------------------------------------------
/src/Shared/DbInitilializer.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Microsoft.EntityFrameworkCore;
3 | using Microsoft.Extensions.DependencyInjection;
4 |
5 | namespace Shared
6 | {
7 | public static class DbInitilializer
8 | {
9 | public static void Migrate(IServiceProvider serviceProvider) where T : DbContext
10 | {
11 | using (var scope = serviceProvider.CreateScope())
12 | {
13 | var context = scope.ServiceProvider.GetRequiredService();
14 | context.Database.Migrate();
15 | }
16 | }
17 | }
18 | }
--------------------------------------------------------------------------------
/src/Shared/Kafka/Consumer/BackGroundKafkaConsumer.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading;
3 | using System.Threading.Tasks;
4 | using Confluent.Kafka;
5 | using Microsoft.Extensions.DependencyInjection;
6 | using Microsoft.Extensions.Hosting;
7 | using Microsoft.Extensions.Options;
8 |
9 | namespace Shared.Kafka.Consumer
10 | {
11 | public class BackGroundKafkaConsumer : BackgroundService
12 | {
13 | private readonly KafkaConsumerConfig _config;
14 | private readonly IServiceScopeFactory _serviceScopeFactory;
15 |
16 | public BackGroundKafkaConsumer(IOptions> config,
17 | IServiceScopeFactory serviceScopeFactory)
18 | {
19 | _serviceScopeFactory = serviceScopeFactory;
20 | _config = config.Value;
21 | }
22 |
23 | protected override Task ExecuteAsync(CancellationToken stoppingToken)
24 | {
25 | return Task.Factory.StartNew(() =>
26 | ConsumeTopic(stoppingToken),
27 | stoppingToken,
28 | TaskCreationOptions.LongRunning,
29 | TaskScheduler.Current);
30 | }
31 |
32 | private async Task ConsumeTopic(CancellationToken stoppingToken)
33 | {
34 | using (var scope = _serviceScopeFactory.CreateScope())
35 | {
36 | var handler = scope.ServiceProvider.GetRequiredService>();
37 |
38 | var builder = new ConsumerBuilder(_config).SetValueDeserializer(new KafkaDeserializer());
39 |
40 | using (IConsumer consumer = builder.Build())
41 | {
42 | consumer.Subscribe(_config.Topic);
43 |
44 | while (stoppingToken.IsCancellationRequested == false)
45 | {
46 | try
47 | {
48 | var result = consumer.Consume(3000);
49 |
50 | if (result != null)
51 | {
52 | await handler.HandleAsync(result.Message.Key, result.Message.Value);
53 |
54 | consumer.Commit(result);
55 | }
56 | }
57 | catch (Exception ex)
58 | {
59 | Console.Write(ex);
60 | }
61 | }
62 | }
63 | }
64 | }
65 | }
66 | }
--------------------------------------------------------------------------------
/src/Shared/Kafka/Consumer/IKafkaHandler.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 |
3 | namespace Shared.Kafka.Consumer
4 | {
5 | public interface IKafkaHandler
6 | {
7 | Task HandleAsync(Tk key, Tv value);
8 | }
9 | }
--------------------------------------------------------------------------------
/src/Shared/Kafka/Consumer/KafkaConsumerConfig.cs:
--------------------------------------------------------------------------------
1 | using Confluent.Kafka;
2 |
3 | namespace Shared.Kafka.Consumer
4 | {
5 | public class KafkaConsumerConfig : ConsumerConfig
6 | {
7 | public string Topic { get; set; }
8 | public KafkaConsumerConfig()
9 | {
10 | AutoOffsetReset = Confluent.Kafka.AutoOffsetReset.Earliest;
11 | EnableAutoOffsetStore = false;
12 | }
13 | }
14 | }
--------------------------------------------------------------------------------
/src/Shared/Kafka/Consumer/KafkaDeserializer.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Text;
3 | using Confluent.Kafka;
4 | using Newtonsoft.Json;
5 |
6 | namespace Shared.Kafka.Consumer
7 | {
8 | internal sealed class KafkaDeserializer : IDeserializer
9 | {
10 | public T Deserialize(ReadOnlySpan data, bool isNull, SerializationContext context)
11 | {
12 | var dataJsonString = Encoding.UTF8.GetString(data);
13 |
14 | // deserializing twice because of double serialization of event payload.
15 | var normalizedJsonString = JsonConvert.DeserializeObject(dataJsonString);
16 |
17 | return JsonConvert.DeserializeObject(normalizedJsonString);
18 | }
19 | }
20 | }
--------------------------------------------------------------------------------
/src/Shared/Kafka/RegisterServiceExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Shared.Kafka.Consumer;
3 | using Microsoft.Extensions.DependencyInjection;
4 |
5 | namespace Shared.Kafka
6 | {
7 | public static class RegisterServiceExtensions
8 | {
9 | public static IServiceCollection AddKafkaConsumer(this IServiceCollection services,
10 | Action> configAction) where THandler : class, IKafkaHandler
11 | {
12 | services.AddScoped, THandler>();
13 |
14 | services.AddHostedService>();
15 |
16 | services.Configure(configAction);
17 |
18 | return services;
19 | }
20 | }
21 | }
--------------------------------------------------------------------------------
/src/Shared/Shared.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net5.0
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------