├── .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 | --------------------------------------------------------------------------------