├── .gitignore ├── DurableStorage.Models ├── CloudBlobItem.cs └── DurableStorageApp.Models.csproj ├── DurableStorageApp ├── .gitignore ├── DurableStorageApp.csproj ├── DurableStorageApp.sln ├── Functions │ ├── Activities │ │ ├── SendEmailNotificationToAdmin.cs │ │ ├── SendMessageToServiceBusQueue.cs │ │ └── SendSmsCallviaTwilio.cs │ ├── Orchestrators │ │ └── AzureStorageOrchestrator.cs │ └── Triggers │ │ └── BlobTriggerStart.cs └── host.json ├── README.md └── local.settings.json /.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 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | [Ll]ogs/ 33 | 34 | # Visual Studio 2015/2017 cache/options directory 35 | .vs/ 36 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | # Visual Studio 2017 auto generated files 40 | Generated\ Files/ 41 | 42 | # MSTest test Results 43 | [Tt]est[Rr]esult*/ 44 | [Bb]uild[Ll]og.* 45 | 46 | # NUnit 47 | *.VisualState.xml 48 | TestResult.xml 49 | nunit-*.xml 50 | 51 | # Build Results of an ATL Project 52 | [Dd]ebugPS/ 53 | [Rr]eleasePS/ 54 | dlldata.c 55 | 56 | # Benchmark Results 57 | BenchmarkDotNet.Artifacts/ 58 | 59 | # .NET Core 60 | project.lock.json 61 | project.fragment.lock.json 62 | artifacts/ 63 | 64 | # StyleCop 65 | StyleCopReport.xml 66 | 67 | # Files built by Visual Studio 68 | *_i.c 69 | *_p.c 70 | *_h.h 71 | *.ilk 72 | *.meta 73 | *.obj 74 | *.iobj 75 | *.pch 76 | *.pdb 77 | *.ipdb 78 | *.pgc 79 | *.pgd 80 | *.rsp 81 | *.sbr 82 | *.tlb 83 | *.tli 84 | *.tlh 85 | *.tmp 86 | *.tmp_proj 87 | *_wpftmp.csproj 88 | *.log 89 | *.vspscc 90 | *.vssscc 91 | .builds 92 | *.pidb 93 | *.svclog 94 | *.scc 95 | 96 | # Chutzpah Test files 97 | _Chutzpah* 98 | 99 | # Visual C++ cache files 100 | ipch/ 101 | *.aps 102 | *.ncb 103 | *.opendb 104 | *.opensdf 105 | *.sdf 106 | *.cachefile 107 | *.VC.db 108 | *.VC.VC.opendb 109 | 110 | # Visual Studio profiler 111 | *.psess 112 | *.vsp 113 | *.vspx 114 | *.sap 115 | 116 | # Visual Studio Trace Files 117 | *.e2e 118 | 119 | # TFS 2012 Local Workspace 120 | $tf/ 121 | 122 | # Guidance Automation Toolkit 123 | *.gpState 124 | 125 | # ReSharper is a .NET coding add-in 126 | _ReSharper*/ 127 | *.[Rr]e[Ss]harper 128 | *.DotSettings.user 129 | 130 | # TeamCity is a build add-in 131 | _TeamCity* 132 | 133 | # DotCover is a Code Coverage Tool 134 | *.dotCover 135 | 136 | # AxoCover is a Code Coverage Tool 137 | .axoCover/* 138 | !.axoCover/settings.json 139 | 140 | # Visual Studio code coverage results 141 | *.coverage 142 | *.coveragexml 143 | 144 | # NCrunch 145 | _NCrunch_* 146 | .*crunch*.local.xml 147 | nCrunchTemp_* 148 | 149 | # MightyMoose 150 | *.mm.* 151 | AutoTest.Net/ 152 | 153 | # Web workbench (sass) 154 | .sass-cache/ 155 | 156 | # Installshield output folder 157 | [Ee]xpress/ 158 | 159 | # DocProject is a documentation generator add-in 160 | DocProject/buildhelp/ 161 | DocProject/Help/*.HxT 162 | DocProject/Help/*.HxC 163 | DocProject/Help/*.hhc 164 | DocProject/Help/*.hhk 165 | DocProject/Help/*.hhp 166 | DocProject/Help/Html2 167 | DocProject/Help/html 168 | 169 | # Click-Once directory 170 | publish/ 171 | 172 | # Publish Web Output 173 | *.[Pp]ublish.xml 174 | *.azurePubxml 175 | # Note: Comment the next line if you want to checkin your web deploy settings, 176 | # but database connection strings (with potential passwords) will be unencrypted 177 | *.pubxml 178 | *.publishproj 179 | 180 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 181 | # checkin your Azure Web App publish settings, but sensitive information contained 182 | # in these scripts will be unencrypted 183 | PublishScripts/ 184 | 185 | # NuGet Packages 186 | *.nupkg 187 | # NuGet Symbol Packages 188 | *.snupkg 189 | # The packages folder can be ignored because of Package Restore 190 | **/[Pp]ackages/* 191 | # except build/, which is used as an MSBuild target. 192 | !**/[Pp]ackages/build/ 193 | # Uncomment if necessary however generally it will be regenerated when needed 194 | #!**/[Pp]ackages/repositories.config 195 | # NuGet v3's project.json files produces more ignorable files 196 | *.nuget.props 197 | *.nuget.targets 198 | 199 | # Microsoft Azure Build Output 200 | csx/ 201 | *.build.csdef 202 | 203 | # Microsoft Azure Emulator 204 | ecf/ 205 | rcf/ 206 | 207 | # Windows Store app package directories and files 208 | AppPackages/ 209 | BundleArtifacts/ 210 | Package.StoreAssociation.xml 211 | _pkginfo.txt 212 | *.appx 213 | *.appxbundle 214 | *.appxupload 215 | 216 | # Visual Studio cache files 217 | # files ending in .cache can be ignored 218 | *.[Cc]ache 219 | # but keep track of directories ending in .cache 220 | !?*.[Cc]ache/ 221 | 222 | # Others 223 | ClientBin/ 224 | ~$* 225 | *~ 226 | *.dbmdl 227 | *.dbproj.schemaview 228 | *.jfm 229 | *.pfx 230 | *.publishsettings 231 | orleans.codegen.cs 232 | 233 | # Including strong name files can present a security risk 234 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 235 | #*.snk 236 | 237 | # Since there are multiple workflows, uncomment next line to ignore bower_components 238 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 239 | #bower_components/ 240 | 241 | # RIA/Silverlight projects 242 | Generated_Code/ 243 | 244 | # Backup & report files from converting an old project file 245 | # to a newer Visual Studio version. Backup files are not needed, 246 | # because we have git ;-) 247 | _UpgradeReport_Files/ 248 | Backup*/ 249 | UpgradeLog*.XML 250 | UpgradeLog*.htm 251 | ServiceFabricBackup/ 252 | *.rptproj.bak 253 | 254 | # SQL Server files 255 | *.mdf 256 | *.ldf 257 | *.ndf 258 | 259 | # Business Intelligence projects 260 | *.rdl.data 261 | *.bim.layout 262 | *.bim_*.settings 263 | *.rptproj.rsuser 264 | *- [Bb]ackup.rdl 265 | *- [Bb]ackup ([0-9]).rdl 266 | *- [Bb]ackup ([0-9][0-9]).rdl 267 | 268 | # Microsoft Fakes 269 | FakesAssemblies/ 270 | 271 | # GhostDoc plugin setting file 272 | *.GhostDoc.xml 273 | 274 | # Node.js Tools for Visual Studio 275 | .ntvs_analysis.dat 276 | node_modules/ 277 | 278 | # Visual Studio 6 build log 279 | *.plg 280 | 281 | # Visual Studio 6 workspace options file 282 | *.opt 283 | 284 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 285 | *.vbw 286 | 287 | # Visual Studio LightSwitch build output 288 | **/*.HTMLClient/GeneratedArtifacts 289 | **/*.DesktopClient/GeneratedArtifacts 290 | **/*.DesktopClient/ModelManifest.xml 291 | **/*.Server/GeneratedArtifacts 292 | **/*.Server/ModelManifest.xml 293 | _Pvt_Extensions 294 | 295 | # Paket dependency manager 296 | .paket/paket.exe 297 | paket-files/ 298 | 299 | # FAKE - F# Make 300 | .fake/ 301 | 302 | # CodeRush personal settings 303 | .cr/personal 304 | 305 | # Python Tools for Visual Studio (PTVS) 306 | __pycache__/ 307 | *.pyc 308 | 309 | # Cake - Uncomment if you are using it 310 | # tools/** 311 | # !tools/packages.config 312 | 313 | # Tabs Studio 314 | *.tss 315 | 316 | # Telerik's JustMock configuration file 317 | *.jmconfig 318 | 319 | # BizTalk build output 320 | *.btp.cs 321 | *.btm.cs 322 | *.odx.cs 323 | *.xsd.cs 324 | 325 | # OpenCover UI analysis results 326 | OpenCover/ 327 | 328 | # Azure Stream Analytics local run output 329 | ASALocalRun/ 330 | 331 | # MSBuild Binary and Structured Log 332 | *.binlog 333 | 334 | # NVidia Nsight GPU debugger configuration file 335 | *.nvuser 336 | 337 | # MFractors (Xamarin productivity tool) working folder 338 | .mfractor/ 339 | 340 | # Local History for Visual Studio 341 | .localhistory/ 342 | 343 | # BeatPulse healthcheck temp database 344 | healthchecksdb 345 | 346 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 347 | MigrationBackup/ 348 | 349 | # Ionide (cross platform F# VS Code tools) working folder 350 | .ionide/ 351 | /DurableStorageApp/Properties/ServiceDependencies/local/storage1.arm.json 352 | /DurableStorageApp/Properties/serviceDependencies.json 353 | /DurableStorageApp/Properties/serviceDependencies.local.json 354 | -------------------------------------------------------------------------------- /DurableStorage.Models/CloudBlobItem.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace DurableStorageApp.Models 5 | { 6 | public class CloudBlobItem 7 | { 8 | public string Name { get; set; } 9 | public string Size { get; set; } 10 | 11 | public string FileType { get; set; } 12 | 13 | public string FileSize { get; set; } 14 | 15 | public string BlobUrl { get; set; } 16 | 17 | public string ETag { get; set; } 18 | 19 | public Dictionary Metadata { get; set; } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /DurableStorage.Models/DurableStorageApp.Models.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /DurableStorageApp/.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 | # User-specific files 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Dd]ebugPublic/ 19 | [Rr]elease/ 20 | [Rr]eleases/ 21 | x64/ 22 | x86/ 23 | bld/ 24 | [Bb]in/ 25 | [Oo]bj/ 26 | [Ll]og/ 27 | 28 | # Visual Studio 2015 cache/options directory 29 | .vs/ 30 | # Uncomment if you have tasks that create the project's static files in wwwroot 31 | #wwwroot/ 32 | 33 | # MSTest test Results 34 | [Tt]est[Rr]esult*/ 35 | [Bb]uild[Ll]og.* 36 | 37 | # NUNIT 38 | *.VisualState.xml 39 | TestResult.xml 40 | 41 | # Build Results of an ATL Project 42 | [Dd]ebugPS/ 43 | [Rr]eleasePS/ 44 | dlldata.c 45 | 46 | # DNX 47 | project.lock.json 48 | project.fragment.lock.json 49 | artifacts/ 50 | 51 | *_i.c 52 | *_p.c 53 | *_i.h 54 | *.ilk 55 | *.meta 56 | *.obj 57 | *.pch 58 | *.pdb 59 | *.pgc 60 | *.pgd 61 | *.rsp 62 | *.sbr 63 | *.tlb 64 | *.tli 65 | *.tlh 66 | *.tmp 67 | *.tmp_proj 68 | *.log 69 | *.vspscc 70 | *.vssscc 71 | .builds 72 | *.pidb 73 | *.svclog 74 | *.scc 75 | 76 | # Chutzpah Test files 77 | _Chutzpah* 78 | 79 | # Visual C++ cache files 80 | ipch/ 81 | *.aps 82 | *.ncb 83 | *.opendb 84 | *.opensdf 85 | *.sdf 86 | *.cachefile 87 | *.VC.db 88 | *.VC.VC.opendb 89 | 90 | # Visual Studio profiler 91 | *.psess 92 | *.vsp 93 | *.vspx 94 | *.sap 95 | 96 | # TFS 2012 Local Workspace 97 | $tf/ 98 | 99 | # Guidance Automation Toolkit 100 | *.gpState 101 | 102 | # ReSharper is a .NET coding add-in 103 | _ReSharper*/ 104 | *.[Rr]e[Ss]harper 105 | *.DotSettings.user 106 | 107 | # JustCode is a .NET coding add-in 108 | .JustCode 109 | 110 | # TeamCity is a build add-in 111 | _TeamCity* 112 | 113 | # DotCover is a Code Coverage Tool 114 | *.dotCover 115 | 116 | # NCrunch 117 | _NCrunch_* 118 | .*crunch*.local.xml 119 | nCrunchTemp_* 120 | 121 | # MightyMoose 122 | *.mm.* 123 | AutoTest.Net/ 124 | 125 | # Web workbench (sass) 126 | .sass-cache/ 127 | 128 | # Installshield output folder 129 | [Ee]xpress/ 130 | 131 | # DocProject is a documentation generator add-in 132 | DocProject/buildhelp/ 133 | DocProject/Help/*.HxT 134 | DocProject/Help/*.HxC 135 | DocProject/Help/*.hhc 136 | DocProject/Help/*.hhk 137 | DocProject/Help/*.hhp 138 | DocProject/Help/Html2 139 | DocProject/Help/html 140 | 141 | # Click-Once directory 142 | publish/ 143 | 144 | # Publish Web Output 145 | *.[Pp]ublish.xml 146 | *.azurePubxml 147 | # TODO: Comment the next line if you want to checkin your web deploy settings 148 | # but database connection strings (with potential passwords) will be unencrypted 149 | #*.pubxml 150 | *.publishproj 151 | 152 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 153 | # checkin your Azure Web App publish settings, but sensitive information contained 154 | # in these scripts will be unencrypted 155 | PublishScripts/ 156 | 157 | # NuGet Packages 158 | *.nupkg 159 | # The packages folder can be ignored because of Package Restore 160 | **/packages/* 161 | # except build/, which is used as an MSBuild target. 162 | !**/packages/build/ 163 | # Uncomment if necessary however generally it will be regenerated when needed 164 | #!**/packages/repositories.config 165 | # NuGet v3's project.json files produces more ignoreable files 166 | *.nuget.props 167 | *.nuget.targets 168 | 169 | # Microsoft Azure Build Output 170 | csx/ 171 | *.build.csdef 172 | 173 | # Microsoft Azure Emulator 174 | ecf/ 175 | rcf/ 176 | 177 | # Windows Store app package directories and files 178 | AppPackages/ 179 | BundleArtifacts/ 180 | Package.StoreAssociation.xml 181 | _pkginfo.txt 182 | 183 | # Visual Studio cache files 184 | # files ending in .cache can be ignored 185 | *.[Cc]ache 186 | # but keep track of directories ending in .cache 187 | !*.[Cc]ache/ 188 | 189 | # Others 190 | ClientBin/ 191 | ~$* 192 | *~ 193 | *.dbmdl 194 | *.dbproj.schemaview 195 | *.jfm 196 | *.pfx 197 | *.publishsettings 198 | node_modules/ 199 | orleans.codegen.cs 200 | 201 | # Since there are multiple workflows, uncomment next line to ignore bower_components 202 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 203 | #bower_components/ 204 | 205 | # RIA/Silverlight projects 206 | Generated_Code/ 207 | 208 | # Backup & report files from converting an old project file 209 | # to a newer Visual Studio version. Backup files are not needed, 210 | # because we have git ;-) 211 | _UpgradeReport_Files/ 212 | Backup*/ 213 | UpgradeLog*.XML 214 | UpgradeLog*.htm 215 | 216 | # SQL Server files 217 | *.mdf 218 | *.ldf 219 | 220 | # Business Intelligence projects 221 | *.rdl.data 222 | *.bim.layout 223 | *.bim_*.settings 224 | 225 | # Microsoft Fakes 226 | FakesAssemblies/ 227 | 228 | # GhostDoc plugin setting file 229 | *.GhostDoc.xml 230 | 231 | # Node.js Tools for Visual Studio 232 | .ntvs_analysis.dat 233 | 234 | # Visual Studio 6 build log 235 | *.plg 236 | 237 | # Visual Studio 6 workspace options file 238 | *.opt 239 | 240 | # Visual Studio LightSwitch build output 241 | **/*.HTMLClient/GeneratedArtifacts 242 | **/*.DesktopClient/GeneratedArtifacts 243 | **/*.DesktopClient/ModelManifest.xml 244 | **/*.Server/GeneratedArtifacts 245 | **/*.Server/ModelManifest.xml 246 | _Pvt_Extensions 247 | 248 | # Paket dependency manager 249 | .paket/paket.exe 250 | paket-files/ 251 | 252 | # FAKE - F# Make 253 | .fake/ 254 | 255 | # JetBrains Rider 256 | .idea/ 257 | *.sln.iml 258 | 259 | # CodeRush 260 | .cr/ 261 | 262 | # Python Tools for Visual Studio (PTVS) 263 | __pycache__/ 264 | *.pyc -------------------------------------------------------------------------------- /DurableStorageApp/DurableStorageApp.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netcoreapp3.1 4 | v3 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | PreserveNewest 17 | 18 | 19 | PreserveNewest 20 | Never 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /DurableStorageApp/DurableStorageApp.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.31729.503 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DurableStorageApp", "DurableStorageApp.csproj", "{A003CEC0-77B8-47CC-87AA-37615877E8F9}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DurableStorageApp.Models", "..\DurableStorage.Models\DurableStorageApp.Models.csproj", "{79C9FA8C-BAEA-44CA-9571-BCF0F5030B6E}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {A003CEC0-77B8-47CC-87AA-37615877E8F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {A003CEC0-77B8-47CC-87AA-37615877E8F9}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {A003CEC0-77B8-47CC-87AA-37615877E8F9}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {A003CEC0-77B8-47CC-87AA-37615877E8F9}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {79C9FA8C-BAEA-44CA-9571-BCF0F5030B6E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {79C9FA8C-BAEA-44CA-9571-BCF0F5030B6E}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {79C9FA8C-BAEA-44CA-9571-BCF0F5030B6E}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {79C9FA8C-BAEA-44CA-9571-BCF0F5030B6E}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {36837AEF-6A5C-476A-BD9D-20EC51143F1B} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /DurableStorageApp/Functions/Activities/SendEmailNotificationToAdmin.cs: -------------------------------------------------------------------------------- 1 | using DurableStorageApp.Models; 2 | using Microsoft.Azure.WebJobs; 3 | using Microsoft.Azure.WebJobs.Extensions.DurableTask; 4 | using Microsoft.Extensions.Configuration; 5 | using Microsoft.Extensions.Logging; 6 | using SendGrid; 7 | using SendGrid.Helpers.Mail; 8 | using System; 9 | using System.Collections.Generic; 10 | using System.Text; 11 | using System.Threading.Tasks; 12 | 13 | namespace DurableStorageApp.Functions.Activities 14 | { 15 | public class SendEmailNotificationToAdmin 16 | { 17 | [FunctionName("SendEmailNotification")] 18 | public static async Task SendEmailNotification([ActivityTrigger] CloudBlobItem uploadedBlob, ILogger log, ExecutionContext executionContext) 19 | { 20 | log.LogInformation($"BLOB already saved to queue."); 21 | 22 | try 23 | { 24 | //Config settings for Azure Service Bus 25 | var sendGridAPIConfig = new ConfigurationBuilder() 26 | .SetBasePath(executionContext.FunctionAppDirectory) 27 | .AddJsonFile("local.settings.json", optional: true, reloadOnChange: true) 28 | .AddEnvironmentVariables() 29 | .Build(); 30 | 31 | var apiKey = sendGridAPIConfig["SendGridAPIKey"]; 32 | var adminEmail = sendGridAPIConfig["Admin_Email"]; 33 | var adminName = sendGridAPIConfig["Admin_Name"]; 34 | var client = new SendGridClient(apiKey); 35 | var from = new EmailAddress(adminEmail, adminName); 36 | 37 | List recipients = new List 38 | { 39 | new EmailAddress("jonah.andersson@forefront.se", "Jonah @Forefront") 40 | }; 41 | 42 | var subject = "New BLOB Uploaded on Azure Service Bus Queue "; 43 | var htmlContent = @"

