├── .devcontainer └── devcontainer.json ├── .github ├── dependabot.yml └── workflows │ └── dotnet.yml ├── .gitignore ├── .vscode ├── extensions.json ├── launch.json └── tasks.json ├── BasicWorkflowSamples ├── Activities │ └── CreateGreetingActivity.cs ├── BasicWorkflowSamples.csproj ├── Program.cs ├── Properties │ └── launchSettings.json ├── ResourcesDocker │ └── statestore.yml ├── ResourcesLocal │ └── statestore.yml ├── TimerWorkflowInput.cs ├── Workflows │ ├── ChainingWorkflow.cs │ ├── ChildWorkflows.cs │ ├── ExternalInteractionWorkflow.cs │ ├── FanOutFanInWorkflow.cs │ ├── HelloWorldWorkflow.cs │ ├── MonitorWorkflow.cs │ └── TimerWorkflow.cs ├── appsettings.Development.json ├── appsettings.json ├── basicworkflows.http ├── dapr.yaml └── dockerfile ├── LICENSE ├── OrderProcess ├── CheckoutService.Tests │ ├── CheckoutService.Tests.csproj │ ├── CheckoutWorkflowTests.cs │ └── GlobalUsings.cs ├── CheckoutService │ ├── Activities │ │ ├── CheckInventoryActivity.cs │ │ ├── NotifyActivity.cs │ │ ├── ProcessPaymentActivity.cs │ │ ├── RefundPaymentActivity.cs │ │ └── UpdateInventoryActivity.cs │ ├── CheckoutService.csproj │ ├── Controllers │ │ └── InventoryController.cs │ ├── Models.cs │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── Resources │ │ └── statestore.yml │ ├── Workflows │ │ └── CheckoutWorkflow.cs │ ├── appsettings.Development.json │ ├── appsettings.json │ └── checkout.http ├── OrderProcess.sln ├── PaymentService │ ├── PaymentService.csproj │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── Resources │ │ └── configstore.yaml │ ├── appsettings.Development.json │ ├── appsettings.json │ └── payment.http └── dapr.yaml ├── README.md ├── dapr-workflow-demos.sln ├── docker-compose.yml ├── images └── checkout_zipkin.png └── scripts └── install.sh /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/dotnet 3 | { 4 | "name": "Dapr Workflow Demo", 5 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile 6 | "image": "mcr.microsoft.com/devcontainers/dotnet:9.0", 7 | "features": { 8 | "ghcr.io/devcontainers/features/docker-in-docker:2": {} 9 | }, 10 | 11 | // Features to add to the dev container. More info: https://containers.dev/features. 12 | // "features": {}, 13 | 14 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 15 | "forwardPorts": [5063, 5064, 5065, 3500, 3501], 16 | // "portsAttributes": { 17 | // "5001": { 18 | // "protocol": "https" 19 | // } 20 | // } 21 | 22 | // Use 'postCreateCommand' to run commands after the container is created. 23 | "postCreateCommand": "bash scripts/install.sh", 24 | 25 | // Configure tool-specific properties. 26 | "customizations": { 27 | "vscode": { 28 | "extensions": [ 29 | "ms-dotnettools.csharp", 30 | "humao.rest-client", 31 | "vsls-contrib.codetour" 32 | ] 33 | } 34 | }, 35 | 36 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. 37 | "remoteUser": "root" 38 | } 39 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "nuget" 4 | directory: "/OrderProcess" 5 | schedule: 6 | interval: "weekly" 7 | day: "monday" 8 | open-pull-requests-limit: 10 9 | - package-ecosystem: "nuget" 10 | directory: "/BasicWorkflowSamples" 11 | schedule: 12 | interval: "weekly" 13 | day: "monday" 14 | open-pull-requests-limit: 10 -------------------------------------------------------------------------------- /.github/workflows/dotnet.yml: -------------------------------------------------------------------------------- 1 | name: .NET Build and Test 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | 17 | - name: Setup .NET 18 | uses: actions/setup-dotnet@v1 19 | with: 20 | dotnet-version: '8.0.x' 21 | 22 | - name: Restore dependencies 23 | run: dotnet restore ./OrderProcess 24 | 25 | - name: Build 26 | run: dotnet build --no-restore ./OrderProcess 27 | 28 | - name: Test 29 | run: dotnet test --no-build --verbosity normal ./OrderProcess -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Ll]og/ 33 | [Ll]ogs/ 34 | 35 | # Visual Studio 2015/2017 cache/options directory 36 | .vs/ 37 | # Uncomment if you have tasks that create the project's static files in wwwroot 38 | #wwwroot/ 39 | 40 | # Visual Studio 2017 auto generated files 41 | Generated\ Files/ 42 | 43 | # MSTest test Results 44 | [Tt]est[Rr]esult*/ 45 | [Bb]uild[Ll]og.* 46 | 47 | # NUnit 48 | *.VisualState.xml 49 | TestResult.xml 50 | nunit-*.xml 51 | 52 | # Build Results of an ATL Project 53 | [Dd]ebugPS/ 54 | [Rr]eleasePS/ 55 | dlldata.c 56 | 57 | # Benchmark Results 58 | BenchmarkDotNet.Artifacts/ 59 | 60 | # .NET Core 61 | project.lock.json 62 | project.fragment.lock.json 63 | artifacts/ 64 | 65 | # ASP.NET Scaffolding 66 | ScaffoldingReadMe.txt 67 | 68 | # StyleCop 69 | StyleCopReport.xml 70 | 71 | # Files built by Visual Studio 72 | *_i.c 73 | *_p.c 74 | *_h.h 75 | *.ilk 76 | *.meta 77 | *.obj 78 | *.iobj 79 | *.pch 80 | *.pdb 81 | *.ipdb 82 | *.pgc 83 | *.pgd 84 | *.rsp 85 | *.sbr 86 | *.tlb 87 | *.tli 88 | *.tlh 89 | *.tmp 90 | *.tmp_proj 91 | *_wpftmp.csproj 92 | *.log 93 | *.tlog 94 | *.vspscc 95 | *.vssscc 96 | .builds 97 | *.pidb 98 | *.svclog 99 | *.scc 100 | 101 | # Chutzpah Test files 102 | _Chutzpah* 103 | 104 | # Visual C++ cache files 105 | ipch/ 106 | *.aps 107 | *.ncb 108 | *.opendb 109 | *.opensdf 110 | *.sdf 111 | *.cachefile 112 | *.VC.db 113 | *.VC.VC.opendb 114 | 115 | # Visual Studio profiler 116 | *.psess 117 | *.vsp 118 | *.vspx 119 | *.sap 120 | 121 | # Visual Studio Trace Files 122 | *.e2e 123 | 124 | # TFS 2012 Local Workspace 125 | $tf/ 126 | 127 | # Guidance Automation Toolkit 128 | *.gpState 129 | 130 | # ReSharper is a .NET coding add-in 131 | _ReSharper*/ 132 | *.[Rr]e[Ss]harper 133 | *.DotSettings.user 134 | 135 | # TeamCity is a build add-in 136 | _TeamCity* 137 | 138 | # DotCover is a Code Coverage Tool 139 | *.dotCover 140 | 141 | # AxoCover is a Code Coverage Tool 142 | .axoCover/* 143 | !.axoCover/settings.json 144 | 145 | # Coverlet is a free, cross platform Code Coverage Tool 146 | coverage*.json 147 | coverage*.xml 148 | coverage*.info 149 | 150 | # Visual Studio code coverage results 151 | *.coverage 152 | *.coveragexml 153 | 154 | # NCrunch 155 | _NCrunch_* 156 | .*crunch*.local.xml 157 | nCrunchTemp_* 158 | 159 | # MightyMoose 160 | *.mm.* 161 | AutoTest.Net/ 162 | 163 | # Web workbench (sass) 164 | .sass-cache/ 165 | 166 | # Installshield output folder 167 | [Ee]xpress/ 168 | 169 | # DocProject is a documentation generator add-in 170 | DocProject/buildhelp/ 171 | DocProject/Help/*.HxT 172 | DocProject/Help/*.HxC 173 | DocProject/Help/*.hhc 174 | DocProject/Help/*.hhk 175 | DocProject/Help/*.hhp 176 | DocProject/Help/Html2 177 | DocProject/Help/html 178 | 179 | # Click-Once directory 180 | publish/ 181 | 182 | # Publish Web Output 183 | *.[Pp]ublish.xml 184 | *.azurePubxml 185 | # Note: Comment the next line if you want to checkin your web deploy settings, 186 | # but database connection strings (with potential passwords) will be unencrypted 187 | *.pubxml 188 | *.publishproj 189 | 190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 191 | # checkin your Azure Web App publish settings, but sensitive information contained 192 | # in these scripts will be unencrypted 193 | PublishScripts/ 194 | 195 | # NuGet Packages 196 | *.nupkg 197 | # NuGet Symbol Packages 198 | *.snupkg 199 | # The packages folder can be ignored because of Package Restore 200 | **/[Pp]ackages/* 201 | # except build/, which is used as an MSBuild target. 202 | !**/[Pp]ackages/build/ 203 | # Uncomment if necessary however generally it will be regenerated when needed 204 | #!**/[Pp]ackages/repositories.config 205 | # NuGet v3's project.json files produces more ignorable files 206 | *.nuget.props 207 | *.nuget.targets 208 | 209 | # Microsoft Azure Build Output 210 | csx/ 211 | *.build.csdef 212 | 213 | # Microsoft Azure Emulator 214 | ecf/ 215 | rcf/ 216 | 217 | # Windows Store app package directories and files 218 | AppPackages/ 219 | BundleArtifacts/ 220 | Package.StoreAssociation.xml 221 | _pkginfo.txt 222 | *.appx 223 | *.appxbundle 224 | *.appxupload 225 | 226 | # Visual Studio cache files 227 | # files ending in .cache can be ignored 228 | *.[Cc]ache 229 | # but keep track of directories ending in .cache 230 | !?*.[Cc]ache/ 231 | 232 | # Others 233 | ClientBin/ 234 | ~$* 235 | *~ 236 | *.dbmdl 237 | *.dbproj.schemaview 238 | *.jfm 239 | *.pfx 240 | *.publishsettings 241 | orleans.codegen.cs 242 | 243 | # Including strong name files can present a security risk 244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 245 | #*.snk 246 | 247 | # Since there are multiple workflows, uncomment next line to ignore bower_components 248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 249 | #bower_components/ 250 | 251 | # RIA/Silverlight projects 252 | Generated_Code/ 253 | 254 | # Backup & report files from converting an old project file 255 | # to a newer Visual Studio version. Backup files are not needed, 256 | # because we have git ;-) 257 | _UpgradeReport_Files/ 258 | Backup*/ 259 | UpgradeLog*.XML 260 | UpgradeLog*.htm 261 | ServiceFabricBackup/ 262 | *.rptproj.bak 263 | 264 | # SQL Server files 265 | *.mdf 266 | *.ldf 267 | *.ndf 268 | 269 | # Business Intelligence projects 270 | *.rdl.data 271 | *.bim.layout 272 | *.bim_*.settings 273 | *.rptproj.rsuser 274 | *- [Bb]ackup.rdl 275 | *- [Bb]ackup ([0-9]).rdl 276 | *- [Bb]ackup ([0-9][0-9]).rdl 277 | 278 | # Microsoft Fakes 279 | FakesAssemblies/ 280 | 281 | # GhostDoc plugin setting file 282 | *.GhostDoc.xml 283 | 284 | # Node.js Tools for Visual Studio 285 | .ntvs_analysis.dat 286 | node_modules/ 287 | 288 | # Visual Studio 6 build log 289 | *.plg 290 | 291 | # Visual Studio 6 workspace options file 292 | *.opt 293 | 294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 295 | *.vbw 296 | 297 | # Visual Studio 6 auto-generated project file (contains which files were open etc.) 298 | *.vbp 299 | 300 | # Visual Studio 6 workspace and project file (working project files containing files to include in project) 301 | *.dsw 302 | *.dsp 303 | 304 | # Visual Studio 6 technical files 305 | *.ncb 306 | *.aps 307 | 308 | # Visual Studio LightSwitch build output 309 | **/*.HTMLClient/GeneratedArtifacts 310 | **/*.DesktopClient/GeneratedArtifacts 311 | **/*.DesktopClient/ModelManifest.xml 312 | **/*.Server/GeneratedArtifacts 313 | **/*.Server/ModelManifest.xml 314 | _Pvt_Extensions 315 | 316 | # Paket dependency manager 317 | .paket/paket.exe 318 | paket-files/ 319 | 320 | # FAKE - F# Make 321 | .fake/ 322 | 323 | # CodeRush personal settings 324 | .cr/personal 325 | 326 | # Python Tools for Visual Studio (PTVS) 327 | __pycache__/ 328 | *.pyc 329 | 330 | # Cake - Uncomment if you are using it 331 | # tools/** 332 | # !tools/packages.config 333 | 334 | # Tabs Studio 335 | *.tss 336 | 337 | # Telerik's JustMock configuration file 338 | *.jmconfig 339 | 340 | # BizTalk build output 341 | *.btp.cs 342 | *.btm.cs 343 | *.odx.cs 344 | *.xsd.cs 345 | 346 | # OpenCover UI analysis results 347 | OpenCover/ 348 | 349 | # Azure Stream Analytics local run output 350 | ASALocalRun/ 351 | 352 | # MSBuild Binary and Structured Log 353 | *.binlog 354 | 355 | # NVidia Nsight GPU debugger configuration file 356 | *.nvuser 357 | 358 | # MFractors (Xamarin productivity tool) working folder 359 | .mfractor/ 360 | 361 | # Local History for Visual Studio 362 | .localhistory/ 363 | 364 | # Visual Studio History (VSHistory) files 365 | .vshistory/ 366 | 367 | # BeatPulse healthcheck temp database 368 | healthchecksdb 369 | 370 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 371 | MigrationBackup/ 372 | 373 | # Ionide (cross platform F# VS Code tools) working folder 374 | .ionide/ 375 | 376 | # Fody - auto-generated XML schema 377 | FodyWeavers.xsd 378 | 379 | # VS Code files for those working on multiple tools 380 | .vscode/* 381 | !.vscode/settings.json 382 | !.vscode/tasks.json 383 | !.vscode/launch.json 384 | !.vscode/extensions.json 385 | *.code-workspace 386 | 387 | # Local History for Visual Studio Code 388 | .history/ 389 | 390 | # Windows Installer files from build outputs 391 | *.cab 392 | *.msi 393 | *.msix 394 | *.msm 395 | *.msp 396 | 397 | # JetBrains Rider 398 | *.sln.iml 399 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. 3 | // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp 4 | 5 | // List of extensions which should be recommended for users of this workspace. 6 | "recommendations": [ 7 | "ms-dotnettools.csharp", 8 | "humao.rest-client", 9 | "vsls-contrib.codetour" 10 | ], 11 | // List of extensions recommended by VS Code that should not be recommended for users of this workspace. 12 | "unwantedRecommendations": [ 13 | ] 14 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Basic Workflows", 6 | "type": "coreclr", 7 | "request": "launch", 8 | "preLaunchTask": "basic-workflows debug", 9 | "postDebugTask": "basic-workflows daprd-down", 10 | "program": "${workspaceFolder}/BasicWorkflowSamples/bin/Debug/net7.0/BasicWorkflowSamples.dll", 11 | "args": [], 12 | "cwd": "${workspaceFolder}/BasicWorkflowSamples", 13 | "stopAtEntry": false, 14 | "serverReadyAction": { 15 | "action": "openExternally", 16 | "pattern": "\\bNow listening on:\\s+(https?://\\S+)" 17 | }, 18 | "env": { 19 | "ASPNETCORE_ENVIRONMENT": "Development", 20 | "DAPR_HTTP_PORT": "3500", 21 | "DAPR_GRPC_PORT": "50000" 22 | }, 23 | "sourceFileMap": { 24 | "/Views": "${workspaceFolder}/Views" 25 | }, 26 | }, 27 | { 28 | "name": "Checkout", 29 | "type": "coreclr", 30 | "request": "launch", 31 | "preLaunchTask": "checkout debug", 32 | "postDebugTask": "checkout daprd-down", 33 | "program": "${workspaceFolder}/CheckoutService/bin/Debug/net7.0/CheckoutService.dll", 34 | "args": [], 35 | "cwd": "${workspaceFolder}/CheckoutService", 36 | "stopAtEntry": false, 37 | "serverReadyAction": { 38 | "action": "openExternally", 39 | "pattern": "\\bNow listening on:\\s+(https?://\\S+)" 40 | }, 41 | "env": { 42 | "ASPNETCORE_ENVIRONMENT": "Development", 43 | "DAPR_HTTP_PORT": "3500", 44 | "DAPR_GRPC_PORT": "50000" 45 | }, 46 | "sourceFileMap": { 47 | "/Views": "${workspaceFolder}/Views" 48 | }, 49 | }, 50 | { 51 | "name": "Payment", 52 | "type": "coreclr", 53 | "request": "launch", 54 | "preLaunchTask": "payment debug", 55 | "postDebugTask": "payment daprd-down", 56 | "program": "${workspaceFolder}/PaymentService/bin/Debug/net7.0/PaymentService.dll", 57 | "args": [], 58 | "cwd": "${workspaceFolder}/CheckoutService", 59 | "stopAtEntry": false, 60 | "serverReadyAction": { 61 | "action": "openExternally", 62 | "pattern": "\\bNow listening on:\\s+(https?://\\S+)" 63 | }, 64 | "env": { 65 | "ASPNETCORE_ENVIRONMENT": "Development", 66 | "DAPR_HTTP_PORT": "3501", 67 | "DAPR_GRPC_PORT": "50001" 68 | }, 69 | "sourceFileMap": { 70 | "/Views": "${workspaceFolder}/Views" 71 | }, 72 | } 73 | ] 74 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "BasicWorkflowSamples Build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/BasicWorkflowSamples/BasicWorkflowSamples.csproj", 11 | "/property:GenerateFullPaths=true", 12 | "/consoleloggerparameters:NoSummary" 13 | ], 14 | "problemMatcher": "$msCompile" 15 | }, 16 | { 17 | "label": "BasicWorkflowSamples Publish", 18 | "command": "dotnet", 19 | "type": "process", 20 | "args": [ 21 | "publish", 22 | "${workspaceFolder}/BasicWorkflowSamples/BasicWorkflowSamples.csproj", 23 | "/property:GenerateFullPaths=true", 24 | "/consoleloggerparameters:NoSummary" 25 | ], 26 | "problemMatcher": "$msCompile" 27 | }, 28 | { 29 | "label": "BasicWorkflowSamples Watch", 30 | "command": "dotnet", 31 | "type": "process", 32 | "args": [ 33 | "watch", 34 | "run", 35 | "--project", 36 | "${workspaceFolder}/BasicWorkflowSamples/BasicWorkflowSamples.csproj" 37 | ], 38 | "problemMatcher": "$msCompile" 39 | }, 40 | { 41 | "label": "CheckoutService Build", 42 | "command": "dotnet", 43 | "type": "process", 44 | "args": [ 45 | "build", 46 | "${workspaceFolder}/CheckoutService/CheckoutService.csproj", 47 | "/property:GenerateFullPaths=true", 48 | "/consoleloggerparameters:NoSummary" 49 | ], 50 | "problemMatcher": "$msCompile" 51 | }, 52 | { 53 | "label": "CheckoutService Publish", 54 | "command": "dotnet", 55 | "type": "process", 56 | "args": [ 57 | "publish", 58 | "${workspaceFolder}/CheckoutService/CheckoutService.csproj", 59 | "/property:GenerateFullPaths=true", 60 | "/consoleloggerparameters:NoSummary" 61 | ], 62 | "problemMatcher": "$msCompile" 63 | }, 64 | { 65 | "label": "CheckoutService Watch", 66 | "command": "dotnet", 67 | "type": "process", 68 | "args": [ 69 | "watch", 70 | "run", 71 | "--project", 72 | "${workspaceFolder}/CheckoutService/CheckoutService.csproj" 73 | ], 74 | "problemMatcher": "$msCompile" 75 | }, 76 | { 77 | "label": "PaymentService Build", 78 | "command": "dotnet", 79 | "type": "process", 80 | "args": [ 81 | "build", 82 | "${workspaceFolder}/PaymentService/PaymentService.csproj", 83 | "/property:GenerateFullPaths=true", 84 | "/consoleloggerparameters:NoSummary" 85 | ], 86 | "problemMatcher": "$msCompile" 87 | }, 88 | { 89 | "label": "PaymentService Publish", 90 | "command": "dotnet", 91 | "type": "process", 92 | "args": [ 93 | "publish", 94 | "${workspaceFolder}/PaymentService/PaymentService.csproj", 95 | "/property:GenerateFullPaths=true", 96 | "/consoleloggerparameters:NoSummary" 97 | ], 98 | "problemMatcher": "$msCompile" 99 | }, 100 | { 101 | "label": "PaymentService Watch", 102 | "command": "dotnet", 103 | "type": "process", 104 | "args": [ 105 | "watch", 106 | "run", 107 | "--project", 108 | "${workspaceFolder}/PaymentService/PaymentService.csproj" 109 | ], 110 | "problemMatcher": "$msCompile" 111 | }, 112 | { 113 | "label": "basic-workflows debug", 114 | "appId": "basic-workflows", 115 | "appPort": 5065, 116 | "httpPort": 3500, 117 | "grpcPort": 50000, 118 | "type": "dapr", 119 | "resourcesPath" : "${workspaceFolder}/BasicWorkflowSamples/ResourcesLocal", 120 | "dependsOn": "BasicWorkflowSamples Build", 121 | }, 122 | { 123 | "label": "basic-workflows daprd-down", 124 | "appId": "basic-workflows", 125 | "type": "daprd-down" 126 | }, 127 | { 128 | "label": "checkout debug", 129 | "appId": "checkout", 130 | "appPort": 5064, 131 | "httpPort": 3500, 132 | "grpcPort": 50000, 133 | "resourcesPath" : "./ResourcesLocal", 134 | "type": "dapr", 135 | "dependsOn": "CheckoutService Build" 136 | }, 137 | { 138 | "label": "checkout daprd-down", 139 | "appId": "checkout", 140 | "type": "daprd-down" 141 | }, 142 | { 143 | "label": "payment debug", 144 | "appId": "payment", 145 | "appPort": 5063, 146 | "httpPort": 3501, 147 | "grpcPort": 50001, 148 | "resourcesPath" : "/PaymentService/Resources", 149 | "type": "dapr", 150 | "dependsOn": "PaymentService Build" 151 | }, 152 | { 153 | "label": "payment daprd-down", 154 | "appId": "payment", 155 | "type": "daprd-down" 156 | } 157 | ] 158 | } -------------------------------------------------------------------------------- /BasicWorkflowSamples/Activities/CreateGreetingActivity.cs: -------------------------------------------------------------------------------- 1 | using Dapr.Workflow; 2 | 3 | namespace BasicWorkflowSamples 4 | { 5 | public class CreateGreetingActivity : WorkflowActivity 6 | { 7 | public override Task RunAsync(WorkflowActivityContext context, string name) 8 | { 9 | Console.WriteLine($"{nameof(CreateGreetingActivity)} with input: {name}"); 10 | 11 | var greetings = new []{"Hello", "Hi", "Hey", "Hola", "Bonjour", "Ciao", "Guten Tag", "Konnichiwa"}; 12 | var selectedGreeting = greetings[new Random().Next(0, greetings.Length)]; 13 | var message = $"{selectedGreeting} {name}"; 14 | 15 | return Task.FromResult(message); 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /BasicWorkflowSamples/BasicWorkflowSamples.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | WorkflowSample 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /BasicWorkflowSamples/Program.cs: -------------------------------------------------------------------------------- 1 | using Dapr.Workflow; 2 | using BasicWorkflowSamples; 3 | 4 | var builder = WebApplication.CreateBuilder(args); 5 | 6 | builder.Services.AddDaprWorkflow(options => 7 | { 8 | // Note that it's also possible to register a lambda function as the workflow 9 | // or activity implementation instead of a class. 10 | options.RegisterWorkflow(); 11 | options.RegisterWorkflow(); 12 | options.RegisterWorkflow(); 13 | options.RegisterWorkflow(); 14 | options.RegisterWorkflow(); 15 | options.RegisterWorkflow(); 16 | options.RegisterWorkflow(); 17 | 18 | // These are the activities that get invoked by the workflow(s). 19 | options.RegisterActivity(); 20 | }); 21 | 22 | // Dapr uses a random port for gRPC by default. If we don't know what that port 23 | // is (because this app was started separate from dapr), then assume 50001. 24 | if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("DAPR_GRPC_PORT"))) 25 | { 26 | Environment.SetEnvironmentVariable("DAPR_GRPC_PORT", "50001"); 27 | } 28 | 29 | var app = builder.Build(); 30 | 31 | app.Run(); 32 | -------------------------------------------------------------------------------- /BasicWorkflowSamples/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:46077", 8 | "sslPort": 44311 9 | } 10 | }, 11 | "profiles": { 12 | "http": { 13 | "commandName": "Project", 14 | "dotnetRunMessages": true, 15 | "launchBrowser": true, 16 | "launchUrl": "swagger", 17 | "applicationUrl": "http://localhost:5065", 18 | "environmentVariables": { 19 | "ASPNETCORE_ENVIRONMENT": "Development" 20 | } 21 | }, 22 | "https": { 23 | "commandName": "Project", 24 | "dotnetRunMessages": true, 25 | "launchBrowser": false, 26 | "launchUrl": "swagger", 27 | "applicationUrl": "https://localhost:7111;http://localhost:5065", 28 | "environmentVariables": { 29 | "ASPNETCORE_ENVIRONMENT": "Development" 30 | } 31 | }, 32 | "IIS Express": { 33 | "commandName": "IISExpress", 34 | "launchBrowser": false, 35 | "launchUrl": "swagger", 36 | "environmentVariables": { 37 | "ASPNETCORE_ENVIRONMENT": "Development" 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /BasicWorkflowSamples/ResourcesDocker/statestore.yml: -------------------------------------------------------------------------------- 1 | apiVersion: dapr.io/v1alpha1 2 | kind: Component 3 | metadata: 4 | name: workflowstore 5 | spec: 6 | type: state.redis 7 | version: v1 8 | metadata: 9 | - name: redisHost 10 | value: redis:6379 11 | - name: redisPassword 12 | value: "" 13 | - name: actorStateStore 14 | value: "true" -------------------------------------------------------------------------------- /BasicWorkflowSamples/ResourcesLocal/statestore.yml: -------------------------------------------------------------------------------- 1 | apiVersion: dapr.io/v1alpha1 2 | kind: Component 3 | metadata: 4 | name: workflowstore 5 | spec: 6 | type: state.redis 7 | version: v1 8 | metadata: 9 | - name: redisHost 10 | value: localhost:6379 11 | - name: redisPassword 12 | value: "" 13 | - name: actorStateStore 14 | value: "true" -------------------------------------------------------------------------------- /BasicWorkflowSamples/TimerWorkflowInput.cs: -------------------------------------------------------------------------------- 1 | namespace BasicWorkflowSamples 2 | { 3 | public record TimerWorkflowInput(DateTime DateTime, string Name); 4 | } -------------------------------------------------------------------------------- /BasicWorkflowSamples/Workflows/ChainingWorkflow.cs: -------------------------------------------------------------------------------- 1 | using Dapr.Workflow; 2 | 3 | namespace BasicWorkflowSamples 4 | { 5 | public class ChainingWorkflow : Workflow 6 | { 7 | public override async Task RunAsync(WorkflowContext context, string input) 8 | { 9 | 10 | // if (context.IsReplaying) 11 | // { 12 | // Console.WriteLine("Replay!"); 13 | // } else { 14 | // Console.WriteLine("First execution!"); 15 | // } 16 | 17 | // Console.WriteLine("Activity 1"); 18 | var message1 = await context.CallActivityAsync( 19 | nameof(CreateGreetingActivity), 20 | input); 21 | 22 | // Console.WriteLine("Activity 2"); 23 | var message2 = await context.CallActivityAsync( 24 | nameof(CreateGreetingActivity), 25 | message1); 26 | 27 | // Console.WriteLine("Activity 3"); 28 | var message3 = await context.CallActivityAsync( 29 | nameof(CreateGreetingActivity), 30 | message2); 31 | 32 | // Console.WriteLine("Finally done!"); 33 | return message3; 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /BasicWorkflowSamples/Workflows/ChildWorkflows.cs: -------------------------------------------------------------------------------- 1 | using Dapr.Workflow; 2 | 3 | namespace BasicWorkflowSamples 4 | { 5 | public class ChildWorkflows : Workflow 6 | { 7 | public override async Task RunAsync(WorkflowContext context, string input) 8 | { 9 | var helloWorld = await context.CallChildWorkflowAsync( 10 | nameof(HelloWorldWorkflow), 11 | input); 12 | 13 | var chaining = await context.CallChildWorkflowAsync( 14 | nameof(ChainingWorkflow), 15 | helloWorld); 16 | 17 | var monitor = await context.CallChildWorkflowAsync( 18 | nameof(MonitorWorkflow), 19 | 0); 20 | 21 | var fanOutFanIn = await context.CallChildWorkflowAsync( 22 | nameof(FanOutFanInWorkflow), 23 | new [] { helloWorld, chaining, monitor }); 24 | 25 | return fanOutFanIn; 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /BasicWorkflowSamples/Workflows/ExternalInteractionWorkflow.cs: -------------------------------------------------------------------------------- 1 | using Dapr.Workflow; 2 | 3 | namespace BasicWorkflowSamples 4 | { 5 | public class ExternalInteractionWorkflow : Workflow 6 | { 7 | public override async Task RunAsync(WorkflowContext context, string input) 8 | { 9 | var message = string.Empty; 10 | try 11 | { 12 | var timeOut = TimeSpan.FromSeconds(20); 13 | var approvalEvent = await context.WaitForExternalEventAsync( 14 | "approval-event", 15 | timeOut); 16 | 17 | if (approvalEvent.IsApproved) 18 | { 19 | message = await context.CallActivityAsync( 20 | nameof(CreateGreetingActivity), 21 | input); 22 | } 23 | } 24 | catch (TaskCanceledException) 25 | { 26 | context.SetCustomStatus("Wait for external event is cancelled due to timeout."); 27 | } 28 | 29 | return message; 30 | } 31 | } 32 | 33 | public record ApprovalEvent(bool IsApproved); 34 | } -------------------------------------------------------------------------------- /BasicWorkflowSamples/Workflows/FanOutFanInWorkflow.cs: -------------------------------------------------------------------------------- 1 | using Dapr.Workflow; 2 | 3 | namespace BasicWorkflowSamples 4 | { 5 | public class FanOutFanInWorkflow : Workflow 6 | { 7 | public override async Task RunAsync(WorkflowContext context, string[] input) 8 | { 9 | var tasks = new List>(); 10 | 11 | foreach (var name in input) 12 | { 13 | tasks.Add(context.CallActivityAsync( 14 | nameof(CreateGreetingActivity), 15 | name)); 16 | } 17 | 18 | var messages = await Task.WhenAll(tasks); 19 | 20 | return messages; 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /BasicWorkflowSamples/Workflows/HelloWorldWorkflow.cs: -------------------------------------------------------------------------------- 1 | using Dapr.Workflow; 2 | 3 | namespace BasicWorkflowSamples 4 | { 5 | public class HelloWorldWorkflow : Workflow 6 | { 7 | public override async Task RunAsync(WorkflowContext context, string input) 8 | { 9 | var message = await context.CallActivityAsync( 10 | nameof(CreateGreetingActivity), 11 | input); 12 | 13 | return message; 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /BasicWorkflowSamples/Workflows/MonitorWorkflow.cs: -------------------------------------------------------------------------------- 1 | using Dapr.Workflow; 2 | 3 | namespace BasicWorkflowSamples 4 | { 5 | public class MonitorWorkflow : Workflow 6 | { 7 | public override async Task RunAsync(WorkflowContext context, int counter) 8 | { 9 | var message = await context.CallActivityAsync( 10 | nameof(CreateGreetingActivity), 11 | counter.ToString()); 12 | 13 | if (counter < 10 ) 14 | { 15 | counter += 1; 16 | await context.CreateTimer(TimeSpan.FromSeconds(1)); 17 | context.ContinueAsNew(counter); 18 | } 19 | 20 | return message; 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /BasicWorkflowSamples/Workflows/TimerWorkflow.cs: -------------------------------------------------------------------------------- 1 | using Dapr.Workflow; 2 | 3 | namespace BasicWorkflowSamples 4 | { 5 | public class TimerWorkflow : Workflow 6 | { 7 | public override async Task RunAsync(WorkflowContext context, TimerWorkflowInput input) 8 | { 9 | if (input.DateTime.ToUniversalTime() > context.CurrentUtcDateTime) 10 | { 11 | context.SetCustomStatus($"Waiting for timer: {input.DateTime:yyyy-MM-dd HH:mm:ss}"); 12 | await context.CreateTimer(input.DateTime, default); 13 | } 14 | 15 | context.SetCustomStatus(null); 16 | 17 | var message = await context.CallActivityAsync( 18 | nameof(CreateGreetingActivity), 19 | $"{input.Name} at {input.DateTime:yyyy-MM-dd HH:mm:ss}"); 20 | 21 | return message; 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /BasicWorkflowSamples/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /BasicWorkflowSamples/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } 10 | -------------------------------------------------------------------------------- /BasicWorkflowSamples/basicworkflows.http: -------------------------------------------------------------------------------- 1 | @dapr_url=http://localhost:3500 2 | @workflow_id={{$guid}} 3 | 4 | ### 5 | ### Run Hello World workflow 6 | ### 7 | // @name wfrequest 8 | POST {{dapr_url}}/v1.0/workflows/dapr/HelloWorldWorkflow/start?instanceID={{workflow_id}} 9 | Content-Type: application/text/plain 10 | 11 | "World" 12 | 13 | 14 | ### Get status for Hello World workflow 15 | @workflow_helloworld_id_response={{wfrequest.response.body.instanceID}} 16 | GET {{dapr_url}}/v1.0/workflows/dapr/{{workflow_helloworld_id_response}} 17 | 18 | ### 19 | ### Run Chaining workflow 20 | ### 21 | // @name wfchainingrequest 22 | POST {{dapr_url}}/v1.0/workflows/dapr/ChainingWorkflow/start?instanceID={{workflow_id}} 23 | Content-Type: application/text/plain 24 | 25 | "World" 26 | 27 | ### Get status for Chaining workflow 28 | @workflow_chaining_id_response={{wfchainingrequest.response.body.instanceID}} 29 | GET {{dapr_url}}/v1.0/workflows/dapr/{{workflow_chaining_id_response}} 30 | 31 | ### 32 | ### Run FanOutFanIn workflow 33 | ### 34 | // @name wffanoutfaningrequest 35 | POST {{dapr_url}}/v1.0/workflows/dapr/FanOutFanInWorkflow/start?instanceID={{workflow_id}} 36 | Content-Type: application/json 37 | 38 | [ 39 | "Amsterdam", 40 | "Porto", 41 | "New York" 42 | ] 43 | 44 | ### Get status for FanOutFanIn workflow 45 | @workflow_fanoutfanin_id_response={{wffanoutfaningrequest.response.body.instanceID}} 46 | GET {{dapr_url}}/v1.0/workflows/dapr/{{workflow_fanoutfanin_id_response}} 47 | 48 | ### 49 | ### Run Monitor workflow 50 | ### 51 | // @name wfcontinuerequest 52 | POST {{dapr_url}}/v1.0/workflows/dapr/MonitorWorkflow/start?instanceID={{workflow_id}} 53 | Content-Type: application/text/plain 54 | 55 | 0 56 | 57 | ### Get status for Monitor workflow 58 | @workflow_continue_id_response={{wfcontinuerequest.response.body.instanceID}} 59 | GET {{dapr_url}}/v1.0/workflows/dapr/{{workflow_continue_id_response}} 60 | 61 | ### 62 | ### Run Timer workflow (use UTC time) 63 | ### 64 | // @name wftimerrequest 65 | POST {{dapr_url}}/v1.0/workflows/dapr/TimerWorkflow/start?instanceID={{workflow_id}} 66 | Content-Type: application/json 67 | 68 | { 69 | "DateTime": "2023-10-19T11:23:00+00:00", 70 | "Name": "World" 71 | } 72 | 73 | ### Get status for Timer workflow 74 | @workflow_timer_id_response={{wftimerrequest.response.body.instanceID}} 75 | GET {{dapr_url}}/v1.0/workflows/dapr/{{workflow_timer_id_response}} 76 | 77 | ### 78 | ### Run External interaction workflow 79 | ### 80 | // @name externalrequest 81 | POST {{dapr_url}}/v1.0/workflows/dapr/ExternalInteractionWorkflow/start?instanceID={{workflow_id}} 82 | Content-Type: application/text/plain 83 | 84 | "World" 85 | 86 | ### 87 | ### Raise an event 88 | ### 89 | @workflow_external_id_response={{externalrequest.response.body.instanceID}} 90 | @event_name=approval-event 91 | POST {{dapr_url}}/v1.0/workflows/dapr/{{workflow_external_id_response}}/raiseEvent/{{event_name}} 92 | Content-Type: application/json 93 | 94 | { 95 | "IsApproved" : true 96 | } 97 | 98 | ### Get status for External interaction workflow 99 | GET {{dapr_url}}/v1.0/workflows/dapr/{{workflow_external_id_response}} 100 | 101 | ### 102 | ### Run ChildWorkflows 103 | ### 104 | // @name childworkflowrequest 105 | POST {{dapr_url}}/v1.0/workflows/dapr/ChildWorkflows/start?instanceID={{workflow_id}} 106 | Content-Type: application/text/plain 107 | 108 | "World" 109 | 110 | ### Get status for ChildWorkflows 111 | @workflow_child_response={{childworkflowrequest.response.body.instanceID}} 112 | GET {{dapr_url}}/v1.0/workflows/dapr/{{workflow_child_response}} -------------------------------------------------------------------------------- /BasicWorkflowSamples/dapr.yaml: -------------------------------------------------------------------------------- 1 | version: 1 2 | common: 3 | resourcesPath: ResourcesLocal 4 | apps: 5 | - appID: basic-workflows 6 | appDirPath: . 7 | appPort: 5065 8 | daprHTTPPort: 3500 9 | command: ["dotnet", "run"] 10 | appLogDestination: console 11 | daprdLogDestination: console 12 | -------------------------------------------------------------------------------- /BasicWorkflowSamples/dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base 2 | WORKDIR /app 3 | EXPOSE 5065 4 | 5 | ENV ASPNETCORE_URLS=http://+:5065 6 | 7 | # Creates a non-root user with an explicit UID and adds permission to access the /app folder 8 | # For more info, please refer to https://aka.ms/vscode-docker-dotnet-configure-containers 9 | RUN adduser -u 5678 --disabled-password --gecos "" appuser && chown -R appuser /app 10 | USER appuser 11 | 12 | FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build 13 | WORKDIR /src 14 | COPY ["BasicWorkflowSamples/BasicWorkflowSamples.csproj", "BasicWorkflowSamples/"] 15 | RUN dotnet restore "BasicWorkflowSamples/BasicWorkflowSamples.csproj" 16 | COPY . . 17 | WORKDIR "/src/BasicWorkflowSamples" 18 | RUN dotnet build "BasicWorkflowSamples.csproj" -c Release -o /app/build 19 | 20 | FROM build AS publish 21 | RUN dotnet publish "BasicWorkflowSamples.csproj" -c Release -o /app/publish /p:UseAppHost=false 22 | 23 | FROM base AS final 24 | WORKDIR /app 25 | COPY --from=publish /app/publish . 26 | ENTRYPOINT ["dotnet", "BasicWorkflowSamples.dll"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /OrderProcess/CheckoutService.Tests/CheckoutService.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | 8 | false 9 | true 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | runtime; build; native; contentfiles; analyzers; buildtransitive 18 | all 19 | 20 | 21 | 22 | runtime; build; native; contentfiles; analyzers; buildtransitive 23 | all 24 | 25 | 26 | runtime; build; native; contentfiles; analyzers; buildtransitive 27 | all 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /OrderProcess/CheckoutService.Tests/CheckoutWorkflowTests.cs: -------------------------------------------------------------------------------- 1 | using CheckoutService.Activities; 2 | using CheckoutService.Models; 3 | using CheckoutService.Workflows; 4 | using Dapr.Workflow; 5 | using FluentAssertions; 6 | using NSubstitute; 7 | 8 | namespace CheckoutService.Tests; 9 | 10 | public class CheckoutWorkflowTests 11 | { 12 | [Fact] 13 | public async void GivenInventoryIsTooLow_WhenCallingCheckInventory_ThenWorkflowResultShouldBeFalse() 14 | { 15 | var workflowContext = GetContextWithInventoryResultIsNotProcessed(); 16 | var checkoutWorkflow = new CheckoutWorkflow(); 17 | var orderItem = new OrderItem("Test", 1); 18 | var workflowResult = await checkoutWorkflow.RunAsync(workflowContext, orderItem); 19 | workflowResult.Processed.Should().BeFalse(); 20 | } 21 | 22 | [Fact] 23 | public async void GivenInventoryIsEnough_WhenCallingCheckInventory_ThenWorkflowResultShouldBeTrue() 24 | { 25 | var workflowContext = GetContextWithInventoryResultIsProcessedAndSuccessfulPayment(); 26 | var checkoutWorkflow = new CheckoutWorkflow(); 27 | var orderItem = new OrderItem("Test", 1); 28 | var workflowResult = await checkoutWorkflow.RunAsync(workflowContext, orderItem); 29 | workflowResult.Processed.Should().BeTrue(); 30 | } 31 | 32 | [Fact] 33 | public async void GivenPaymentProcessIsUnsuccessful_WhenCallingProcessPaymentActivity_ThenWorkflowResultShouldBeFalse() 34 | { 35 | var workflowContext = GetContextWithInventoryResultIsProcessedAndUnsuccessfulPayment(); 36 | var checkoutWorkflow = new CheckoutWorkflow(); 37 | var orderItem = new OrderItem("Test", 1); 38 | var workflowResult = await checkoutWorkflow.RunAsync(workflowContext, orderItem); 39 | workflowResult.Processed.Should().BeFalse(); 40 | } 41 | 42 | private static WorkflowContext GetContextWithInventoryResultIsProcessedAndSuccessfulPayment() 43 | { 44 | var workflowContext = Substitute.For(); 45 | var inventoryItem = new InventoryItem(1, "TestItem", 100, 1); 46 | workflowContext.CallActivityAsync( 47 | Arg.Is("CheckInventoryActivity"), 48 | Arg.Any(), 49 | Arg.Any()) 50 | .Returns(Task.FromResult(new InventoryResult( 51 | InStock: true, 52 | inventoryItem, 53 | TotalCost: 100))); 54 | 55 | workflowContext.CallActivityAsync( 56 | Arg.Is(nameof(ProcessPaymentActivity)), 57 | Arg.Any(), 58 | Arg.Any()) 59 | .Returns(Task.FromResult(new PaymentResponse( 60 | "", 61 | IsPaymentSuccess: true))); 62 | 63 | return workflowContext; 64 | } 65 | 66 | private static WorkflowContext GetContextWithInventoryResultIsProcessedAndUnsuccessfulPayment() 67 | { 68 | var workflowContext = Substitute.For(); 69 | var inventoryItem = new InventoryItem(1, "TestItem", 100, 1); 70 | workflowContext.CallActivityAsync( 71 | Arg.Is("CheckInventoryActivity"), 72 | Arg.Any(), 73 | Arg.Any()) 74 | .Returns(Task.FromResult(new InventoryResult( 75 | InStock: true, 76 | inventoryItem, 77 | TotalCost: 100))); 78 | 79 | workflowContext.CallActivityAsync( 80 | Arg.Is(nameof(ProcessPaymentActivity)), 81 | Arg.Any(), 82 | Arg.Any()) 83 | .Returns(Task.FromResult(new PaymentResponse( 84 | "", 85 | IsPaymentSuccess: false))); 86 | 87 | return workflowContext; 88 | } 89 | 90 | private static WorkflowContext GetContextWithInventoryResultIsNotProcessed() 91 | { 92 | var workflowContext = Substitute.For(); 93 | var inventoryItem = new InventoryItem(1, "TestItem", 100, 1); 94 | workflowContext.CallActivityAsync( 95 | Arg.Is(nameof(CheckInventoryActivity)), 96 | Arg.Any(), 97 | Arg.Any()) 98 | .Returns(Task.FromResult(new InventoryResult( 99 | InStock: false, 100 | inventoryItem, 101 | TotalCost: 100))); 102 | 103 | return workflowContext; 104 | } 105 | } -------------------------------------------------------------------------------- /OrderProcess/CheckoutService.Tests/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using Xunit; -------------------------------------------------------------------------------- /OrderProcess/CheckoutService/Activities/CheckInventoryActivity.cs: -------------------------------------------------------------------------------- 1 | using Dapr.Client; 2 | using Dapr.Workflow; 3 | using CheckoutService.Models; 4 | 5 | namespace CheckoutService.Activities 6 | { 7 | public class CheckInventoryActivity(ILoggerFactory loggerFactory, DaprClient client) : WorkflowActivity 8 | { 9 | static readonly string storeName = "statestore"; 10 | 11 | public override async Task RunAsync(WorkflowActivityContext context, InventoryRequest req) 12 | { 13 | var logger = loggerFactory.CreateLogger(); 14 | logger.LogInformation( 15 | "Checking inventory for order '{requestId}' of {quantity} {name}", 16 | req.RequestId, 17 | req.Quantity, 18 | req.ItemName); 19 | 20 | // Ensure that the store has items 21 | var product = await client.GetStateAsync( 22 | storeName, 23 | req.ItemName.ToLowerInvariant()); 24 | 25 | // Catch for the case where the statestore isn't setup 26 | if (product == null) 27 | { 28 | // Not enough items. 29 | return new InventoryResult(false, null, 0); 30 | } 31 | 32 | logger.LogInformation( 33 | "There are {quantity} {name} available for purchase", 34 | product.Quantity, 35 | product.Name); 36 | 37 | var totalCost = product.PerItemCost * req.Quantity; 38 | // See if there're enough items to purchase 39 | if (product.Quantity >= req.Quantity) 40 | { 41 | // Simulate slow processing 42 | await Task.Delay(TimeSpan.FromSeconds(3)); 43 | 44 | return new InventoryResult(true, product, totalCost); 45 | } 46 | 47 | // Not enough items. 48 | return new InventoryResult(false, product, totalCost); 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /OrderProcess/CheckoutService/Activities/NotifyActivity.cs: -------------------------------------------------------------------------------- 1 | using CheckoutService.Models; 2 | using Dapr.Workflow; 3 | 4 | namespace CheckoutService.Activities 5 | { 6 | public class NotifyActivity(ILoggerFactory loggerFactory) : WorkflowActivity 7 | { 8 | public override Task RunAsync(WorkflowActivityContext context, Notification notification) 9 | { 10 | var logger = loggerFactory.CreateLogger(); 11 | logger.LogInformation(notification.Message); 12 | 13 | return Task.FromResult(null); 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /OrderProcess/CheckoutService/Activities/ProcessPaymentActivity.cs: -------------------------------------------------------------------------------- 1 | using CheckoutService.Models; 2 | using Dapr.Client; 3 | using Dapr.Workflow; 4 | 5 | namespace CheckoutService.Activities 6 | { 7 | public class ProcessPaymentActivity(HttpClient httpClient, ILoggerFactory loggerFactory, DaprClient client) : WorkflowActivity 8 | { 9 | public override async Task RunAsync(WorkflowActivityContext context, PaymentRequest request) 10 | { 11 | var logger = loggerFactory.CreateLogger(); 12 | logger.LogInformation( 13 | "Calling PaymentService for: {requestId} {name} at ${totalCost}", 14 | request.RequestId, 15 | request.Name, 16 | request.TotalCost); 17 | 18 | // Simulate slow processing 19 | await Task.Delay(TimeSpan.FromSeconds(3)); 20 | 21 | try 22 | { 23 | const string INVOKE_APP = "payment-service"; 24 | var httpClient = DaprClient.CreateInvokeHttpClient(appId: INVOKE_APP); 25 | var httpResponse = await httpClient.PostAsJsonAsync("/pay", request); 26 | logger.LogInformation( 27 | "Payment for request ID '{requestId}' processed successfully", 28 | request.RequestId); 29 | 30 | return new PaymentResponse(request.RequestId, true); 31 | } 32 | catch (Exception ex) 33 | { 34 | if (ex.InnerException != null && ex.InnerException.Message.Contains("500")) 35 | { 36 | // Throw internal server errors up to the workflow so 37 | // this activity can be retried. 38 | throw; 39 | } 40 | 41 | // Any other exception is treated as a failed payment. 42 | logger.LogWarning( 43 | "Payment for request ID '{requestId}' failed", 44 | request.RequestId); 45 | 46 | return new PaymentResponse(request.RequestId, false); 47 | 48 | } 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /OrderProcess/CheckoutService/Activities/RefundPaymentActivity.cs: -------------------------------------------------------------------------------- 1 | using Dapr.Client; 2 | using Dapr.Workflow; 3 | using CheckoutService.Models; 4 | 5 | namespace CheckoutService.Activities 6 | { 7 | public class RefundPaymentActivity(ILoggerFactory loggerFactory) : WorkflowActivity 8 | { 9 | 10 | public override async Task RunAsync(WorkflowActivityContext context, PaymentRequest request) 11 | { 12 | var logger = loggerFactory.CreateLogger(); 13 | logger.LogInformation( 14 | "Refunding payment: {requestId} for {name} at ${totalCost}", 15 | request.RequestId, 16 | request.Name, 17 | request.TotalCost); 18 | 19 | // Simulate slow processing 20 | await Task.Delay(TimeSpan.FromSeconds(3)); 21 | 22 | try 23 | { 24 | const string INVOKE_APP = "payment-service"; 25 | var httpClient = DaprClient.CreateInvokeHttpClient(appId: INVOKE_APP); 26 | var httpResponse = await httpClient.PostAsJsonAsync("/refund", request); 27 | if (httpResponse.IsSuccessStatusCode) 28 | { 29 | logger.LogInformation( 30 | "Refund for request ID '{requestId}' processed successfully", 31 | request.RequestId); 32 | return new PaymentResponse(request.RequestId, true); 33 | } 34 | return new PaymentResponse(request.RequestId, false); 35 | } 36 | catch (Exception) 37 | { 38 | logger.LogWarning( 39 | "Refund for request ID '{requestId}' failed", 40 | request.RequestId); 41 | return new PaymentResponse(request.RequestId, false); 42 | } 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /OrderProcess/CheckoutService/Activities/UpdateInventoryActivity.cs: -------------------------------------------------------------------------------- 1 | using Dapr.Client; 2 | using Dapr.Workflow; 3 | using CheckoutService.Models; 4 | 5 | namespace CheckoutService.Activities 6 | { 7 | class UpdateInventoryActivity(ILoggerFactory loggerFactory, DaprClient client) : WorkflowActivity 8 | { 9 | static readonly string storeName = "statestore"; 10 | 11 | public override async Task RunAsync(WorkflowActivityContext context, InventoryRequest req) 12 | { 13 | var logger = loggerFactory.CreateLogger(); 14 | logger.LogInformation( 15 | "Re-checking inventory for order '{requestId}' for {quantity} {itemName}", 16 | req.RequestId, 17 | req.Quantity, 18 | req.ItemName); 19 | 20 | // Simulate slow processing 21 | await Task.Delay(TimeSpan.FromSeconds(5)); 22 | 23 | // Determine if there are enough Items for purchase 24 | var product = await client.GetStateAsync( 25 | storeName, 26 | req.ItemName.ToLowerInvariant()); 27 | int newQuantity = product.Quantity - req.Quantity; 28 | if (newQuantity < 0) 29 | { 30 | logger.LogInformation( 31 | "Inventory update for request ID '{requestId}' could not be processed. Insufficient inventory.", 32 | req.RequestId); 33 | throw new InvalidOperationException("Insufficient inventory."); 34 | } 35 | 36 | // Update the statestore with the new amount of the item 37 | await client.SaveStateAsync( 38 | storeName, 39 | req.ItemName.ToLowerInvariant(), 40 | new InventoryItem(ProductId: product.ProductId, Name: req.ItemName, PerItemCost: product.PerItemCost, Quantity: newQuantity)); 41 | 42 | logger.LogInformation( 43 | "There are now {quantity} {name} left in stock", 44 | newQuantity, 45 | product.Name); 46 | 47 | return null; 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /OrderProcess/CheckoutService/CheckoutService.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | WorkflowSample 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /OrderProcess/CheckoutService/Controllers/InventoryController.cs: -------------------------------------------------------------------------------- 1 | using Dapr.Client; 2 | using Microsoft.AspNetCore.Mvc; 3 | using CheckoutService.Models; 4 | 5 | namespace CheckoutService.Controllers; 6 | 7 | [ApiController] 8 | [Route("[controller]")] 9 | public class InventoryController(ILogger logger, DaprClient client) : ControllerBase 10 | { 11 | private static readonly string storeName = "statestore"; 12 | private readonly static string[] itemKeys = new [] {"Paperclips", "Cars", "Computers"}; 13 | 14 | [HttpGet] 15 | public async Task GetInventory() 16 | { 17 | var inventory = new List(); 18 | 19 | foreach (var itemKey in itemKeys) 20 | { 21 | var item = await client.GetStateAsync(storeName, itemKey.ToLowerInvariant()); 22 | inventory.Add(item); 23 | } 24 | 25 | return new OkObjectResult(inventory); 26 | } 27 | 28 | [HttpPost("restock")] 29 | public async void RestockInventory() 30 | { 31 | var baseInventory = new List 32 | { 33 | new InventoryItem(ProductId: 1, Name: itemKeys[0], PerItemCost: 5, Quantity: 100), 34 | new InventoryItem(ProductId: 2, Name: itemKeys[1], PerItemCost: 15000, Quantity: 100), 35 | new InventoryItem(ProductId: 3, Name: itemKeys[2], PerItemCost: 500, Quantity: 100), 36 | }; 37 | 38 | foreach (var item in baseInventory) 39 | { 40 | await client.SaveStateAsync(storeName, item.Name.ToLowerInvariant(), item); 41 | } 42 | 43 | logger.LogInformation("Inventory Restocked!"); 44 | } 45 | 46 | [HttpDelete] 47 | public async void ClearInventory() 48 | { 49 | var baseInventory = new List 50 | { 51 | new InventoryItem(ProductId: 1, Name: itemKeys[0], PerItemCost: 5, Quantity: 0), 52 | new InventoryItem(ProductId: 2, Name: itemKeys[1], PerItemCost: 15000, Quantity: 0), 53 | new InventoryItem(ProductId: 3, Name: itemKeys[2], PerItemCost: 500, Quantity: 0), 54 | }; 55 | 56 | foreach (var item in baseInventory) 57 | { 58 | await client.SaveStateAsync(storeName, item.Name.ToLowerInvariant(), item); 59 | } 60 | 61 | logger.LogInformation("Cleared inventory!"); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /OrderProcess/CheckoutService/Models.cs: -------------------------------------------------------------------------------- 1 | namespace CheckoutService.Models 2 | { 3 | public record OrderItem(string Name, int Quantity = 1); 4 | public record CheckoutResult(bool Processed); 5 | public record InventoryItem(int ProductId, string Name, double PerItemCost, int Quantity); 6 | public record InventoryRequest(string RequestId, string ItemName, int Quantity); 7 | public record InventoryResult(bool InStock, InventoryItem? OrderPayload, double TotalCost); 8 | public record PaymentRequest(string RequestId, string Name, double TotalCost); 9 | public record PaymentResponse(string RequestId, bool IsPaymentSuccess); 10 | public record Notification(string Message); 11 | } -------------------------------------------------------------------------------- /OrderProcess/CheckoutService/Program.cs: -------------------------------------------------------------------------------- 1 | using Dapr.Client; 2 | using Dapr.Workflow; 3 | using CheckoutService.Activities; 4 | using CheckoutService.Workflows; 5 | 6 | var builder = WebApplication.CreateBuilder(args); 7 | 8 | builder.Services.AddControllers(); 9 | builder.Services.AddDaprWorkflow(options => 10 | { 11 | // Note that it's also possible to register a lambda function as the workflow 12 | // or activity implementation instead of a class. 13 | options.RegisterWorkflow(); 14 | 15 | // These are the activities that get invoked by the workflow(s). 16 | options.RegisterActivity(); 17 | options.RegisterActivity(); 18 | options.RegisterActivity(); 19 | options.RegisterActivity(); 20 | options.RegisterActivity(); 21 | }); 22 | 23 | // Dapr uses a random port for gRPC by default. If we don't know what that port 24 | // is (because this app was started separate from dapr), then assume 50001. 25 | if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("DAPR_GRPC_PORT"))) 26 | { 27 | Environment.SetEnvironmentVariable("DAPR_GRPC_PORT", "50001"); 28 | } 29 | 30 | // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle 31 | builder.Services.AddEndpointsApiExplorer(); 32 | builder.Services.AddSwaggerGen(); 33 | 34 | var app = builder.Build(); 35 | 36 | // Configure the HTTP request pipeline. 37 | if (app.Environment.IsDevelopment()) 38 | { 39 | app.UseSwagger(); 40 | app.UseSwaggerUI(); 41 | } 42 | 43 | app.UseHttpsRedirection(); 44 | 45 | app.UseAuthorization(); 46 | 47 | app.MapControllers(); 48 | 49 | app.Run(); 50 | -------------------------------------------------------------------------------- /OrderProcess/CheckoutService/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:46076", 8 | "sslPort": 44311 9 | } 10 | }, 11 | "profiles": { 12 | "http": { 13 | "commandName": "Project", 14 | "dotnetRunMessages": true, 15 | "launchBrowser": true, 16 | "launchUrl": "swagger", 17 | "applicationUrl": "http://localhost:5064", 18 | "environmentVariables": { 19 | "ASPNETCORE_ENVIRONMENT": "Development" 20 | } 21 | }, 22 | "https": { 23 | "commandName": "Project", 24 | "dotnetRunMessages": true, 25 | "launchBrowser": true, 26 | "launchUrl": "swagger", 27 | "applicationUrl": "https://localhost:7110;http://localhost:5064", 28 | "environmentVariables": { 29 | "ASPNETCORE_ENVIRONMENT": "Development" 30 | } 31 | }, 32 | "IIS Express": { 33 | "commandName": "IISExpress", 34 | "launchBrowser": true, 35 | "launchUrl": "swagger", 36 | "environmentVariables": { 37 | "ASPNETCORE_ENVIRONMENT": "Development" 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /OrderProcess/CheckoutService/Resources/statestore.yml: -------------------------------------------------------------------------------- 1 | apiVersion: dapr.io/v1alpha1 2 | kind: Component 3 | metadata: 4 | name: statestore 5 | spec: 6 | type: state.redis 7 | version: v1 8 | metadata: 9 | - name: redisHost 10 | value: localhost:6379 11 | - name: redisPassword 12 | value: "" 13 | - name: actorStateStore 14 | value: "true" -------------------------------------------------------------------------------- /OrderProcess/CheckoutService/Workflows/CheckoutWorkflow.cs: -------------------------------------------------------------------------------- 1 | using CheckoutService.Activities; 2 | using CheckoutService.Models; 3 | using Dapr.Workflow; 4 | using Microsoft.DurableTask; 5 | 6 | namespace CheckoutService.Workflows 7 | { 8 | public class CheckoutWorkflow : Workflow 9 | { 10 | public override async Task RunAsync(WorkflowContext context, OrderItem order) 11 | { 12 | string orderId = context.InstanceId; 13 | 14 | // Notify the user that an order has come through 15 | await context.CallActivityAsync( 16 | nameof(NotifyActivity), 17 | new Notification($"Received order {orderId} for {order.Quantity} {order.Name}")); 18 | 19 | // Determine if there is enough of the item available for purchase by checking the inventory. 20 | var inventoryRequest = new InventoryRequest(RequestId: orderId, order.Name, order.Quantity); 21 | var inventoryResult = await context.CallActivityAsync( 22 | nameof(CheckInventoryActivity), 23 | inventoryRequest); 24 | 25 | // If there is insufficient inventory, inform the user and stop the workflow. 26 | if (!inventoryResult.InStock) 27 | { 28 | // End the workflow here since we don't have sufficient inventory. 29 | await context.CallActivityAsync( 30 | nameof(NotifyActivity), 31 | new Notification($"Insufficient inventory for {order.Name}")); 32 | 33 | context.SetCustomStatus("Stopped order process due to insufficient inventory."); 34 | 35 | return new CheckoutResult(Processed: false); 36 | } 37 | 38 | // Create a RetryPolicy to retry calling the ProcessPaymentActivity in case it fails. 39 | var taskOptions = new WorkflowTaskOptions( 40 | new WorkflowRetryPolicy(10, TimeSpan.FromSeconds(1), 2)); 41 | 42 | // Process the payment (calls the PaymentService). 43 | var paymentResponse = await context.CallActivityAsync( 44 | nameof(ProcessPaymentActivity), 45 | new PaymentRequest(orderId, order.Name, inventoryResult.TotalCost), 46 | taskOptions); 47 | 48 | // In case the payment fails, notify the user and stop the workflow. 49 | if (!paymentResponse.IsPaymentSuccess) 50 | { 51 | await context.CallActivityAsync( 52 | nameof(NotifyActivity), 53 | new Notification($"Payment failed for order {orderId}")); 54 | 55 | context.SetCustomStatus("Stopped order process due to payment issue."); 56 | 57 | return new CheckoutResult(Processed: false); 58 | } 59 | 60 | try 61 | { 62 | // Check and update the inventory. 63 | await context.CallActivityAsync( 64 | nameof(UpdateInventoryActivity), 65 | inventoryRequest); 66 | } 67 | catch (TaskFailedException) 68 | { 69 | // The inventory is insufficient, notify the user, 70 | // perform the compensation action (refund), and end the workflow. 71 | await context.CallActivityAsync( 72 | nameof(NotifyActivity), 73 | new Notification($"Order {orderId} Failed! You are now getting a refund")); 74 | 75 | await context.CallActivityAsync( 76 | nameof(RefundPaymentActivity), 77 | new PaymentRequest(RequestId: orderId, order.Name, inventoryResult.TotalCost)); 78 | 79 | context.SetCustomStatus("Stopped order process due to error in inventory update."); 80 | 81 | return new CheckoutResult(Processed: false); 82 | } 83 | 84 | await context.CallActivityAsync( 85 | nameof(NotifyActivity), 86 | new Notification($"Order {orderId} has completed!")); 87 | 88 | return new CheckoutResult(Processed: true); 89 | } 90 | } 91 | } -------------------------------------------------------------------------------- /OrderProcess/CheckoutService/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /OrderProcess/CheckoutService/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } 10 | -------------------------------------------------------------------------------- /OrderProcess/CheckoutService/checkout.http: -------------------------------------------------------------------------------- 1 | @app_url=http://localhost:5064 2 | 3 | ### Get inventory 4 | GET {{app_url}}/inventory 5 | 6 | ### Restock inventory 7 | POST {{app_url}}/inventory/restock 8 | 9 | ### Clear inventory 10 | DELETE {{app_url}}/inventory 11 | 12 | ### Run workflow 13 | @dapr_url=http://localhost:3500 14 | @workflow_id={{$guid}} 15 | 16 | // @name wfrequest 17 | POST {{dapr_url}}/v1.0/workflows/dapr/CheckoutWorkflow/start?instanceID={{workflow_id}} 18 | Content-Type: application/json 19 | 20 | { 21 | "Name": "Paperclips", 22 | "Quantity": 25 23 | } 24 | 25 | ### Get status 26 | @workflow_id_response={{wfrequest.response.body.instanceID}} 27 | GET {{dapr_url}}/v1.0/workflows/dapr/{{workflow_id_response}} -------------------------------------------------------------------------------- /OrderProcess/OrderProcess.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.5.002.0 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CheckoutService", "CheckoutService\CheckoutService.csproj", "{0DCAA384-B8B7-4392-A690-AE82357DFEAE}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CheckoutService.Tests", "CheckoutService.Tests\CheckoutService.Tests.csproj", "{48315C68-7842-4B01-BB63-B64346858D89}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PaymentService", "PaymentService\PaymentService.csproj", "{061F3BC3-223A-46D2-AB29-F58749C8B136}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|Any CPU = Debug|Any CPU 15 | Release|Any CPU = Release|Any CPU 16 | EndGlobalSection 17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 18 | {0DCAA384-B8B7-4392-A690-AE82357DFEAE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 19 | {0DCAA384-B8B7-4392-A690-AE82357DFEAE}.Debug|Any CPU.Build.0 = Debug|Any CPU 20 | {0DCAA384-B8B7-4392-A690-AE82357DFEAE}.Release|Any CPU.ActiveCfg = Release|Any CPU 21 | {0DCAA384-B8B7-4392-A690-AE82357DFEAE}.Release|Any CPU.Build.0 = Release|Any CPU 22 | {48315C68-7842-4B01-BB63-B64346858D89}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {48315C68-7842-4B01-BB63-B64346858D89}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {48315C68-7842-4B01-BB63-B64346858D89}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {48315C68-7842-4B01-BB63-B64346858D89}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {061F3BC3-223A-46D2-AB29-F58749C8B136}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {061F3BC3-223A-46D2-AB29-F58749C8B136}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {061F3BC3-223A-46D2-AB29-F58749C8B136}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {061F3BC3-223A-46D2-AB29-F58749C8B136}.Release|Any CPU.Build.0 = Release|Any CPU 30 | EndGlobalSection 31 | GlobalSection(SolutionProperties) = preSolution 32 | HideSolutionNode = FALSE 33 | EndGlobalSection 34 | GlobalSection(ExtensibilityGlobals) = postSolution 35 | SolutionGuid = {90BF8A9A-92C4-42C4-A5E3-2FB7448EEE59} 36 | EndGlobalSection 37 | EndGlobal 38 | -------------------------------------------------------------------------------- /OrderProcess/PaymentService/PaymentService.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /OrderProcess/PaymentService/Program.cs: -------------------------------------------------------------------------------- 1 | using Dapr.Client; 2 | 3 | var builder = WebApplication.CreateBuilder(args); 4 | builder.Services.AddEndpointsApiExplorer(); 5 | builder.Services.AddSwaggerGen(); 6 | var app = builder.Build(); 7 | 8 | // Dapr uses a random port for gRPC by default. If we don't know what that port 9 | // is (because this app was started separate from dapr), then assume 50001. 10 | if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("DAPR_GRPC_PORT"))) 11 | { 12 | Environment.SetEnvironmentVariable("DAPR_GRPC_PORT", "50001"); 13 | } 14 | 15 | var daprClient = new DaprClientBuilder().Build(); 16 | const string CONFIG_STORE_NAME = "configstore"; 17 | const string configKey = "isPaymentSuccess"; 18 | var configItems = new List { configKey }; 19 | 20 | app.MapPost("/pay", async (PaymentRequest request) => 21 | { 22 | Console.WriteLine("PaymentRequest received : " + request.RequestId); 23 | bool isPaymentSuccess = await GetConfigItemAsync(); 24 | if (isPaymentSuccess) 25 | { 26 | Console.WriteLine("Payment successful : " + request.RequestId); 27 | 28 | return Results.Accepted(); 29 | } 30 | else 31 | { 32 | Console.WriteLine("Payment failed : " + request.RequestId); 33 | 34 | return Results.BadRequest(); 35 | } 36 | }); 37 | 38 | app.MapPost("/refund", async (RefundRequest request) => 39 | { 40 | Console.WriteLine("RefundRequest received : " + request.RequestId); 41 | bool isPaymentSuccess = await GetConfigItemAsync(); 42 | if (isPaymentSuccess) 43 | { 44 | Console.WriteLine("Refund successful : " + request.RequestId); 45 | 46 | return Results.Accepted(); 47 | } 48 | else 49 | { 50 | Console.WriteLine("Refund failed : " + request.RequestId); 51 | 52 | return Results.BadRequest(); 53 | } 54 | }); 55 | 56 | await app.RunAsync(); 57 | 58 | 59 | async Task GetConfigItemAsync() 60 | { 61 | var config = await daprClient.GetConfiguration(CONFIG_STORE_NAME, configItems); 62 | if (config.Items.TryGetValue(configKey, out var isPaymentSuccessItem)) 63 | { 64 | if (!bool.TryParse(isPaymentSuccessItem.Value, out bool isPaymentSuccess)) 65 | { 66 | Console.WriteLine("Can't parse isPaymentSuccessItem to boolean."); 67 | } 68 | 69 | return isPaymentSuccess; 70 | } 71 | else 72 | { 73 | Console.WriteLine("isPaymentSuccessItem not found"); 74 | 75 | //default to true so the happy path of the CheckoutWorkflow always works. 76 | return true; 77 | } 78 | } 79 | 80 | public record PaymentRequest(string RequestId, string Name, double TotalCost); 81 | public record RefundRequest(string RequestId, string Name, double TotalCost); 82 | -------------------------------------------------------------------------------- /OrderProcess/PaymentService/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:2614", 8 | "sslPort": 44342 9 | } 10 | }, 11 | "profiles": { 12 | "http": { 13 | "commandName": "Project", 14 | "dotnetRunMessages": true, 15 | "launchBrowser": true, 16 | "launchUrl": "swagger", 17 | "applicationUrl": "http://localhost:5063", 18 | "environmentVariables": { 19 | "ASPNETCORE_ENVIRONMENT": "Development" 20 | } 21 | }, 22 | "https": { 23 | "commandName": "Project", 24 | "dotnetRunMessages": true, 25 | "launchBrowser": true, 26 | "launchUrl": "swagger", 27 | "applicationUrl": "https://localhost:7009;http://localhost:5063", 28 | "environmentVariables": { 29 | "ASPNETCORE_ENVIRONMENT": "Development" 30 | } 31 | }, 32 | "IIS Express": { 33 | "commandName": "IISExpress", 34 | "launchBrowser": true, 35 | "launchUrl": "swagger", 36 | "environmentVariables": { 37 | "ASPNETCORE_ENVIRONMENT": "Development" 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /OrderProcess/PaymentService/Resources/configstore.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: dapr.io/v1alpha1 2 | kind: Component 3 | metadata: 4 | name: configstore 5 | spec: 6 | type: configuration.redis 7 | version: v1 8 | metadata: 9 | - name: redisHost 10 | value: localhost:6379 11 | - name: redisPassword 12 | value: "" -------------------------------------------------------------------------------- /OrderProcess/PaymentService/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /OrderProcess/PaymentService/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } 10 | -------------------------------------------------------------------------------- /OrderProcess/PaymentService/payment.http: -------------------------------------------------------------------------------- 1 | @app_url=http://localhost:5063 2 | @dapr_http_url=http://localhost:3501 3 | 4 | 5 | ### Pay via Dapr endpoint 6 | POST {{dapr_http_url}}/v1.0/invoke/payment/method/pay 7 | Content-Type: application/json 8 | 9 | { 10 | "requestId": "{{$guid}}", 11 | "name": "Marc", 12 | "totalCost": 100 13 | } 14 | 15 | 16 | ### Pay via app endpoint 17 | POST {{app_url}}/pay 18 | Content-Type: application/json 19 | 20 | { 21 | "requestId": "{{$guid}}", 22 | "name": "Marc", 23 | "totalCost": 100 24 | } 25 | 26 | 27 | ### Refund via app endpoint 28 | POST {{app_url}}/refund 29 | Content-Type: application/json 30 | 31 | { 32 | "requestId": "{{$guid}}", 33 | "name": "Marc", 34 | "totalCost": 100 35 | } -------------------------------------------------------------------------------- /OrderProcess/dapr.yaml: -------------------------------------------------------------------------------- 1 | version: 1 2 | common: 3 | #logLevel: debug 4 | apps: 5 | - appID: checkout-service 6 | appDirPath: CheckoutService 7 | appPort: 5064 8 | daprHTTPPort: 3500 9 | command: ["dotnet", "run"] 10 | appLogDestination: console 11 | daprdLogDestination: console 12 | resourcesPath: Resources 13 | 14 | - appID: payment-service 15 | appDirPath: PaymentService 16 | appPort: 5063 17 | daprHTTPPort: 3501 18 | command: ["dotnet", "run"] 19 | appLogDestination: console 20 | daprdLogDestination: console 21 | resourcesPath: Resources 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dapr workflow demos 2 | 3 | Demos applications that use the Dapr Workflow building block. 4 | 5 | **Read the blog posts:** 6 | - [Understanding the Dapr Workflow engine & authoring workflows in .NET](https://www.diagrid.io/blog/authoring-dapr-workflows-in-dotnet) 7 | 8 | - [An in-depth guide to Dapr workflow patterns in .NET](https://www.diagrid.io/blog/in-depth-guide-to-dapr-workflow-patterns) 9 | 10 | 11 | ## Prerequisites 12 | 13 | 1. [.NET 9 SDK](https://dotnet.microsoft.com/download/dotnet/9.0) 14 | 2. [Dapr CLI](https://docs.dapr.io/getting-started/install-dapr-cli/) 15 | - Ensure that you're using v1.15 (or higher) of the Dapr runtime and the CLI. 16 | 3. A REST client, such as [cURL](https://curl.se/), or the VSCode [REST client extension](https://marketplace.visualstudio.com/items?itemName=humao.rest-client). 17 | 18 | > The VSCode REST client is configured as a recommended extension when opening this repo in VSCode. 19 | 20 | ## Hello world workflow sample 21 | 22 | The Hello World Workflow sample is a very basic workflow with just one activity that returns a random greeting. The workflow takes a name as input and returns a greeting with the name as output. 23 | 24 | ```mermaid 25 | graph TB 26 | A[Start] 27 | B[CreateGreetingActivity] 28 | C[End] 29 | A -->|"input: {name}"| B -->|"output: {greeting} {name}"| C 30 | ``` 31 | 32 | ### Run the HelloWorldWorkflowSample app 33 | 34 | 1. Change to the BasicWorkflowSamples directory and build the ASP.NET app: 35 | 36 | ```bash 37 | cd BasicWorkflowSamples 38 | dotnet build 39 | ``` 40 | 41 | 2. Run the app using the Dapr CLI: 42 | 43 | ```bash 44 | dapr run --app-id basic-workflows --app-port 5065 --dapr-http-port 3500 --resources-path ./ResourcesLocal dotnet run 45 | ``` 46 | 47 | > Ensure the --app-port is the same as the port specified in the launchSettings.json file. 48 | 49 | 3. Start the `HelloWorldWorkflow` via the Workflow HTTP API using cURL, or use the [basicworkflows.http](BasicWorkflowSamples/basicworkflows.http) file if you're using VSCode with the REST client: 50 | 51 | ```bash 52 | curl -i -X POST http://localhost:3500/v1.0/workflows/dapr/HelloWorldWorkflow/start?instanceID=1234a \ 53 | -H "Content-Type: application/text/plain" \ 54 | -d '"World"' 55 | ``` 56 | 57 | > Note that `1234a` in the URL is the workflow instance ID. This can be any string you want. 58 | 59 | Expected result: 60 | 61 | ```json 62 | { 63 | "instanceID": "" 64 | } 65 | ``` 66 | 67 | 4. Check the workflow status via Workflow HTTP API: 68 | 69 | ```bash 70 | curl -i -X GET http://localhost:3500/v1.0/workflows/dapr/1234a 71 | ``` 72 | 73 | Expected result: 74 | 75 | ```json 76 | { 77 | "instanceID": "", 78 | "workflowName": "HelloWorldWorkflow", 79 | "createdAt": "2023-06-19T13:19:18.316956600Z", 80 | "lastUpdatedAt": "2023-06-19T13:19:18.333226200Z", 81 | "runtimeStatus": "COMPLETED", 82 | "properties": { 83 | "dapr.workflow.custom_status": "", 84 | "dapr.workflow.input": "\"World\"", 85 | "dapr.workflow.output": "\"Ciao World\"" 86 | } 87 | } 88 | ``` 89 | 90 | ## Chaining workflow sample 91 | 92 | The Chaining Workflow sample is a workflow that chains three activities of the same type `CreateGreetingActivity`, each prepending a random greeting to the input. The output of the activity is used as an input for the next activity in the chain. 93 | 94 | ```mermaid 95 | graph TB 96 | A[Start] 97 | B1[CreateGreetingActivity] 98 | B2[CreateGreetingActivity] 99 | B3[CreateGreetingActivity] 100 | C[End] 101 | A -->|"input: {name}"| B1 -->|"output/input: {greeting1} {name}"| B2 102 | B2 -->|"output/input: {greeting2} {greeting1} {name}"| B3 103 | B3 -->|"output: {greeting3} {greeting2} {greeting1} {name}"| C 104 | ``` 105 | 106 | 1. Ensure that the BasicWorkflowSamples app is still running, if not change to the BasicWorkflowSamples directory, build the app, and run the app using the Dapr CLI: 107 | 108 | ```bash 109 | cd BasicWorkflowSamples 110 | dotnet build 111 | dapr run --app-id basic-workflows --app-port 5065 --dapr-http-port 3500 --resources-path ./ResourcesLocal dotnet run 112 | ``` 113 | 114 | 2. Start the `ChainingWorkflow` via the Workflow HTTP API using cURL, or use the [basicworkflows.http](BasicWorkflowSamples/basicworkflows.http) file if you're using VSCode with the REST client: 115 | 116 | ```bash 117 | curl -i -X POST http://localhost:3500/v1.0/workflows/dapr/ChainingWorkflow/start?instanceID=1234b \ 118 | -H "Content-Type: application/text/plain" \ 119 | -d '"World"' 120 | ``` 121 | 122 | > Note that `1234b` in the URL is the workflow instance ID. This can be any string you want. 123 | 124 | Expected result: 125 | 126 | ```json 127 | { 128 | "instanceID": "" 129 | } 130 | ``` 131 | 132 | 3. Check the workflow status via Workflow HTTP API: 133 | 134 | ```bash 135 | curl -i -X GET http://localhost:3500/v1.0/workflows/dapr/1234b 136 | ``` 137 | 138 | Expected result: 139 | 140 | ```json 141 | { 142 | "instanceID": "", 143 | "workflowName": "ChainingWorkflow", 144 | "createdAt": "2023-06-19T13:21:08.611149200Z", 145 | "lastUpdatedAt": "2023-06-19T13:21:08.647482Z", 146 | "runtimeStatus": "COMPLETED", 147 | "properties": { 148 | "dapr.workflow.custom_status": "", 149 | "dapr.workflow.input": "\"World\"", 150 | "dapr.workflow.output": "\"Hello Hi Konnichiwa World\"" 151 | } 152 | } 153 | ``` 154 | 155 | ## Fan-out / Fan-in workflow sample 156 | 157 | The Fan-out / Fan-in Workflow sample is a workflow that fans out and schedules an activity (`CreateGreetingActivity`) for each element in the input array and waits until all of the activities are completed. The output of the workflow is an array of greetings. 158 | 159 | ```mermaid 160 | graph TB 161 | A[Start] 162 | A1([for each name in names]) 163 | B1[CreateGreetingActivity] 164 | B2[CreateGreetingActivity] 165 | B3[CreateGreetingActivity] 166 | C([Task.WhenAll]) 167 | D[End] 168 | A --> |"input: [{name1}, {name2}, {name3}]"| A1 169 | A1 -->|"input: {name1}"| B1 -->|"output: {greeting1} {name1}"| C 170 | A1 -->|"input: {name2}"| B2 -->|"output: {greeting2} {name2}"| C 171 | A1 -->|"input: {name3}"| B3 -->|"output: {greeting3} {name3}"| C 172 | C -->|"output: [{greeting1} {name1}, {greeting2} {name2}, {greeting3} {name3}]"| D 173 | ``` 174 | 175 | 1. Ensure that the BasicWorkflowSamples app is still running, if not change to the BasicWorkflowSamples directory, build the app, and run the app using the Dapr CLI: 176 | 177 | ```bash 178 | cd BasicWorkflowSamples 179 | dotnet build 180 | dapr run --app-id basic-workflows --app-port 5065 --dapr-http-port 3500 --resources-path ./ResourcesLocal dotnet run 181 | ``` 182 | 183 | 2. Start the `FanOutFanInWorkflow` via the Workflow HTTP API using cURL, or use the [basicworkflows.http](BasicWorkflowSamples/basicworkflows.http) file if you're using VSCode with the REST client: 184 | 185 | ```bash 186 | curl -i -X POST http://localhost:3500/v1.0/workflows/dapr/FanOutFanInWorkflow/start?instanceID=1234c \ 187 | -H "Content-Type: application/json" \ 188 | -d '["Amsterdam", "Chicago", "New York"]' 189 | ``` 190 | 191 | > Note that `1234c` in the URL is the workflow instance ID. This can be any string you want. 192 | 193 | Expected result: 194 | 195 | ```json 196 | { 197 | "instanceID": "" 198 | } 199 | ``` 200 | 201 | 3. Check the workflow status via Workflow HTTP API: 202 | 203 | ```bash 204 | curl -i -X GET http://localhost:3500/v1.0/workflows/dapr/1234c 205 | ``` 206 | 207 | Expected result: 208 | 209 | ```json 210 | { 211 | "instanceID": "", 212 | "workflowName": "FanOutFanInWorkflow", 213 | "createdAt": "2023-06-19T13:22:42.056622400Z", 214 | "lastUpdatedAt": "2023-06-19T13:22:42.093666600Z", 215 | "runtimeStatus": "COMPLETED", 216 | "properties": { 217 | "dapr.workflow.custom_status": "", 218 | "dapr.workflow.input": "[\"Amsterdam\",\"Chicago\",\"New York\"]", 219 | "dapr.workflow.output": "[\"Hi Amsterdam\",\"Hola Chicago\",\"Guten Tag New York\"]" 220 | } 221 | } 222 | ``` 223 | 224 | ## Monitor workflow sample 225 | 226 | The Monitor sample is a workflow with a numeric input (counter) and restarts the workflow (with an updated counter) until the counter reaches 10. The workflow diagram is as follows: 227 | 228 | ```mermaid 229 | graph TB 230 | A[Start] 231 | B[CreateGreetingActivity] 232 | C{counter < 10} 233 | D[End] 234 | A --> |"input: {counter}"| B 235 | B -->|"output: {greeting} {counter}"| C 236 | C --> |"[Yes] {counter+=1}"| A 237 | C -->|"[No] output: {greeting} 10"| D 238 | ``` 239 | 240 | 1. Ensure that the BasicWorkflowSamples app is still running, if not change to the BasicWorkflowSamples directory, build the app, and run the app using the Dapr CLI: 241 | 242 | ```bash 243 | cd BasicWorkflowSamples 244 | dotnet build 245 | dapr run --app-id basic-workflows --app-port 5065 --dapr-http-port 3500 --resources-path ./ResourcesLocal dotnet run 246 | ``` 247 | 248 | 2. Start the `MonitorWorkflow` via the Workflow HTTP API using cURL, or use the [basicworkflows.http](BasicWorkflowSamples/basicworkflows.http) file if you're using VSCode with the REST client: 249 | 250 | ```bash 251 | curl -i -X POST http://localhost:3500/v1.0/workflows/dapr/MonitorWorkflow/start?instanceID=1234d \ 252 | -H "Content-Type: application/text/plain" \ 253 | -d '0' 254 | ``` 255 | 256 | > Note that `1234d` in the URL is the workflow instance ID. This can be any string you want. 257 | 258 | Expected result: 259 | 260 | ```json 261 | { 262 | "instanceID": "" 263 | } 264 | ``` 265 | 266 | 3. Check the workflow status via Workflow HTTP API: 267 | 268 | ```bash 269 | curl -i -X GET http://localhost:3500/v1.0/workflows/dapr/1234d 270 | ``` 271 | 272 | Expected result: 273 | 274 | ```json 275 | { 276 | "instanceID": "", 277 | "workflowName": "MonitorWorkflow", 278 | "createdAt": "2023-06-19T13:24:23.004744700Z", 279 | "lastUpdatedAt": "2023-06-19T13:24:23.016307900Z", 280 | "runtimeStatus": "COMPLETED", 281 | "properties": { 282 | "dapr.workflow.custom_status": "", 283 | "dapr.workflow.input": "10", 284 | "dapr.workflow.output": "\"Hey 10\"" 285 | } 286 | } 287 | ``` 288 | 289 | ## Timer workflow sample 290 | 291 | The Timer sample is a workflow with a TimerWorkflowInput object that contains a name and a date. If the date from the input is larger that the current date, a timer is started and the workflow will only continue once the timer has completed.: 292 | 293 | ```mermaid 294 | graph TB 295 | A[Start] 296 | B{input date > current date} 297 | C([CreateTimer]) 298 | D[CreateGreetingActivity] 299 | E[End] 300 | A --> |"input: {date, name}"| B 301 | B -->|"[Yes]"| C 302 | C -->|"Wait"| A 303 | B -->|"[No] input: {name}"| D 304 | D -->|"output: {greeting} {name}"| E 305 | 306 | ``` 307 | 308 | 1. Ensure that the BasicWorkflowSamples app is still running, if not change to the BasicWorkflowSamples directory, build the app, and run the app using the Dapr CLI: 309 | 310 | ```bash 311 | cd BasicWorkflowSamples 312 | dotnet build 313 | dapr run --app-id basic-workflows --app-port 5065 --dapr-http-port 3500 --resources-path ./ResourcesLocal dotnet run 314 | ``` 315 | 316 | 2. Start the `TimerWorkflow` via the Workflow HTTP API using cURL, or use the [basicworkflows.http](BasicWorkflowSamples/basicworkflows.http) file if you're using VSCode with the REST client: 317 | 318 | ```bash 319 | curl -i -X POST http://localhost:3500/v1.0/workflows/dapr/TimerWorkflow/start?instanceID=1234e \ 320 | -H "Content-Type: application/json" \ 321 | -d '{"DateTime": "2023-05-29T13:44:00+00:00","Name":"World"}' 322 | ``` 323 | 324 | > Note that `1234e` in the URL is the workflow instance ID. This can be any string you want. 325 | 326 | Expected result: 327 | 328 | ```json 329 | { 330 | "instanceID": "" 331 | } 332 | ``` 333 | 334 | 3. Check the workflow status via Workflow HTTP API: 335 | 336 | ```bash 337 | curl -i -X GET http://localhost:3500/v1.0/workflows/dapr/1234e 338 | ``` 339 | 340 | Expected result: 341 | 342 | ```json 343 | { 344 | "instanceID": "", 345 | "workflowName": "TimerWorkflow", 346 | "createdAt": "2023-06-19T13:25:59.745344700Z", 347 | "lastUpdatedAt": "2023-06-19T13:25:59.768925500Z", 348 | "runtimeStatus": "COMPLETED", 349 | "properties": { 350 | "dapr.workflow.custom_status": "", 351 | "dapr.workflow.input": "{\"DateTime\": \"2023-05-29T13:44:00+00:00\", \"Name\": \"World\"}", 352 | "dapr.workflow.output": "\"Guten Tag World at 2023-05-29 15:44:00\"" 353 | } 354 | } 355 | ``` 356 | 357 | ## External interaction workflow sample 358 | 359 | The External interaction sample is a workflow that will wait with execution of the workflow until an external event has been received or the timeout has expired. 360 | 361 | ```mermaid 362 | graph TB 363 | A[Start] 364 | B([WaitForExternalEventAsync]) 365 | C{IsApproved} 366 | D[CreateGreetingActivity] 367 | F[ExternalEvent] 368 | E[End] 369 | A --> B 370 | F --> B 371 | B --> C 372 | C -->|"[Yes]"| D 373 | C -->|"[No / Timeout]"| E 374 | D --> E 375 | ``` 376 | 377 | 1. Ensure that the BasicWorkflowSamples app is still running, if not change to the BasicWorkflowSamples directory, build the app, and run the app using the Dapr CLI: 378 | 379 | ```bash 380 | cd BasicWorkflowSamples 381 | dotnet build 382 | dapr run --app-id basic-workflows --app-port 5065 --dapr-http-port 3500 --resources-path ./ResourcesLocal dotnet run 383 | ``` 384 | 385 | 2. Start the `ExternalInteractionWorkflow` via the Workflow HTTP API using cURL, or use the [basicworkflows.http](BasicWorkflowSamples/basicworkflows.http) file if you're using VSCode with the REST client: 386 | 387 | ```bash 388 | curl -i -X POST http://localhost:3500/v1.0/workflows/dapr/ExternalInteractionWorkflow/start?instanceID=1234f \ 389 | -H "Content-Type: application/text/plain" \ 390 | -d '"World"' 391 | ``` 392 | 393 | > Note that `1234f` in the URL is the workflow instance ID. This can be any string you want. 394 | 395 | Expected result: 396 | 397 | ```json 398 | { 399 | "instanceID": "" 400 | } 401 | ``` 402 | 403 | 3. Check the workflow status via Workflow HTTP API: 404 | 405 | ```bash 406 | curl -i -X GET http://localhost:3500/v1.0/workflows/dapr/1234f 407 | ``` 408 | 409 | If you check the status within the specified timeout, the `runtimeStatus` will be `RUNNING`: 410 | 411 | ```json 412 | { 413 | "instanceID": "", 414 | "workflowName": "ExternalInteractionWorkflow", 415 | "createdAt": "2023-07-27T11:35:54.446941200Z", 416 | "lastUpdatedAt": "2023-07-27T11:35:55.694310900Z", 417 | "runtimeStatus": "RUNNING", 418 | "properties": { 419 | "dapr.workflow.custom_status": "", 420 | "dapr.workflow.input": "\"World\"" 421 | } 422 | } 423 | ``` 424 | 425 | If you wait until the timeout has expired, the `runtimeStatus` will be `COMPLETED`, with a `custom_status` that the wait for external event is cancelled due to a timeout: 426 | 427 | ```json 428 | { 429 | "instanceID": "2283569d-5d56-4041-bd63-7df35fa3c879", 430 | "workflowName": "ExternalInteractionWorkflow", 431 | "createdAt": "2023-07-27T14:29:37.084711800Z", 432 | "lastUpdatedAt": "2023-07-27T14:29:57.011878800Z", 433 | "runtimeStatus": "COMPLETED", 434 | "properties": { 435 | "dapr.workflow.custom_status": "\"Wait for external event is cancelled due to timeout.\"", 436 | "dapr.workflow.input": "\"World\"", 437 | "dapr.workflow.output": "\"\"" 438 | } 439 | } 440 | ``` 441 | 442 | 4. Now start the workflow again, raise an event within the timeout duration by calling the `raiseEvent` endpoint, and retrieve the status of the workflow. You can use cURL, or use the [basicworkflows.http](BasicWorkflowSamples/basicworkflows.http) file if you're using VSCode with the REST client: 443 | 444 | ```bash 445 | curl -i -X POST http://localhost:3500/v1.0/workflows/dapr/ExternalInteractionWorkflow/start?instanceID=1234f \ 446 | -H "Content-Type: application/text/plain" \ 447 | -d '"World"' 448 | ``` 449 | 450 | ```bash 451 | curl -i -X POST http://localhost:3500/v1.0/workflows/dapr/1234f/raiseEvent/ \ 452 | -H "Content-Type: application/json" \ 453 | -d '{"IsApproved":true}' 454 | ``` 455 | 456 | ```bash 457 | curl -i -X GET http://localhost:3500/v1.0/workflows/dapr/1234f 458 | ``` 459 | 460 | The `runtimeStatus` should now be `COMPLETED`: 461 | 462 | ```json 463 | { 464 | "instanceID": "", 465 | "workflowName": "ExternalInteractionWorkflow", 466 | "createdAt": "2023-07-27T11:50:16.526722900Z", 467 | "lastUpdatedAt": "2023-07-27T11:50:19.172489800Z", 468 | "runtimeStatus": "COMPLETED", 469 | "properties": { 470 | "dapr.workflow.custom_status": "", 471 | "dapr.workflow.input": "\"World\"", 472 | "dapr.workflow.output": "\"Hello World\"" 473 | } 474 | } 475 | ``` 476 | 477 | ## CheckoutService sample 478 | 479 | ```mermaid 480 | flowchart LR 481 | A[CheckoutService] 482 | B[PaymentService] 483 | C[(Inventory DB)] 484 | A --> B 485 | A --> C 486 | ``` 487 | 488 | The CheckoutService app contains workflow that processes an order. The workflow takes an `OrderItem` as input and returns a `CheckoutResult` as output. The `CheckoutWorkflow` workflow uses these activities: 489 | 490 | - `NotifyActivity`: Notifies the customer of the progress of the order. 491 | - `CheckInventoryActivity`: Checks if the inventory is sufficient. 492 | - `ProcessPaymentActivity`: Processes the payment, by calling another Dapr service (PaymentService). 493 | - `UpdateInventoryActivity`: Updates the inventory after checking it again. 494 | - `RefundPaymentActivity`: Refunds the payment if the `UpdateInventoryActivity` throws an exception. 495 | 496 | ```mermaid 497 | graph TD 498 | A[Start] 499 | B[NotifyActivity] 500 | C[CheckInventoryActivity] 501 | X{Sufficient Inventory?} 502 | D[ProcessPaymentActivity] 503 | P{Payment successful?} 504 | E[UpdateInventoryActivity] 505 | F[RefundPaymentActivity] 506 | BB[NotifyActivity] 507 | BBB[NotifyActivity] 508 | XX{Sufficient Inventory?} 509 | Z[End] 510 | A --> |OrderItem| B --> C 511 | C --> X 512 | X -->|"[Yes]"| D 513 | X -->|"[No] CheckoutResult(Processed:false)"| Z 514 | D --> P 515 | P -->|"[Yes]"| E --> XX 516 | P -->|"[No] CheckoutResult(Processed:false)"| Z 517 | XX -->|"[Yes]"| BB --> |"CheckoutResult(Processed:true)"| Z 518 | XX -->|"[No]"| BBB --> F --> |"CheckoutResult(Processed:false)"| Z 519 | ``` 520 | 521 | The `CheckInventoryActivity` and `UpdateInventoryActivity` classes use [Dapr's state management building block](https://docs.dapr.io/developing-applications/building-blocks/state-management/) to manage the inventory in a Redis state store. 522 | 523 | The `ProcessPaymentActivity` and `RefundPaymentActivity` call out to the PaymentService. 524 | 525 | Next to the workflow, this application has an `InventoryController` with the following endpoints: 526 | 527 | - `GET http://localhost:5064/inventory`: retrieves the inventory 528 | - `DELETE http://localhost:5064/inventory`: clears the inventory 529 | - `POST http://localhost:5064/inventory/restock`: restocks the inventory 530 | 531 | The `InventoryController` also uses Dapr's state management building block. 532 | 533 | ### Changing the isPaymentSuccess flag 534 | 535 | The CheckoutService app relies on the PaymentService app to process the payment. The PaymentService app is a small ASP.NET app that exposes two endpoints: 536 | 537 | - `/pay`: processes the payment 538 | - `/refund`: refunds the payment 539 | 540 | The PaymentService app uses the Dapr Configuration API to read the `isPaymentSuccess` configuration item from the configuration store (Redis). If the item key is not present, or if the item value is set to the string "true", the payment will be successful. If the item value is set to the string "false", the payment will fail. Use this setting to simulate a failed payment and check the workflow result. 541 | 542 | Setting the configuration item is done via the redis-cli in the dapr_redis docker container: 543 | 544 | ```bash 545 | docker exec dapr_redis redis-cli MSET isPaymentSuccess "true" 546 | ``` 547 | 548 | To configure the PaymentService to return a failed payment response use: 549 | 550 | ```bash 551 | docker exec dapr_redis redis-cli MSET isPaymentSuccess "false" 552 | ``` 553 | 554 | Set the `isPaymentSuccess` config item to "true" before continuing. 555 | 556 | ### Build the services 557 | 558 | 1. Open a terminal, change to the OrderProcess/PaymentService directory and build the ASP.NET app: 559 | 560 | ```bash 561 | cd OrderProcess/PaymentService 562 | dotnet build 563 | ``` 564 | 565 | 2. Change to the OrderProcess/CheckoutService directory and build the ASP.NET app: 566 | 567 | ```bash 568 | cd OrderProcess/CheckoutService 569 | dotnet build 570 | ``` 571 | 572 | ### Run the services 573 | 574 | 1. Using the terminal, change to the OrderProcess directory and start the services using the Dapr CLI with multi-app run: 575 | 576 | ```bash 577 | dapr run -f . 578 | ``` 579 | 580 | > This will start both the CheckoutService and PaymentService using the multi-app run configuration specified in the [dapr.yaml](/OrderProcess/dapr.yaml) file. 581 | 582 | 2. Check the inventory using cURL, or use the [checkout.http](OrderProcess/CheckoutService/checkout.http) file if you're using VSCode with the REST client: 583 | 584 | ```bash 585 | curl -X POST http://localhost:5064/inventory/restock 586 | ``` 587 | 588 | If the quantity of the items is not 0, clear the inventory by running: 589 | 590 | ```bash 591 | curl -X POST http://localhost:5064/inventory/clear 592 | ``` 593 | 594 | 3. Try ordering 100 paperclips while the inventory is not sufficient. Start the `CheckoutWorkflow` via the Workflow HTTP API: 595 | 596 | ```bash 597 | curl -i -X POST http://localhost:3500/v1.0/workflows/dapr/CheckoutWorkflow/start?instanceID=1234f \ 598 | -H "Content-Type: application/json" \ 599 | -d '{"Name": "Paperclips", "Quantity": 100}' 600 | ``` 601 | 602 | > Note that `1234f` in the URL is the workflow instance ID. This can be any string you want. 603 | 604 | Expected result: 605 | 606 | ```json 607 | { 608 | "instanceID": "" 609 | } 610 | ``` 611 | 612 | > Pay attention to the console output. A message will appear that indicates the inventory is insufficient. 613 | 614 | 4. Check the workflow status via Workflow HTTP API: 615 | 616 | ```bash 617 | curl -i -X GET http://localhost:3500/v1.0/workflows/dapr/1234f 618 | ``` 619 | 620 | Expected result: 621 | 622 | ```json 623 | { 624 | "instanceID": "", 625 | "workflowName": "CheckoutWorkflow", 626 | "createdAt": "2023-06-19T13:37:30.261385700Z", 627 | "lastUpdatedAt": "2023-06-19T13:37:30.303315100Z", 628 | "runtimeStatus": "COMPLETED", 629 | "properties": { 630 | "dapr.workflow.custom_status": "\"Stopped order process due to insufficient inventory.\"", 631 | "dapr.workflow.input": "{\"Name\": \"Paperclips\", \"Quantity\": 125}", 632 | "dapr.workflow.output": "{\"Processed\":false}" 633 | } 634 | } 635 | ``` 636 | 637 | > Depending on how quick the status is retrieved after starting the workflow, the `dapr.workflow.runtime_status` could still be `"RUNNING"`. Repeat the GET status request until the status is `"COMPLETED"`. 638 | 639 | 5. Restock the inventory: 640 | 641 | ```bash 642 | curl -X POST http://localhost:5064/inventory/restock 643 | ``` 644 | 645 | Expected result: `HTTP 200 OK` 646 | 647 | 6. Try ordering paperclips again, now within the limits of the inventory. Start the `CheckoutWorkflow` via the Workflow HTTP API: 648 | 649 | ```bash 650 | curl -i -X POST http://localhost:3500/v1.0/workflows/dapr/CheckoutWorkflow/start?instanceID=1234g \ 651 | -H "Content-Type: application/json" \ 652 | -d '{"Name": "Paperclips", "Quantity": 25}' 653 | ``` 654 | 655 | > Note that `1234g` in the URL is the workflow instance ID. This can be any string you want. 656 | 657 | Expected result: 658 | 659 | ```json 660 | { 661 | "instanceID": "" 662 | } 663 | ``` 664 | 665 | > Pay attention to the console output. Messages will appear that indicate the inventory is sufficient and payment has been processed successfully. 666 | 667 | 7. Check the workflow status via Workflow HTTP API: 668 | 669 | ```bash 670 | curl -i -X GET http://localhost:3500/v1.0/workflows/dapr/1234g 671 | ``` 672 | 673 | Expected result: 674 | 675 | ```json 676 | { 677 | "instanceID": "", 678 | "workflowName": "CheckoutWorkflow", 679 | "createdAt": "2023-06-19T13:35:42.126109Z", 680 | "lastUpdatedAt": "2023-06-19T13:35:53.644632200Z", 681 | "runtimeStatus": "COMPLETED", 682 | "properties": { 683 | "dapr.workflow.custom_status": "", 684 | "dapr.workflow.input": "{\"Name\": \"Paperclips\",\"Quantity\": 25}", 685 | "dapr.workflow.output": "{\"Processed\":true}" 686 | } 687 | } 688 | ``` 689 | 690 | 8. Inspect the logs in ZipKin: [`localhost:9411/zipkin`](http://localhost:9411/zipkin). Find the entry marked `checkout:create_orchestration||checkoutworkflow` and show the details. You'll now see a timeline of the workflow at the top, and the activities underneath. 691 | 692 | ![Checkout workflow in Zipkin](images/checkout_zipkin.png) 693 | 694 | ### Unhappy paths 695 | 696 | Now try these different scenarios and check the workflow result. 697 | 698 | Start the CheckoutWorkflow with: 699 | 700 | 1. Shutting down the CheckoutService app once the CheckoutWorkflow has started. Restart the CheckoutService and watch the logs. 701 | 2. A failing payment (set the `isPaymentSuccess` configuration item to "false"). 702 | 3. Shut down the PaymentService completely (check the logs for the retry attempts). 703 | 704 | ## Resources 705 | 706 | 1. [Dapr Workflow overview](https://docs.dapr.io/developing-applications/building-blocks/workflow/workflow-overview/). 707 | 2. [How to: Author and manage Dapr Workflow in the .NET SDK](https://docs.dapr.io/developing-applications/sdks/dotnet/dotnet-workflow/dotnet-workflow-howto/) 708 | 709 | ## More information 710 | 711 | Any questions or comments about this sample? Join the [Dapr discord](https://bit.ly/dapr-discord) and post a message the `#workflow` channel. 712 | Have you made something with Dapr? Post a message in the `#show-and-tell` channel, we love to see your creations! 713 | -------------------------------------------------------------------------------- /dapr-workflow-demos.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.5.002.0 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BasicWorkflowSamples", "BasicWorkflowSamples\BasicWorkflowSamples.csproj", "{D545B091-4B6E-4592-9523-D4013551B1F1}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {D545B091-4B6E-4592-9523-D4013551B1F1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {D545B091-4B6E-4592-9523-D4013551B1F1}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {D545B091-4B6E-4592-9523-D4013551B1F1}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {D545B091-4B6E-4592-9523-D4013551B1F1}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {DD6978CF-F677-4DB3-967C-56ACE711CC38} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | services: 3 | ############################ 4 | # BasicWorkflowSamples App + Dapr sidecar 5 | ############################ 6 | workflow-app: 7 | environment: 8 | - DAPR_HTTP_PORT=3500 9 | - DAPR_GRPC_PORT=4001 10 | build: 11 | context: . 12 | dockerfile: BasicWorkflowSamples/Dockerfile 13 | ports: 14 | - "3500:3500" # only important so we can reach the sidecar from the host for testing purposes 15 | depends_on: 16 | - redis 17 | - placement 18 | networks: 19 | - network 20 | workflow-dapr: 21 | image: "daprio/daprd:1.11.0" 22 | command: ["./daprd", 23 | "-app-id", "basic-workflows", 24 | "-app-port", "5065", 25 | "-dapr-http-port", "3500", 26 | "-placement-host-address", "placement:50006", 27 | "-dapr-grpc-port", "4001", 28 | "-resources-path", "/BasicWorkflowSamples/ResourcesDocker", 29 | "-log-level","debug"] 30 | volumes: 31 | - "./BasicWorkflowSamples/ResourcesDocker/:/BasicWorkflowSamples/ResourcesDocker" 32 | depends_on: 33 | - workflow-app 34 | network_mode: "service:workflow-app" 35 | ############################ 36 | # Dapr placement service 37 | ############################ 38 | placement: 39 | image: "daprio/dapr:1.11.0" 40 | command: ["./placement", "-port", "50006"] 41 | ports: 42 | - "50006:50006" 43 | networks: 44 | - network 45 | ############################ 46 | # Redis state store 47 | ############################ 48 | redis: 49 | image: "redis:alpine" 50 | ports: 51 | - "6380:6379" 52 | networks: 53 | - network 54 | networks: 55 | network: -------------------------------------------------------------------------------- /images/checkout_zipkin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diagrid-labs/dapr-workflow-demos/080221e8cb5aea025074e5301c30835a5851f3e8/images/checkout_zipkin.png -------------------------------------------------------------------------------- /scripts/install.sh: -------------------------------------------------------------------------------- 1 | dapr uninstall 2 | dapr init 3 | dotnet build ./BasicWorkflowSamples 4 | dotnet build ./OrderProcess 5 | --------------------------------------------------------------------------------