├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── codetour_watch.yml │ ├── dotnet.yml │ └── frauddetection │ └── frauddetection_webhook.yml ├── .gitignore ├── .tours ├── DurableFunctionsTheory │ └── .tours │ │ ├── 1-orchestratorfunction.tour │ │ ├── 10-updatingentitystate.tour │ │ ├── 2-activityfunction.tour │ │ ├── 3-clientfunction.tour │ │ ├── 4-sub-orchestrations.tour │ │ ├── 5-eternal-orchestrations.tour │ │ ├── 6-waiting-for-events.tour │ │ ├── 7-raising-events.tour │ │ ├── 8-statefulentities.tour │ │ └── 9-readingentitystate.tour └── NotifySupport │ └── .tours │ └── 90-notify-support-solution.tour ├── .vscode └── extensions.json ├── DurableFunctionsTheory ├── diagrams │ ├── durablefunction_types.png │ ├── eternal_orchestrations.png │ ├── orchestrator_replay.png │ ├── raiseevents.png │ ├── stateful_entities.png │ ├── sub-orchestrators.png │ └── waitforexternalevent.png ├── durablefunctions.md ├── eternalorchestrations.md ├── events.md ├── statefulentities.md └── suborchestrations.md ├── FraudDetection ├── _azurite │ └── README.md ├── challenge │ ├── README.md │ ├── diagrams │ │ ├── frauddetection_functions1.png │ │ └── frauddetection_overview.png │ └── prerequisites.md ├── src │ ├── .vscode │ │ ├── extensions.json │ │ ├── launch.json │ │ ├── settings.json │ │ └── tasks.json │ └── DurableFunctions.UseCases.FraudDetection │ │ ├── .gitignore │ │ ├── Activities │ │ ├── AnalyzeAuditRecordActivity.cs │ │ ├── GetCustomerActivity.cs │ │ └── StoreAuditRecordActivity.cs │ │ ├── Application │ │ └── Startup.cs │ │ ├── Builders │ │ ├── AuditRecordBuilder.cs │ │ └── FakeCustomerBuilder.cs │ │ ├── Clients │ │ ├── FraudDetectionClient.cs │ │ └── FraudResultWebhookClient.cs │ │ ├── DurableFunctions.UseCases.FraudDetection.csproj │ │ ├── Entities │ │ └── FraudDetectionOrchestratorEntity.cs │ │ ├── Models │ │ ├── AuditRecord.cs │ │ ├── Constants.cs │ │ ├── Customer.cs │ │ ├── FraudResult.cs │ │ ├── Transaction.cs │ │ └── WorkflowDispatchEvent.cs │ │ ├── Orchestrators │ │ └── FraudDetectionOrchestrator.cs │ │ ├── Services │ │ ├── AuthHandler.cs │ │ ├── FakeCustomerDataService.cs │ │ ├── ICustomerDataService.cs │ │ └── IFraudAnalysisService.cs │ │ ├── host.json │ │ └── local.settings.json.example └── tst │ └── frauddetection.http ├── LICENSE ├── NotifySupport ├── _azurite │ └── README.md ├── challenge │ ├── README.md │ ├── diagrams │ │ ├── notifysupport_functions1.png │ │ ├── notifysupport_functions2.png │ │ └── notifysupport_overview.png │ ├── notifysupport-tips.md │ ├── notifysupport.drawio │ └── prerequisites.md ├── data │ └── SupportContacts.csv ├── src │ ├── .vscode │ │ ├── extensions.json │ │ ├── launch.json │ │ ├── settings.json │ │ └── tasks.json │ └── DurableFunctions.UseCases.NotifySupport │ │ ├── .gitignore │ │ ├── Activities │ │ ├── GetSupportContactActivity.cs │ │ └── SendNotificationActivity.cs │ │ ├── Builders │ │ ├── NotifySupportOrchestratorInputBuilder.cs │ │ └── SendNotificationOrchestratorInputBuilder.cs │ │ ├── Clients │ │ ├── CallbackHttpClient.cs │ │ └── NotifySupportHttpClient.cs │ │ ├── DurableFunctions.UseCases.NotifySupport.csproj │ │ ├── Entities │ │ └── NotificationOrchestratorInstanceEntity.cs │ │ ├── Models │ │ ├── EventNames.cs │ │ ├── NotifySupportClientInput.cs │ │ ├── NotifySupportOrchestratorInput.cs │ │ ├── SendNotificationActivityInput.cs │ │ ├── SendNotificationOrchestratorInput.cs │ │ ├── SendNotificationOrchestratorResult.cs │ │ └── SupportContactEntity.cs │ │ ├── Orchestrators │ │ ├── NotifySupportOrchestrator.cs │ │ └── SendNotificationOrchestrator.cs │ │ ├── host.json │ │ └── local.settings.json └── tst │ └── notifysupport.http ├── README.md ├── fraud-detection.code-workspace └── notify-support.code-workspace /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # Support me 🙏 2 | 3 | github: [marcduiker] 4 | ko_fi: marcduiker 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | 4 | - package-ecosystem: "nuget" 5 | directory: "/FraudDetection/src/DurableFunctions.UseCases.FraudDetection" 6 | schedule: 7 | interval: "daily" 8 | time: "19:00" 9 | open-pull-requests-limit: 10 10 | reviewers: 11 | - "marcduiker" 12 | 13 | - package-ecosystem: "nuget" 14 | directory: "/NotifySupport/src/DurableFunctions.UseCases.NotifySupport" 15 | schedule: 16 | interval: "daily" 17 | time: "19:01" 18 | open-pull-requests-limit: 10 19 | reviewers: 20 | - "marcduiker" -------------------------------------------------------------------------------- /.github/workflows/codetour_watch.yml: -------------------------------------------------------------------------------- 1 | name: CodeTour Watch And Link Checker 2 | 3 | on: 4 | pull_request: 5 | types: [opened, edited, synchronize, reopened] 6 | 7 | jobs: 8 | codetour-and-linkchecker: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: 'Checkout source code' 12 | uses: actions/checkout@v2 13 | 14 | - name: 'Watch CodeTour changes' 15 | uses: pozil/codetour-watch@v1.3.0 16 | with: 17 | repo-token: ${{ secrets.GITHUB_TOKEN }} 18 | 19 | - name: Link Checker 20 | id: lychee 21 | uses: lycheeverse/lychee-action@v1.0.8 22 | with: 23 | args: --verbose --no-progress --exclude-mail --exclude-loopback --exclude "https?://localhost.*" -- "**/*.md" 24 | env: 25 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 26 | 27 | - name: Fail if there were link errors 28 | run: exit ${{ steps.lychee.outputs.exit_code }} 29 | 30 | -------------------------------------------------------------------------------- /.github/workflows/dotnet.yml: -------------------------------------------------------------------------------- 1 | name: '.NET builds' 2 | on: 3 | pull_request: 4 | types: [opened, edited, synchronize, reopened] 5 | paths: 6 | - 'FraudDetection/src/**' 7 | - 'NotifySupport/src/**' 8 | - '.github/workflows/dotnet.yml' 9 | workflow_dispatch: 10 | 11 | env: 12 | DOTNET_VERSION: '3.1.x' 13 | 14 | jobs: 15 | Application: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: 'Checkout GitHub Action' 19 | uses: actions/checkout@main 20 | - name: Setup .NET ${{ env.DOTNET_VERSION }} Environment 21 | uses: actions/setup-dotnet@v1 22 | with: 23 | dotnet-version: ${{ env.DOTNET_VERSION }} 24 | - name: 'Build Notify Support Function App' 25 | shell: bash 26 | run: | 27 | pushd './NotifySupport/src/DurableFunctions.UseCases.NotifySupport' 28 | dotnet build --configuration Release --output ./output 29 | popd 30 | 31 | # Currently fails due to a MSBuild 16.8.0 dependency of Refit. 32 | # - name: 'Build Fraud Detection Function App' 33 | # shell: bash 34 | # run: | 35 | # pushd './FraudDetection/src/DurableFunctions.UseCases.FraudDetection' 36 | # dotnet build --configuration Release --output ./output 37 | # popd 38 | 39 | -------------------------------------------------------------------------------- /.github/workflows/frauddetection/frauddetection_webhook.yml: -------------------------------------------------------------------------------- 1 | name: FraudDetection 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | recordId: 7 | description: 'ID of the transaction' 8 | required: true 9 | default: '12345' 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Pretend to be busy 16 | uses: kibertoad/wait-action@1.0.1 17 | with: 18 | time: '30s' 19 | - name: Call function webhook 20 | run: | 21 | if [ $RANDOM%2==0 ] 22 | then 23 | SUSPICIOUS='true' 24 | else 25 | SUSPICIOUS='false' 26 | fi 27 | CODE=`curl --write-out '%{http_code}' \ 28 | --silent \ 29 | --output /dev/null \ 30 | --request POST \ 31 | --header 'content-type: application/json' \ 32 | --url '${{ secrets.FRAUDDETECTION_WEBHOOK }}' \ 33 | --data '{ \ 34 | "recordId": "${{ github.event.inputs.recordId }}", \ 35 | "isSuspiciousTransaction": '$SUSPICIOUS' \ 36 | }'` 37 | if [ $CODE!="202" ] 38 | then 39 | echo 1 40 | else 41 | echo 0 42 | fi -------------------------------------------------------------------------------- /.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/master/VisualStudio.gitignore 5 | 6 | # Azurite files/folders 7 | __blobstorage__ 8 | __queuestorage__ 9 | __tablestorage__ 10 | __azurite* 11 | 12 | # User-specific files 13 | *.rsuser 14 | *.suo 15 | *.user 16 | *.userosscache 17 | *.sln.docstates 18 | 19 | # User-specific files (MonoDevelop/Xamarin Studio) 20 | *.userprefs 21 | 22 | # Mono auto generated files 23 | mono_crash.* 24 | 25 | # Build results 26 | [Dd]ebug/ 27 | [Dd]ebugPublic/ 28 | [Rr]elease/ 29 | [Rr]eleases/ 30 | x64/ 31 | x86/ 32 | [Aa][Rr][Mm]/ 33 | [Aa][Rr][Mm]64/ 34 | bld/ 35 | [Bb]in/ 36 | [Oo]bj/ 37 | [Ll]og/ 38 | [Ll]ogs/ 39 | 40 | # Visual Studio 2015/2017 cache/options directory 41 | .vs/ 42 | # Uncomment if you have tasks that create the project's static files in wwwroot 43 | #wwwroot/ 44 | 45 | # Visual Studio 2017 auto generated files 46 | Generated\ Files/ 47 | 48 | # MSTest test Results 49 | [Tt]est[Rr]esult*/ 50 | [Bb]uild[Ll]og.* 51 | 52 | # NUnit 53 | *.VisualState.xml 54 | TestResult.xml 55 | nunit-*.xml 56 | 57 | # Build Results of an ATL Project 58 | [Dd]ebugPS/ 59 | [Rr]eleasePS/ 60 | dlldata.c 61 | 62 | # Benchmark Results 63 | BenchmarkDotNet.Artifacts/ 64 | 65 | # .NET Core 66 | project.lock.json 67 | project.fragment.lock.json 68 | artifacts/ 69 | 70 | # StyleCop 71 | StyleCopReport.xml 72 | 73 | # Files built by Visual Studio 74 | *_i.c 75 | *_p.c 76 | *_h.h 77 | *.ilk 78 | *.meta 79 | *.obj 80 | *.iobj 81 | *.pch 82 | *.pdb 83 | *.ipdb 84 | *.pgc 85 | *.pgd 86 | *.rsp 87 | *.sbr 88 | *.tlb 89 | *.tli 90 | *.tlh 91 | *.tmp 92 | *.tmp_proj 93 | *_wpftmp.csproj 94 | *.log 95 | *.vspscc 96 | *.vssscc 97 | .builds 98 | *.pidb 99 | *.svclog 100 | *.scc 101 | 102 | # Chutzpah Test files 103 | _Chutzpah* 104 | 105 | # Visual C++ cache files 106 | ipch/ 107 | *.aps 108 | *.ncb 109 | *.opendb 110 | *.opensdf 111 | *.sdf 112 | *.cachefile 113 | *.VC.db 114 | *.VC.VC.opendb 115 | 116 | # Visual Studio profiler 117 | *.psess 118 | *.vsp 119 | *.vspx 120 | *.sap 121 | 122 | # Visual Studio Trace Files 123 | *.e2e 124 | 125 | # TFS 2012 Local Workspace 126 | $tf/ 127 | 128 | # Guidance Automation Toolkit 129 | *.gpState 130 | 131 | # ReSharper is a .NET coding add-in 132 | _ReSharper*/ 133 | *.[Rr]e[Ss]harper 134 | *.DotSettings.user 135 | 136 | # TeamCity is a build add-in 137 | _TeamCity* 138 | 139 | # DotCover is a Code Coverage Tool 140 | *.dotCover 141 | 142 | # AxoCover is a Code Coverage Tool 143 | .axoCover/* 144 | !.axoCover/settings.json 145 | 146 | # Visual Studio code coverage results 147 | *.coverage 148 | *.coveragexml 149 | 150 | # NCrunch 151 | _NCrunch_* 152 | .*crunch*.local.xml 153 | nCrunchTemp_* 154 | 155 | # MightyMoose 156 | *.mm.* 157 | AutoTest.Net/ 158 | 159 | # Web workbench (sass) 160 | .sass-cache/ 161 | 162 | # Installshield output folder 163 | [Ee]xpress/ 164 | 165 | # DocProject is a documentation generator add-in 166 | DocProject/buildhelp/ 167 | DocProject/Help/*.HxT 168 | DocProject/Help/*.HxC 169 | DocProject/Help/*.hhc 170 | DocProject/Help/*.hhk 171 | DocProject/Help/*.hhp 172 | DocProject/Help/Html2 173 | DocProject/Help/html 174 | 175 | # Click-Once directory 176 | publish/ 177 | 178 | # Publish Web Output 179 | *.[Pp]ublish.xml 180 | *.azurePubxml 181 | # Note: Comment the next line if you want to checkin your web deploy settings, 182 | # but database connection strings (with potential passwords) will be unencrypted 183 | *.pubxml 184 | *.publishproj 185 | 186 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 187 | # checkin your Azure Web App publish settings, but sensitive information contained 188 | # in these scripts will be unencrypted 189 | PublishScripts/ 190 | 191 | # NuGet Packages 192 | *.nupkg 193 | # NuGet Symbol Packages 194 | *.snupkg 195 | # The packages folder can be ignored because of Package Restore 196 | **/[Pp]ackages/* 197 | # except build/, which is used as an MSBuild target. 198 | !**/[Pp]ackages/build/ 199 | # Uncomment if necessary however generally it will be regenerated when needed 200 | #!**/[Pp]ackages/repositories.config 201 | # NuGet v3's project.json files produces more ignorable files 202 | *.nuget.props 203 | *.nuget.targets 204 | 205 | # Microsoft Azure Build Output 206 | csx/ 207 | *.build.csdef 208 | 209 | # Microsoft Azure Emulator 210 | ecf/ 211 | rcf/ 212 | 213 | # Windows Store app package directories and files 214 | AppPackages/ 215 | BundleArtifacts/ 216 | Package.StoreAssociation.xml 217 | _pkginfo.txt 218 | *.appx 219 | *.appxbundle 220 | *.appxupload 221 | 222 | # Visual Studio cache files 223 | # files ending in .cache can be ignored 224 | *.[Cc]ache 225 | # but keep track of directories ending in .cache 226 | !?*.[Cc]ache/ 227 | 228 | # Others 229 | ClientBin/ 230 | ~$* 231 | *~ 232 | *.dbmdl 233 | *.dbproj.schemaview 234 | *.jfm 235 | *.pfx 236 | *.publishsettings 237 | orleans.codegen.cs 238 | 239 | # Including strong name files can present a security risk 240 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 241 | #*.snk 242 | 243 | # Since there are multiple workflows, uncomment next line to ignore bower_components 244 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 245 | #bower_components/ 246 | 247 | # RIA/Silverlight projects 248 | Generated_Code/ 249 | 250 | # Backup & report files from converting an old project file 251 | # to a newer Visual Studio version. Backup files are not needed, 252 | # because we have git ;-) 253 | _UpgradeReport_Files/ 254 | Backup*/ 255 | UpgradeLog*.XML 256 | UpgradeLog*.htm 257 | ServiceFabricBackup/ 258 | *.rptproj.bak 259 | 260 | # SQL Server files 261 | *.mdf 262 | *.ldf 263 | *.ndf 264 | 265 | # Business Intelligence projects 266 | *.rdl.data 267 | *.bim.layout 268 | *.bim_*.settings 269 | *.rptproj.rsuser 270 | *- [Bb]ackup.rdl 271 | *- [Bb]ackup ([0-9]).rdl 272 | *- [Bb]ackup ([0-9][0-9]).rdl 273 | 274 | # Microsoft Fakes 275 | FakesAssemblies/ 276 | 277 | # GhostDoc plugin setting file 278 | *.GhostDoc.xml 279 | 280 | # Node.js Tools for Visual Studio 281 | .ntvs_analysis.dat 282 | node_modules/ 283 | 284 | # Visual Studio 6 build log 285 | *.plg 286 | 287 | # Visual Studio 6 workspace options file 288 | *.opt 289 | 290 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 291 | *.vbw 292 | 293 | # Visual Studio LightSwitch build output 294 | **/*.HTMLClient/GeneratedArtifacts 295 | **/*.DesktopClient/GeneratedArtifacts 296 | **/*.DesktopClient/ModelManifest.xml 297 | **/*.Server/GeneratedArtifacts 298 | **/*.Server/ModelManifest.xml 299 | _Pvt_Extensions 300 | 301 | # Paket dependency manager 302 | .paket/paket.exe 303 | paket-files/ 304 | 305 | # FAKE - F# Make 306 | .fake/ 307 | 308 | # CodeRush personal settings 309 | .cr/personal 310 | 311 | # Python Tools for Visual Studio (PTVS) 312 | __pycache__/ 313 | *.pyc 314 | 315 | # Cake - Uncomment if you are using it 316 | # tools/** 317 | # !tools/packages.config 318 | 319 | # Tabs Studio 320 | *.tss 321 | 322 | # Telerik's JustMock configuration file 323 | *.jmconfig 324 | 325 | # BizTalk build output 326 | *.btp.cs 327 | *.btm.cs 328 | *.odx.cs 329 | *.xsd.cs 330 | 331 | # OpenCover UI analysis results 332 | OpenCover/ 333 | 334 | # Azure Stream Analytics local run output 335 | ASALocalRun/ 336 | 337 | # MSBuild Binary and Structured Log 338 | *.binlog 339 | 340 | # NVidia Nsight GPU debugger configuration file 341 | *.nvuser 342 | 343 | # MFractors (Xamarin productivity tool) working folder 344 | .mfractor/ 345 | 346 | # Local History for Visual Studio 347 | .localhistory/ 348 | 349 | # BeatPulse healthcheck temp database 350 | healthchecksdb 351 | 352 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 353 | MigrationBackup/ 354 | 355 | # Ionide (cross platform F# VS Code tools) working folder 356 | .ionide/ 357 | -------------------------------------------------------------------------------- /.tours/DurableFunctionsTheory/.tours/1-orchestratorfunction.tour: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://aka.ms/codetour-schema", 3 | "title": "01 - Orchestrator Function", 4 | "steps": [ 5 | { 6 | "file": "../../DurableFunctionsTheory/durablefunctions.md", 7 | "description": "The `[OrchestrationTrigger]` is a binding that indicates this function is an orchestrator function.", 8 | "selection": { 9 | "start": { 10 | "line": 32, 11 | "character": 5 12 | }, 13 | "end": { 14 | "line": 32, 15 | "character": 27 16 | } 17 | }, 18 | "line": 32 19 | }, 20 | { 21 | "file": "../DurableFunctionsTheory/durablefunctions.md", 22 | "description": "The `GetInput` method on `IDurableOrchestrationContext` is used to retrieve the input that is passed in when this orchestrator instance was started. In this case the expected input is of type `string`.", 23 | "selection": { 24 | "start": { 25 | "line": 35, 26 | "character": 26 27 | }, 28 | "end": { 29 | "line": 35, 30 | "character": 34 31 | } 32 | }, 33 | "line": 35 34 | }, 35 | { 36 | "file": "/DurableFunctionsTheory/durablefunctions.md", 37 | "description": "Use `CallActivityAsync` to schedule a call to an activity function. Here the output is expected to be of type `User`.", 38 | "line": 37, 39 | "selection": { 40 | "start": { 41 | "line": 37, 42 | "character": 30 43 | }, 44 | "end": { 45 | "line": 37, 46 | "character": 47 47 | } 48 | } 49 | }, 50 | { 51 | "file": "../../DurableFunctionsTheory/durablefunctions.md", 52 | "description": "The first argument is the name of the actvity function that is called.", 53 | "line": 38 54 | }, 55 | { 56 | "file": "../../DurableFunctionsTheory/durablefunctions.md", 57 | "description": "The second argument is the input for the activity function. Activity functions only support one input parameter. ", 58 | "line": 39 59 | }, 60 | { 61 | "file": "../../DurableFunctionsTheory/durablefunctions.md", 62 | "description": "Activity functions do not require an output parameter. When the activity output is `void` (or `Task` for async activities) no output type is required for the `CallActivityAsync` method.", 63 | "line": 41 64 | }, 65 | { 66 | "file": "../../DurableFunctionsTheory/durablefunctions.md", 67 | "description": "Due to the replay functionality of the orchestrator function there are limits of what code is allowed inside an orchestrator. See [orchestrator code constraints](https://docs.microsoft.com/en-us/azure/azure-functions/durable/durable-functions-code-constraints) for additional info.", 68 | "line": 44 69 | } 70 | ], 71 | "ref": "None", 72 | "isPrimary": true 73 | } -------------------------------------------------------------------------------- /.tours/DurableFunctionsTheory/.tours/10-updatingentitystate.tour: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://aka.ms/codetour-schema", 3 | "title": "10 - Updating Entity State", 4 | "steps": [ 5 | { 6 | "file": "../DurableFunctionsTheory/statefulentities.md", 7 | "description": "First a new `EntityId` is created based on the `PlayerScoreEntity` class name and the player name.", 8 | "line": 68 9 | }, 10 | { 11 | "file": "../DurableFunctionsTheory/statefulentities.md", 12 | "description": "The `SignalEntity` method on the `IDurableOrchestrationContext` interface is used to signal (one way communication) to the `PlayerScoreEntity` instance.", 13 | "line": 69 14 | }, 15 | { 16 | "file": "../DurableFunctionsTheory/statefulentities.md", 17 | "description": "The first parameter is the entity ID.", 18 | "line": 70 19 | }, 20 | { 21 | "file": "../DurableFunctionsTheory/statefulentities.md", 22 | "description": "The second parameter is the name of the operation that will be signalled. Here the `Set` method will be signalled.", 23 | "line": 71 24 | }, 25 | { 26 | "file": "../DurableFunctionsTheory/statefulentities.md", 27 | "description": "The third parameter is the value that is passed to the signalled method. In this case the player score is sent to the entity.", 28 | "line": 72 29 | } 30 | ], 31 | "ref": "None" 32 | } -------------------------------------------------------------------------------- /.tours/DurableFunctionsTheory/.tours/2-activityfunction.tour: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://aka.ms/codetour-schema", 3 | "title": "02 - Activity Function", 4 | "steps": [ 5 | { 6 | "file": "../../DurableFunctionsTheory/durablefunctions.md", 7 | "description": "The `[ActivityTrigger]` is a binding that indicates the function is an activity function.\r\nThe input type can be a simple type or serializable POCO. \r\nActivity functions only support one input parameter.", 8 | "line": 62 9 | }, 10 | { 11 | "file": "../../DurableFunctionsTheory/durablefunctions.md", 12 | "description": "There are no restrictions on what you can execute inside an activity function. Usually there is communication with other APIs and databases, or performing CPU intensive work.", 13 | "line": 65 14 | } 15 | ], 16 | "ref": "None" 17 | } -------------------------------------------------------------------------------- /.tours/DurableFunctionsTheory/.tours/3-clientfunction.tour: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://aka.ms/codetour-schema", 3 | "title": "03 - Client Function", 4 | "steps": [ 5 | { 6 | "file": "../../DurableFunctionsTheory/durablefunctions.md", 7 | "description": "The `[DurableClient]` binding indicates this function is a client function.", 8 | "line": 82 9 | }, 10 | { 11 | "file": "../../DurableFunctionsTheory/durablefunctions.md", 12 | "description": "The `IDurableClient` client context contains methods to manage orchestrator functions. In this case the `StartNewAsync` method is used to start a new instance of the `UpdateUserScoreOrchestrator` orchestrator function.", 13 | "line": 87 14 | }, 15 | { 16 | "file": "../../DurableFunctionsTheory/durablefunctions.md", 17 | "description": "The first parameter of `StartNewAsync` is the name of the orchestrator function to be started.", 18 | "line": 88 19 | }, 20 | { 21 | "file": "../../DurableFunctionsTheory/durablefunctions.md", 22 | "description": "The second parameter of `StartNewAsync` is the input for the orchestrator.", 23 | "line": 89 24 | } 25 | ], 26 | "ref": "None" 27 | } -------------------------------------------------------------------------------- /.tours/DurableFunctionsTheory/.tours/4-sub-orchestrations.tour: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://aka.ms/codetour-schema", 3 | "title": "04 - Sub-orchestrations", 4 | "steps": [ 5 | { 6 | "file": "../../DurableFunctionsTheory/suborchestrations.md", 7 | "description": "Use the `CallSubOrchestratorAsync` method on the `IDurableOrchestrationContext` interface to call a sub-orchestrator. If the orchestrator returns a result, the output type needs to be specified as the generic parameter. Here an output of type `UserGameStats` is expected.", 8 | "line": 19, 9 | "selection": { 10 | "start": { 11 | "line": 19, 12 | "character": 28 13 | }, 14 | "end": { 15 | "line": 19, 16 | "character": 56 17 | } 18 | } 19 | }, 20 | { 21 | "file": "../../DurableFunctionsTheory/suborchestrations.md", 22 | "description": "The first parameter is the name of the orchestrator to start.", 23 | "line": 20 24 | }, 25 | { 26 | "file": "../../DurableFunctionsTheory/suborchestrations.md", 27 | "description": "The second parameter is the input for the orchestrator. If no input is required a value of `null` can be used.", 28 | "line": 21 29 | }, 30 | { 31 | "file": "../../DurableFunctionsTheory/suborchestrations.md", 32 | "description": "Here a sub-orchestrator is called that has no return value.", 33 | "line": 23 34 | } 35 | ], 36 | "ref": "None" 37 | } -------------------------------------------------------------------------------- /.tours/DurableFunctionsTheory/.tours/5-eternal-orchestrations.tour: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://aka.ms/codetour-schema", 3 | "title": "05 - Eternal orchestrations", 4 | "steps": [ 5 | { 6 | "file": "../DurableFunctionsTheory/eternalorchestrations.md", 7 | "description": "Retrieve the list of items to clean up.", 8 | "line": 16 9 | }, 10 | { 11 | "file": "../DurableFunctionsTheory/eternalorchestrations.md", 12 | "description": "Call the activity to perform the clean up. This returns an updated list of items to clean up.", 13 | "line": 18 14 | }, 15 | { 16 | "file": "../DurableFunctionsTheory/eternalorchestrations.md", 17 | "description": "If there are still items left to clean up, a timer is created using the `CreateTime` method on the `IDurableOrchestrationContext` interface. Once the `nextCleanup` time has passed the orchestrator will continue the execution.", 18 | "line": 25, 19 | "selection": { 20 | "start": { 21 | "line": 25, 22 | "character": 28 23 | }, 24 | "end": { 25 | "line": 25, 26 | "character": 56 27 | } 28 | } 29 | }, 30 | { 31 | "file": "../DurableFunctionsTheory/eternalorchestrations.md", 32 | "description": "The `ContinueAsNew` method on the `IDurableOrchestrationContext` interface is used to restart the orchestrator. Here the updated list of items to cleanup are passed in. If the orchestrator does not expect any input, `null` should be passed in.", 33 | "line": 27, 34 | "selection": { 35 | "start": { 36 | "line": 27, 37 | "character": 28 38 | }, 39 | "end": { 40 | "line": 27, 41 | "character": 56 42 | } 43 | } 44 | } 45 | ], 46 | "ref": "None" 47 | } -------------------------------------------------------------------------------- /.tours/DurableFunctionsTheory/.tours/6-waiting-for-events.tour: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://aka.ms/codetour-schema", 3 | "title": "06 - Waiting for Events", 4 | "steps": [ 5 | { 6 | "file": "../DurableFunctionsTheory/events.md", 7 | "description": "The `WaitForExternalEvent` method is used to indicate the orchestrator function should wait before executing the rest of the workflow. In this case we expect to receive a boolean value for the event named *ApprovalEvent*.\r\n\r\n- The first parameter is the name of the event it should wait for.\r\n- The second parameter is a timeout value. If the event is not raised within this timeout the execution will continue.\r\n- The third parameter is the default value which is used when the wait event has timed out.", 8 | "line": 17 9 | }, 10 | { 11 | "file": "../DurableFunctionsTheory/events.md", 12 | "description": "The result of an event can be used to allow different execution paths in the orchestrator code.", 13 | "line": 18 14 | } 15 | ], 16 | "ref": "None" 17 | } -------------------------------------------------------------------------------- /.tours/DurableFunctionsTheory/.tours/7-raising-events.tour: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://aka.ms/codetour-schema", 3 | "title": "07 - Raising Events", 4 | "steps": [ 5 | { 6 | "file": "../DurableFunctionsTheory/events.md", 7 | "description": "Use the `RaiseEventAsync` method on the `IDurableClient` interface to raise events to a specific orchestrator instance.", 8 | "line": 47, 9 | "selection": { 10 | "start": { 11 | "line": 47, 12 | "character": 18 13 | }, 14 | "end": { 15 | "line": 47, 16 | "character": 33 17 | } 18 | } 19 | }, 20 | { 21 | "file": "../DurableFunctionsTheory/events.md", 22 | "description": "- The first parameter is the instance ID of the orchestrator that is waiting for the event.\r\n- The second parameter is the name of the event that is raised.\r\n- The third parameter is the value of the event. This can be a simple type or a serializable POCO.", 23 | "line": 50 24 | } 25 | ], 26 | "ref": "None" 27 | } -------------------------------------------------------------------------------- /.tours/DurableFunctionsTheory/.tours/8-statefulentities.tour: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://aka.ms/codetour-schema", 3 | "title": "08 - Entity Definition", 4 | "steps": [ 5 | { 6 | "file": "../DurableFunctionsTheory/statefulentities.md", 7 | "description": "This entity is created as a class based definition. It it recommended to use the `[JsonObject(MemberSerialization.OptIn)]` attribute and explicitly specify the members of this class that require to be serialized and stored.", 8 | "line": 13, 9 | "selection": { 10 | "start": { 11 | "line": 13, 12 | "character": 1 13 | }, 14 | "end": { 15 | "line": 13, 16 | "character": 40 17 | } 18 | } 19 | }, 20 | { 21 | "file": "../DurableFunctionsTheory/statefulentities.md", 22 | "description": "The `PlayerScoreEntity` has a Score property which is the only member that is serialized/stored. Updating this property is done via methods on the entity.", 23 | "line": 16, 24 | "selection": { 25 | "start": { 26 | "line": 16, 27 | "character": 14 28 | }, 29 | "end": { 30 | "line": 16, 31 | "character": 31 32 | } 33 | } 34 | }, 35 | { 36 | "file": "../DurableFunctionsTheory/statefulentities.md", 37 | "description": "The `Set` method updates the `Score` property with the supplied value.", 38 | "line": 18 39 | }, 40 | { 41 | "file": "../DurableFunctionsTheory/statefulentities.md", 42 | "description": "The `Reset` method sets the `Score` to 0.", 43 | "line": 20 44 | }, 45 | { 46 | "file": "../DurableFunctionsTheory/statefulentities.md", 47 | "description": "The `[EntityTrigger]` binding defines this function is an entity function.", 48 | "line": 24, 49 | "selection": { 50 | "start": { 51 | "line": 24, 52 | "character": 25 53 | }, 54 | "end": { 55 | "line": 24, 56 | "character": 46 57 | } 58 | } 59 | }, 60 | { 61 | "file": "../DurableFunctionsTheory/statefulentities.md", 62 | "description": "The `DispatchAsync` method in the `IDurableEntityContext` interface dispatches the incoming entity operation using reflection.", 63 | "line": 25 64 | } 65 | ], 66 | "ref": "None" 67 | } -------------------------------------------------------------------------------- /.tours/DurableFunctionsTheory/.tours/9-readingentitystate.tour: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://aka.ms/codetour-schema", 3 | "title": "09 - Reading Entity State", 4 | "steps": [ 5 | { 6 | "file": "../DurableFunctionsTheory/statefulentities.md", 7 | "description": "A new `EntityId` is created based on the name of the entity class and the entity key. In this case, the key is the player name.", 8 | "line": 46 9 | }, 10 | { 11 | "file": "../DurableFunctionsTheory/statefulentities.md", 12 | "description": "Using the `ReadEntityStateAsync` method on the `IDurableClient` interface the entity state is read for the provided `entityId`. The return value is of type `EntityStateResponse`.", 13 | "line": 47, 14 | "selection": { 15 | "start": { 16 | "line": 47, 17 | "character": 21 18 | }, 19 | "end": { 20 | "line": 47, 21 | "character": 35 22 | } 23 | } 24 | }, 25 | { 26 | "file": "../DurableFunctionsTheory/statefulentities.md", 27 | "description": "It could be that no entity exists for the given combination of entity class name and player name. So it's a good practice to use the `EntityExists` method on the `EntityStateResponse` before accessing the entity properties.", 28 | "line": 48 29 | }, 30 | { 31 | "file": "../DurableFunctionsTheory/statefulentities.md", 32 | "description": "Here the `Score` property is accessed on the `EntityStateResponse` type.", 33 | "line": 50 34 | } 35 | ], 36 | "ref": "None" 37 | } -------------------------------------------------------------------------------- /.tours/NotifySupport/.tours/90-notify-support-solution.tour: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://aka.ms/codetour-schema", 3 | "title": "90 - Notify Support Solution", 4 | "steps": [ 5 | { 6 | "file": "../../NotifySupport/src/DurableFunctions.UseCases.NotifySupport/Models/NotifySupportClientInput.cs", 7 | "description": "The `NotifySupportClientInput` class is used as the payload when triggering the `NotifySupportHttpClient` function. It only contains a message (string) and a severity (int) related to the alert.", 8 | "line": 3, 9 | "selection": { 10 | "start": { 11 | "line": 3, 12 | "character": 18 13 | }, 14 | "end": { 15 | "line": 3, 16 | "character": 37 17 | } 18 | }, 19 | "title": "NotifySupportClientInput" 20 | }, 21 | { 22 | "file": "./NotifySupport/src/DurableFunctions.UseCases.NotifySupport/Clients/NotifySupportHttpClient.cs", 23 | "description": "An `HttpTrigger` function is used to act as the client function. This function will start a new instance of the `NotifySupportOrchestrator` function, the main orchestrator.\r\n\r\nThe `[DurableClient]` binding indicates this is a Durable Client function.", 24 | "line": 19, 25 | "selection": { 26 | "start": { 27 | "line": 32, 28 | "character": 23 29 | }, 30 | "end": { 31 | "line": 32, 32 | "character": 48 33 | } 34 | }, 35 | "title": "NotifySupportHttpClient bindings" 36 | }, 37 | { 38 | "file": "../NotifySupport/src/DurableFunctions.UseCases.NotifySupport/Clients/NotifySupportHttpClient.cs", 39 | "description": "The `NotifySupportClientInput` object is read from the request body.", 40 | "line": 22, 41 | "selection": { 42 | "start": { 43 | "line": 22, 44 | "character": 65 45 | }, 46 | "end": { 47 | "line": 22, 48 | "character": 89 49 | } 50 | }, 51 | "title": "NotifySupportHttpClient read input" 52 | }, 53 | { 54 | "file": "../NotifySupport/src/DurableFunctions.UseCases.NotifySupport/Clients/NotifySupportHttpClient.cs", 55 | "description": "Two values are read from environment variables. These are stored in the `local.settings.json` file when developing locally. Once the function app is published they're read from the App Settings.\r\n\r\n- `WaitTimeForEscalationInSeconds`, is the wait time before escalating to another member of the support team.\r\n- `MaxNotificationAttempts`, is the number of attempts that is made to notify one support contact.", 56 | "line": 24, 57 | "selection": { 58 | "start": { 59 | "line": 24, 60 | "character": 92 61 | }, 62 | "end": { 63 | "line": 24, 64 | "character": 115 65 | } 66 | }, 67 | "title": "NotifySupportHttpClient read env vars" 68 | }, 69 | { 70 | "file": "../NotifySupport/src/DurableFunctions.UseCases.NotifySupport/local.settings.json", 71 | "description": "The `local.settings.json` contains the values for `MaxNotificationAttempts` and `WaitTimeForEscalationInSeconds`.", 72 | "line": 10, 73 | "selection": { 74 | "start": { 75 | "line": 10, 76 | "character": 6 77 | }, 78 | "end": { 79 | "line": 10, 80 | "character": 36 81 | } 82 | }, 83 | "title": "local.settings.json" 84 | }, 85 | { 86 | "file": "../NotifySupport/src/DurableFunctions.UseCases.NotifySupport/Clients/NotifySupportHttpClient.cs", 87 | "description": "I regularly use Builders to create new object instances. Here the `NotifySupportOrchestratorInputBuilder` is called to create the input for the `NotifySupportOrchestrator`.", 88 | "line": 29, 89 | "selection": { 90 | "start": { 91 | "line": 26, 92 | "character": 37 93 | }, 94 | "end": { 95 | "line": 26, 96 | "character": 74 97 | } 98 | }, 99 | "title": "NotifySupportHttpClient build input" 100 | }, 101 | { 102 | "file": "../NotifySupport/src/DurableFunctions.UseCases.NotifySupport/Models/NotifySupportOrchestratorInput.cs", 103 | "description": "The `NotifySupportOrchestratorInput` defines the input for the `NotifySupportOrchestrator` function, the main orchestrator.\r\n\r\nThe orchestrator needs the following information:\r\n\r\n- The message and severity of the alert.\r\n- The list of support contacts.\r\n- The current support contact.\r\n- The wait time between escalations.\r\n- The max number of notification attempts per contact.", 104 | "line": 11, 105 | "selection": { 106 | "start": { 107 | "line": 3, 108 | "character": 18 109 | }, 110 | "end": { 111 | "line": 3, 112 | "character": 43 113 | } 114 | }, 115 | "title": "NotifySupportOrchestratorInput" 116 | }, 117 | { 118 | "file": "../NotifySupport/src/DurableFunctions.UseCases.NotifySupport/Clients/NotifySupportHttpClient.cs", 119 | "description": "A new `NotifySupportOrchestrator` instance is created & started. This is the main orchestrator.", 120 | "line": 33, 121 | "selection": { 122 | "start": { 123 | "line": 32, 124 | "character": 23 125 | }, 126 | "end": { 127 | "line": 32, 128 | "character": 48 129 | } 130 | }, 131 | "title": "NotifySupportHttpClient start main orchestrator" 132 | }, 133 | { 134 | "file": "../NotifySupport/src/DurableFunctions.UseCases.NotifySupport/Clients/NotifySupportHttpClient.cs", 135 | "description": "A response is sent back to the client to indicate the `NotifySupportOrchestrator` has been started.", 136 | "line": 35, 137 | "title": "NotifySupportHttpClient response" 138 | }, 139 | { 140 | "file": "../NotifySupport/src/DurableFunctions.UseCases.NotifySupport/Orchestrators/NotifySupportOrchestrator.cs", 141 | "description": "This is the main orchestrator, `NotifySupportOrchestrator`. This orchestrator will call an activity to read the support contacts from Table Storage, iterate over the contacts and start a sub-orchestration to notify the contact.\r\n\r\nThe `[OrchestrationTrigger]` binding is used to indicate this is an orchestrator function.", 142 | "line": 15, 143 | "selection": { 144 | "start": { 145 | "line": 14, 146 | "character": 14 147 | }, 148 | "end": { 149 | "line": 14, 150 | "character": 34 151 | } 152 | }, 153 | "title": "NotifySupportOrchestrator purpose and bindings" 154 | }, 155 | { 156 | "file": "../NotifySupport/src/DurableFunctions.UseCases.NotifySupport/Orchestrators/NotifySupportOrchestrator.cs", 157 | "description": "The `NotifySupportOrchestratorInput` is read from the orchestration context.", 158 | "line": 17, 159 | "selection": { 160 | "start": { 161 | "line": 17, 162 | "character": 42 163 | }, 164 | "end": { 165 | "line": 17, 166 | "character": 72 167 | } 168 | }, 169 | "title": "NotifySupportOrchestrator read input" 170 | }, 171 | { 172 | "file": "../NotifySupport/src/DurableFunctions.UseCases.NotifySupport/Orchestrators/NotifySupportOrchestrator.cs", 173 | "description": "The `SupportContacts` property is initially null. It will be populated with support contacts once the `GetSupportContactActivity` is called. \r\nBecause the `ContinueAsNew` functionalityy is used later in this orchestrator this `if` statement was added so it will only run for the first full execution (not taking into account the regular replay behaviour of the orchestrator).\r\n\r\n", 174 | "line": 19, 175 | "selection": { 176 | "start": { 177 | "line": 42, 178 | "character": 25 179 | }, 180 | "end": { 181 | "line": 42, 182 | "character": 38 183 | } 184 | }, 185 | "title": "NotifySupportOrchestrator SupportContacts check" 186 | }, 187 | { 188 | "file": "../NotifySupport/src/DurableFunctions.UseCases.NotifySupport/Orchestrators/NotifySupportOrchestrator.cs", 189 | "description": "The `GetSupportContactActivity` function is scheduled which returns a list of `SupportContactEntity`. The input for the activity function is hardcoded, `\"A\"`, since we're only interested in support contacts that belong to team A.", 190 | "line": 24, 191 | "selection": { 192 | "start": { 193 | "line": 22, 194 | "character": 83 195 | }, 196 | "end": { 197 | "line": 22, 198 | "character": 103 199 | } 200 | }, 201 | "title": "NotifySupportOrchestrator get support contacts" 202 | }, 203 | { 204 | "file": "../NotifySupport/src/DurableFunctions.UseCases.NotifySupport/Activities/GetSupportContactActivity.cs", 205 | "description": "The `GetSupportContactActivity` function uses two bindings:\r\n\r\n- `[ActivityTrigger]` to indicate this is an activity function.\r\n- `[Table]` input binding configured to the `SupportContact` table.", 206 | "line": 14, 207 | "selection": { 208 | "start": { 209 | "line": 16, 210 | "character": 46 211 | }, 212 | "end": { 213 | "line": 16, 214 | "character": 66 215 | } 216 | }, 217 | "title": "GetSupportContactActivity bindings" 218 | }, 219 | { 220 | "file": "../NotifySupport/src/DurableFunctions.UseCases.NotifySupport/Activities/GetSupportContactActivity.cs", 221 | "description": "A `TableQuery` is created to query and filter the `SupportContactEntity` objects based on the team they are in (this is the `PartitionKey`), and they are ordered by their `Order` property.", 222 | "line": 22, 223 | "title": "GetSupportContactActivity filter" 224 | }, 225 | { 226 | "file": "../NotifySupport/src/DurableFunctions.UseCases.NotifySupport/Activities/GetSupportContactActivity.cs", 227 | "description": "The query is executed on the `CloudTable` from the input binding and returns a collection of `SupportContactEntity`.", 228 | "line": 23, 229 | "selection": { 230 | "start": { 231 | "line": 23, 232 | "character": 59 233 | }, 234 | "end": { 235 | "line": 23, 236 | "character": 79 237 | } 238 | }, 239 | "title": "GetSupportContactActivity ExecuteQuery" 240 | }, 241 | { 242 | "file": "../NotifySupport/src/DurableFunctions.UseCases.NotifySupport/Orchestrators/NotifySupportOrchestrator.cs", 243 | "description": "Once we have the support contacts we add them to the `NotifySupportOrchestratorInput` object so we're reusing it when `ContinueAsNew` is used on the orchestrator.", 244 | "line": 26, 245 | "selection": { 246 | "start": { 247 | "line": 17, 248 | "character": 42 249 | }, 250 | "end": { 251 | "line": 17, 252 | "character": 72 253 | } 254 | }, 255 | "title": "NotifySupportOrchestrator update SupportContacts" 256 | }, 257 | { 258 | "file": "../NotifySupport/src/DurableFunctions.UseCases.NotifySupport/Orchestrators/NotifySupportOrchestrator.cs", 259 | "description": "A builder is used to create the input object for the sub-orchestrator (`SendNotificationOrchestrator`).", 260 | "line": 29, 261 | "selection": { 262 | "start": { 263 | "line": 34, 264 | "character": 28 265 | }, 266 | "end": { 267 | "line": 34, 268 | "character": 56 269 | } 270 | }, 271 | "title": "NotifySupportOrchestrator build input" 272 | }, 273 | { 274 | "file": "../NotifySupport/src/DurableFunctions.UseCases.NotifySupport/Models/SendNotificationOrchestratorInput.cs", 275 | "description": "The `SendNotificationOrchestratorInput` is the input for the `SendNotificationOrchestrator` sub-orchestrator function. The sub-orchestrator needs to know the following:\r\n\r\n- Who to send the notification to.\r\n- The message for the notification.\r\n- The current notification attempt.\r\n- The maximum notificaiton attempts.\r\n- The max time between escalations.\r\n", 276 | "line": 10, 277 | "selection": { 278 | "start": { 279 | "line": 3, 280 | "character": 18 281 | }, 282 | "end": { 283 | "line": 3, 284 | "character": 46 285 | } 286 | }, 287 | "title": "SendNotificationOrchestratorInput" 288 | }, 289 | { 290 | "file": "../NotifySupport/src/DurableFunctions.UseCases.NotifySupport/Orchestrators/NotifySupportOrchestrator.cs", 291 | "description": "The sub-orchestrator, `SendNotificationOrchestrator`, is scheduled and awaited. The response is a `SendNotificationOrchestratorResult` object.", 292 | "line": 33, 293 | "title": "NotifySupportOrchestrator call sub-orchestrator" 294 | }, 295 | { 296 | "file": "../NotifySupport/src/DurableFunctions.UseCases.NotifySupport/Models/SendNotificationOrchestratorResult.cs", 297 | "description": "The `SendNotificationOrchestratorResult` contains:\r\n\r\n- The number of attempts that has been made to notify a support contact. \r\n- An indicator if a callback event has been received in the sub-orchestrator.\r\n- The phone number of the support contact.", 298 | "line": 8, 299 | "selection": { 300 | "start": { 301 | "line": 9, 302 | "character": 1 303 | }, 304 | "end": { 305 | "line": 9, 306 | "character": 2 307 | } 308 | }, 309 | "title": "SendNotificationOrchestratorResult" 310 | }, 311 | { 312 | "file": "../NotifySupport/src/DurableFunctions.UseCases.NotifySupport/Orchestrators/NotifySupportOrchestrator.cs", 313 | "description": "When no callback event has been received and the current support contact is not equal to the last contact in the support contact array, the `SupportContactIndex` is incremented by one and the (main) orchestrator is continued as new instance. This means the orchestrator will now use the next support contact in the list to try and notify.", 314 | "line": 41, 315 | "title": "NotifySupportOrchestrator ContinueAsNew" 316 | }, 317 | { 318 | "file": "../NotifySupport/src/DurableFunctions.UseCases.NotifySupport/Orchestrators/NotifySupportOrchestrator.cs", 319 | "description": "When the callback has been received or if the current contact is equal to the last support contact in the array the main orchestrator is complete.", 320 | "line": 46, 321 | "title": "NotifySupportOrchestrator complete" 322 | }, 323 | { 324 | "file": "../NotifySupport/src/DurableFunctions.UseCases.NotifySupport/Orchestrators/SendNotificationOrchestrator.cs", 325 | "description": "The `SendNotificationOrchestrator` function is the sub-orchestrator. It handles scheduling of the notification using the `SendNotificationActivity` activity, and it waits for the corresponding callback event. \r\n\r\nThe `ContinueAsNew` functionality is used in this sub-orchestration to retry the `SendNotificationActivity` and wait for the callback for the configured number of retries within the maximum time before escalation is needed.\r\n\r\nThe `[OrchestrationTrigger]` binding is used to indicate this is an orchestrator function.", 326 | "line": 14, 327 | "selection": { 328 | "start": { 329 | "line": 31, 330 | "character": 24 331 | }, 332 | "end": { 333 | "line": 31, 334 | "character": 48 335 | } 336 | }, 337 | "title": "SendNotificationOrchestrator purpose" 338 | }, 339 | { 340 | "file": "../NotifySupport/src/DurableFunctions.UseCases.NotifySupport/Orchestrators/SendNotificationOrchestrator.cs", 341 | "description": "The `SendNotificationOrchestratorInput` is read from the orchestrator context.", 342 | "line": 16, 343 | "selection": { 344 | "start": { 345 | "line": 16, 346 | "character": 42 347 | }, 348 | "end": { 349 | "line": 16, 350 | "character": 75 351 | } 352 | }, 353 | "title": "SendNotificationOrchestrator read input" 354 | }, 355 | { 356 | "file": "../NotifySupport/src/DurableFunctions.UseCases.NotifySupport/Orchestrators/SendNotificationOrchestrator.cs", 357 | "description": "Before the `SendNotificationActivity` is called and we wait for the callback event we need to store the instance ID of this sub-orchestrator. This is neccesary since events can only be raised for a specific orchestrator instance and we don't receive the instance ID as part of the callback request. Here we'll use Durable Entities to save the instance ID using the phone number as the key. In the `CallbackHttpClient` we retrieve the instance ID using the phone number from the request.\r\n\r\nFirst an EntityId is created based on the name of the entity (`NotificationOrchestratorInstanceEntity`) and the key (phone number of the support contact).", 358 | "line": 19, 359 | "selection": { 360 | "start": { 361 | "line": 19, 362 | "character": 48 363 | }, 364 | "end": { 365 | "line": 19, 366 | "character": 86 367 | } 368 | }, 369 | "title": "SendNotificationOrchestrator Durable Entities" 370 | }, 371 | { 372 | "file": "../NotifySupport/src/DurableFunctions.UseCases.NotifySupport/Entities/NotificationOrchestratorInstanceEntity.cs", 373 | "description": "The `NotificationOrchestratorInstanceEntity` class defines the Durable Entity which captures the instance ID of the sub-orchestrator which is required to raise the callback event to correct `SendNotificationOrchestrator` instance.", 374 | "line": 9, 375 | "selection": { 376 | "start": { 377 | "line": 9, 378 | "character": 18 379 | }, 380 | "end": { 381 | "line": 9, 382 | "character": 56 383 | } 384 | }, 385 | "title": "NotificationOrchestratorInstanceEntity class" 386 | }, 387 | { 388 | "file": "../NotifySupport/src/DurableFunctions.UseCases.NotifySupport/Entities/NotificationOrchestratorInstanceEntity.cs", 389 | "description": "Only the `InstanceId` property will be persisted to storage.", 390 | "line": 12, 391 | "title": "NotificationOrchestratorInstanceEntity InstanceId" 392 | }, 393 | { 394 | "file": "../NotifySupport/src/DurableFunctions.UseCases.NotifySupport/Entities/NotificationOrchestratorInstanceEntity.cs", 395 | "description": "The `Set` method is used to update the `InstanceId` property from the `SendNotificationOrchestrator` function.", 396 | "line": 14, 397 | "title": "NotificationOrchestratorInstanceEntity Set" 398 | }, 399 | { 400 | "file": "../NotifySupport/src/DurableFunctions.UseCases.NotifySupport/Entities/NotificationOrchestratorInstanceEntity.cs", 401 | "description": "The `[EntityTrigger]` binding indicates this is an entity function. The `DispatchAsync` method ensures we can perform operations on the entity.", 402 | "line": 21, 403 | "selection": { 404 | "start": { 405 | "line": 21, 406 | "character": 20 407 | }, 408 | "end": { 409 | "line": 21, 410 | "character": 33 411 | } 412 | }, 413 | "title": "NotificationOrchestratorInstanceEntity EntityTrigger" 414 | }, 415 | { 416 | "file": "../NotifySupport/src/DurableFunctions.UseCases.NotifySupport/Orchestrators/SendNotificationOrchestrator.cs", 417 | "description": "The entity is signalled (one-way communication) to perform the `Set` operation with the `InstanceId` as the value.", 418 | "line": 23, 419 | "title": "SendNotificationOrchestrator SignalEntity" 420 | }, 421 | { 422 | "file": "../NotifySupport/src/DurableFunctions.UseCases.NotifySupport/Orchestrators/SendNotificationOrchestrator.cs", 423 | "description": "An input object for the `SendNotificationActivity` function is built that contains the number of attempts sending the notification, the alert message, and the phone number of the support contact.", 424 | "line": 28, 425 | "selection": { 426 | "start": { 427 | "line": 30, 428 | "character": 24 429 | }, 430 | "end": { 431 | "line": 30, 432 | "character": 48 433 | } 434 | }, 435 | "title": "SendNotificationOrchestrator build input" 436 | }, 437 | { 438 | "file": "../NotifySupport/src/DurableFunctions.UseCases.NotifySupport/Orchestrators/SendNotificationOrchestrator.cs", 439 | "description": "The `SendNotificationActivity` function is scheduled which takes care of sending the notification. The implementation of this activity can be skipped since we can simulate the callback by manually calling the `CallbackHttpClient` function. \r\n\r\nFor the real-life scenario you can implement a call to Twillio or Azure Communication Services. This should then also include configuration of the callback once the support contact has responded to the notification.", 440 | "line": 31, 441 | "title": "SendNotificationOrchestrator call SendNotificationActivity" 442 | }, 443 | { 444 | "file": "../NotifySupport/src/DurableFunctions.UseCases.NotifySupport/Orchestrators/SendNotificationOrchestrator.cs", 445 | "description": "The Durable Functions API contains a very useful method to wait for events, where we can also specify the maximum time we want to wait for the event (and a fallback value is used). Here we divide the time for the next escalation by the maximum number of notification retries for this contact. This results in a time we'll use as a timeout for the event.", 446 | "line": 33, 447 | "title": "SendNotificationOrchestrator calculate wait time" 448 | }, 449 | { 450 | "file": "../NotifySupport/src/DurableFunctions.UseCases.NotifySupport/Orchestrators/SendNotificationOrchestrator.cs", 451 | "description": "The sub-orchestrator now waits for an external event of type `bool` named `Callback`. The maximum wait time and the default value have been added to ensure we respect the escalation time between support contacts.", 452 | "line": 36, 453 | "selection": { 454 | "start": { 455 | "line": 36, 456 | "character": 96 457 | }, 458 | "end": { 459 | "line": 36, 460 | "character": 116 461 | } 462 | }, 463 | "title": "SendNotificationOrchestrator WaitForExternalEvent" 464 | }, 465 | { 466 | "file": "../NotifySupport/src/DurableFunctions.UseCases.NotifySupport/host.json", 467 | "description": "To ensure more accurate timings of the WaitForExternalEvent operation set `storageProvider.maxQueuePollingInterval` to a lower value (default is 30 seconds).", 468 | "line": 7, 469 | "selection": { 470 | "start": { 471 | "line": 6, 472 | "character": 14 473 | }, 474 | "end": { 475 | "line": 6, 476 | "character": 29 477 | } 478 | }, 479 | "title": "host.json maxQueuePollingInterval" 480 | }, 481 | { 482 | "file": "../NotifySupport/src/DurableFunctions.UseCases.NotifySupport/Orchestrators/SendNotificationOrchestrator.cs", 483 | "description": "When a callback is not received within the given time and the number of notification attemps is lower than the maximum number of attempts the `NotificationAttemptCount` property of the input is increased and the orchestration is continued as new.", 484 | "line": 42, 485 | "selection": { 486 | "start": { 487 | "line": 40, 488 | "character": 23 489 | }, 490 | "end": { 491 | "line": 40, 492 | "character": 47 493 | } 494 | }, 495 | "title": "SendNotificationOrchestrator ContinueAsNew" 496 | }, 497 | { 498 | "file": "../NotifySupport/src/DurableFunctions.UseCases.NotifySupport/Orchestrators/SendNotificationOrchestrator.cs", 499 | "description": "When the callback has been received or the maximum number of notification attempts (for this contact) has been reached the sub-orchestrator returns its result to the main orchestrator.", 500 | "line": 50, 501 | "title": "SendNotificationOrchestrator return result" 502 | } 503 | ], 504 | "ref": "None" 505 | } -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "ms-azuretools.vscode-azurefunctions", 4 | "ms-dotnettools.csharp", 5 | "Azurite.azurite", 6 | "vsls-contrib.codetour" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /DurableFunctionsTheory/diagrams/durablefunction_types.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcduiker/durable-functions-use-cases/a7e57a0d3b13b63aa1bf8ee110bc6b9ec44197a3/DurableFunctionsTheory/diagrams/durablefunction_types.png -------------------------------------------------------------------------------- /DurableFunctionsTheory/diagrams/eternal_orchestrations.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcduiker/durable-functions-use-cases/a7e57a0d3b13b63aa1bf8ee110bc6b9ec44197a3/DurableFunctionsTheory/diagrams/eternal_orchestrations.png -------------------------------------------------------------------------------- /DurableFunctionsTheory/diagrams/orchestrator_replay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcduiker/durable-functions-use-cases/a7e57a0d3b13b63aa1bf8ee110bc6b9ec44197a3/DurableFunctionsTheory/diagrams/orchestrator_replay.png -------------------------------------------------------------------------------- /DurableFunctionsTheory/diagrams/raiseevents.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcduiker/durable-functions-use-cases/a7e57a0d3b13b63aa1bf8ee110bc6b9ec44197a3/DurableFunctionsTheory/diagrams/raiseevents.png -------------------------------------------------------------------------------- /DurableFunctionsTheory/diagrams/stateful_entities.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcduiker/durable-functions-use-cases/a7e57a0d3b13b63aa1bf8ee110bc6b9ec44197a3/DurableFunctionsTheory/diagrams/stateful_entities.png -------------------------------------------------------------------------------- /DurableFunctionsTheory/diagrams/sub-orchestrators.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcduiker/durable-functions-use-cases/a7e57a0d3b13b63aa1bf8ee110bc6b9ec44197a3/DurableFunctionsTheory/diagrams/sub-orchestrators.png -------------------------------------------------------------------------------- /DurableFunctionsTheory/diagrams/waitforexternalevent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcduiker/durable-functions-use-cases/a7e57a0d3b13b63aa1bf8ee110bc6b9ec44197a3/DurableFunctionsTheory/diagrams/waitforexternalevent.png -------------------------------------------------------------------------------- /DurableFunctionsTheory/durablefunctions.md: -------------------------------------------------------------------------------- 1 | # Durable Functions 2 | 3 | Durable Functions allows you to write workflows (orchestrations) in code. The state of the workflow instances are stored which enables many use cases such as: 4 | 5 | - Chaining function calls. 6 | - Execute functions in parallel and wait for their completion (fan-out/fan-in). 7 | - Waiting for external events. 8 | - Executing long running, and even eternal workflows. 9 | 10 | You can use Durable Functions in C# by referencing the [Microsoft.Azure.WebJobs.Extensions.DurableTask](https://www.nuget.org/packages/Microsoft.Azure.WebJobs.Extensions.DurableTask) NuGet package. 11 | 12 | ## Function types 13 | 14 | Durable Functions uses four types of functions: 15 | 16 | - Client functions 17 | - Orchestrator functions 18 | - Activity functions 19 | - Entity functions 20 | 21 | ![Durable Function Types](diagrams/durablefunction_types.png) 22 | 23 | ### Orchestrator Functions 24 | 25 | Orchestrator functions define the workflow as code. It contains the logic which steps (activities) are performed, and in which order. 26 | 27 | This is an example of an orchestrator where two functions are chained. The second activity function (`UpdatePlayerScoreActivity`) requires the output of the first activity function (`GetPlayerActivity`). 28 | 29 | ```csharp 30 | [FunctionName(nameof(UpdatePlayerScoreOrchestrator))] 31 | public async Task Run( 32 | [OrchestrationTrigger] IDurableOrchestrationContext context, 33 | ILogger logger) 34 | { 35 | var playerId = context.GetInput(); 36 | 37 | var player = await context.CallActivityAsync( 38 | nameof(GetPlayerActivity), 39 | playerId); 40 | 41 | await context.CallActivityAsync( 42 | nameof(UpdatePlayerScoreActivity), 43 | player); 44 | } 45 | ``` 46 | 47 | #### Orchestrator Replay 48 | 49 | ![Orchestrator Replay](diagrams/orchestrator_replay.png) 50 | 51 | It is important to realize that an orchestrator function is not executed once. It is replayed several times, depending on the number of activities it calls. For each activity the orchestrator calls, the orchestrator execution is stopped. As soon as an activity is done, the orchestrator restarts. Since the state of the orchestrator instance is persisted (including the inputs and outputs of activity functions) it won't execute activity functions multiple times. The Durable Functions framework checks if the activity function has been executed and if so, it will retrieve the state to use that in the orchestrator. 52 | 53 | ### Activity Functions 54 | 55 | Activity functions are the units of work which the workflow orchestrates. These functions usually deal with calling external APIs and interacting with databases. 56 | 57 | The Durable Function framework guarantees that activity functions are executed at least once as part of an orchestrator. 58 | 59 | ```csharp 60 | [FunctionName(nameof(GetPlayerActivity))] 61 | public async Task Run( 62 | [ActivityTrigger] string playerId, 63 | ILogger logger) 64 | { 65 | // Call external API or database. 66 | // var player = await gamePlayerService.GetPlayerAsync(playerId); 67 | // return player; 68 | } 69 | ``` 70 | 71 | ### Client Functions 72 | 73 | A client function is responsible for creating a new instance of a workflow, also known as an orchestrator instance. It can perform other operations on an orchestrator instance as well, such as: querying the orchestrator state, and terminating the instance. 74 | 75 | ```csharp 76 | [FunctionName(nameof(UpdatePlayerScoreClient))] 77 | public static async Task Run( 78 | [HttpTrigger( 79 | AuthorizationLevel.Function, 80 | nameof(HttpMethod.Post), 81 | Route = null)] HttpRequestMessage message, 82 | [DurableClient] IDurableClient client, 83 | ILogger logger) 84 | { 85 | var playerId = await message.Content.ReadAsAsync(); 86 | 87 | string instanceId = await client.StartNewAsync( 88 | nameof(UpdatePlayerScoreOrchestrator), 89 | playerId); 90 | 91 | return client.CreateCheckStatusResponse(message, instanceId); 92 | } 93 | ``` 94 | 95 | ### Entity Functions 96 | 97 | Entity functions are described in [this section](statefulentities.md). 98 | 99 | ## Official Docs 100 | 101 | [Durable Functions Overview](https://docs.microsoft.com/en-us/azure/azure-functions/durable/durable-functions-overview?tabs=csharp) 102 | 103 | --- 104 | [🔼 Notify Support Challenge](../NotifySupport/challenge/README.md) | 105 | [🔼 Fraud Detection Challenge](../FraudDetection/challenge/README.md) | [Sub-orchestrations ▶](suborchestrations.md) 106 | -------------------------------------------------------------------------------- /DurableFunctionsTheory/eternalorchestrations.md: -------------------------------------------------------------------------------- 1 | # Eternal Orchestrations 2 | 3 | Eternal orchestrators are orchestrators that never finish. They are useful when you want to perform a workflow in a loop. 4 | 5 | ![Eternal Orchestrations](diagrams/eternal_orchestrations.png) 6 | 7 | You can make a conditional eternal orchestrator that only continues the orchestrator inside an `if statement` based on logic you provide (e.g. a result of an activity, sub-orchestrator, or event). 8 | 9 | In this example a clean up action is performed as long as there are items to clean up: 10 | 11 | ```csharp 12 | [FunctionName(nameof(PeriodicCleanupOrchestrator))] 13 | public static async Task Run( 14 | [OrchestrationTrigger] IDurableOrchestrationContext context) 15 | { 16 | var itemsToCleanup = context.GetInput(); 17 | 18 | var updatedItemsToCleanup = await context.CallActivityAsync( 19 | nameof(DoCleanupActivity), 20 | itemsToCleanUp); 21 | 22 | if (updatedItemsToCleanup.Any()) 23 | { 24 | DateTime nextCleanup = context.CurrentUtcDateTime.AddHours(4); 25 | await context.CreateTimer(nextCleanup, CancellationToken.None); 26 | 27 | context.ContinueAsNew(updatedItemsToCleanup); 28 | } 29 | } 30 | ``` 31 | 32 | ## Official Docs 33 | 34 | [Eternal orchestrations in Durable Functions](https://docs.microsoft.com/en-us/azure/azure-functions/durable/durable-functions-eternal-orchestrations?tabs=csharp) 35 | 36 | --- 37 | [◀ Sub-orchestrations](suborchestrations.md) | [🔼 Notify Support Challenge](../NotifySupport/challenge/README.md) | [🔼 Fraud Detection Challenge](../FraudDetection/challenge/README.md) | [Events ▶](events.md) 38 | -------------------------------------------------------------------------------- /DurableFunctionsTheory/events.md: -------------------------------------------------------------------------------- 1 | # Events 2 | 3 | Orchestrator functions have the ability to wait and listen for external events. This is useful when interaction with another system is required, e.g. the orchestrator can't continue unless a callback from an external API is received. Or a person need to perform an approval step before the orchestrator can continue. 4 | 5 | ## Waiting for events 6 | 7 | ![Waiting for Events](diagrams/waitforexternalevent.png) 8 | 9 | This code sample shows how an orchestrator waits for events. 10 | 11 | ```csharp 12 | [FunctionName(nameof(ApprovalOrchestrator))] 13 | public static async Task Run( 14 | [OrchestrationTrigger] IDurableOrchestrationContext context) 15 | { 16 | var timeout = TimeSpan.FromHours(1); 17 | bool approved = await context.WaitForExternalEvent("ApprovalEvent", timeout, false); 18 | if (approved) 19 | { 20 | // approval granted - do the approved action 21 | } 22 | else 23 | { 24 | // approval denied - do the not approved action 25 | } 26 | } 27 | ``` 28 | 29 | ## Raising events 30 | 31 | ![Raise an Event](diagrams/raiseevents.png) 32 | 33 | This example shows how to raise an event from a Durable Functions client function. 34 | 35 | ```csharp 36 | [FunctionName(nameof(CallbackHttpClient))] 37 | public static async Task Run( 38 | [HttpTrigger( 39 | AuthorizationLevel.Function, 40 | nameof(HttpMethod.Post), 41 | Route = null)] HttpRequestMessage message, 42 | [DurableClient] IDurableClient client, 43 | ILogger logger) 44 | { 45 | var approvalResult = await message.Content.ReadAsAsync(); 46 | 47 | await client.RaiseEventAsync( 48 | approvalResult.OrchestratorInstanceId, 49 | approvalResult.EventName, 50 | approvalResult.IsApproved); 51 | 52 | return new AcceptedResult(); 53 | } 54 | 55 | ``` 56 | 57 | Besides using the .NET API, events can also be raised using the [built-in HTTP API](https://docs.microsoft.com/en-us/azure/azure-functions/durable/durable-functions-http-api#raise-event). 58 | 59 | ## Official Docs 60 | 61 | [Handling external events in Durable Functions](https://docs.microsoft.com/en-us/azure/azure-functions/durable/durable-functions-external-events?tabs=csharp) 62 | 63 | --- 64 | [◀ Eternal Orchestrations](eternalorchestrations.md) | [🔼 Notify Support Challenge](../NotifySupport/challenge/README.md) | [🔼 Fraud Detection Challenge](../FraudDetection/challenge/README.md) | [Stateful Entities ▶](statefulentities.md) 65 | -------------------------------------------------------------------------------- /DurableFunctionsTheory/statefulentities.md: -------------------------------------------------------------------------------- 1 | # Stateful Entities 2 | 3 | Entity functions allow you to read and write small pieces of state, known as stateful entities. Entities can be accessed from client functions and orchestrator functions. 4 | 5 | ![Stateful Entities](diagrams/stateful_entities.png) 6 | 7 | ## Entity Definition 8 | 9 | This is an example of a class-based entity function. It stores the game score for a player, and it allows setting & resetting of the score. 10 | 11 | ```csharp 12 | [JsonObject(MemberSerialization.OptIn)] 13 | public class PlayerScoreEntity 14 | { 15 | [JsonProperty("score")] 16 | public int Score { get; set; } 17 | 18 | public void Set(int score) => Score = score; 19 | 20 | public void Reset() => Score = 0; 21 | 22 | [FunctionName(nameof(PlayerScoreEntity))] 23 | public static Task Run( 24 | [EntityTrigger] IDurableEntityContext context) 25 | => context.DispatchAsync(); 26 | } 27 | ``` 28 | 29 | Stateful entities are always accessed via their ID. The Entity ID is a combination of the entity type (the class name) and the entity key, a string value that uniquely identifies an entity instance. The key can be a GUID, a user name, an email address, as long as it is unique. 30 | 31 | ## Reading Entity State 32 | 33 | In this example the `PlayerScoreEntity` state is read in a client function to retrieve the game score of the player: 34 | 35 | ```csharp 36 | [FunctionName(nameof(CallbackHttpClient))] 37 | public static async Task Run( 38 | [HttpTrigger( 39 | AuthorizationLevel.Function, 40 | nameof(HttpMethod.Post), 41 | Route = null)] HttpRequestMessage message, 42 | [DurableClient] IDurableClient client, 43 | ILogger logger) 44 | { 45 | var playerName = await message.Content.ReadAsAsync(); 46 | var entityId = new EntityId(nameof(PlayerScoreEntity), playerName); 47 | var playerScoreEntity = await client.ReadEntityStateAsync(entityId); 48 | if (playerScoreEntity.EntityExists) 49 | { 50 | return new OkObjectResult($"{playerName}:{playerScoreEntity.EntityState.Score}"); 51 | } 52 | 53 | return return new BadRequestObjectResult($"{playerName} not found."); 54 | } 55 | ``` 56 | 57 | ## Updating Entity State 58 | 59 | In this example the `PlayerScoreEntity` state is updated by signalling (one way communication) the entity: 60 | 61 | ```csharp 62 | [FunctionName(nameof(UpdatePlayerScoreOrchestrator))] 63 | public async Task Run( 64 | [OrchestrationTrigger] IDurableOrchestrationContext context, 65 | ILogger logger) 66 | { 67 | var playerScore = context.GetInput(); 68 | var entityId = new EntityId(nameof(PlayerScoreEntity), playerScore.Name); 69 | context.SignalEntity( 70 | entityId, 71 | nameof(PlayerScoreEntity.Set), 72 | playerScore.Score); 73 | 74 | // Rest of the orchestrator logic 75 | } 76 | ``` 77 | 78 | ## Official Docs 79 | 80 | - [Entity functions](https://docs.microsoft.com/en-us/azure/azure-functions/durable/durable-functions-entities?tabs=csharp) 81 | - [Developer's guide to durable entities in .NET](https://docs.microsoft.com/en-us/azure/azure-functions/durable/durable-functions-dotnet-entities) 82 | 83 | --- 84 | [◀ Events](events.md) | [🔼 Notify Support Challenge](../NotifySupport/challenge/README.md) | [🔼 Fraud Detection Challenge](../FraudDetection/challenge/README.md) 85 | -------------------------------------------------------------------------------- /DurableFunctionsTheory/suborchestrations.md: -------------------------------------------------------------------------------- 1 | # Sub-orchestrations 2 | 3 | Orchestrator functions can also call other orchestrator functions (sub-orchestrators). 4 | 5 | ![Sub-orchestrations](diagrams/sub-orchestrators.png) 6 | 7 | This enables re-use of orchestrators, and keeping them small and maintainable. Just like activity functions, sub-orchestrators can be chained, or executed in parallel. 8 | 9 | This is an example where two sub-orchestrators are chained: 10 | 11 | ```csharp 12 | [FunctionName(nameof(CollectAndUpdateGameStatsOrchestrator))] 13 | public async Task Run( 14 | [OrchestrationTrigger] IDurableOrchestrationContext context, 15 | ILogger logger) 16 | { 17 | var playerId = context.GetInput(); 18 | 19 | var playerGameStats = await context.CallSubOrchestratorAsync( 20 | nameof(CollectPlayerGameStatsOrchestrator), 21 | playerId); 22 | 23 | await context.CallSubOrchestratorAsync( 24 | nameof(UpdatePlayerGameStatsOrchestrator), 25 | playerGameStats); 26 | } 27 | 28 | ``` 29 | 30 | ## Official Docs 31 | 32 | [Sub-orchestrations in Durable Functions](https://docs.microsoft.com/en-us/azure/azure-functions/durable/durable-functions-sub-orchestrations?tabs=csharp) 33 | 34 | --- 35 | [◀ Durable Functions](durablefunctions.md) | [🔼 Notify Support Challenge](../NotifySupport/challenge/README.md) | [🔼 Fraud Detection Challenge](../FraudDetection/challenge/README.md) | [Eternal Orchestrations ▶](eternalorchestrations.md) 36 | -------------------------------------------------------------------------------- /FraudDetection/_azurite/README.md: -------------------------------------------------------------------------------- 1 | Use this folder for the Azurite files when you use the [VSCode Azurite extension](https://marketplace.visualstudio.com/items?itemName=Azurite.azurite). -------------------------------------------------------------------------------- /FraudDetection/challenge/README.md: -------------------------------------------------------------------------------- 1 | # Fraud Detection Challenge 2 | 3 | ## Goal 4 | 5 | The goal of this challenge is to write a Function App that: 6 | 7 | - responds to incoming financial transactions (posted via HTTP), 8 | - combines the transaction with customer data, 9 | - sends the combined data to a 3rd party web service to be analyzed, 10 | - waits for the analysis result to be returned via a webhook, 11 | - stores the combined data and analysis result to CosmosDB. 12 | 13 | ![Fraud Detection overview diagram](diagrams/frauddetection_overview.png) 14 | 15 | ## Flow Diagram 16 | 17 | ![Fraud Detection Flow diagram](diagrams/frauddetection_functions1.png) 18 | 19 | ## Prerequisites 20 | 21 | Read the [prerequisites](prerequisites.md) to ensure you have all the right tools installed. 22 | 23 | ## Durable Functions Theory 24 | 25 | Please familiarize yourself with some Durable Functions theory and code samples. These are the building blocks for the solution. 26 | 27 | - [Client, Orchestrator & Activity functions](../../DurableFunctionsTheory/durablefunctions.md) 28 | - [Events](../../DurableFunctionsTheory/events.md) 29 | - [Stateful Entities](../../DurableFunctionsTheory/statefulentities.md) 30 | 31 | ## Requirements 32 | 33 | The following requirements have been defined for the serverless application you'll write: 34 | 35 | 1. The Function App should respond to an incoming HTTP POST request on a static URL (e.g. `http://localhost:7071/api/FraudDetectionClient`). The body should contain one transaction in the following format (also see the [`Transaction`(link)](../src/DurableFunctions.UseCases.FraudDetection/Models/Transaction.cs) definition): 36 | 37 | ```json 38 | { 39 | "id" : "{{$guid}}", 40 | "creditorBankAccount" : "CR{{$guid}}", 41 | "debtorBankAccount" : "DB{{$guid}}", 42 | "amount" : "{{$randomInt 100 1000}}", 43 | "currency" : "USD", 44 | "timeStampUtc" : "{{$datetime iso8601}}", 45 | } 46 | ``` 47 | 48 | > This json example uses the [VS Code REST client](https://marketplace.visualstudio.com/items?itemName=humao.rest-client) extension to generate random values. 49 | 50 | > For more info about *Azure Functions HttpTriggers* see this [Azure Functions University lesson](https://github.com/marcduiker/azure-functions-university/blob/main/lessons/dotnetcore31/http/README.md). 51 | 2. The transaction contains bank accounts but no customer data (like names or country codes) that is required by the fraud detection service. The Function App should call a service to retrieve customer data for both the creditor and the debtor based on the bank account number from the transaction. This service returns a [`Customer`(link)](../src/DurableFunctions.UseCases.FraudDetection/Models/Customer.cs) object. 52 | 53 | > For this challenge I don't expect you to use a real customer data service. You can use my approach and have an activity function call a [FakeCustomerDataService](../src/DurableFunctions.UseCases.FraudDetection/Services/FakeCustomerDataService.cs) that generates customer data based on [Bogus](https://github.com/bchavez/Bogus). 54 | 55 | 3. The Function App should call a 3rd party web service that performs fraud detection analysis on the combined set of transaction and customer data, aka an [`AuditRecord`(link)](../src/DurableFunctions.UseCases.FraudDetection/Models/AuditRecord.cs). This web service accepts a POST with one `AuditRecord` as the payload. This service does not return the analysis result immediately, instead it will send back an Accepted response (HTTP status 202). Once the result is ready, the web service triggers a webhook with a `FraudResult` result in the body (see step 4). 56 | 57 | > For this challenge I don't expect you to use a real fraud detection service. You can either make (or fake) a call to some other service (e.g [webhook.site](https://webhook.site/) and 'trigger' the webhook yourself via a REST client. Or you can follow [my example](../src/DurableFunctions.UseCases.FraudDetection/Activities/AnalyzeAuditRecordActivity.cs) and make a call to GitHub to start a [workflow](../../.github/workflows/frauddetection/frauddetection_webhook.yml) which calls the webhook to your local Function App (via ngrok). 58 | 59 | 4. The Function App should expose an HTTP trigger function on a static URL (e.g. `http://localhost:7071/api/FraudResultWebhookClient`) that is used as the webhook in step 3. This endpoint should only allow POST requests and the request body is expected to contain the following payload (also see the [`FraudResult`(link)](../src/DurableFunctions.UseCases.FraudDetection/Models/FraudResult.cs) definition): 60 | 61 | ```json 62 | { 63 | "recordId": "", 64 | "isSuspiciousTransaction": "" 65 | } 66 | ``` 67 | 68 | 5. When `isSuspiciousTransaction` is `true`, the Function App should store the `AuditResult` (including the `IsSuspiciousTransaction` property) in a document collection (*auditrecords*) in a CosmosDB database (*frauddetection*). 69 | 70 | --- 71 | [🔼 Main README](../../README.md) -------------------------------------------------------------------------------- /FraudDetection/challenge/diagrams/frauddetection_functions1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcduiker/durable-functions-use-cases/a7e57a0d3b13b63aa1bf8ee110bc6b9ec44197a3/FraudDetection/challenge/diagrams/frauddetection_functions1.png -------------------------------------------------------------------------------- /FraudDetection/challenge/diagrams/frauddetection_overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcduiker/durable-functions-use-cases/a7e57a0d3b13b63aa1bf8ee110bc6b9ec44197a3/FraudDetection/challenge/diagrams/frauddetection_overview.png -------------------------------------------------------------------------------- /FraudDetection/challenge/prerequisites.md: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | 3 | ## Frameworks & Tooling 🧰 4 | 5 | In order to complete the the lessons you need to install the following: 6 | 7 | |Prerequisite|Description 8 | |-|- 9 | |[.NET Core 3.1](https://dotnet.microsoft.com/download/dotnet-core)|The .NET runtime and SDK. 10 | |[VSCode](https://code.visualstudio.com/Download)|A great cross platform code editor. 11 | |[VSCode AzureFunctions extension](https://github.com/Microsoft/vscode-azurefunctions)|Extension for VSCode to easily develop and manage Azure Functions. 12 | |[Azure Functions Core Tools](https://github.com/Azure/azure-functions-core-tools)|Azure Functions runtime and CLI for local development. 13 | |[RESTClient for VSCode](https://marketplace.visualstudio.com/items?itemName=humao.rest-client) or [Postman](https://www.postman.com/)|A VSCode extension or application to make HTTP requests. 14 | |[Azurite for VSCode](https://marketplace.visualstudio.com/items?itemName=Azurite.azurite)|[Cross platform storage emulator](https://docs.microsoft.com/en-us/azure/storage/common/storage-use-azurite?tabs=visual-studio) for using Azure Storage services if you want to develop locally without connecting to a Storage Account in the cloud. If you can't use the emulator you need an [Azure Storage Account](https://docs.microsoft.com/en-us/azure/storage/common/storage-account-create?tabs=azure-portal). 15 | |[Azure Storage Explorer](https://azure.microsoft.com/en-us/features/storage-explorer/)|Application to manage Azure Storage resources (both in the cloud and local emulated). 16 | |[Azure CosmosDB Emulator](https://docs.microsoft.com/en-us/azure/cosmos-db/local-emulator)|Cross platform CosmosDB emulator (NoSQL storage), that allows you to develop & test locally without connecting to an CosmosDB service in Azure. 17 | |[CodeTour for VSCode](https://marketplace.visualstudio.com/items?itemName=vsls-contrib.codetour)|**OPTIONAL:** Code samples in the markdown files use CodeTour to explain the samples in more detail. CodeTour is a VS Code extension that guides you through source code. 18 | | A localhost tunnel such as [ngrok](https://ngrok.com/) | **OPTIONAL:** You only need this if you're calling a 3rd party web service in the cloud and using its webhook functionality to call back to your local running Function App. 19 | 20 | ## Creating your local workspace 👩‍💻 21 | 22 | Create a new folder (local git repository) and use the repository you're currently reading from for reference only (for when you're stuck). 23 | 24 | - Create a new folder to work in: 25 | 26 | ```cmd 27 | C:\dev\mkdir fraud-detection 28 | C:\dev\cd .\fraud-detection\ 29 | ``` 30 | 31 | - Turn this into a git repository: 32 | 33 | ```cmd 34 | C:\dev\fraud-detection\git init 35 | ``` 36 | 37 | - Add subfolders for the source code and test files: 38 | 39 | ```cmd 40 | C:\dev\fraud-detection\mkdir src 41 | C:\dev\fraud-detection\mkdir tst 42 | ``` 43 | 44 | You should be good to go now! 45 | 46 | --- 47 | [🔼 Main README](../../README.md) | [Fraud Detection Challenge ▶](README.md) 48 | -------------------------------------------------------------------------------- /FraudDetection/src/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "ms-azuretools.vscode-azurefunctions", 4 | "ms-dotnettools.csharp", 5 | "Azurite.azurite", 6 | "humao.rest-client", 7 | "vsls-contrib.codetour" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /FraudDetection/src/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Attach to .NET Functions", 6 | "type": "coreclr", 7 | "request": "attach", 8 | "processId": "${command:azureFunctions.pickProcess}" 9 | } 10 | ] 11 | } -------------------------------------------------------------------------------- /FraudDetection/src/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "azureFunctions.deploySubpath": "src\\DurableFunctions.UseCases.NotifySupport/bin/Release/netcoreapp3.1/publish", 3 | "azureFunctions.projectLanguage": "C#", 4 | "azureFunctions.projectRuntime": "~3", 5 | "debug.internalConsoleOptions": "neverOpen", 6 | "azureFunctions.pickProcessTimeout" : 120, 7 | "azureFunctions.preDeployTask": "publish", 8 | "cSpell.ignoreWords": [ 9 | "durablefunctions", 10 | "eternalorchestrations", 11 | "notifysupport", 12 | "statefulentities", 13 | "suborchestrations", 14 | "twilio" 15 | ] 16 | } -------------------------------------------------------------------------------- /FraudDetection/src/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "clean", 6 | "command": "dotnet", 7 | "args": [ 8 | "clean", 9 | "/property:GenerateFullPaths=true", 10 | "/consoleloggerparameters:NoSummary" 11 | ], 12 | "type": "process", 13 | "problemMatcher": "$msCompile", 14 | "options": { 15 | "cwd": "${workspaceFolder}/DurableFunctions.UseCases.FraudDetection" 16 | } 17 | }, 18 | { 19 | "label": "build", 20 | "command": "dotnet", 21 | "args": [ 22 | "build", 23 | "/property:GenerateFullPaths=true", 24 | "/consoleloggerparameters:NoSummary" 25 | ], 26 | "type": "process", 27 | "dependsOn": "clean", 28 | "group": { 29 | "kind": "build", 30 | "isDefault": true 31 | }, 32 | "problemMatcher": "$msCompile", 33 | "options": { 34 | "cwd": "${workspaceFolder}/DurableFunctions.UseCases.FraudDetection" 35 | } 36 | }, 37 | { 38 | "label": "clean release", 39 | "command": "dotnet", 40 | "args": [ 41 | "clean", 42 | "--configuration", 43 | "Release", 44 | "/property:GenerateFullPaths=true", 45 | "/consoleloggerparameters:NoSummary" 46 | ], 47 | "type": "process", 48 | "problemMatcher": "$msCompile", 49 | "options": { 50 | "cwd": "${workspaceFolder}/DurableFunctions.UseCases.FraudDetection" 51 | } 52 | }, 53 | { 54 | "label": "publish", 55 | "command": "dotnet", 56 | "args": [ 57 | "publish", 58 | "--configuration", 59 | "Release", 60 | "/property:GenerateFullPaths=true", 61 | "/consoleloggerparameters:NoSummary" 62 | ], 63 | "type": "process", 64 | "dependsOn": "clean release", 65 | "problemMatcher": "$msCompile", 66 | "options": { 67 | "cwd": "${workspaceFolder}/DurableFunctions.UseCases.FraudDetection" 68 | } 69 | }, 70 | { 71 | "type": "func", 72 | "dependsOn": "build", 73 | "options": { 74 | "cwd": "${workspaceFolder}/DurableFunctions.UseCases.FraudDetection/bin/Debug/netcoreapp3.1" 75 | }, 76 | "command": "host start", 77 | "isBackground": true, 78 | "problemMatcher": "$func-dotnet-watch" 79 | } 80 | ] 81 | } -------------------------------------------------------------------------------- /FraudDetection/src/DurableFunctions.UseCases.FraudDetection/.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # Azure Functions localsettings file 5 | local.settings.json 6 | 7 | # Azurite files/folders 8 | __blobstorage__ 9 | __queuestorage__ 10 | __tablestorage__ 11 | __azurite* 12 | 13 | # User-specific files 14 | *.suo 15 | *.user 16 | *.userosscache 17 | *.sln.docstates 18 | 19 | # User-specific files (MonoDevelop/Xamarin Studio) 20 | *.userprefs 21 | 22 | # Build results 23 | [Dd]ebug/ 24 | [Dd]ebugPublic/ 25 | [Rr]elease/ 26 | [Rr]eleases/ 27 | x64/ 28 | x86/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Ll]og/ 33 | 34 | # Visual Studio 2015 cache/options directory 35 | .vs/ 36 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | # MSTest test Results 40 | [Tt]est[Rr]esult*/ 41 | [Bb]uild[Ll]og.* 42 | 43 | # NUNIT 44 | *.VisualState.xml 45 | TestResult.xml 46 | 47 | # Build Results of an ATL Project 48 | [Dd]ebugPS/ 49 | [Rr]eleasePS/ 50 | dlldata.c 51 | 52 | # DNX 53 | project.lock.json 54 | project.fragment.lock.json 55 | artifacts/ 56 | 57 | *_i.c 58 | *_p.c 59 | *_i.h 60 | *.ilk 61 | *.meta 62 | *.obj 63 | *.pch 64 | *.pdb 65 | *.pgc 66 | *.pgd 67 | *.rsp 68 | *.sbr 69 | *.tlb 70 | *.tli 71 | *.tlh 72 | *.tmp 73 | *.tmp_proj 74 | *.log 75 | *.vspscc 76 | *.vssscc 77 | .builds 78 | *.pidb 79 | *.svclog 80 | *.scc 81 | 82 | # Chutzpah Test files 83 | _Chutzpah* 84 | 85 | # Visual C++ cache files 86 | ipch/ 87 | *.aps 88 | *.ncb 89 | *.opendb 90 | *.opensdf 91 | *.sdf 92 | *.cachefile 93 | *.VC.db 94 | *.VC.VC.opendb 95 | 96 | # Visual Studio profiler 97 | *.psess 98 | *.vsp 99 | *.vspx 100 | *.sap 101 | 102 | # TFS 2012 Local Workspace 103 | $tf/ 104 | 105 | # Guidance Automation Toolkit 106 | *.gpState 107 | 108 | # ReSharper is a .NET coding add-in 109 | _ReSharper*/ 110 | *.[Rr]e[Ss]harper 111 | *.DotSettings.user 112 | 113 | # JustCode is a .NET coding add-in 114 | .JustCode 115 | 116 | # TeamCity is a build add-in 117 | _TeamCity* 118 | 119 | # DotCover is a Code Coverage Tool 120 | *.dotCover 121 | 122 | # NCrunch 123 | _NCrunch_* 124 | .*crunch*.local.xml 125 | nCrunchTemp_* 126 | 127 | # MightyMoose 128 | *.mm.* 129 | AutoTest.Net/ 130 | 131 | # Web workbench (sass) 132 | .sass-cache/ 133 | 134 | # Installshield output folder 135 | [Ee]xpress/ 136 | 137 | # DocProject is a documentation generator add-in 138 | DocProject/buildhelp/ 139 | DocProject/Help/*.HxT 140 | DocProject/Help/*.HxC 141 | DocProject/Help/*.hhc 142 | DocProject/Help/*.hhk 143 | DocProject/Help/*.hhp 144 | DocProject/Help/Html2 145 | DocProject/Help/html 146 | 147 | # Click-Once directory 148 | publish/ 149 | 150 | # Publish Web Output 151 | *.[Pp]ublish.xml 152 | *.azurePubxml 153 | # TODO: Comment the next line if you want to checkin your web deploy settings 154 | # but database connection strings (with potential passwords) will be unencrypted 155 | #*.pubxml 156 | *.publishproj 157 | 158 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 159 | # checkin your Azure Web App publish settings, but sensitive information contained 160 | # in these scripts will be unencrypted 161 | PublishScripts/ 162 | 163 | # NuGet Packages 164 | *.nupkg 165 | # The packages folder can be ignored because of Package Restore 166 | **/packages/* 167 | # except build/, which is used as an MSBuild target. 168 | !**/packages/build/ 169 | # Uncomment if necessary however generally it will be regenerated when needed 170 | #!**/packages/repositories.config 171 | # NuGet v3's project.json files produces more ignoreable files 172 | *.nuget.props 173 | *.nuget.targets 174 | 175 | # Microsoft Azure Build Output 176 | csx/ 177 | *.build.csdef 178 | 179 | # Microsoft Azure Emulator 180 | ecf/ 181 | rcf/ 182 | 183 | # Windows Store app package directories and files 184 | AppPackages/ 185 | BundleArtifacts/ 186 | Package.StoreAssociation.xml 187 | _pkginfo.txt 188 | 189 | # Visual Studio cache files 190 | # files ending in .cache can be ignored 191 | *.[Cc]ache 192 | # but keep track of directories ending in .cache 193 | !*.[Cc]ache/ 194 | 195 | # Others 196 | ClientBin/ 197 | ~$* 198 | *~ 199 | *.dbmdl 200 | *.dbproj.schemaview 201 | *.jfm 202 | *.pfx 203 | *.publishsettings 204 | node_modules/ 205 | orleans.codegen.cs 206 | 207 | # Since there are multiple workflows, uncomment next line to ignore bower_components 208 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 209 | #bower_components/ 210 | 211 | # RIA/Silverlight projects 212 | Generated_Code/ 213 | 214 | # Backup & report files from converting an old project file 215 | # to a newer Visual Studio version. Backup files are not needed, 216 | # because we have git ;-) 217 | _UpgradeReport_Files/ 218 | Backup*/ 219 | UpgradeLog*.XML 220 | UpgradeLog*.htm 221 | 222 | # SQL Server files 223 | *.mdf 224 | *.ldf 225 | 226 | # Business Intelligence projects 227 | *.rdl.data 228 | *.bim.layout 229 | *.bim_*.settings 230 | 231 | # Microsoft Fakes 232 | FakesAssemblies/ 233 | 234 | # GhostDoc plugin setting file 235 | *.GhostDoc.xml 236 | 237 | # Node.js Tools for Visual Studio 238 | .ntvs_analysis.dat 239 | 240 | # Visual Studio 6 build log 241 | *.plg 242 | 243 | # Visual Studio 6 workspace options file 244 | *.opt 245 | 246 | # Visual Studio LightSwitch build output 247 | **/*.HTMLClient/GeneratedArtifacts 248 | **/*.DesktopClient/GeneratedArtifacts 249 | **/*.DesktopClient/ModelManifest.xml 250 | **/*.Server/GeneratedArtifacts 251 | **/*.Server/ModelManifest.xml 252 | _Pvt_Extensions 253 | 254 | # Paket dependency manager 255 | .paket/paket.exe 256 | paket-files/ 257 | 258 | # FAKE - F# Make 259 | .fake/ 260 | 261 | # JetBrains Rider 262 | .idea/ 263 | *.sln.iml 264 | 265 | # CodeRush 266 | .cr/ 267 | 268 | # Python Tools for Visual Studio (PTVS) 269 | __pycache__/ 270 | *.pyc 271 | 272 | # Azurite 273 | 274 | __azurite* 275 | __blobstorage__ 276 | __tablestorage__ 277 | __queuestorage__ -------------------------------------------------------------------------------- /FraudDetection/src/DurableFunctions.UseCases.FraudDetection/Activities/AnalyzeAuditRecordActivity.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using DurableFunctions.UseCases.FraudDetection.Models; 4 | using DurableFunctions.UseCases.FraudDetection.Services; 5 | using Microsoft.Azure.WebJobs; 6 | using Microsoft.Azure.WebJobs.Extensions.DurableTask; 7 | 8 | namespace DurableFunctions.UseCases.FraudDetection.Activities 9 | { 10 | public class AnalyzeAuditRecordActivity 11 | { 12 | private readonly IFraudAnalysisService _fraudAnalysisService; 13 | 14 | public AnalyzeAuditRecordActivity(IFraudAnalysisService fraudAnalysisService) 15 | { 16 | _fraudAnalysisService = fraudAnalysisService; 17 | } 18 | 19 | // This function simulates a call to a 3rd party service that analyses the AuditRecord. 20 | // The service is asynchronous and will not return a result. The service is capable of 21 | // sending a webhook that contains the analysis result once that is ready. 22 | // The webhook calls back to the FraudResultWebhookClient function in this Function App. 23 | // 24 | // GitHub is being used here as since that can trigger webhooks easily. 25 | // In this case the webhook will be called when a new GitHub issue is created. 26 | [FunctionName(nameof(AnalyzeAuditRecordActivity))] 27 | public async Task Run( 28 | [ActivityTrigger] AuditRecord auditRecord) 29 | { 30 | var owner = Environment.GetEnvironmentVariable("GitHubOwner"); 31 | var repo = Environment.GetEnvironmentVariable("GitHubRepoName"); 32 | var workflowFile = Environment.GetEnvironmentVariable("GitHubWorkflowFile"); 33 | var input = new WorkflowDispatchEvent(auditRecord.Id); 34 | var response = await _fraudAnalysisService.AnalyzeAsync(input, owner, repo, workflowFile); 35 | if (!response.IsSuccessStatusCode) 36 | { 37 | throw response.Error.GetBaseException(); 38 | } 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /FraudDetection/src/DurableFunctions.UseCases.FraudDetection/Activities/GetCustomerActivity.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using DurableFunctions.UseCases.FraudDetection.Models; 3 | using DurableFunctions.UseCases.FraudDetection.Services; 4 | using Microsoft.Azure.WebJobs; 5 | using Microsoft.Azure.WebJobs.Extensions.DurableTask; 6 | 7 | namespace DurableFunctions.UseCases.FraudDetection.Activities 8 | { 9 | public class GetCustomerActivity 10 | { 11 | private readonly ICustomerDataService _customerDataService; 12 | 13 | public GetCustomerActivity(ICustomerDataService customerDataService) 14 | { 15 | _customerDataService = customerDataService; 16 | } 17 | 18 | [FunctionName(nameof(GetCustomerActivity))] 19 | public async Task Run( 20 | [ActivityTrigger] string bankAccount) 21 | { 22 | return await _customerDataService.GetCustomerAsync(bankAccount); 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /FraudDetection/src/DurableFunctions.UseCases.FraudDetection/Activities/StoreAuditRecordActivity.cs: -------------------------------------------------------------------------------- 1 | using DurableFunctions.UseCases.FraudDetection.Models; 2 | using Microsoft.Azure.WebJobs; 3 | using Microsoft.Azure.WebJobs.Extensions.DurableTask; 4 | 5 | namespace DurableFunctions.UseCases 6 | { 7 | public class StoreAuditRecordActivity 8 | { 9 | [FunctionName(nameof(StoreAuditRecordActivity))] 10 | [return: CosmosDB("%CosmosDBName%", "%CosmosDBCollection%", ConnectionStringSetting = "CosmosDBConnection")] 11 | public AuditRecord Run( 12 | [ActivityTrigger] AuditRecord auditRecord) 13 | { 14 | return auditRecord; 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /FraudDetection/src/DurableFunctions.UseCases.FraudDetection/Application/Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using DurableFunctions.UseCases.FraudDetection.Application; 3 | using DurableFunctions.UseCases.FraudDetection.Services; 4 | using Microsoft.Azure.Functions.Extensions.DependencyInjection; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using Refit; 7 | 8 | [assembly: FunctionsStartup(typeof(Startup))] 9 | namespace DurableFunctions.UseCases.FraudDetection.Application 10 | { 11 | public class Startup : FunctionsStartup 12 | { 13 | public override void Configure(IFunctionsHostBuilder builder) 14 | { 15 | builder.Services.AddSingleton(); 16 | builder.Services.AddTransient(); 17 | builder.Services.AddRefitClient(new RefitSettings 18 | { 19 | ContentSerializer = new NewtonsoftJsonContentSerializer( 20 | new Newtonsoft.Json.JsonSerializerSettings 21 | { 22 | DefaultValueHandling = Newtonsoft.Json.DefaultValueHandling.Ignore, 23 | NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore 24 | } 25 | ) 26 | }) 27 | .AddHttpMessageHandler() 28 | .ConfigureHttpClient(c => c.BaseAddress = new Uri("https://api.github.com")); 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /FraudDetection/src/DurableFunctions.UseCases.FraudDetection/Builders/AuditRecordBuilder.cs: -------------------------------------------------------------------------------- 1 | using DurableFunctions.UseCases.FraudDetection.Models; 2 | 3 | namespace DurableFunctions.UseCases.FraudDetection.Builders 4 | { 5 | public static class AuditRecordBuilder 6 | { 7 | public static AuditRecord Create( 8 | string id, 9 | Transaction transaction, 10 | Customer creditor, 11 | Customer debtor) 12 | { 13 | return new AuditRecord 14 | { 15 | Id = id, 16 | Transaction = transaction, 17 | Creditor = creditor, 18 | Debtor = debtor 19 | }; 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /FraudDetection/src/DurableFunctions.UseCases.FraudDetection/Builders/FakeCustomerBuilder.cs: -------------------------------------------------------------------------------- 1 | using Bogus; 2 | using DurableFunctions.UseCases.FraudDetection.Models; 3 | 4 | namespace DurableFunctions.UseCases.FraudDetection.Builders 5 | { 6 | public static class FakeCustomerBuilder 7 | { 8 | public static Customer Create(string bankAccount) 9 | { 10 | var fakeCustomer = new Faker("en_US") 11 | .RuleFor(c => c.BankAccount, bankAccount) 12 | .RuleFor(c => c.CountryCode, f => f.Address.CountryCode()) 13 | .RuleFor(c => c.Id, f => f.Random.Guid()) 14 | .RuleFor(c => c.Name, f => f.Person.FullName) 15 | .Generate(); 16 | 17 | return fakeCustomer; 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /FraudDetection/src/DurableFunctions.UseCases.FraudDetection/Clients/FraudDetectionClient.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.Azure.WebJobs; 3 | using Microsoft.Azure.WebJobs.Extensions.Http; 4 | using Microsoft.Extensions.Logging; 5 | using Microsoft.Azure.WebJobs.Extensions.DurableTask; 6 | using DurableFunctions.UseCases.FraudDetection.Models; 7 | using System.Net.Http; 8 | 9 | namespace DurableFunctions.UseCases.FraudDetection.Clients 10 | { 11 | public static class FraudDetectionClient 12 | { 13 | [FunctionName(nameof(FraudDetectionClient))] 14 | public static async Task Run( 15 | [HttpTrigger(AuthorizationLevel.Function, "post", Route = null)] HttpRequestMessage message, 16 | [DurableClient] IDurableClient client, 17 | ILogger log) 18 | { 19 | var transaction = await message.Content.ReadAsAsync(); 20 | var instanceId = await client.StartNewAsync( 21 | nameof(FraudDetectionOrchestrator), 22 | transaction); 23 | 24 | return client.CreateCheckStatusResponse(message, instanceId); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /FraudDetection/src/DurableFunctions.UseCases.FraudDetection/Clients/FraudResultWebhookClient.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.Azure.WebJobs; 4 | using Microsoft.Azure.WebJobs.Extensions.Http; 5 | using Microsoft.Extensions.Logging; 6 | using Microsoft.Azure.WebJobs.Extensions.DurableTask; 7 | using DurableFunctions.UseCases.FraudDetection.Models; 8 | using DurableFunctions.UseCases.FraudDetection.Entities; 9 | 10 | namespace DurableFunctions.UseCases.FraudDetection.Clients 11 | { 12 | public static class FraudResultWebhookClient 13 | { 14 | [FunctionName(nameof(FraudResultWebhookClient))] 15 | public static async Task Run( 16 | [HttpTrigger(AuthorizationLevel.Function, "post", Route = null)] FraudResult fraudResult, 17 | [DurableClient] IDurableClient client, 18 | ILogger log) 19 | { 20 | var entityId = new EntityId( 21 | nameof(FraudDetectionOrchestratorEntity), 22 | fraudResult.RecordId); 23 | 24 | var entityStateResponse = await client.ReadEntityStateAsync(entityId); 25 | if (entityStateResponse.EntityExists) 26 | { 27 | await client.RaiseEventAsync( 28 | entityStateResponse.EntityState.InstanceId, 29 | Constants.FraudResultCompletedEvent, 30 | fraudResult.IsSuspiciousTransaction); 31 | return new AcceptedResult(); 32 | } 33 | else 34 | { 35 | return new BadRequestObjectResult($"Entity {entityId} does not exist."); 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /FraudDetection/src/DurableFunctions.UseCases.FraudDetection/DurableFunctions.UseCases.FraudDetection.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netcoreapp3.1 4 | v3 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | PreserveNewest 20 | 21 | 22 | PreserveNewest 23 | Never 24 | 25 | 26 | -------------------------------------------------------------------------------- /FraudDetection/src/DurableFunctions.UseCases.FraudDetection/Entities/FraudDetectionOrchestratorEntity.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.Azure.WebJobs; 3 | using Microsoft.Azure.WebJobs.Extensions.DurableTask; 4 | using Newtonsoft.Json; 5 | 6 | namespace DurableFunctions.UseCases.FraudDetection.Entities 7 | { 8 | [JsonObject(MemberSerialization.OptIn)] 9 | public class FraudDetectionOrchestratorEntity 10 | { 11 | [JsonProperty("instanceId")] 12 | public string InstanceId { get; set; } 13 | 14 | public void Set(string instanceId) => InstanceId = instanceId; 15 | 16 | public void Reset() => InstanceId = string.Empty; 17 | 18 | [FunctionName(nameof(FraudDetectionOrchestratorEntity))] 19 | public static Task Run( 20 | [EntityTrigger] IDurableEntityContext ctx) 21 | => ctx.DispatchAsync(); 22 | } 23 | } -------------------------------------------------------------------------------- /FraudDetection/src/DurableFunctions.UseCases.FraudDetection/Models/AuditRecord.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace DurableFunctions.UseCases.FraudDetection.Models 4 | { 5 | public class AuditRecord 6 | { 7 | [JsonProperty("id")] 8 | public string Id { get; set; } 9 | 10 | [JsonProperty("transaction")] 11 | public Transaction Transaction { get; set; } 12 | 13 | [JsonProperty("creditor")] 14 | public Customer Creditor { get; set; } 15 | 16 | [JsonProperty("debtor")] 17 | public Customer Debtor { get; set; } 18 | 19 | [JsonProperty("isSuspiciousTransaction")] 20 | public bool IsSuspiciousTransaction { get; set; } 21 | } 22 | } -------------------------------------------------------------------------------- /FraudDetection/src/DurableFunctions.UseCases.FraudDetection/Models/Constants.cs: -------------------------------------------------------------------------------- 1 | namespace DurableFunctions.UseCases.FraudDetection.Models 2 | { 3 | public static class Constants 4 | { 5 | public const string FraudResultCompletedEvent = "FraudResultCompleted"; 6 | } 7 | } -------------------------------------------------------------------------------- /FraudDetection/src/DurableFunctions.UseCases.FraudDetection/Models/Customer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Newtonsoft.Json; 3 | 4 | namespace DurableFunctions.UseCases.FraudDetection.Models 5 | { 6 | public class Customer 7 | { 8 | [JsonProperty("id")] 9 | public Guid Id { get; set; } 10 | 11 | [JsonProperty("name")] 12 | public string Name { get; set; } 13 | 14 | [JsonProperty("bankAccount")] 15 | public string BankAccount { get; set; } 16 | 17 | [JsonProperty("countryCode")] 18 | public string CountryCode { get; set; } 19 | } 20 | } -------------------------------------------------------------------------------- /FraudDetection/src/DurableFunctions.UseCases.FraudDetection/Models/FraudResult.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace DurableFunctions.UseCases.FraudDetection.Models 4 | { 5 | public class FraudResult 6 | { 7 | [JsonProperty("recordId")] 8 | public string RecordId { get; set; } 9 | 10 | [JsonProperty("isSuspiciousTransaction")] 11 | public bool IsSuspiciousTransaction { get; set; } 12 | } 13 | } -------------------------------------------------------------------------------- /FraudDetection/src/DurableFunctions.UseCases.FraudDetection/Models/Transaction.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Newtonsoft.Json; 3 | 4 | namespace DurableFunctions.UseCases.FraudDetection.Models 5 | { 6 | public class Transaction 7 | { 8 | [JsonProperty("id")] 9 | public string Id { get; set; } 10 | 11 | [JsonProperty("creditorBankAccount")] 12 | public string CreditorBankAccount { get; set; } 13 | 14 | [JsonProperty("debtorBankAccount")] 15 | public string DebtorBankAccount { get; set; } 16 | 17 | [JsonProperty("amount")] 18 | public decimal Amount { get; set; } 19 | 20 | [JsonProperty("currency")] 21 | public string Currency { get; set; } 22 | 23 | [JsonProperty("timeStampUtc")] 24 | public DateTime TimeStampUtc { get; set; } 25 | 26 | } 27 | } -------------------------------------------------------------------------------- /FraudDetection/src/DurableFunctions.UseCases.FraudDetection/Models/WorkflowDispatchEvent.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace DurableFunctions.UseCases.FraudDetection.Models 4 | { 5 | public class WorkflowDispatchEvent 6 | { 7 | public WorkflowDispatchEvent(string recordId) 8 | { 9 | Ref = "main"; 10 | Inputs = new { recordId = recordId }; 11 | } 12 | 13 | [JsonProperty("ref")] 14 | public string Ref { get; set;} 15 | 16 | [JsonProperty("inputs")] 17 | public object Inputs { get; set; } 18 | } 19 | } -------------------------------------------------------------------------------- /FraudDetection/src/DurableFunctions.UseCases.FraudDetection/Orchestrators/FraudDetectionOrchestrator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using DurableFunctions.UseCases.FraudDetection.Activities; 4 | using DurableFunctions.UseCases.FraudDetection.Builders; 5 | using DurableFunctions.UseCases.FraudDetection.Entities; 6 | using DurableFunctions.UseCases.FraudDetection.Models; 7 | using Microsoft.Azure.WebJobs; 8 | using Microsoft.Azure.WebJobs.Extensions.DurableTask; 9 | using Microsoft.Extensions.Logging; 10 | 11 | namespace DurableFunctions.UseCases.FraudDetection 12 | { 13 | public class FraudDetectionOrchestrator 14 | { 15 | [FunctionName(nameof(FraudDetectionOrchestrator))] 16 | public async Task Run( 17 | [OrchestrationTrigger] IDurableOrchestrationContext context, 18 | ILogger logger) 19 | { 20 | var transaction = context.GetInput(); 21 | 22 | (Customer Creditor, Customer Debtor) customers = await GetCustomersForTransactionAsync(context, transaction); 23 | 24 | var auditRecord = AuditRecordBuilder.Create( 25 | context.NewGuid().ToString(), 26 | transaction, 27 | customers.Creditor, 28 | customers.Debtor); 29 | 30 | // Create an entity based on the record Id to store the current orchestration Id. 31 | var entityId = new EntityId( 32 | nameof(FraudDetectionOrchestratorEntity), 33 | auditRecord.Id); 34 | context.SignalEntity( 35 | entityId, 36 | nameof(FraudDetectionOrchestratorEntity.Set), 37 | context.InstanceId); 38 | 39 | await context.CallActivityAsync( 40 | nameof(AnalyzeAuditRecordActivity), 41 | auditRecord); 42 | 43 | var timeOut = TimeSpan.FromMinutes(5); 44 | var defaultResult = true; 45 | var isSuspiciousTransaction = await context.WaitForExternalEvent( 46 | Constants.FraudResultCompletedEvent, 47 | timeOut, 48 | defaultResult); 49 | auditRecord.IsSuspiciousTransaction = isSuspiciousTransaction; 50 | 51 | if (auditRecord.IsSuspiciousTransaction) 52 | { 53 | await context.CallActivityAsync( 54 | nameof(StoreAuditRecordActivity), 55 | auditRecord); 56 | } 57 | } 58 | 59 | private async Task<(Customer Creditor, Customer Debtor)> GetCustomersForTransactionAsync( 60 | IDurableOrchestrationContext context, 61 | Transaction transaction) 62 | { 63 | var creditorTask = context.CallActivityAsync( 64 | nameof(GetCustomerActivity), 65 | transaction.CreditorBankAccount); 66 | 67 | var debtorTask = context.CallActivityAsync( 68 | nameof(GetCustomerActivity), 69 | transaction.DebtorBankAccount); 70 | 71 | await Task.WhenAll(creditorTask, debtorTask); 72 | 73 | return (creditorTask.Result, debtorTask.Result); 74 | } 75 | } 76 | } -------------------------------------------------------------------------------- /FraudDetection/src/DurableFunctions.UseCases.FraudDetection/Services/AuthHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using System.Net.Http.Headers; 4 | using System.Text; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace DurableFunctions.UseCases.FraudDetection.Services 9 | { 10 | public class AuthHandler : DelegatingHandler 11 | { 12 | protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) 13 | { 14 | var token = Convert.ToBase64String( 15 | Encoding.UTF8.GetBytes( 16 | $"{Environment.GetEnvironmentVariable("GitHubOwner")}:{Environment.GetEnvironmentVariable("GitHubWebhookPAT")}")); 17 | request.Headers.Add("User-Agent", "webhook-playground"); 18 | request.Headers.Authorization = new AuthenticationHeaderValue("Basic", token); 19 | 20 | return await base.SendAsync(request, cancellationToken).ConfigureAwait(false); 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /FraudDetection/src/DurableFunctions.UseCases.FraudDetection/Services/FakeCustomerDataService.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using DurableFunctions.UseCases.FraudDetection.Builders; 3 | using DurableFunctions.UseCases.FraudDetection.Models; 4 | 5 | namespace DurableFunctions.UseCases.FraudDetection.Services 6 | { 7 | public class FakeCustomerDataService : ICustomerDataService 8 | { 9 | public Task GetCustomerAsync(string bankAccount) 10 | { 11 | return Task.FromResult(FakeCustomerBuilder.Create(bankAccount)); 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /FraudDetection/src/DurableFunctions.UseCases.FraudDetection/Services/ICustomerDataService.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using DurableFunctions.UseCases.FraudDetection.Models; 3 | 4 | namespace DurableFunctions.UseCases.FraudDetection.Services 5 | { 6 | public interface ICustomerDataService 7 | { 8 | Task GetCustomerAsync(string bankAccount); 9 | } 10 | } -------------------------------------------------------------------------------- /FraudDetection/src/DurableFunctions.UseCases.FraudDetection/Services/IFraudAnalysisService.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using DurableFunctions.UseCases.FraudDetection.Models; 3 | using Refit; 4 | 5 | namespace DurableFunctions.UseCases.FraudDetection.Services 6 | { 7 | 8 | public interface IFraudAnalysisService 9 | { 10 | [Headers("Authorization: token", "Accept: application/vnd.github.v3+json")] 11 | [Post("/repos/{repoOwner}/{repoName}/actions/workflows/{workflowFile}/dispatches")] 12 | public Task> AnalyzeAsync( 13 | [Body] WorkflowDispatchEvent dispatch, 14 | string repoOwner, 15 | string repoName, 16 | string workflowFile); 17 | } 18 | } -------------------------------------------------------------------------------- /FraudDetection/src/DurableFunctions.UseCases.FraudDetection/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "extensions": { 4 | "durableTask": { 5 | "hubName": "%TaskHubName%", 6 | "storageProvider": { 7 | "maxQueuePollingInterval": "00:00:05" 8 | } 9 | } 10 | }, 11 | "logging": { 12 | "applicationInsights": { 13 | "samplingSettings": { 14 | "isEnabled": true, 15 | "excludedTypes": "Request" 16 | } 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /FraudDetection/src/DurableFunctions.UseCases.FraudDetection/local.settings.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "IsEncrypted": false, 3 | "Values": { 4 | "TaskHubName" : "FraudDetectionV1", 5 | "AzureWebJobsStorage": "UseDevelopmentStorage=true", 6 | "FUNCTIONS_WORKER_RUNTIME": "dotnet", 7 | "CosmosDBConnection": "AccountEndpoint=https://localhost:8081/;AccountKey=C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==", 8 | "CosmosDBName": "frauddetection", 9 | "CosmosDBCollection": "auditrecords", 10 | "GitHubWebhookPAT" : "", 11 | "GitHubWorkflowFile": "frauddetection_webhook.yml", 12 | "GitHubOwner": "marcduiker", 13 | "GitHubRepoName": "durable-functions-use-cases" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /FraudDetection/tst/frauddetection.http: -------------------------------------------------------------------------------- 1 | # @name fraudDetectionRequest 2 | POST http://localhost:7071/api/FraudDetectionClient 3 | Content-Type: application/json 4 | 5 | { 6 | "id" : "{{$guid}}", 7 | "creditorBankAccount" : "CR{{$guid}}", 8 | "debtorBankAccount" : "DB{{$guid}}", 9 | "amount" : "{{$randomInt 100 1000}}", 10 | "currency" : "USD", 11 | "timeStampUtc" : "{{$datetime iso8601}}", 12 | } 13 | 14 | ### Get status of orchestration 15 | @orchestratorId = {{fraudDetectionRequest.response.body.$.id}} 16 | GET http://localhost:7071/runtime/webhooks/durabletask/instances/{{orchestratorId}} 17 | ?taskHub=FraudDetectionV1 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Marc Duiker 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /NotifySupport/_azurite/README.md: -------------------------------------------------------------------------------- 1 | Use this folder for the Azurite files when you use the [VSCode Azurite extension](https://marketplace.visualstudio.com/items?itemName=Azurite.azurite). -------------------------------------------------------------------------------- /NotifySupport/challenge/README.md: -------------------------------------------------------------------------------- 1 | # Notify Support Challenge 2 | 3 | ## Goal 4 | 5 | The goal of this challenge is to write a Function App which responds to alerts (posted as HTTP request) and notifies members of the support team so they can investigate the issue. 6 | 7 | ![Notify Support overview diagram](diagrams/notifysupport_overview.png) 8 | 9 | ### Recording 10 | 11 | There is a [recording available on YouTube](https://youtu.be/O6YCRhyqR2w) which explains this challenge and the relevant Durable Functions theory. The solution of this challenge is covered in a separate video. 12 | 13 | ## Flow Diagram 14 | 15 | ![Notify Support Flow diagram](diagrams/notifysupport_functions1.png) 16 | 17 | ## Prerequisites 18 | 19 | Read the [prerequisites](prerequisites.md) to ensure you have all the right tools installed. 20 | 21 | ## Durable Functions Theory 22 | 23 | Please familiarize yourself with some Durable Functions theory and code samples. These are the building blocks for the solution. 24 | 25 | - [Client, Orchestrator & Activity functions](../../DurableFunctionsTheory/durablefunctions.md) 26 | - [Sub-orchestrations](../../DurableFunctionsTheory/suborchestrations.md) 27 | - [Events](../../DurableFunctionsTheory/events.md) 28 | - [Eternal orchestrations](../../DurableFunctionsTheory/eternalorchestrations.md) 29 | - [Stateful Entities](../../DurableFunctionsTheory/statefulentities.md) 30 | 31 | ## Requirements 32 | 33 | The serverless application you'll write, need to do the following things: 34 | 35 | 1. Respond to an incoming HTTP POST request on a static URL (e.g. `https://localhost:7071/api/NotifySupportHttpClient`). The json body is as follows: 36 | 37 | ```json 38 | { 39 | "message" : "The server is on fire!", 40 | "severity" : 1 41 | } 42 | ``` 43 | 44 | > For more info about *Azure Functions HttpTriggers* see this [Azure Functions University lesson](https://github.com/marcduiker/azure-functions-university/blob/main/lessons/dotnetcore31/http/README.md). 45 | 46 | 2. Support contact information must be read from Table Storage. A [data export](../data/SupportContacts.csv) has been made available which can be imported in a `SupportContacts` table. Only the people in **Team A** are part of the support schedule. 47 | 48 | > For more info about *Table Storage bindings* see [this Azure Functions University lesson](https://github.com/marcduiker/azure-functions-university/blob/main/lessons/dotnetcore31/table/README.md). 49 | 50 | 3. The notification process must start with the first support contact in the list (ordered ascending by the `Order` field), if the contact do not respond within 5 minutes the next contact should be notified. This continues until there's no-one available in the support contact list. The time should be a configurable value. 51 | 52 | 4. Each support contact must be notified 3 times (in total) in case they do not respond immediately. These notification attempts should occur evenly spread within the 5 minute window. The retry number should be a configurable value. 53 | 54 | 5. Once a support contact responds with a callback the notification process should stop and no other contacts should be notified. 55 | 56 | 6. The callback response is received as a POST request on a static URL (e.g. `https://localhost:7071/api/CallbackHttpClient`). The body of the request only contains the phone number of the support contact who was notified: 57 | 58 | ```json 59 | { 60 | "+31611111111" 61 | } 62 | ``` 63 | 64 | 7. For times sake, the actual notification functionality, making phone calls or sending text messages (incl callbacks) can be faked for this challenge. The callback, described in the previous requirement, can be triggered manually via a REST client. If you do have the time, you could try Twilio or Azure Communication Services. 65 | 66 | ## Build it! 67 | 68 | Now it's time to build your solution. 69 | 70 | If you need some tips, check [this page](notifysupport-tips.md). 71 | 72 | --- 73 | [🔼 Main README](../../README.md) 74 | -------------------------------------------------------------------------------- /NotifySupport/challenge/diagrams/notifysupport_functions1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcduiker/durable-functions-use-cases/a7e57a0d3b13b63aa1bf8ee110bc6b9ec44197a3/NotifySupport/challenge/diagrams/notifysupport_functions1.png -------------------------------------------------------------------------------- /NotifySupport/challenge/diagrams/notifysupport_functions2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcduiker/durable-functions-use-cases/a7e57a0d3b13b63aa1bf8ee110bc6b9ec44197a3/NotifySupport/challenge/diagrams/notifysupport_functions2.png -------------------------------------------------------------------------------- /NotifySupport/challenge/diagrams/notifysupport_overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcduiker/durable-functions-use-cases/a7e57a0d3b13b63aa1bf8ee110bc6b9ec44197a3/NotifySupport/challenge/diagrams/notifysupport_overview.png -------------------------------------------------------------------------------- /NotifySupport/challenge/notifysupport-tips.md: -------------------------------------------------------------------------------- 1 | # Tips for building the Notify Support solution 2 | 3 | ![](diagrams/notifysupport_functions2.png) 4 | 5 | ## Main orchestrator 6 | 7 | Use the main orchestrator with an activity function to get the list of Support Contacts. This orchestrator needs to iterate over the contacts. Do not use a for/each loop, use the `ContinueAsNew` functionality to restart the orchestrator with the next Support Contact. The main orchestrator also calls a sub-orchestrator that takes care of sending the notifications and waiting for the callback event. 8 | 9 | ## Sub-orchestrator 10 | 11 | Use a sub-orchestrator with an activity function to send the notification to a Support Contact. This sub-orchestrator also waits for the callback event. Use a time-out and a default value when waiting for the event. The `ContinueAsNew` functionality can be used to restart the sub-orchestrator in order to do a notification retry for the same Support Contact. Make sure to only restart the orchestration for the max number of retries per Support Contact. 12 | 13 | ## Timings when waiting for events 14 | 15 | Messaging in Durable Functions is done using Storage Queues. The Durable Functions framework polls these queues to determine if there are tasks to be done. The polling frequency is a configurable value called `maxQueuePollingInterval` in the `hosts.json` file. By default this value is set at 30 seconds. For short wait times regarding events it is useful to set this to a smaller value (such as 2 seconds) to allow more accurate timings. 16 | 17 | For more info read: 18 | 19 | - [Durable Functions Queue Polling](https://docs.microsoft.com/en-us/azure/azure-functions/durable/durable-functions-perf-and-scale#queue-polling). 20 | - [Durable Functions hosts.json defaults](https://docs.microsoft.com/en-us/azure/azure-functions/durable/durable-functions-bindings#durable-functions-2-0-host-json) 21 | 22 | ## Handling the callback 23 | 24 | Create an HttpTrigger client function to handle the callback and raise the event. When the callback is received, the only information we have is a phone number. The sub-orchestrator is waiting for the callback event, and that event should be raised to that specific sub-orchestrator instance. But we don't have the instance ID of the orchestrator in the callback request. A solution for this is to use Stateful Entities to store the instance ID when the notification is sent, and to read the instance ID when we receive the callback. Use the phone number as entity key. 25 | 26 | --- 27 | [🔼 Notify Support Challenge](README.md) -------------------------------------------------------------------------------- /NotifySupport/challenge/notifysupport.drawio: -------------------------------------------------------------------------------- 1 | 5Vpde6I4FP41XraPEEC9bGvbmd1On+7j7HZ6tU+ECGmRuCF+za+fhCQgRCtV0Y5705JjCOS85z1fpAVuxot7CifRNxKguGW3g0UL9Fu27fUs/lcIllLgeB0pCCkOpMgqBAP8EylhW0mnOEBpaSIjJGZ4Uhb6JEmQz0oySCmZl6eNSFx+6gSGyBAMfBib0mccsEhKu3ankH9BOIz0ky2vJ38ZQz1Z7SSNYEDmKyJw2wI3lBAmr8aLGxQL3Wm9PH9dPscPb979H3+l/8G/r//8/vjPhVzs7iO35FugKGE7L417t69DMCWzRfu1/+L9mLev6IVly7VnMJ4qhanNsqXWIAq4QtWQUBaRkCQwvi2k15RMkwCJ57T5qJjzQMiECy0ufEWMLZV1wCkjXBSxcax+lc8UD6qAtmXHal5KptRH72xTIcggDRF7Z56dw8rpgMgYMbrk91EUQ4Zn5ZeDyjDDfF6hfH6h9P8BLNoGFI+E4dFyMJ1MuE5bthfzd78eUn4ViqsvjE2+UxyGiBqgpYySt9zi7bK6uSVPxLzxIhScvxzFZO5HkLLLlKuI/SumzyPM0GACM73O+bQcphmiDC12AMpUrFrFBmrvS+071HheMDaXRats1cKDg2G5n4IYXKt0+UPdnw1exODS1cP+YvXH/nJ19IQo5trg1iGFTbNMu/etLPP2pJm69Ylg/t65FXXsshU5wC0vIV9M3VWxj/w19jCZrmEyLzzyrbOaBzjkUbaENIxxmPBrn2MhELsWNMM8jl2pH8Y4CKRRoRT/hMNsPQHrRGwo26J73XL77/FUBVl1cxHaVk3gHTpsJPBF+9IGPU8utiuMegoZjVLUCEK2idAj2YHW/pTOMlZnBJ6OJ3p6QhL0OSNgbW52ThkCLQOfGxjHQ+i/tYSpVgMgRT7CAgkRKViEE7FD7vL4vxiPMZ9yd6jAGCAfp5gkR46MbiUygq576shoZoz3gq3tVKcpwssw6DPT9RXh0Nqgx1WHOExJPGXoivqKNZm0GDkCMAPNY+UotlMzR+k0lqKYCeMpUpSmPZdb03N1T+m4zGxxgBLhlxKRwvMwzoTvOGc+OHVz9sb44G2nw47OHyYcxgzCrDTSqVqMRiIKxSKZeyIpzjAGfSr3KvMyRG9nSKZnVsY2uQ4f9gTfAphGuQE0BZXDTaNUXgEziLhrkHLcpqDqGFDdESq8CPSjtZHeCC4GtFxnrLmMWri9tGBdU0iBXhkpUJdUoLEgY6Zk5xhkujWDzAb4jhNkzOrlGWLBiVFGHj9Plc8ozIDuZwszvRqESIIr0boWzieGaYr9SldngVnW1JF9HD56WfmlaOmIQamjs3JT3gva0glqmjl2TeZsaCBsCUFatmdrKF9HW1GvYh1ym0ZryFgod8h6IaeyUMM9Jts5oPWt2F5hiRusjw+q7cRNFmltschS96Rx8+z8DvaZ22PVy33UPl2n0i+opnAb7JMbDFyuTFMZ0MYXrrplt1v6+MQv5IqHNX6zytrQvvvt+quS1+/1Vy2rU9L4BdjT9I7QbjUrsgHjCd+BqrLTfKvqWBWGnb4jpy2hxoeHs+tr1/bvAKyHdU/P7VYcoVfNLJpOCMxS+htcZDU033CbjCo9qDTbMi+zBc7n1h3PxyfkolkvD1AsDrZwHESLYk2j3EDhnAo2r1fTPzZWsAEzZ/7fuEfp9eq4x5N2NvRrrkD0VXgqFiGKWusagnAdn8xpmmFn5+q87uldXZ0jMttq0R1ryLXnW3Rhq4vcXQvboxaotRm66dTMaSpUzztQhepZzVSoHXf9CzdaoQKz4jmXChVsPQHUdvRZYf2J/NMfCAJm9vxxF7ZDO+24HmZDZDmN43B3dRx29UtANWE8kOPIE1HtONy9HAcfFkfE5fTinD24/QU= -------------------------------------------------------------------------------- /NotifySupport/challenge/prerequisites.md: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | 3 | ## Frameworks & Tooling 🧰 4 | 5 | In order to complete the the lessons you need to install the following: 6 | 7 | |Prerequisite|Description 8 | |-|- 9 | |[.NET Core 3.1](https://dotnet.microsoft.com/download/dotnet-core)|The .NET runtime and SDK. 10 | |[VSCode](https://code.visualstudio.com/Download)|A great cross platform code editor. 11 | |[VSCode AzureFunctions extension](https://github.com/Microsoft/vscode-azurefunctions)|Extension for VSCode to easily develop and manage Azure Functions. 12 | |[Azure Functions Core Tools](https://github.com/Azure/azure-functions-core-tools)|Azure Functions runtime and CLI for local development. 13 | |[RESTClient for VSCode](https://marketplace.visualstudio.com/items?itemName=humao.rest-client) or [Postman](https://www.postman.com/)|A VSCode extension or application to make HTTP requests. 14 | |[Azurite for VSCode](https://marketplace.visualstudio.com/items?itemName=Azurite.azurite)|[Cross platform storage emulator](https://docs.microsoft.com/en-us/azure/storage/common/storage-use-azurite?tabs=visual-studio) for using Azure Storage services if you want to develop locally without connecting to a Storage Account in the cloud. If you can't use the emulator you need an [Azure Storage Account](https://docs.microsoft.com/en-us/azure/storage/common/storage-account-create?tabs=azure-portal). 15 | |[Azure Storage Explorer](https://azure.microsoft.com/en-us/features/storage-explorer/)|Application to manage Azure Storage resources (both in the cloud and local emulated). 16 | |[CodeTour for VSCode](https://marketplace.visualstudio.com/items?itemName=vsls-contrib.codetour)|**OPTIONAL:** Code samples in the markdown files use CodeTour to explain the samples in more detail. CodeTour is a VS Code extension that guides you through source code. 17 | 18 | ## Creating your local workspace 👩‍💻 19 | 20 | Create a new folder (local git repository) and use the repository you're currently reading from for reference only (for when you're stuck). 21 | 22 | - Create a new folder to work in: 23 | 24 | ```cmd 25 | C:\dev\mkdir notify-support 26 | C:\dev\cd .\notify-support\ 27 | ``` 28 | 29 | - Turn this into a git repository: 30 | 31 | ```cmd 32 | C:\dev\notify-support\git init 33 | ``` 34 | 35 | - Add subfolders for the source code and test files: 36 | 37 | ```cmd 38 | C:\dev\notify-support\mkdir src 39 | C:\dev\notify-support\mkdir tst 40 | ``` 41 | 42 | You should be good to go now! 43 | 44 | --- 45 | [🔼 Main README](../../README.md) | [Notify Support Challenge ▶](README.md) 46 | -------------------------------------------------------------------------------- /NotifySupport/data/SupportContacts.csv: -------------------------------------------------------------------------------- 1 | PartitionKey,RowKey,Timestamp,Name,Name@type,Team,Team@type,PhoneNumber,PhoneNumber@type,Order,Order@type 2 | A,+31611111111,2021-02-22T21:10:04.713Z,Angela,Edm.String,A,Edm.String,+31611111111,Edm.String,1,Edm.String 3 | A,+31622222222,2021-02-22T21:10:13.583Z,Brian,Edm.String,A,Edm.String,+31622222222,Edm.String,2,Edm.String 4 | A,+31633333333,2021-02-22T21:10:18.310Z,Carol,Edm.String,A,Edm.String,+31633333333,Edm.String,3,Edm.String 5 | B,+31644444444,2021-02-22T21:10:10.580Z,Adrian,Edm.String,B,Edm.String,+31644444444,Edm.String,1,Edm.String 6 | -------------------------------------------------------------------------------- /NotifySupport/src/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "ms-azuretools.vscode-azurefunctions", 4 | "ms-dotnettools.csharp", 5 | "Azurite.azurite", 6 | "humao.rest-client", 7 | "vsls-contrib.codetour" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /NotifySupport/src/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Attach to .NET Functions", 6 | "type": "coreclr", 7 | "request": "attach", 8 | "processId": "${command:azureFunctions.pickProcess}" 9 | } 10 | ] 11 | } -------------------------------------------------------------------------------- /NotifySupport/src/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "azureFunctions.deploySubpath": "DurableFunctions.UseCases.NotifySupport/bin/Release/netcoreapp3.1/publish", 3 | "azureFunctions.projectLanguage": "C#", 4 | "azureFunctions.projectRuntime": "~3", 5 | "debug.internalConsoleOptions": "neverOpen", 6 | "azureFunctions.pickProcessTimeout" : 120, 7 | "azureFunctions.preDeployTask": "publish" 8 | } -------------------------------------------------------------------------------- /NotifySupport/src/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "clean", 6 | "command": "dotnet", 7 | "args": [ 8 | "clean", 9 | "/property:GenerateFullPaths=true", 10 | "/consoleloggerparameters:NoSummary" 11 | ], 12 | "type": "process", 13 | "problemMatcher": "$msCompile", 14 | "options": { 15 | "cwd": "${workspaceFolder}/DurableFunctions.UseCases.NotifySupport" 16 | } 17 | }, 18 | { 19 | "label": "build", 20 | "command": "dotnet", 21 | "args": [ 22 | "build", 23 | "/property:GenerateFullPaths=true", 24 | "/consoleloggerparameters:NoSummary" 25 | ], 26 | "type": "process", 27 | "dependsOn": "clean", 28 | "group": { 29 | "kind": "build", 30 | "isDefault": true 31 | }, 32 | "problemMatcher": "$msCompile", 33 | "options": { 34 | "cwd": "${workspaceFolder}/DurableFunctions.UseCases.NotifySupport" 35 | } 36 | }, 37 | { 38 | "label": "clean release", 39 | "command": "dotnet", 40 | "args": [ 41 | "clean", 42 | "--configuration", 43 | "Release", 44 | "/property:GenerateFullPaths=true", 45 | "/consoleloggerparameters:NoSummary" 46 | ], 47 | "type": "process", 48 | "problemMatcher": "$msCompile", 49 | "options": { 50 | "cwd": "${workspaceFolder}/DurableFunctions.UseCases.NotifySupport" 51 | } 52 | }, 53 | { 54 | "label": "publish", 55 | "command": "dotnet", 56 | "args": [ 57 | "publish", 58 | "--configuration", 59 | "Release", 60 | "/property:GenerateFullPaths=true", 61 | "/consoleloggerparameters:NoSummary" 62 | ], 63 | "type": "process", 64 | "dependsOn": "clean release", 65 | "problemMatcher": "$msCompile", 66 | "options": { 67 | "cwd": "${workspaceFolder}/DurableFunctions.UseCases.NotifySupport" 68 | } 69 | }, 70 | { 71 | "type": "func", 72 | "dependsOn": "build", 73 | "options": { 74 | "cwd": "${workspaceFolder}/DurableFunctions.UseCases.NotifySupport/bin/Debug/netcoreapp3.1" 75 | }, 76 | "command": "host start", 77 | "isBackground": true, 78 | "problemMatcher": "$func-dotnet-watch" 79 | } 80 | ] 81 | } -------------------------------------------------------------------------------- /NotifySupport/src/DurableFunctions.UseCases.NotifySupport/.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # Azure Functions localsettings file 5 | # local.settings.json 6 | 7 | # Azurite files/folders 8 | __blobstorage__ 9 | __queuestorage__ 10 | __tablestorage__ 11 | __azurite* 12 | 13 | # User-specific files 14 | *.suo 15 | *.user 16 | *.userosscache 17 | *.sln.docstates 18 | 19 | # User-specific files (MonoDevelop/Xamarin Studio) 20 | *.userprefs 21 | 22 | # Build results 23 | [Dd]ebug/ 24 | [Dd]ebugPublic/ 25 | [Rr]elease/ 26 | [Rr]eleases/ 27 | x64/ 28 | x86/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Ll]og/ 33 | 34 | # Visual Studio 2015 cache/options directory 35 | .vs/ 36 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | # MSTest test Results 40 | [Tt]est[Rr]esult*/ 41 | [Bb]uild[Ll]og.* 42 | 43 | # NUNIT 44 | *.VisualState.xml 45 | TestResult.xml 46 | 47 | # Build Results of an ATL Project 48 | [Dd]ebugPS/ 49 | [Rr]eleasePS/ 50 | dlldata.c 51 | 52 | # DNX 53 | project.lock.json 54 | project.fragment.lock.json 55 | artifacts/ 56 | 57 | *_i.c 58 | *_p.c 59 | *_i.h 60 | *.ilk 61 | *.meta 62 | *.obj 63 | *.pch 64 | *.pdb 65 | *.pgc 66 | *.pgd 67 | *.rsp 68 | *.sbr 69 | *.tlb 70 | *.tli 71 | *.tlh 72 | *.tmp 73 | *.tmp_proj 74 | *.log 75 | *.vspscc 76 | *.vssscc 77 | .builds 78 | *.pidb 79 | *.svclog 80 | *.scc 81 | 82 | # Chutzpah Test files 83 | _Chutzpah* 84 | 85 | # Visual C++ cache files 86 | ipch/ 87 | *.aps 88 | *.ncb 89 | *.opendb 90 | *.opensdf 91 | *.sdf 92 | *.cachefile 93 | *.VC.db 94 | *.VC.VC.opendb 95 | 96 | # Visual Studio profiler 97 | *.psess 98 | *.vsp 99 | *.vspx 100 | *.sap 101 | 102 | # TFS 2012 Local Workspace 103 | $tf/ 104 | 105 | # Guidance Automation Toolkit 106 | *.gpState 107 | 108 | # ReSharper is a .NET coding add-in 109 | _ReSharper*/ 110 | *.[Rr]e[Ss]harper 111 | *.DotSettings.user 112 | 113 | # JustCode is a .NET coding add-in 114 | .JustCode 115 | 116 | # TeamCity is a build add-in 117 | _TeamCity* 118 | 119 | # DotCover is a Code Coverage Tool 120 | *.dotCover 121 | 122 | # NCrunch 123 | _NCrunch_* 124 | .*crunch*.local.xml 125 | nCrunchTemp_* 126 | 127 | # MightyMoose 128 | *.mm.* 129 | AutoTest.Net/ 130 | 131 | # Web workbench (sass) 132 | .sass-cache/ 133 | 134 | # Installshield output folder 135 | [Ee]xpress/ 136 | 137 | # DocProject is a documentation generator add-in 138 | DocProject/buildhelp/ 139 | DocProject/Help/*.HxT 140 | DocProject/Help/*.HxC 141 | DocProject/Help/*.hhc 142 | DocProject/Help/*.hhk 143 | DocProject/Help/*.hhp 144 | DocProject/Help/Html2 145 | DocProject/Help/html 146 | 147 | # Click-Once directory 148 | publish/ 149 | 150 | # Publish Web Output 151 | *.[Pp]ublish.xml 152 | *.azurePubxml 153 | # TODO: Comment the next line if you want to checkin your web deploy settings 154 | # but database connection strings (with potential passwords) will be unencrypted 155 | #*.pubxml 156 | *.publishproj 157 | 158 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 159 | # checkin your Azure Web App publish settings, but sensitive information contained 160 | # in these scripts will be unencrypted 161 | PublishScripts/ 162 | 163 | # NuGet Packages 164 | *.nupkg 165 | # The packages folder can be ignored because of Package Restore 166 | **/packages/* 167 | # except build/, which is used as an MSBuild target. 168 | !**/packages/build/ 169 | # Uncomment if necessary however generally it will be regenerated when needed 170 | #!**/packages/repositories.config 171 | # NuGet v3's project.json files produces more ignoreable files 172 | *.nuget.props 173 | *.nuget.targets 174 | 175 | # Microsoft Azure Build Output 176 | csx/ 177 | *.build.csdef 178 | 179 | # Microsoft Azure Emulator 180 | ecf/ 181 | rcf/ 182 | 183 | # Windows Store app package directories and files 184 | AppPackages/ 185 | BundleArtifacts/ 186 | Package.StoreAssociation.xml 187 | _pkginfo.txt 188 | 189 | # Visual Studio cache files 190 | # files ending in .cache can be ignored 191 | *.[Cc]ache 192 | # but keep track of directories ending in .cache 193 | !*.[Cc]ache/ 194 | 195 | # Others 196 | ClientBin/ 197 | ~$* 198 | *~ 199 | *.dbmdl 200 | *.dbproj.schemaview 201 | *.jfm 202 | *.pfx 203 | *.publishsettings 204 | node_modules/ 205 | orleans.codegen.cs 206 | 207 | # Since there are multiple workflows, uncomment next line to ignore bower_components 208 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 209 | #bower_components/ 210 | 211 | # RIA/Silverlight projects 212 | Generated_Code/ 213 | 214 | # Backup & report files from converting an old project file 215 | # to a newer Visual Studio version. Backup files are not needed, 216 | # because we have git ;-) 217 | _UpgradeReport_Files/ 218 | Backup*/ 219 | UpgradeLog*.XML 220 | UpgradeLog*.htm 221 | 222 | # SQL Server files 223 | *.mdf 224 | *.ldf 225 | 226 | # Business Intelligence projects 227 | *.rdl.data 228 | *.bim.layout 229 | *.bim_*.settings 230 | 231 | # Microsoft Fakes 232 | FakesAssemblies/ 233 | 234 | # GhostDoc plugin setting file 235 | *.GhostDoc.xml 236 | 237 | # Node.js Tools for Visual Studio 238 | .ntvs_analysis.dat 239 | 240 | # Visual Studio 6 build log 241 | *.plg 242 | 243 | # Visual Studio 6 workspace options file 244 | *.opt 245 | 246 | # Visual Studio LightSwitch build output 247 | **/*.HTMLClient/GeneratedArtifacts 248 | **/*.DesktopClient/GeneratedArtifacts 249 | **/*.DesktopClient/ModelManifest.xml 250 | **/*.Server/GeneratedArtifacts 251 | **/*.Server/ModelManifest.xml 252 | _Pvt_Extensions 253 | 254 | # Paket dependency manager 255 | .paket/paket.exe 256 | paket-files/ 257 | 258 | # FAKE - F# Make 259 | .fake/ 260 | 261 | # JetBrains Rider 262 | .idea/ 263 | *.sln.iml 264 | 265 | # CodeRush 266 | .cr/ 267 | 268 | # Python Tools for Visual Studio (PTVS) 269 | __pycache__/ 270 | *.pyc -------------------------------------------------------------------------------- /NotifySupport/src/DurableFunctions.UseCases.NotifySupport/Activities/GetSupportContactActivity.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using DurableFunctions.UseCases.NotifySupport; 3 | using Microsoft.Azure.Cosmos.Table; 4 | using Microsoft.Azure.WebJobs; 5 | using Microsoft.Azure.WebJobs.Extensions.DurableTask; 6 | 7 | namespace DurableFunctions.UseCases 8 | { 9 | public class GetSupportContactActivity 10 | { 11 | [FunctionName(nameof(GetSupportContactActivity))] 12 | public IEnumerable Run( 13 | [ActivityTrigger] string team, 14 | [Table("%SupportContactTableName%", Connection ="SupportContactStorage")] CloudTable cloudTable) 15 | { 16 | var teamFilter = new TableQuery() 17 | .Where( 18 | TableQuery.GenerateFilterCondition( 19 | nameof(SupportContactEntity.PartitionKey), 20 | QueryComparisons.Equal, 21 | team)) 22 | .OrderBy(nameof(SupportContactEntity.Order)); 23 | var supportContacts = cloudTable.ExecuteQuery(teamFilter); 24 | 25 | return supportContacts; 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /NotifySupport/src/DurableFunctions.UseCases.NotifySupport/Activities/SendNotificationActivity.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Azure.WebJobs; 2 | using Microsoft.Azure.WebJobs.Extensions.DurableTask; 3 | using Microsoft.Extensions.Logging; 4 | 5 | namespace DurableFunctions.UseCases.NotifySupport 6 | { 7 | public class SendNotificationActivity 8 | { 9 | [FunctionName(nameof(SendNotificationActivity))] 10 | public void Run( 11 | [ActivityTrigger] SendNotificationActivityInput input, 12 | ILogger logger) 13 | { 14 | logger.LogInformation($"=== Calling {input.PhoneNumber}, Attempt={input.Attempt}, Message={input.Message} ==="); 15 | 16 | return; 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /NotifySupport/src/DurableFunctions.UseCases.NotifySupport/Builders/NotifySupportOrchestratorInputBuilder.cs: -------------------------------------------------------------------------------- 1 | namespace DurableFunctions.UseCases.NotifySupport 2 | { 3 | public static class NotifySupportOrchestratorInputBuilder 4 | { 5 | public static NotifySupportOrchestratorInput Build( 6 | NotifySupportClientInput clientInput, 7 | int maxNotificationAttempts, 8 | int waitTimeForEscalationInSeconds) 9 | { 10 | return new NotifySupportOrchestratorInput 11 | { 12 | MaxNotificationAttempts = maxNotificationAttempts, 13 | Message = clientInput.Message, 14 | SupportContactIndex = 0, 15 | // The SupportContacts is *not* set here, they are added later. 16 | Severity = clientInput.Severity, 17 | WaitTimeForEscalationInSeconds = waitTimeForEscalationInSeconds 18 | }; 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /NotifySupport/src/DurableFunctions.UseCases.NotifySupport/Builders/SendNotificationOrchestratorInputBuilder.cs: -------------------------------------------------------------------------------- 1 | namespace DurableFunctions.UseCases.NotifySupport 2 | { 3 | public static class SendNotificationOrchestratorInputBuilder 4 | { 5 | public static SendNotificationOrchestratorInput Build( 6 | NotifySupportOrchestratorInput orchestratorInput) 7 | { 8 | return new SendNotificationOrchestratorInput 9 | { 10 | MaxNotificationAttempts = orchestratorInput.MaxNotificationAttempts, 11 | Message = orchestratorInput.Message, 12 | NotificationAttemptCount = 1, 13 | SupportContact = orchestratorInput.SupportContacts[orchestratorInput.SupportContactIndex], 14 | WaitTimeForEscalationInSeconds = orchestratorInput.WaitTimeForEscalationInSeconds 15 | }; 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /NotifySupport/src/DurableFunctions.UseCases.NotifySupport/Clients/CallbackHttpClient.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http; 2 | using System.Threading.Tasks; 3 | using Microsoft.AspNetCore.Mvc; 4 | using Microsoft.Azure.WebJobs; 5 | using Microsoft.Azure.WebJobs.Extensions.DurableTask; 6 | using Microsoft.Azure.WebJobs.Extensions.Http; 7 | using Microsoft.Extensions.Logging; 8 | 9 | namespace DurableFunctions.UseCases.NotifySupport 10 | { 11 | public static class CallbackHttpClient 12 | { 13 | [FunctionName(nameof(CallbackHttpClient))] 14 | public static async Task Run( 15 | [HttpTrigger( 16 | AuthorizationLevel.Function, 17 | nameof(HttpMethod.Post), 18 | Route = null)] HttpRequestMessage message, 19 | [DurableClient] IDurableClient client, 20 | ILogger logger) 21 | { 22 | var phoneNumber = await message.Content.ReadAsAsync(); 23 | var entityId = new EntityId(nameof(NotificationOrchestratorInstanceEntity), phoneNumber); 24 | var instanceEntity = await client.ReadEntityStateAsync(entityId); 25 | if (instanceEntity.EntityExists) 26 | { 27 | await client.RaiseEventAsync( 28 | instanceEntity.EntityState.InstanceId, 29 | EventNames.Callback, 30 | true); 31 | } 32 | else 33 | { 34 | logger.LogError($"=== No instanceId found for {phoneNumber}. ==="); 35 | } 36 | 37 | return new AcceptedResult(); 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /NotifySupport/src/DurableFunctions.UseCases.NotifySupport/Clients/NotifySupportHttpClient.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.Azure.WebJobs; 3 | using Microsoft.Azure.WebJobs.Extensions.Http; 4 | using Microsoft.Extensions.Logging; 5 | using Microsoft.Azure.WebJobs.Extensions.DurableTask; 6 | using System.Net.Http; 7 | using System; 8 | 9 | namespace DurableFunctions.UseCases.NotifySupport 10 | { 11 | public static class NotifySupportHttpClient 12 | { 13 | [FunctionName(nameof(NotifySupportHttpClient))] 14 | public static async Task Run( 15 | [HttpTrigger( 16 | AuthorizationLevel.Function, 17 | nameof(HttpMethod.Post), 18 | Route = null)] HttpRequestMessage message, 19 | [DurableClient] IDurableClient client, 20 | ILogger logger) 21 | { 22 | var clientInput = await message.Content.ReadAsAsync(); 23 | var waitTimeForEscalationInSeconds = int.TryParse(Environment.GetEnvironmentVariable("WaitTimeForEscalationInSeconds"), out int waitTime) ? waitTime : 60; 24 | var maxNotificationAttempts = int.TryParse(Environment.GetEnvironmentVariable("MaxNotificationAttempts"), out int maxAttempts) ? maxAttempts : 3; 25 | 26 | var orchestratorInput = NotifySupportOrchestratorInputBuilder.Build( 27 | clientInput, 28 | maxNotificationAttempts, 29 | waitTimeForEscalationInSeconds); 30 | 31 | string instanceId = await client.StartNewAsync( 32 | nameof(NotifySupportOrchestrator), 33 | orchestratorInput); 34 | 35 | return client.CreateCheckStatusResponse(message, instanceId); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /NotifySupport/src/DurableFunctions.UseCases.NotifySupport/DurableFunctions.UseCases.NotifySupport.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netcoreapp3.1 4 | v3 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | PreserveNewest 14 | 15 | 16 | PreserveNewest 17 | Never 18 | 19 | 20 | -------------------------------------------------------------------------------- /NotifySupport/src/DurableFunctions.UseCases.NotifySupport/Entities/NotificationOrchestratorInstanceEntity.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.Azure.WebJobs; 3 | using Microsoft.Azure.WebJobs.Extensions.DurableTask; 4 | using Newtonsoft.Json; 5 | 6 | namespace DurableFunctions.UseCases.NotifySupport 7 | { 8 | [JsonObject(MemberSerialization.OptIn)] 9 | public class NotificationOrchestratorInstanceEntity 10 | { 11 | [JsonProperty("instanceId")] 12 | public string InstanceId { get; set; } 13 | 14 | public void Set(string instanceId) => InstanceId = instanceId; 15 | 16 | public void Reset() => InstanceId = string.Empty; 17 | 18 | [FunctionName(nameof(NotificationOrchestratorInstanceEntity))] 19 | public static Task Run( 20 | [EntityTrigger] IDurableEntityContext ctx) 21 | => ctx.DispatchAsync(); 22 | } 23 | } -------------------------------------------------------------------------------- /NotifySupport/src/DurableFunctions.UseCases.NotifySupport/Models/EventNames.cs: -------------------------------------------------------------------------------- 1 | namespace DurableFunctions.UseCases.NotifySupport 2 | { 3 | public static class EventNames 4 | { 5 | public static string Callback = "Callback"; 6 | } 7 | } -------------------------------------------------------------------------------- /NotifySupport/src/DurableFunctions.UseCases.NotifySupport/Models/NotifySupportClientInput.cs: -------------------------------------------------------------------------------- 1 | namespace DurableFunctions.UseCases.NotifySupport 2 | { 3 | public class NotifySupportClientInput 4 | { 5 | public string Message { get; set; } 6 | public int Severity { get; set; } 7 | } 8 | } -------------------------------------------------------------------------------- /NotifySupport/src/DurableFunctions.UseCases.NotifySupport/Models/NotifySupportOrchestratorInput.cs: -------------------------------------------------------------------------------- 1 | namespace DurableFunctions.UseCases.NotifySupport 2 | { 3 | public class NotifySupportOrchestratorInput 4 | { 5 | public int MaxNotificationAttempts { get; set; } 6 | public string Message { get; set; } 7 | public int Severity { get; set; } 8 | public int SupportContactIndex { get; set; } 9 | public SupportContactEntity[] SupportContacts { get; set; } 10 | public int WaitTimeForEscalationInSeconds { get; set; } 11 | } 12 | } -------------------------------------------------------------------------------- /NotifySupport/src/DurableFunctions.UseCases.NotifySupport/Models/SendNotificationActivityInput.cs: -------------------------------------------------------------------------------- 1 | namespace DurableFunctions.UseCases.NotifySupport 2 | { 3 | public class SendNotificationActivityInput 4 | { 5 | public int Attempt { get; set; } 6 | public string Message { get; set; } 7 | public string PhoneNumber { get; set; } 8 | } 9 | } -------------------------------------------------------------------------------- /NotifySupport/src/DurableFunctions.UseCases.NotifySupport/Models/SendNotificationOrchestratorInput.cs: -------------------------------------------------------------------------------- 1 | namespace DurableFunctions.UseCases.NotifySupport 2 | { 3 | public class SendNotificationOrchestratorInput 4 | { 5 | public int MaxNotificationAttempts { get; set; } 6 | public string Message { get; set; } 7 | public int NotificationAttemptCount { get; set; } 8 | public SupportContactEntity SupportContact { get; set; } 9 | public int WaitTimeForEscalationInSeconds { get; set; } 10 | } 11 | } -------------------------------------------------------------------------------- /NotifySupport/src/DurableFunctions.UseCases.NotifySupport/Models/SendNotificationOrchestratorResult.cs: -------------------------------------------------------------------------------- 1 | namespace DurableFunctions.UseCases.NotifySupport 2 | { 3 | public class SendNotificationOrchestratorResult 4 | { 5 | public int Attempt { get; set; } 6 | public bool CallbackReceived { get; set; } 7 | public string PhoneNumber { get; set; } 8 | } 9 | } -------------------------------------------------------------------------------- /NotifySupport/src/DurableFunctions.UseCases.NotifySupport/Models/SupportContactEntity.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Azure.Cosmos.Table; 2 | 3 | namespace DurableFunctions.UseCases.NotifySupport 4 | { 5 | public class SupportContactEntity : TableEntity 6 | { 7 | public SupportContactEntity() 8 | { 9 | } 10 | 11 | public SupportContactEntity(string team, string name, string phoneNumber, int order) 12 | { 13 | PartitionKey = team; 14 | RowKey = phoneNumber; 15 | Name = name; 16 | PhoneNumber = phoneNumber; 17 | Order = order; 18 | Team = team; 19 | } 20 | 21 | public string Name { get; set; } 22 | public int Order { get; set; } 23 | public string PhoneNumber { get; set; } 24 | public string Team { get; set; } 25 | } 26 | } -------------------------------------------------------------------------------- /NotifySupport/src/DurableFunctions.UseCases.NotifySupport/Orchestrators/NotifySupportOrchestrator.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using Microsoft.Azure.WebJobs; 5 | using Microsoft.Azure.WebJobs.Extensions.DurableTask; 6 | using Microsoft.Extensions.Logging; 7 | 8 | namespace DurableFunctions.UseCases.NotifySupport 9 | { 10 | public class NotifySupportOrchestrator 11 | { 12 | [FunctionName(nameof(NotifySupportOrchestrator))] 13 | public async Task Run( 14 | [OrchestrationTrigger] IDurableOrchestrationContext context, 15 | ILogger logger) 16 | { 17 | var input = context.GetInput(); 18 | 19 | if (input.SupportContacts == null) 20 | { 21 | // Let's get the support contacts from storage. 22 | var supportContacts = await context.CallActivityAsync>( 23 | nameof(GetSupportContactActivity), 24 | "A"); 25 | 26 | input.SupportContacts = supportContacts.ToArray(); 27 | } 28 | 29 | var notificationOrchestratorInput = SendNotificationOrchestratorInputBuilder.Build(input); 30 | 31 | var notificationResult = await context.CallSubOrchestratorAsync( 32 | nameof(SendNotificationOrchestrator), 33 | notificationOrchestratorInput); 34 | 35 | if (!notificationResult.CallbackReceived && 36 | notificationOrchestratorInput.SupportContact != input.SupportContacts.Last()) 37 | { 38 | // Calls have not been answered, let's try the next contact. 39 | input.SupportContactIndex++; 40 | logger.LogInformation($"=== Next Contact={input.SupportContacts[input.SupportContactIndex].PhoneNumber} ==="); 41 | context.ContinueAsNew(input); 42 | } 43 | else 44 | { 45 | logger.LogInformation($"=== Completed {nameof(NotifySupportOrchestrator)} for {notificationResult.PhoneNumber} with callback received={notificationResult.CallbackReceived} on attempt={notificationResult.Attempt}. ==="); 46 | } 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /NotifySupport/src/DurableFunctions.UseCases.NotifySupport/Orchestrators/SendNotificationOrchestrator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Microsoft.Azure.WebJobs; 4 | using Microsoft.Azure.WebJobs.Extensions.DurableTask; 5 | using Microsoft.Extensions.Logging; 6 | 7 | namespace DurableFunctions.UseCases.NotifySupport 8 | { 9 | public class SendNotificationOrchestrator 10 | { 11 | [FunctionName(nameof(SendNotificationOrchestrator))] 12 | public async Task Run( 13 | [OrchestrationTrigger] IDurableOrchestrationContext context, 14 | ILogger logger) 15 | { 16 | var input = context.GetInput(); 17 | 18 | // Use Durable Entity to store orchestrator instanceId based on phonenumber 19 | var entityId = new EntityId(nameof(NotificationOrchestratorInstanceEntity), input.SupportContact.PhoneNumber); 20 | context.SignalEntity( 21 | entityId, 22 | nameof(NotificationOrchestratorInstanceEntity.Set), 23 | context.InstanceId); 24 | 25 | var activityInput = new SendNotificationActivityInput { 26 | Attempt = input.NotificationAttemptCount, 27 | Message = input.Message, 28 | PhoneNumber = input.SupportContact.PhoneNumber}; 29 | await context.CallActivityAsync( 30 | nameof(SendNotificationActivity), 31 | activityInput); 32 | 33 | var waitTimeBetweenRetry = TimeSpan.FromSeconds(input.WaitTimeForEscalationInSeconds / input.MaxNotificationAttempts); 34 | 35 | // Orchestrator will wait until the event is received or waitTimeBetweenRetry is passed, defaults to false. 36 | var callBackResult = await context.WaitForExternalEvent(EventNames.Callback, waitTimeBetweenRetry, false); 37 | if (!callBackResult && input.NotificationAttemptCount < input.MaxNotificationAttempts) 38 | { 39 | // Call has not been answered, let's try again! 40 | input.NotificationAttemptCount++; 41 | context.ContinueAsNew(input); 42 | } 43 | 44 | // Call has been answered or MaxNotificationAttempts has been reached. 45 | var result = new SendNotificationOrchestratorResult { 46 | Attempt = input.NotificationAttemptCount, 47 | PhoneNumber = input.SupportContact.PhoneNumber, 48 | CallbackReceived = callBackResult }; 49 | 50 | return result; 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /NotifySupport/src/DurableFunctions.UseCases.NotifySupport/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "extensions": { 4 | "durableTask": { 5 | "hubName": "%TaskHubName%", 6 | "storageProvider": { 7 | "maxQueuePollingInterval": "00:00:01" 8 | } 9 | } 10 | }, 11 | "logging": { 12 | "applicationInsights": { 13 | "samplingSettings": { 14 | "isEnabled": true 15 | } 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /NotifySupport/src/DurableFunctions.UseCases.NotifySupport/local.settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "IsEncrypted": false, 3 | "Values": { 4 | "TaskHubName" : "NotifySupportV1", 5 | "AzureWebJobsStorage": "UseDevelopmentStorage=true", 6 | "FUNCTIONS_WORKER_RUNTIME": "dotnet", 7 | "SupportContactStorage" : "UseDevelopmentStorage=true", 8 | "SupportContactTableName" : "SupportContacts", 9 | "MaxNotificationAttempts" : 3, 10 | "WaitTimeForEscalationInSeconds" : 60 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /NotifySupport/tst/notifysupport.http: -------------------------------------------------------------------------------- 1 | # @name notifyRequest 2 | POST http://localhost:7071/api/NotifySupportHttpClient 3 | Content-Type: application/json 4 | 5 | { 6 | "message" : "Everything is NOT fine!", 7 | "severity" : 1 8 | } 9 | 10 | ### Get status of orchestration 11 | @orchestratorId = {{notifyRequest.response.body.$.id}} 12 | GET http://localhost:7071/runtime/webhooks/durabletask/instances/{{orchestratorId}} 13 | ?taskHub=NotifySupportV1 14 | &connection=Storage 15 | &code=iKw1SaXtu0acB5oGzYLvmiqpjGBE5HNzaAVJrZbEr1ZqWU4jVKCzBg== 16 | 17 | ### Send Callback event 18 | 19 | # @name callbackRequest 20 | POST http://localhost:7071/api/CallbackHttpClient 21 | Content-Type: application/json 22 | 23 | "+31622222222" 24 | 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Durable Functions Challenges 2 | 3 | In this repository, I'll collect some real-life challenges you can solve using Azure Functions & Durable Functions. 4 | 5 | These challenges do assume some experience with Azure Functions (in .NET) and a basic understanding of Durable Functions. If you want to learn more about those first, then please visit [Azure Functions University](https://github.com/marcduiker/azure-functions-university). 6 | 7 | ## 1. [Notify Support](NotifySupport/challenge/README.md) 8 | 9 | Write a Function App that responds to alerts and notifies members of a support team to investigate the issue. 10 | 11 | ![Notify Support overview diagram](NotifySupport/challenge/diagrams/notifysupport_overview.png) 12 | 13 | This challenge combines sub-orchestrations, eternal orchestrations, webhooks, waiting for external events, and using stateful entities. 14 | 15 | Go to the [Notify Support Challenge](NotifySupport/challenge/README.md). 16 | 17 | ## 2. [Fraud Detection](FraudDetection/challenge/README.md) 18 | 19 | Write a Function App that takes in financial transactions, calls a web service to analyze the transactions to detect fraud,waits for a call back with the analysis result and finally stores an audit record of the transaction. 20 | 21 | ![Fraud Detection overview diagram](FraudDetection/challenge/diagrams/frauddetection_overview.png) 22 | 23 | This challenge combines orchestrations, webhooks, waiting for external events, and stateful entities. 24 | 25 | Go to the [Fraud Detection Challenge](FraudDetection/challenge/README.md). -------------------------------------------------------------------------------- /fraud-detection.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "name": "Durable Functions Theory", 5 | "path": "DurableFunctionsTheory" 6 | }, 7 | { 8 | "name": "Fraud Detection Challenge", 9 | "path": "FraudDetection/challenge" 10 | }, 11 | { 12 | "name": "Source Code", 13 | "path": "FraudDetection/src" 14 | }, 15 | { 16 | "name": "Test", 17 | "path": "FraudDetection/tst" 18 | }, 19 | { 20 | "name": "GitHub Workflow", 21 | "path": ".github/workflows" 22 | }, 23 | { 24 | "name": "(Azurite)", 25 | "path": "FraudDetection/_azurite" 26 | }, 27 | { 28 | "name": "(Code Tour)", 29 | "path": ".tours" 30 | } 31 | ], 32 | "settings": { 33 | "debug.internalConsoleOptions": "neverOpen", 34 | "azureFunctions.pickProcessTimeout" : 120, 35 | }, 36 | "extensions": { 37 | "recommendations": [ 38 | "ms-azuretools.vscode-azurefunctions", 39 | "ms-dotnettools.csharp", 40 | "azurite.azurite", 41 | "vsls-contrib.codetour" 42 | ] 43 | } 44 | } -------------------------------------------------------------------------------- /notify-support.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "name": "Durable Functions Theory", 5 | "path": "DurableFunctionsTheory" 6 | }, 7 | { 8 | "name": "Notify Support Challenge", 9 | "path": "NotifySupport/challenge" 10 | }, 11 | { 12 | "name": "Table Storage Sample Data", 13 | "path": "NotifySupport/data" 14 | }, 15 | { 16 | "name": "Source Code", 17 | "path": "NotifySupport/src" 18 | }, 19 | { 20 | "name": "Test", 21 | "path": "NotifySupport/tst" 22 | }, 23 | { 24 | "name": "(Azurite)", 25 | "path": "NotifySupport/_azurite" 26 | }, 27 | { 28 | "name": "(Code Tour Notify Support)", 29 | "path": ".tours/NotifySupport" 30 | }, 31 | { 32 | "name": "(Code Tours Durable Functions Theory)", 33 | "path": ".tours/DurableFunctionsTheory" 34 | } 35 | ], 36 | "settings": { 37 | "debug.internalConsoleOptions": "neverOpen" 38 | }, 39 | "extensions": { 40 | "recommendations": [ 41 | "ms-azuretools.vscode-azurefunctions", 42 | "ms-dotnettools.csharp", 43 | "azurite.azurite", 44 | "vsls-contrib.codetour" 45 | ] 46 | } 47 | } --------------------------------------------------------------------------------