A new cloud blob file added to Azure Service Bus queue. BLOB Url: 44 | " + uploadedBlob.BlobUrl + "
Message from Jonahs app.

"; 45 | var displayRecipients = false; // set this to true if you want recipients to see each others mail id 46 | var msg = MailHelper.CreateSingleEmailToMultipleRecipients(from, recipients, subject, "", htmlContent, displayRecipients); 47 | var isEmailSent = await client.SendEmailAsync(msg); 48 | 49 | if (isEmailSent.IsSuccessStatusCode) 50 | return true; 51 | else return false; 52 | } 53 | catch (Exception ex) 54 | { 55 | //Error handling 56 | log.LogError($"Receiving Service Bus Queue Message failed and email not sent: {ex.InnerException}"); 57 | throw; 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /DurableStorageApp/Functions/Activities/SendMessageToServiceBusQueue.cs: -------------------------------------------------------------------------------- 1 | using Azure.Messaging.ServiceBus; 2 | using DurableStorageApp.Models; 3 | using Microsoft.Azure.WebJobs; 4 | using Microsoft.Azure.WebJobs.Extensions.DurableTask; 5 | using Microsoft.Extensions.Configuration; 6 | using Microsoft.Extensions.Logging; 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Text; 10 | using System.Threading.Tasks; 11 | 12 | 13 | namespace DurableStorageApp.Functions.Activities 14 | { 15 | public class SendMessageToServiceBusQueue 16 | { 17 | [FunctionName("SendMessageToServiceBusQueue")] 18 | public static async Task SendMessageToAzureServiceBusQueueAsync([ActivityTrigger] CloudBlobItem uploadedcloudBlob, ILogger log, ExecutionContext executionContext) 19 | { 20 | log.LogInformation($"Received event data with an uploaded cloud blob {uploadedcloudBlob.Name} with format {uploadedcloudBlob.FileType}."); 21 | //Config settings for Azure Service Bus 22 | var azureServiceBusConfig = new ConfigurationBuilder() 23 | .SetBasePath(executionContext.FunctionAppDirectory) 24 | .AddJsonFile("local.settings.json", optional: true, reloadOnChange: true) 25 | .AddEnvironmentVariables() 26 | .Build(); 27 | 28 | var serviceBusConnection = azureServiceBusConfig["AzureServiceBusConnectionString"]; 29 | var serviceBusQueue = azureServiceBusConfig["ServiceBusQueueName"]; 30 | string composedMessage = ""; 31 | 32 | try 33 | { 34 | if (uploadedcloudBlob != null) 35 | { 36 | log.LogInformation($"Composing message to be sent to the queue"); 37 | 38 | composedMessage = $"A blob image {uploadedcloudBlob.Name} was uploaded to Azure Service Bus Queue azdurablefunctioncloudqueue.
" + 39 | $"Blob Type: {uploadedcloudBlob.FileType}
" + 40 | $"Blob URL: {uploadedcloudBlob.BlobUrl}
" + 41 | $"Message sent via Azure Durable Functions App"; 42 | 43 | await using (ServiceBusClient client = new ServiceBusClient(serviceBusConnection)) 44 | { 45 | //Create sender 46 | ServiceBusSender sender = client.CreateSender(serviceBusQueue); 47 | 48 | //Create message 49 | ServiceBusMessage message = new ServiceBusMessage(composedMessage); 50 | 51 | //Send Message to ServiceBus Queue 52 | await sender.SendMessageAsync(message); 53 | log.LogInformation($"Sent a message to Service Bus Queue: {serviceBusQueue}"); 54 | return composedMessage; 55 | } 56 | } 57 | else 58 | return composedMessage; 59 | } 60 | catch (Exception ex) 61 | { 62 | log.LogInformation($"Something went wrong sending the message to the queue : {serviceBusQueue}. Exception {ex.InnerException}"); 63 | throw; 64 | } 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /DurableStorageApp/Functions/Activities/SendSmsCallviaTwilio.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Azure.WebJobs; 2 | using Microsoft.Azure.WebJobs.Extensions.DurableTask; 3 | using Microsoft.Extensions.Configuration; 4 | using Microsoft.Extensions.Logging; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | using Twilio; 10 | using Twilio.Exceptions; 11 | using Twilio.Rest.Api.V2010.Account; 12 | using Twilio.Types; 13 | 14 | namespace DurableStorageApp.Functions.Activities 15 | { 16 | public static class SendSmsCallviaTwilio 17 | { 18 | [FunctionName("SendSmsCallviaTwilio")] 19 | public static async Task SendSMSCallMessageTwilio([ActivityTrigger] string serviceBusQueueMessage, ILogger log, ExecutionContext executionContext) 20 | { 21 | log.LogInformation($"BLOB already saved to Service Bus Queue"); 22 | 23 | //Config settings for Azure Service Bus 24 | var config = new ConfigurationBuilder() 25 | .SetBasePath(executionContext.FunctionAppDirectory) 26 | .AddJsonFile("local.settings.json", optional: true, reloadOnChange: true) 27 | .AddEnvironmentVariables() 28 | .Build(); 29 | 30 | // You API secrets should be on the set up on a localsettings.json, app settings or keyvault 31 | var twilioAccountId = config["Twilio_SID"]; 32 | var twilioSecret = config["Twilio_Secret"]; 33 | var twilioAdminMobile = config["Admin_Mobile"]; 34 | var twilioVerifiedNumber = config["Twilio_Verified_Number"]; 35 | var serviceBusName = config["blobstoragenotifierqueue"]; 36 | 37 | TwilioClient.Init(twilioAccountId, twilioSecret); 38 | log.LogInformation($"Composing message with "); 39 | try 40 | { 41 | if (serviceBusQueueMessage != null) 42 | { 43 | //Send SMS to Azure Service Bus Admin User 44 | var smsMessage = await MessageResource.CreateAsync( 45 | body: $"Hi Admin! A new cloud blob file was uploaded to your Azure Storage " + 46 | $" and a queue message was sent to Azure Service Bus Queue {serviceBusName}. \n" + 47 | $" Queue Message was {serviceBusQueueMessage}", 48 | // mediaUrl is used in Twilio if you want to send the Blob as MMS using image url 49 | //mediaUrl: uploadedBlobUrl, 50 | from: new PhoneNumber(twilioVerifiedNumber), 51 | to: new PhoneNumber(twilioAdminMobile) 52 | ); 53 | 54 | //Backend logging 55 | log.LogInformation($"Sms sent to the number provided. \n " + 56 | $"Message Id : {smsMessage.Sid} \n " + 57 | $"Date Sent : {smsMessage.DateSent} \n " + 58 | $"Message : {smsMessage.Body}"); 59 | 60 | //Initiate call reminder to admin 61 | var call = CallResource.CreateAsync( 62 | twiml: new Twiml("Hi! Call reminder. New BLOB added to Service Bus Queue!"), 63 | from: new PhoneNumber(twilioVerifiedNumber), 64 | to: new PhoneNumber(twilioAdminMobile) 65 | ); 66 | 67 | //Backend logging 68 | log.LogInformation($"Called admin on number provided. \n " + 69 | $"Call Id : {call.Id} \n " + 70 | $"Call Status : {call.Status} \n " + 71 | $"Call Completed : {call.IsCompleted} \n "); 72 | 73 | return true; 74 | } 75 | else return false; 76 | } 77 | catch (ApiException e) 78 | { 79 | if (e.Code == 21614) 80 | { 81 | log.LogError("Uh oh, looks like this caller can't receive SMS messages."); 82 | } 83 | throw; 84 | } 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /DurableStorageApp/Functions/Orchestrators/AzureStorageOrchestrator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Net.Http; 4 | using System.Threading.Tasks; 5 | using DurableStorageApp.Models; 6 | using Microsoft.Azure.WebJobs; 7 | using Microsoft.Azure.WebJobs.Extensions.DurableTask; 8 | using Microsoft.Azure.WebJobs.Extensions.Http; 9 | using Microsoft.Azure.WebJobs.Host; 10 | using Microsoft.Extensions.Logging; 11 | 12 | /// 13 | /// The orchestrator for Azure Durable App 14 | /// Triggered by BLOB Trigger BlobTriggerStart.cs 15 | /// Describes the stateful workflow of the orchestration 16 | /// 17 | namespace DurableStorageApp 18 | { 19 | public static class AzureStorageOrchestrator 20 | { 21 | [FunctionName("AzureStorageOrchestrator")] 22 | public static async Task RunOrchestrator( 23 | [OrchestrationTrigger] IDurableOrchestrationContext context, ILogger log) 24 | { 25 | try 26 | { 27 | var uploadedCloudBlob = context.GetInput(); 28 | bool isEmailSentToAdmin; 29 | 30 | //Chain #1 Send Message with BLOB details to Service Bus Queue. Returns the ServiceBus Message 31 | var serviceBusMessage = await context.CallActivityAsync("SendMessageToServiceBusQueue", uploadedCloudBlob); 32 | 33 | if (serviceBusMessage != null) 34 | { 35 | //Chain #2 Send SMS and call using TwilioAPI to set admin user that queue was updated with new blob 36 | var isSmsSentAndCalledUser = await context.CallActivityAsync("SendSmsCallviaTwilio", serviceBusMessage); 37 | 38 | //Chain #3 send email using Sendgrid API 39 | if (isSmsSentAndCalledUser) 40 | { 41 | isEmailSentToAdmin = await context.CallActivityAsync("SendEmailNotification", uploadedCloudBlob); 42 | } 43 | 44 | //TODO Chain #4 Freestyle Activity as Serverless LAB! 45 | // Example 1. An activity function that saves the uploaded BLOB to Azure Cosmos DB 46 | // Example 2. An activity function that reads and receives the queue message on the Azure Service Bus Queue 47 | 48 | log.LogInformation($"A new cloud blob named {uploadedCloudBlob.Name} was uploaded to Azure Storage " + 49 | $"and added to service bus queue. \n" + 50 | $" SMS sent = {isSmsSentAndCalledUser} to assigned user. \n" + 51 | $" Access via BLOB URL: {uploadedCloudBlob.BlobUrl}" + 52 | $" and email sent."); 53 | } 54 | 55 | return $"Done with the orchestration with Durable Context Id: {context.InstanceId}"; 56 | } 57 | catch (Exception ex) 58 | { 59 | //TODO Handle possible errors and do a retry if needed or retry a function 60 | log.LogError($"Something went wrong " + ex.Message); 61 | throw; 62 | } 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /DurableStorageApp/Functions/Triggers/BlobTriggerStart.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Threading.Tasks; 5 | using DurableStorageApp.Models; 6 | using Microsoft.Azure.Storage.Blob; 7 | using Microsoft.Azure.WebJobs; 8 | using Microsoft.Azure.WebJobs.Extensions.DurableTask; 9 | using Microsoft.Azure.WebJobs.Host; 10 | using Microsoft.Extensions.Logging; 11 | 12 | namespace DurableStorageApp.Functions.Triggers 13 | { 14 | public static class BlobTriggerStart 15 | { 16 | [FunctionName("BlobTriggerStart")] 17 | public static async Task BlobTriggerClientFunction([BlobTrigger("photoscontainer/{name}", Connection ="StorageConnectionString")] CloudBlockBlob myBlob, string name, 18 | ILogger log, [DurableClient] IDurableOrchestrationClient starter) 19 | { 20 | try 21 | { 22 | log.LogInformation($"Started orchestration trigged by BLOB trigger. A blob item with name = '{name}'"); 23 | log.LogInformation($"BLOB Name {myBlob.Name}"); 24 | 25 | // Function input comes from the request content. 26 | if (myBlob != null) 27 | { 28 | var newUploadedBlobItem = new CloudBlobItem 29 | { 30 | Name = myBlob.Name, 31 | BlobUrl = myBlob.Uri.AbsoluteUri.ToString(), 32 | Metadata = (Dictionary)myBlob.Metadata, 33 | FileType = myBlob.BlobType.ToString(), 34 | Size = myBlob.Name.Length.ToString(), 35 | ETag = myBlob.Properties.ETag.ToString() 36 | }; 37 | 38 | var instanceId = await starter.StartNewAsync("AzureStorageOrchestrator", newUploadedBlobItem); 39 | log.LogInformation($"Started orchestration with ID = '{instanceId}'."); 40 | } 41 | else 42 | { 43 | log.LogError($"The blob was trigged but myCloudBlob was empty"); 44 | } 45 | } 46 | catch (Exception ex) 47 | { 48 | //TODO Errorhandling 49 | log.LogError("Something went wrong. Error : " + ex.InnerException); 50 | throw; 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /DurableStorageApp/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "logging": { 4 | "applicationInsights": { 5 | "samplingSettings": { 6 | "isEnabled": true, 7 | "excludedTypes": "Request" 8 | } 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Azure Durable Functions (Function Chaining Example) in C# .NET (Starter Template) 2 | ##### Starter Template for Azure Serverless Durable Functions .NET Core 3 | *Author: Jonah Andersson* 4 | 5 | #### DESCRIPTION: 6 | 7 | This is a starter template for Serverless deveöpment with Azure Durable Functions available to those you wants to try it out. 8 | A hands-on lab created with Azure Durable Functions with [Function Chaining](https://docs.microsoft.com/en-us/azure/azure-functions/durable/durable-functions-sequence?tabs=csharp) application pattern. 9 | 10 | Function chaining - Azure Durable Functions 11 | 12 | This template is prepared as starter template that allows you to develop Azure Functions (Durable Functions) serverless workflow with integration to other APIs such as Twilio API and Sendgrid API and Azure services - Azure Storage, ServiceBus etc. 13 | 14 | #### OVERVIEW OF WORKFLOW 15 | 16 | 17 | Azure Durable Functions - Function Chaining Example with Azure Service Bus, Twilio API, Sendgrid API and Azure BLOB Storage 18 | 19 | ### EXPECTED RESULTS 20 | 21 | - Orchestration get trigged by an image or BLOB uploaded to the Azure Storage 22 | - Chain 1 - Sends queue message to Azure Service Bus 23 | - Chain 2 - Send SMS or make call using Twilio API 24 | - Chain 3 - Send email to configured email address using SendGrid API 25 | - Chain 4 - Lab Exercise to send to Azure Cosmos DB etc. (Guide: How to create Azure Cosmos DB Trigger) 26 | 27 | #### PREREQUISITES AND LAB ENVIRONMENT SETUP 28 | 29 | * Basics concepts of Azure Serverless Computing, Azure Functions and Durable Functions
30 | (*If you are a student of Forefront's Serverless course, great. Otherwise, check *Recommended Learning* below*) 31 | * Microsoft Azure account - Private or Organization subscription account
32 | (*If you don't have any Azure Account, sign up https://azure.microsoft.com/en-us/free/*) 33 | * Latest version of [Visual Studio](https://visualstudio.microsoft.com/) or [VS Code](https://code.visualstudio.com/) 34 | * Azure Storage Explorer (https://azure.microsoft.com/en-us/features/storage-explorer/) 35 | * Programming Language C# .NET (You can code in other supported languages as well - see supported languages) 36 | * Install latest .NET Core 3.1 (LTS) or latest supported like .NET 6 https://dotnet.microsoft.com/download 37 | * Install [Azure Functions Core Tools](https://docs.microsoft.com/en-us/azure/azure-functions/functions-run-local?tabs=v3%2Cwindows%2Ccsharp%2Cportal%2Cbash%2Ckeda) 38 | * If you are using .NET Core 3.1 it will not be supported by the end of 2022 and you need to upgrade to functions version 4.0 39 | * Postman for HTTP Requests/Triggers 40 | * DEVELOPMENT LOCALLY use file local.settings.json with your own configuration strings, API keys 41 | 42 | #### local.settings.json (Local Development Only) 43 | *RECOMMENDATION:* Use [Azure Key Vault](https://docs.microsoft.com/en-us/azure/app-service/app-service-key-vault-references?tabs=azure-cli?WT.mc_id=AZ-MVP-5004251) and Managed Identities to secure your function application for Azure Durable Functions 44 | 45 | ```yaml 46 | { 47 | "IsEncrypted": false, 48 | "Values": { 49 | "AzureWebJobsStorage": "" 50 | "FUNCTIONS_WORKER_RUNTIME": "dotnet", 51 | "StorageConnectionString": "" 52 | "AzureServiceBusConnectionString": "", 53 | "ServiceBusQueueName": "blobstoragenotifierqueue", 54 | "Twilio_SID": "", 55 | "Twilio_Secret": "", 56 | "Admin_Email": "", 58 | "Admin_Mobile": "", 59 | "Twilio_Verified_Number": "", 60 | "SendGridAPIKey": "", 61 | "CosmosDBEndPointUri": "" 63 | } 64 | 65 | ``` 66 | 67 | ### REQUIRED AZURE SERVICES AND API INTEGRATIONS 68 | 69 | - Azure Storage Account for Azure Function App and a BLOB container to upload image files and for the Blob Storage Trigger 70 | - How to create an Azure Storage Account 71 | - How to create a Azure Blob Storage Container 72 | - How to create a Azure Blob Storage Trigger 73 | 74 | - Azure Service Bus Namespace with a queue name that matches the queue name of your app configuration 75 | - How to create a queue in an Azure Service Bus Namespace 76 | - Twilio API Account - API Keys and Secret are used to code the logic to send SMS and make call from the function app 77 | - Instructions for TwilioAPI 78 | - SendGrid API Account - API Keys and Secret are used to code logic in sending email 79 | - Instructions for SendGrid 80 | 81 | 82 | ### WHEN DEBUGGING AND DEVELOPING LOCALLY 83 | 84 | When developing Azure Functions locally using this project. You should see similar like this when it is finished. 85 | It logs what is happening with your orchestration. You may also check on 86 | 87 | 88 | 89 | ### TALKS - Azure Durable Functions at NDC Oslo Developer Conference 2021
90 | Click on the image below to watch the recording of my talk for this session at the NDC Olso Conferene
91 |
92 | 93 | Watch session on YouTube https://www.youtube.com/watch?v=C199S4R7cy8 94 | 95 | #### RECOMMENDED LEARNING AND HANDS-ON RESOURCES 96 | 97 | - [Azure Durable Functions Documentation](https://docs.microsoft.com/en-us/azure/azure-functions/durable?WT.mc_id=AZ-MVP-5004251)
98 | - [Microsoft Learn](https://docs.microsoft.com/en-us/azure/azure-functions/durable/durable-functions-create-first-csharp?pivots=code-editor-vscode?WT.mc_id=AZ-MVP-5004251)
99 | - [Azure Functions University on YouTube by Marc Duiker](https://www.youtube.com/channel/UCmoWqg6T-c8zEGm4sZdnwbA)
100 | - [Azure Functions University Lessons on GitHub by Marc Duiker and Community](https://github.com/marcduiker/azure-functions-university)
101 | - [My article about Azure Durable Functions at DEV Community Blog](https://dev.to/jonahandersson/azure-durable-functions-developing-serverless-stateful-workflow-4787)
102 | - [Serverless .NET Development with Azure Durable Functions by Jonah Andersson at Philippine .NET User Group (PHINUG)](https://www.youtube.com/watch?v=zByq3wB7fIQ&t=31s)
103 | - [Azure Durable Functions Fundamentals at Azure User Group Sundsvall by Jonah Andersson](https://www.youtube.com/watch?v=fDej9n-kzNM)
104 | - [Azure Serverless Community Library](https://serverlesslibrary.net/)
105 | - [Azure Storage Account](https://docs.microsoft.com/en-us/azure/storage/common/storage-account-overview)
106 | - [SendGrid with Azure Functions](https://learn.microsoft.com/en-us/azure/azure-functions/functions-bindings-sendgrid?tabs=in-process%2Cfunctionsv2&pivots=programming-language-csharp)
107 | 108 | 109 | -------------------------------------------------------------------------------- /local.settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "IsEncrypted": false, 3 | "Values": { 4 | "AzureWebJobsStorage": "" 5 | "FUNCTIONS_WORKER_RUNTIME": "dotnet", 6 | "StorageConnectionString": "" 7 | "AzureServiceBusConnectionString": "", 8 | "ServiceBusQueueName": "blobstoragenotifierqueue", 9 | "Twilio_SID": "", 10 | "Twilio_Secret": "", 11 | "Admin_Email": "", 13 | "Admin_Mobile": "", 14 | "Twilio_Verified_Number": "", 15 | "SendGridAPIKey": "", 16 | "CosmosDBEndPointUri": "" 18 | } 19 | } 20 | --------------------------------------------------------------------------------