├── .devcontainer └── static-content-hosting │ └── devcontainer.json ├── .editorconfig ├── .gitattributes ├── .github └── dependabot.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.MD ├── async-request-reply ├── README.md ├── deploy.bicep └── src │ ├── AsyncOperationStatusChecker.cs │ ├── AsyncProcessingBackgroundWorker.cs │ ├── AsyncProcessingWorkAcceptor.cs │ ├── CloudBlockBlobExtensions.cs │ ├── CustomerPOCO.cs │ ├── Program.cs │ ├── asyncpattern.csproj │ ├── asyncpattern.sln │ ├── host.json │ └── local.settings.json ├── choreography ├── README.md ├── choreography-example.drawio └── choreography-example.png ├── claim-check ├── .gitattributes ├── .gitignore ├── README.md └── code-samples │ ├── all-samples.sln │ ├── sample-1 │ ├── FunctionConsumer1 │ │ ├── FunctionConsumer1.cs │ │ ├── FunctionConsumer1.csproj │ │ ├── ISampleBlobDataMover.cs │ │ ├── Program.cs │ │ ├── SampleBlobDataMover.cs │ │ ├── host.json │ │ └── local.settings.json.template │ ├── bicep │ │ └── main.bicep │ ├── images │ │ ├── sample-1-diagram.drawio │ │ └── sample-1-diagram.png │ ├── readme.md │ └── sample-1.sln │ ├── sample-2 │ ├── ClientConsumer2 │ │ ├── ClientConsumer2.csproj │ │ ├── EventHubsConsumer.cs │ │ ├── Program.cs │ │ ├── SampleBlobDataMover.cs │ │ ├── SampleSettingsValidator.cs │ │ └── appsettings.json.template │ ├── bicep │ │ └── main.bicep │ ├── images │ │ ├── sample-2-diagram.drawio │ │ └── sample-2-diagram.png │ ├── readme.md │ └── sample-2.sln │ ├── sample-3 │ ├── FunctionConsumer3 │ │ ├── FunctionConsumer3.cs │ │ ├── FunctionConsumer3.csproj │ │ ├── ISampleBlobDataMover.cs │ │ ├── Program.cs │ │ ├── SampleBlobDataMover.cs │ │ ├── host.json │ │ └── local.settings.json.template │ ├── bicep │ │ └── main.bicep │ ├── images │ │ ├── sample-3-diagram.drawio │ │ └── sample-3-diagram.png │ ├── readme.md │ └── sample-3.sln │ └── sample-4 │ ├── ClientProducer4 │ ├── ClientProducer4.csproj │ ├── ISampleBlobDataMover.cs │ ├── ISampleKafkaProducer.cs │ ├── Program.cs │ ├── SampleBlobDataMover.cs │ ├── SampleKafkaProducer.cs │ ├── SampleSettingsValidator.cs │ ├── SampleTokenCredentialHelper.cs │ └── appsettings.json.template │ ├── FunctionConsumer4 │ ├── FunctionConsumer4.cs │ ├── FunctionConsumer4.csproj │ ├── ISampleBlobDataMover.cs │ ├── Program.cs │ ├── SampleBlobDataMover.cs │ ├── host.json │ └── local.settings.json.template │ ├── bicep │ └── main.bicep │ ├── images │ ├── sample-4-diagram.drawio │ └── sample-4-diagram.png │ ├── readme.md │ └── sample-4.sln ├── leader-election ├── DistributedMutex │ ├── BlobDistributedMutex.cs │ ├── BlobLeaseManager.cs │ └── DistributedMutex.csproj ├── LeaderElection.sln ├── LeaderElectionConsoleWorker │ ├── LeaderElectionConsoleWorker.csproj │ ├── Program.cs │ └── app.config └── Readme.md ├── pipes-and-filters ├── .devcontainer │ └── devcontainer.json ├── .vscode │ ├── extensions.json │ ├── launch.json │ ├── settings.json │ └── tasks.json ├── ImageProcessingPipeline │ ├── .gitignore │ ├── ImageProcessingPipeline.csproj │ ├── ImageProcessingPipeline.sln │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── PublishFinal.cs │ ├── Resize.cs │ ├── Watermark.cs │ ├── host.json │ ├── local.settings.template.json │ └── resources │ │ └── watermark.png ├── README.md ├── bicep │ └── main.bicep ├── images │ └── clouds.png └── pipes-and-filters.drawio ├── priority-queue ├── PriorityQueue.sln ├── PriorityQueueConsumerHigh │ ├── .gitignore │ ├── PriorityQueueConsumerHigh.csproj │ ├── PriorityQueueConsumerHighFn.cs │ ├── Properties │ │ ├── serviceDependencies.json │ │ └── serviceDependencies.local.json │ ├── host.json │ └── local.settings.json ├── PriorityQueueConsumerLow │ ├── .gitignore │ ├── PriorityQueueConsumerLow.csproj │ ├── PriorityQueueConsumerLowFn.cs │ ├── Properties │ │ ├── serviceDependencies.json │ │ └── serviceDependencies.local.json │ ├── host.json │ └── local.settings.json ├── PriorityQueueSender │ ├── .gitignore │ ├── Priority.cs │ ├── PriorityQueueSender.csproj │ ├── PriorityQueueSenderFn.cs │ ├── Properties │ │ ├── serviceDependencies.json │ │ └── serviceDependencies.local.json │ ├── host.json │ └── local.settings.json └── Readme.md ├── sharding ├── README.md ├── sharding-example.drawio └── sharding-example.png ├── static-content-hosting ├── README.md ├── bicep │ └── main.bicep ├── src │ ├── index.template.html │ └── static │ │ ├── 404.html │ │ └── style.css ├── static-content-hosting-pattern.drawio └── static-content-hosting-pattern.png └── valet-key ├── .gitignore ├── .vscode └── extensions.json ├── README.md ├── ValetKey.Client ├── Program.cs ├── ValetKey.Client.csproj └── appsettings.json ├── ValetKey.Web ├── .gitignore ├── .vscode │ ├── extensions.json │ ├── launch.json │ ├── settings.json │ └── tasks.json ├── FileServices.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── ValetKey.Web.csproj ├── host.json └── local.settings.template.json ├── bicep └── main.bicep ├── valet-key-example.drawio ├── valet-key-example.png └── valet-key.sln /.devcontainer/static-content-hosting/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/dotnet 3 | { 4 | "name": "static-content-hosting-devcontainer", 5 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile 6 | "image": "mcr.microsoft.com/devcontainers/base:ubuntu", 7 | "features": { 8 | "ghcr.io/devcontainers/features/azure-cli:1": { 9 | "version": "latest" 10 | } 11 | }, 12 | "customizations": { 13 | "vscode": { 14 | "extensions": [ 15 | "ms-vscode.azurecli", 16 | "ms-azuretools.vscode-bicep" 17 | ] 18 | } 19 | } 20 | 21 | // Features to add to the dev container. More info: https://containers.dev/features. 22 | // "features": {}, 23 | 24 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 25 | // "forwardPorts": [5000, 5001], 26 | // "portsAttributes": { 27 | // "5001": { 28 | // "protocol": "https" 29 | // } 30 | // } 31 | 32 | // Use 'postCreateCommand' to run commands after the container is created. 33 | // "postCreateCommand": "dotnet restore", 34 | 35 | // Configure tool-specific properties. 36 | // "customizations": {}, 37 | 38 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. 39 | // "remoteUser": "root" 40 | } 41 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | insert_final_newline = true 7 | 8 | # 4 space indentation 9 | indent_style = space 10 | indent_size = 4 -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "nuget" # See documentation for possible values 9 | directories: 10 | - "/async-request-reply/src" 11 | - "/claim-check/code-samples" 12 | - "/leader-election" 13 | - "/pipes-and-filters/ImageProcessingPipeline" 14 | - "/priority-queue" 15 | - "/valet-key/ValetKey.*" 16 | schedule: 17 | interval: "weekly" 18 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Cloud Design Patterns 2 | 3 | This project welcomes contributions and suggestions. Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us the rights to use your contribution. For details, visit [Contributor License Agreements](https://cla.opensource.microsoft.com). 4 | 5 | When you submit a pull request, a CLA bot will automatically determine whether you need to provide a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions provided by the bot. You will only need to do this once across all repos using our CLA. 6 | 7 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 8 | 9 | - [Code of Conduct](#coc) 10 | - [Issues and Bugs](#issue) 11 | - [Feature Requests](#feature) 12 | - [Submission Guidelines](#submit) 13 | 14 | ## Code of Conduct 15 | 16 | Help us keep this project open and inclusive. Please read and follow our [Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 17 | 18 | ## Found an Issue? 19 | 20 | If you find a bug in the source code or a mistake in the documentation, you can help us by [submitting an issue](#submit-issue) to the GitHub repository. Even better, you can [submit a Pull Request](#submit-pr) with a fix. 21 | 22 | ## Want a Feature? 23 | 24 | You can *request* a new feature by [submitting an issue](#submit-issue) to the GitHub repository. If you would like to *implement* a new feature, please submit an issue with a proposal for your work first, to be sure that we can use it. 25 | 26 | - **Small features** can be crafted and directly [submitted as a Pull Request](#submit-pr). 27 | 28 | ## Submission Guidelines 29 | 30 | ### Submitting an Issue 31 | 32 | Before you submit an issue, search the archive, maybe your question was already answered. 33 | 34 | If your issue appears to be a bug, and hasn't been reported, open a new issue. 35 | Help us to maximize the effort we can spend fixing issues and adding new 36 | features, by not reporting duplicate issues. Providing the following information will increase the chances of your issue being dealt with quickly: 37 | 38 | - **Overview of the Issue** - if an error is being thrown a non-minified stack trace helps 39 | - **Version** - what version is affected (e.g. 0.1.2) 40 | - **Motivation for or Use Case** - explain what are you trying to do and why the current behavior is a bug for you 41 | - **Browsers and Operating System** - is this a problem with all browsers? 42 | - **Reproduce the Error** - provide a live example or a unambiguous set of steps 43 | - **Related Issues** - has a similar issue been reported before? 44 | - **Suggest a Fix** - if you can't fix the bug yourself, perhaps you can point to what might be 45 | causing the problem (line of code or commit) 46 | 47 | You can file new issues by providing the above information this repository's [issues list](https://github.com/mspnp/cloud-design-patterns/issues/new). 48 | 49 | ### Submitting a Pull Request (PR) 50 | 51 | Before you submit your Pull Request (PR) consider the following guidelines: 52 | 53 | * Search the repository's [pull requests](https://github.com/mspnp/cloud-design-patterns/pulls) for an open or closed PR that relates to your submission. You don't want to duplicate effort. 54 | 55 | - Make your changes in a new git fork: 56 | - Commit your changes using a descriptive commit message 57 | - Push your fork to GitHub: 58 | - In GitHub, create a pull request 59 | - If we suggest changes then: 60 | - Make the required updates. 61 | - Rebase your fork and force push to your GitHub repository (this will update your Pull Request): 62 | 63 | ```shell 64 | git rebase main -i 65 | git push -f 66 | ``` 67 | 68 | That's it! Thank you for your contribution! -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Cloud Design Patterns 2 | 3 | Copyright (c) Microsoft Corporation 4 | 5 | All rights reserved. 6 | 7 | MIT License 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining a copy of 10 | this software and associated documentation files (the ""Software""), to deal in 11 | the Software without restriction, including without limitation the rights to 12 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 13 | the Software, and to permit persons to whom the Software is furnished to do so, 14 | subject to the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be included in all 17 | copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 21 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 22 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 23 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 24 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cloud Design Patterns 2 | 3 | This repository contains companion code for the article series found in the [Cloud Design Patterns](https://aka.ms/cloud-design-patterns) series in Azure Architecture Center. 4 | 5 | ## Patterns demonstrated 6 | 7 | | Pattern definition | Sample code | 8 | | :----------------- | :---------- | 9 | | [Asynchronous Request-Reply](https://learn.microsoft.com/azure/architecture/patterns/async-request-reply) | ./async-request-reply/ | 10 | | [Choreography](https://learn.microsoft.com/azure/architecture/patterns/choreography) | ./choreography/ | 11 | | [Claim Check](https://learn.microsoft.com/azure/architecture/patterns/claim-check) | ./claim-check/ | 12 | | [Deployment Stamps](https://learn.microsoft.com/azure/architecture/patterns/deployment-stamp) | [mspnp/solution-architectures#deployment-stamp](https://github.com/mspnp/solution-architectures/tree/master/apps/deployment-stamp)$^{*}$ | 13 | | [Geode](https://learn.microsoft.com/azure/architecture/patterns/geodes) | [mspnp/geode-pattern-accelerator](https://github.com/mspnp/geode-pattern-accelerator)$^{*}$ | 14 | | [Health Endpoint Monitoring](https://learn.microsoft.com/azure/architecture/patterns/health-endpoint-monitoring) | [dotnet/AspNetCore.Docs#health-checks](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/host-and-deploy/health-checks/samples/7.x/HealthChecksSample)$^{*}$ | 15 | | [Leader Election](https://learn.microsoft.com/azure/architecture/patterns/leader-election) | ./leader-election/ | 16 | | [Pipes & Filters](https://learn.microsoft.com/azure/architecture/patterns/pipes-and-filters) | ./pipes-and-filters/ | 17 | | [Priority Queue](https://learn.microsoft.com/azure/architecture/patterns/priority-queue) | ./priority-queue/ | 18 | | [Rate Limiting](https://learn.microsoft.com/azure/architecture/patterns/rate-limiting-pattern) | Go: [mspnp/go-batcher](https://github.com/mspnp/go-batcher)\*
Java: [Azure-Samples/java-rate-limiting-pattern-sample](https://github.com/Azure-Samples/java-rate-limiting-pattern-sample)\* | 19 | | [Saga](https://learn.microsoft.com/azure/architecture/reference-architectures/saga/saga) | [Azure-Samples/saga-orchestration-serverless](https://github.com/Azure-Samples/saga-orchestration-serverless)$^{*}$ | 20 | | [Sharding](https://learn.microsoft.com/azure/architecture/patterns/sharding) | ./sharding/ | 21 | | [Static Content Hosting](https://learn.microsoft.com/azure/architecture/patterns/static-content-hosting) | ./static-content-hosting/ | 22 | | [Valet Key](https://learn.microsoft.com/en-us/azure/architecture/patterns/valet-key) | ./valet-key/ | 23 | 24 | > Items donated with $^{*}$ are part of the Azure Architecture Center series, but the sample code is hosted in a different repository. 25 | 26 | ## Contributions 27 | 28 | Please see our [Contributor guide](./CONTRIBUTING.md). 29 | 30 | ## Microsoft Open Source Code of Conduct 31 | 32 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 33 | 34 | Resources: 35 | 36 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 37 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 38 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 39 | 40 | With :heart: from Azure patterns & practices, [Azure Architecture Center](https://azure.com/architecture). 41 | 42 | -------------------------------------------------------------------------------- /SECURITY.MD: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), and [Xamarin](https://github.com/xamarin). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/security.md/definition), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/security.md/msrc/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/security.md/msrc/pgp). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/security.md/msrc/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/security.md/cvd). 40 | 41 | -------------------------------------------------------------------------------- /async-request-reply/src/AsyncOperationStatusChecker.cs: -------------------------------------------------------------------------------- 1 | using Azure.Storage.Blobs; 2 | using Azure.Storage.Blobs.Specialized; 3 | using Microsoft.AspNetCore.Http; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Microsoft.Azure.Functions.Worker; 6 | using Microsoft.Extensions.Logging; 7 | 8 | namespace Asyncpattern 9 | { 10 | public class AsyncOperationStatusChecker(ILogger _logger) 11 | { 12 | [Function("AsyncOperationStatusChecker")] 13 | public async Task Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "RequestStatus/{thisGUID}")] HttpRequest req, 14 | [BlobInput("data/{thisGUID}.blobdata", Connection = "DataStorage")] BlockBlobClient inputBlob, string thisGUID) 15 | { 16 | OnCompleteEnum OnComplete = Enum.Parse(req.Query["OnComplete"].FirstOrDefault() ?? "Redirect"); 17 | OnPendingEnum OnPending = Enum.Parse(req.Query["OnPending"].FirstOrDefault() ?? "OK"); 18 | 19 | _logger.LogInformation($"C# HTTP trigger function processed a request for status on {thisGUID} - OnComplete {OnComplete} - OnPending {OnPending}"); 20 | 21 | // ** Check to see if the blob is present ** 22 | if (await inputBlob.ExistsAsync()) 23 | { 24 | // ** If it's present, depending on the value of the optional "OnComplete" parameter choose what to do. ** 25 | return await OnCompleted(OnComplete, inputBlob, thisGUID); 26 | } 27 | else 28 | { 29 | // ** If it's NOT present, then we need to back off, so depending on the value of the optional "OnPending" parameter choose what to do. ** 30 | string rqs = $"http://{Environment.GetEnvironmentVariable("WEBSITE_HOSTNAME")}/api/RequestStatus/{thisGUID}"; 31 | 32 | switch (OnPending) 33 | { 34 | case OnPendingEnum.OK: 35 | { 36 | // Return an HTTP 200 status code with the 37 | return new OkObjectResult(new { status = "In progress", Location = rqs }); 38 | } 39 | 40 | case OnPendingEnum.Synchronous: 41 | { 42 | // Back off and retry. Time out if the backoff period hits one minute 43 | int backoff = 250; 44 | 45 | while (!await inputBlob.ExistsAsync() && backoff < 64000) 46 | { 47 | _logger.LogInformation($"Synchronous mode {thisGUID}.blob - retrying in {backoff} ms"); 48 | backoff = backoff * 2; 49 | await Task.Delay(backoff); 50 | } 51 | 52 | if (await inputBlob.ExistsAsync()) 53 | { 54 | _logger.LogInformation($"Synchronous Redirect mode {thisGUID}.blob - completed after {backoff} ms"); 55 | return await OnCompleted(OnComplete, inputBlob, thisGUID); 56 | } 57 | else 58 | { 59 | _logger.LogInformation($"Synchronous mode {thisGUID}.blob - NOT FOUND after timeout {backoff} ms"); 60 | return new NotFoundResult(); 61 | } 62 | } 63 | 64 | default: 65 | { 66 | throw new InvalidOperationException($"Unexpected value: {OnPending}"); 67 | } 68 | } 69 | } 70 | } 71 | private async Task OnCompleted(OnCompleteEnum OnComplete, BlockBlobClient inputBlob, string thisGUID) 72 | { 73 | switch (OnComplete) 74 | { 75 | case OnCompleteEnum.Redirect: 76 | 77 | { 78 | // The typical way to generate a SAS token in code requires the storage account key. 79 | //If you need to use “Managed Identity” to control access to your storage accounts in code, which is something I highly recommend wherever possible as this is a security best practice. 80 | // In this scenario, you won't have a storage account key, so you'll need to find another way to generate the shared access signatures. 81 | // To do that, we need to use an approach called user delegation SAS . By using a user delegation SAS, we can sign the signature with the Microsoft Entra ID credentials instead of the storage account key. 82 | BlobServiceClient blobServiceClient = inputBlob.GetParentBlobContainerClient().GetParentBlobServiceClient(); 83 | var userDelegationKey = await blobServiceClient.GetUserDelegationKeyAsync(DateTimeOffset.UtcNow, DateTimeOffset.UtcNow.AddDays(7)); 84 | // Redirect to the SAS URI to blob storage 85 | return new RedirectResult(inputBlob.GenerateSASURI(userDelegationKey)); 86 | } 87 | 88 | case OnCompleteEnum.Stream: 89 | { 90 | // Download the file and return it directly to the caller. 91 | // For larger files, use a stream to minimize RAM usage. 92 | return new OkObjectResult(await inputBlob.DownloadContentAsync()); 93 | } 94 | 95 | default: 96 | { 97 | throw new InvalidOperationException($"Unexpected value: {OnComplete}"); 98 | } 99 | } 100 | } 101 | } 102 | 103 | public enum OnCompleteEnum 104 | { 105 | 106 | Redirect, 107 | Stream 108 | } 109 | 110 | public enum OnPendingEnum 111 | { 112 | 113 | OK, 114 | Synchronous 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /async-request-reply/src/AsyncProcessingBackgroundWorker.cs: -------------------------------------------------------------------------------- 1 | using Azure.Messaging.ServiceBus; 2 | using Azure.Storage.Blobs; 3 | using Microsoft.Azure.Functions.Worker; 4 | 5 | namespace Asyncpattern 6 | { 7 | public class AsyncProcessingBackgroundWorker(BlobContainerClient _blobContainerClient) 8 | { 9 | [Function(nameof(AsyncProcessingBackgroundWorker))] 10 | public async Task Run([ServiceBusTrigger("outqueue", Connection = "ServiceBusConnection")] ServiceBusReceivedMessage message) 11 | { 12 | //Perform an actual action against the blob data source for the async readers to be able to check against. 13 | // This is where your actual service worker processing will be performed 14 | 15 | var requestGuid = message.ApplicationProperties["RequestGUID"].ToString(); 16 | string blobName = $"{requestGuid}.blobdata"; 17 | 18 | await _blobContainerClient.CreateIfNotExistsAsync(); 19 | 20 | var blobClient = _blobContainerClient.GetBlobClient(blobName); 21 | using (MemoryStream memoryStream = new MemoryStream()) 22 | using (StreamWriter writer = new StreamWriter(memoryStream)) 23 | { 24 | writer.Write(message.Body.ToString()); 25 | writer.Flush(); 26 | memoryStream.Position = 0; 27 | 28 | await blobClient.UploadAsync(memoryStream, overwrite: true); 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /async-request-reply/src/AsyncProcessingWorkAcceptor.cs: -------------------------------------------------------------------------------- 1 | using Azure.Messaging.ServiceBus; 2 | using Microsoft.AspNetCore.Http; 3 | using Microsoft.AspNetCore.Mvc; 4 | using Microsoft.Azure.Functions.Worker; 5 | using Microsoft.Extensions.Logging; 6 | using Newtonsoft.Json; 7 | using FromBodyAttribute = Microsoft.Azure.Functions.Worker.Http.FromBodyAttribute; 8 | 9 | namespace Asyncpattern 10 | { 11 | public class AsyncProcessingWorkAcceptor(ILogger _logger, ServiceBusClient _serviceBusClient) 12 | { 13 | [Function("AsyncProcessingWorkAcceptor")] 14 | public async Task RunAsync([HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)] HttpRequest req, [FromBody] CustomerPOCO customer) 15 | { 16 | if (string.IsNullOrEmpty(customer.id) || string.IsNullOrEmpty(customer.customername)) 17 | { 18 | return new BadRequestResult(); 19 | } 20 | 21 | var reqid = Guid.NewGuid().ToString(); 22 | 23 | var rqs = $"http://{Environment.GetEnvironmentVariable("WEBSITE_HOSTNAME")}/api/RequestStatus/{reqid}"; 24 | 25 | var messagePayload = JsonConvert.SerializeObject(customer); 26 | var message = new ServiceBusMessage(messagePayload); 27 | message.ApplicationProperties.Add("RequestGUID", reqid); 28 | message.ApplicationProperties.Add("RequestSubmittedAt", DateTime.Now); 29 | message.ApplicationProperties.Add("RequestStatusURL", rqs); 30 | var sender = _serviceBusClient.CreateSender("outqueue"); 31 | 32 | await sender.SendMessageAsync(message); 33 | return new AcceptedResult(rqs, $"Request Accepted for Processing{Environment.NewLine}ProxyStatus: {rqs}"); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /async-request-reply/src/CloudBlockBlobExtensions.cs: -------------------------------------------------------------------------------- 1 | using Azure.Storage.Blobs; 2 | using Azure.Storage.Blobs.Models; 3 | using Azure.Storage.Blobs.Specialized; 4 | using Azure.Storage.Sas; 5 | 6 | namespace Asyncpattern 7 | { 8 | public static class CloudBlockBlobExtensions 9 | { 10 | public static string GenerateSASURI(this BlockBlobClient inputBlob, UserDelegationKey userDelegationKey) 11 | { 12 | BlobServiceClient blobServiceClient = inputBlob.GetParentBlobContainerClient().GetParentBlobServiceClient(); 13 | 14 | BlobSasBuilder blobSasBuilder = new BlobSasBuilder() 15 | { 16 | BlobContainerName = inputBlob.BlobContainerName, 17 | BlobName = inputBlob.Name, 18 | Resource = "b", 19 | StartsOn = DateTimeOffset.UtcNow, 20 | ExpiresOn = DateTimeOffset.UtcNow.AddMinutes(10) 21 | }; 22 | blobSasBuilder.SetPermissions(BlobSasPermissions.Read); 23 | 24 | var blobUriBuilder = new BlobUriBuilder(inputBlob.Uri) 25 | { 26 | Sas = blobSasBuilder.ToSasQueryParameters(userDelegationKey, blobServiceClient.AccountName) 27 | }; 28 | 29 | //Generate the shared access signature on the blob, setting the constraints directly on the signature. 30 | Uri sasUri = blobUriBuilder.ToUri(); 31 | 32 | //Return the URI string for the container, including the SAS token. 33 | return sasUri.ToString(); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /async-request-reply/src/CustomerPOCO.cs: -------------------------------------------------------------------------------- 1 | namespace Asyncpattern 2 | { 3 | public class CustomerPOCO 4 | { 5 | public string id { get; set; } 6 | public string customername { get; set; } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /async-request-reply/src/Program.cs: -------------------------------------------------------------------------------- 1 | using Azure.Identity; 2 | using Azure.Messaging.ServiceBus; 3 | using Azure.Storage.Blobs; 4 | using Microsoft.Azure.Functions.Worker; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using Microsoft.Extensions.Hosting; 7 | 8 | var host = new HostBuilder() 9 | .ConfigureFunctionsWebApplication() 10 | .ConfigureServices(services => 11 | { 12 | services.AddApplicationInsightsTelemetryWorkerService(); 13 | services.ConfigureFunctionsApplicationInsights(); 14 | 15 | // blobServiceClient using Managed Identity 16 | var storageAccountUriString = Environment.GetEnvironmentVariable("DataStorage__blobServiceUri"); 17 | var storageContainerEndpoint = storageAccountUriString + "/data"; 18 | 19 | // Get a credential and create a client object for the blob container. 20 | BlobContainerClient containerClient = new BlobContainerClient(new Uri(storageContainerEndpoint),new DefaultAzureCredential()); 21 | services.AddSingleton(containerClient); 22 | 23 | // ServiceBusClient using Managed Identity 24 | var fullyQualifiedNamespace = Environment.GetEnvironmentVariable("ServiceBusConnection__fullyQualifiedNamespace"); 25 | var serviceBusClient = new ServiceBusClient(fullyQualifiedNamespace, new DefaultAzureCredential()); 26 | services.AddSingleton(serviceBusClient); 27 | }) 28 | .Build(); 29 | 30 | host.Run(); 31 | -------------------------------------------------------------------------------- /async-request-reply/src/asyncpattern.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net8.0 4 | v4 5 | Exe 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | PreserveNewest 26 | 27 | 28 | PreserveNewest 29 | Never 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /async-request-reply/src/asyncpattern.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.9.34902.65 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Asyncpattern", "Asyncpattern.csproj", "{97782A87-CCF4-4C3B-A0B4-3424A446B50F}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {97782A87-CCF4-4C3B-A0B4-3424A446B50F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {97782A87-CCF4-4C3B-A0B4-3424A446B50F}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {97782A87-CCF4-4C3B-A0B4-3424A446B50F}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {97782A87-CCF4-4C3B-A0B4-3424A446B50F}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {AD2DBADC-2860-4112-A8E3-AF7EF7B68E61} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /async-request-reply/src/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "logging": { 4 | "applicationInsights": { 5 | "samplingSettings": { 6 | "isEnabled": true, 7 | "excludedTypes": "Request" 8 | }, 9 | "enableLiveMetricsFilters": true 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /async-request-reply/src/local.settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "IsEncrypted": false, 3 | "Values": { 4 | "AzureWebJobsStorage": "UseDevelopmentStorage=true", 5 | "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated", 6 | "ServiceBusConnection__fullyQualifiedNamespace": "", 7 | "DataStorage__blobServiceUri": "" 8 | } 9 | } -------------------------------------------------------------------------------- /choreography/README.md: -------------------------------------------------------------------------------- 1 | # Choregraphy pattern example 2 | 3 | The [Choreography cloud design pattern](https://learn.microsoft.com/azure/architecture/patterns/choreography) does not have code associated with it. You'll find an example of this pattern in the Example section of that article. 4 | 5 | The scenario presented is how code running in an Azure Container Apps can use a message broker to exchange messages without central coordination to process a business transaction that requires ordered handling of unbounded sequences of related messages. The scenario uses a [Drone Delivery app](https://github.com/mspnp/microservices-reference-implementation). 6 | 7 | ![Diagram of an event driven cloud native example workload implementing choreography pattern](./choreography-example.png) 8 | 9 | ## Related documentation 10 | 11 | - [Choose between Azure messaging services](https://learn.microsoft.com/azure/service-bus-messaging/compare-messaging-services) 12 | - Use asynchronous distributed messaging through the [publisher-subscriber pattern](https://learn.microsoft.com/azure/architecture/patterns/publisher-subscriber) 13 | - Managing consistency issues implementing [Saga](https://learn.microsoft.com/azure/architecture/reference-architectures/saga/saga) 14 | 15 | ## Contributions 16 | 17 | Please see our [contributor guide](../CONTRIBUTING.md). 18 | 19 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information, see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact with any additional questions or comments. 20 | 21 | With :heart: from Microsoft Patterns & Practices, [Azure Architecture Center](https://aka.ms/architecture). 22 | -------------------------------------------------------------------------------- /choreography/choreography-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mspnp/cloud-design-patterns/ec4c70c30df8012150ac4fe02486a7508bf7ed3a/choreography/choreography-example.png -------------------------------------------------------------------------------- /claim-check/.gitattributes: -------------------------------------------------------------------------------- 1 | *.sh text=auto eol=lf 2 | -------------------------------------------------------------------------------- /claim-check/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | azcli-execution.log 3 | .vscode/ 4 | .vs/ 5 | local.settings.json 6 | App.config 7 | obj/ -------------------------------------------------------------------------------- /claim-check/README.md: -------------------------------------------------------------------------------- 1 | # Claim-check cloud pattern examples 2 | 3 | This folder contains several examples illustrating the use of the [Claim-Check design pattern](https://learn.microsoft.com/azure/architecture/patterns/claim-check) with various Azure services. 4 | 5 | > All the examples in this folder use [`DefaultAzureCredential`](https://learn.microsoft.com/dotnet/azure/sdk/authentication/#defaultazurecredential) to authenticate when accessing Azure resources. The Bicep script on each sample assigns the necessary RBAC (Role-Based Access Control) permissions to an account principal, which is provided as a parameter, during deployment. 6 | 7 | ## [Automatic claim check token generation, Azure Queue Storage as messaging system](./code-samples/sample-1/) 8 | 9 | > Technologies used: Azure Blob Storage, Azure Event Grid, Azure Functions, Azure Blob Queue, .NET 10 | 11 | This example uses Azure Blob Storage to store the payload, but any service that supports [Event Grid](https://azure.microsoft.com/services/event-grid/) integration can be used. A client application uploads the payload to Azure Blob Store and Event Grid automatically generates an event, with a reference to the blob, that can be used as a claim check token. The event is forwarded to an Azure Queue Storage queue from where it can be retrieved by the consumer sample app. 12 | 13 | This approach allows a client application to poll the queue, get the message, extract the reference to the payload blob, and use it to download the payload directly from Azure Blob Storage. Azure Functions can also consume the Event Grid message directly. 14 | 15 | ## [Automatic claim check token generation, Event Hubs as messaging system](./code-samples/sample-2/) 16 | 17 | > Technologies used: Azure Blob Storage, Azure Event Grid, Azure Event Hubs, .NET 18 | 19 | Like in the previous example, a reference message is automatically generated by Event Grid when a payload is uploaded to Azure Blob Storage. In this sample, Event Hubs is used as the messaging system. 20 | 21 | ## [Automatic claim check token generation, Service Bus as messaging system](./code-samples/sample-3/) 22 | 23 | > Technologies used: Azure Blob Storage, Azure Event Grid, Azure Service Bus, Azure Functions, .NET 24 | 25 | Like in the previous two examples, a reference message is automatically generated by Event Grid when a payload is uploaded to Azure Blob Storage. In this sample, Service Bus is used as the messaging system. 26 | 27 | ## [Manual claim check token generation, Kafka API-enabled Event Hubs as messaging system](./code-samples/sample-4/) 28 | 29 | > Technologies used: Azure Blob Storage, Azure Event Hubs with Kafka Api, Azure Functions, .NET 30 | 31 | In this example the client application uploads the payload to Azure Blob Storage and manually generates the message to be used as a claim check token, which is sent via Event Hubs. 32 | 33 | This example uses [Event Hubs with Kafka enabled](https://learn.microsoft.com/azure/event-hubs/event-hubs-create-kafka-enabled), to demonstrate the ease of using other Azure services like Azure Blob Storage, Azure functions etc. with a different messaging protocol like Kafka. 34 | 35 | ## Contributions 36 | 37 | Please see our [Contributor guide](../CONTRIBUTING.md). 38 | 39 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact with any additional questions or comments. 40 | 41 | With :heart: from Azure patterns & practices, [Azure Architecture Center](https://azure.com/architecture). -------------------------------------------------------------------------------- /claim-check/code-samples/all-samples.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.9.34723.18 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FunctionConsumer3", "sample-3\FunctionConsumer3\FunctionConsumer3.csproj", "{67AC55F4-7647-466F-8A3E-83E2878347AF}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "sample-3", "sample-3", "{81F731CC-6F42-4816-BDD4-0FA541895909}" 9 | ProjectSection(SolutionItems) = preProject 10 | sample-3\readme.md = sample-3\readme.md 11 | EndProjectSection 12 | EndProject 13 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "sample-1", "sample-1", "{F1A6CB79-1241-4B93-A011-4A8051228840}" 14 | ProjectSection(SolutionItems) = preProject 15 | sample-1\readme.md = sample-1\readme.md 16 | EndProjectSection 17 | EndProject 18 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "sample-2", "sample-2", "{873C40EF-AF5B-4B46-8BDE-DEA007079812}" 19 | ProjectSection(SolutionItems) = preProject 20 | sample-2\readme.md = sample-2\readme.md 21 | EndProjectSection 22 | EndProject 23 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "sample-4", "sample-4", "{EA1E4C36-059D-41E8-89DF-389985CF5FBD}" 24 | ProjectSection(SolutionItems) = preProject 25 | sample-4\readme.md = sample-4\readme.md 26 | EndProjectSection 27 | EndProject 28 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FunctionConsumer1", "sample-1\FunctionConsumer1\FunctionConsumer1.csproj", "{6022107B-519D-4B28-937E-DFC348933E3F}" 29 | EndProject 30 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FunctionConsumer4", "sample-4\FunctionConsumer4\FunctionConsumer4.csproj", "{5A7E5D39-BF09-4A3B-A7A1-7084A5359BAA}" 31 | EndProject 32 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ClientProducer4", "sample-4\ClientProducer4\ClientProducer4.csproj", "{CA4D4A68-55B4-496F-B976-D3464076FC62}" 33 | EndProject 34 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{4C721EF7-6533-46D2-9BA5-A3639D38B715}" 35 | ProjectSection(SolutionItems) = preProject 36 | ..\README.md = ..\README.md 37 | EndProjectSection 38 | EndProject 39 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ClientConsumer2", "sample-2\ClientConsumer2\ClientConsumer2.csproj", "{8FD964BC-7351-443C-AC9D-573F821FCF2D}" 40 | EndProject 41 | Global 42 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 43 | Debug|Any CPU = Debug|Any CPU 44 | Release|Any CPU = Release|Any CPU 45 | EndGlobalSection 46 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 47 | {67AC55F4-7647-466F-8A3E-83E2878347AF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 48 | {67AC55F4-7647-466F-8A3E-83E2878347AF}.Debug|Any CPU.Build.0 = Debug|Any CPU 49 | {67AC55F4-7647-466F-8A3E-83E2878347AF}.Release|Any CPU.ActiveCfg = Release|Any CPU 50 | {67AC55F4-7647-466F-8A3E-83E2878347AF}.Release|Any CPU.Build.0 = Release|Any CPU 51 | {6022107B-519D-4B28-937E-DFC348933E3F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 52 | {6022107B-519D-4B28-937E-DFC348933E3F}.Debug|Any CPU.Build.0 = Debug|Any CPU 53 | {6022107B-519D-4B28-937E-DFC348933E3F}.Release|Any CPU.ActiveCfg = Release|Any CPU 54 | {6022107B-519D-4B28-937E-DFC348933E3F}.Release|Any CPU.Build.0 = Release|Any CPU 55 | {5A7E5D39-BF09-4A3B-A7A1-7084A5359BAA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 56 | {5A7E5D39-BF09-4A3B-A7A1-7084A5359BAA}.Debug|Any CPU.Build.0 = Debug|Any CPU 57 | {5A7E5D39-BF09-4A3B-A7A1-7084A5359BAA}.Release|Any CPU.ActiveCfg = Release|Any CPU 58 | {5A7E5D39-BF09-4A3B-A7A1-7084A5359BAA}.Release|Any CPU.Build.0 = Release|Any CPU 59 | {CA4D4A68-55B4-496F-B976-D3464076FC62}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 60 | {CA4D4A68-55B4-496F-B976-D3464076FC62}.Debug|Any CPU.Build.0 = Debug|Any CPU 61 | {CA4D4A68-55B4-496F-B976-D3464076FC62}.Release|Any CPU.ActiveCfg = Release|Any CPU 62 | {CA4D4A68-55B4-496F-B976-D3464076FC62}.Release|Any CPU.Build.0 = Release|Any CPU 63 | {8FD964BC-7351-443C-AC9D-573F821FCF2D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 64 | {8FD964BC-7351-443C-AC9D-573F821FCF2D}.Debug|Any CPU.Build.0 = Debug|Any CPU 65 | {8FD964BC-7351-443C-AC9D-573F821FCF2D}.Release|Any CPU.ActiveCfg = Release|Any CPU 66 | {8FD964BC-7351-443C-AC9D-573F821FCF2D}.Release|Any CPU.Build.0 = Release|Any CPU 67 | EndGlobalSection 68 | GlobalSection(SolutionProperties) = preSolution 69 | HideSolutionNode = FALSE 70 | EndGlobalSection 71 | GlobalSection(NestedProjects) = preSolution 72 | {67AC55F4-7647-466F-8A3E-83E2878347AF} = {81F731CC-6F42-4816-BDD4-0FA541895909} 73 | {6022107B-519D-4B28-937E-DFC348933E3F} = {F1A6CB79-1241-4B93-A011-4A8051228840} 74 | {5A7E5D39-BF09-4A3B-A7A1-7084A5359BAA} = {EA1E4C36-059D-41E8-89DF-389985CF5FBD} 75 | {CA4D4A68-55B4-496F-B976-D3464076FC62} = {EA1E4C36-059D-41E8-89DF-389985CF5FBD} 76 | {8FD964BC-7351-443C-AC9D-573F821FCF2D} = {873C40EF-AF5B-4B46-8BDE-DEA007079812} 77 | EndGlobalSection 78 | GlobalSection(ExtensibilityGlobals) = postSolution 79 | SolutionGuid = {1B1FEA88-54B9-477B-927B-F3542A3AA1B6} 80 | EndGlobalSection 81 | EndGlobal 82 | -------------------------------------------------------------------------------- /claim-check/code-samples/sample-1/FunctionConsumer1/FunctionConsumer1.cs: -------------------------------------------------------------------------------- 1 | 2 | using Azure.Storage.Queues.Models; 3 | using Microsoft.Azure.Functions.Worker; 4 | using Microsoft.Extensions.Logging; 5 | using System.Text; 6 | using System.Text.Json; 7 | 8 | namespace Pnp.Samples.ClaimCheckPattern 9 | { 10 | /// 11 | /// Sample function illustrating the processing of Storage Queue messages containing claim-check style events forwarded by Event Grid 12 | /// 13 | public class FunctionConsumer1(ILoggerFactory loggerFactory, ISampleBlobDataMover sampleBlobDataMover) 14 | { 15 | readonly ILogger _logger = loggerFactory.CreateLogger(); 16 | readonly ISampleBlobDataMover _downloader = sampleBlobDataMover; 17 | 18 | /// 19 | /// Function that processes Storage Queue messages auto-generated by Event Grid when files are uploaded to a storage blob container. 20 | /// 21 | [Function(nameof(FunctionConsumer1))] 22 | public async Task RunAsync( 23 | [QueueTrigger("%StorageQueue%", Connection = "StorageQueueConnectionString")] QueueMessage receivedMessage 24 | ) 25 | { 26 | _logger.LogInformation("Message Queue message received: {MessageId}", receivedMessage.MessageId); 27 | 28 | var messageText = Encoding.UTF8.GetString(receivedMessage.Body); 29 | var jsonMessage = JsonDocument.Parse(messageText).RootElement; 30 | var payloadUri = new Uri(jsonMessage.GetProperty("data").GetProperty("url").GetString()!); 31 | _logger.LogInformation("Message received. Payload Url: {Uri}", payloadUri); 32 | 33 | // download the payload 34 | var payload = await _downloader.DownloadAsync(payloadUri); 35 | _logger.LogInformation("Payload content (Url {Uri)}\n {Payload}", payloadUri, payload); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /claim-check/code-samples/sample-1/FunctionConsumer1/FunctionConsumer1.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net9.0 4 | v4 5 | Exe 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | PreserveNewest 26 | 27 | 28 | PreserveNewest 29 | 30 | 31 | Never 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /claim-check/code-samples/sample-1/FunctionConsumer1/ISampleBlobDataMover.cs: -------------------------------------------------------------------------------- 1 |  2 | namespace Pnp.Samples.ClaimCheckPattern 3 | { 4 | public interface ISampleBlobDataMover 5 | { 6 | Task DownloadAsync(Uri blobUri, bool deleteAfter = true); 7 | Task UploadAsync(string blobUri, string containerName, string content); 8 | } 9 | } -------------------------------------------------------------------------------- /claim-check/code-samples/sample-1/FunctionConsumer1/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Azure.Functions.Worker; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Microsoft.Extensions.Hosting; 4 | using Pnp.Samples.ClaimCheckPattern; 5 | 6 | var host = new HostBuilder() 7 | .ConfigureFunctionsWebApplication() 8 | .ConfigureServices(services => 9 | { 10 | services.AddApplicationInsightsTelemetryWorkerService(); 11 | services.ConfigureFunctionsApplicationInsights(); 12 | services.AddScoped(); 13 | }) 14 | .Build(); 15 | 16 | host.Run(); 17 | -------------------------------------------------------------------------------- /claim-check/code-samples/sample-1/FunctionConsumer1/SampleBlobDataMover.cs: -------------------------------------------------------------------------------- 1 | using Azure.Identity; 2 | using Azure.Storage.Blobs; 3 | using Azure.Storage.Blobs.Specialized; 4 | using Microsoft.Extensions.Logging; 5 | using System.Text; 6 | 7 | namespace Pnp.Samples.ClaimCheckPattern 8 | { 9 | /// 10 | /// A sample Storage Blob uploader/downloader. Uses Azure Entra ID for authentication. 11 | /// 12 | public class SampleBlobDataMover(ILoggerFactory loggerFactory) : ISampleBlobDataMover 13 | { 14 | readonly ILogger _logger = loggerFactory.CreateLogger(); 15 | 16 | /// 17 | /// Downloads the referenced payload from Azure Blobs and returns the content. 18 | /// Optionally deletes the blob after download 19 | /// 20 | public async Task DownloadAsync(Uri blobUri, bool deleteAfter = true) 21 | { 22 | var blobClient = new BlockBlobClient(blobUri, new DefaultAzureCredential()); 23 | if (!await blobClient.ExistsAsync()) 24 | { 25 | _logger.LogError("Blob {BlobUri} does not exist.", blobUri.AbsoluteUri); 26 | return string.Empty; 27 | } 28 | var response = await blobClient.DownloadContentAsync(); 29 | if (deleteAfter) 30 | { 31 | await blobClient.DeleteIfExistsAsync(); 32 | } 33 | return Encoding.UTF8.GetString(response.Value.Content); 34 | } 35 | 36 | /// 37 | /// Upload sample text content to Azure Blob Storage and returns Url to the newly created blob 38 | /// 39 | public async Task UploadAsync(string blobUri, string containerName, string content) 40 | { 41 | var serviceClient = new BlobServiceClient(new Uri(blobUri), new DefaultAzureCredential()); 42 | 43 | var containerClient = serviceClient.GetBlobContainerClient(containerName); 44 | await containerClient.CreateIfNotExistsAsync(); 45 | 46 | var blobClient = containerClient.GetBlobClient(Guid.NewGuid().ToString()); 47 | await blobClient.UploadAsync(new BinaryData(content), overwrite: true); 48 | _logger.LogInformation("Uploaded content to {Uri}", blobClient.Uri.AbsoluteUri); 49 | return blobClient.Uri.AbsoluteUri; 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /claim-check/code-samples/sample-1/FunctionConsumer1/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "logging": { 4 | "applicationInsights": { 5 | "samplingSettings": { 6 | "isEnabled": true, 7 | "excludedTypes": "Request" 8 | } 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /claim-check/code-samples/sample-1/FunctionConsumer1/local.settings.json.template: -------------------------------------------------------------------------------- 1 | { 2 | "IsEncrypted": false, 3 | "Values": { 4 | "AzureWebJobsStorage": "UseDevelopmentStorage=true", 5 | "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated", 6 | 7 | "AzureFunctionsJobHost__logging__applicationInsights__samplingSettings__isEnabled": "false", 8 | 9 | "StorageQueueConnectionString:serviceUri": "https://{STORAGE_ACCOUNT_NAME}.queue.core.windows.net", 10 | "StorageQueue": "claimcheckqueue" 11 | } 12 | } -------------------------------------------------------------------------------- /claim-check/code-samples/sample-1/bicep/main.bicep: -------------------------------------------------------------------------------- 1 | targetScope = 'resourceGroup' 2 | 3 | @minLength(5) 4 | @description('Location of the resources. Defaults to resource group location.') 5 | param location string = resourceGroup().location 6 | 7 | @minLength(36) 8 | @description('The guid of the principal running the valet key generation code. In Azure this would be replaced with the managed identity of the Azure Function, when running locally it will be your user.') 9 | param principalId string 10 | 11 | @minLength(3) 12 | @maxLength(5) 13 | @description('The globally unique prefix naming resources.') 14 | param namePrefix string 15 | 16 | /*** EXISTING RESOURCES ***/ 17 | 18 | @description('Built-in Azure RBAC role that is applied to a Storage account to grant "Storage Blob Data Contributor" privileges.') 19 | resource storageBlobDataContributorRole 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = { 20 | name: 'ba92f5b4-2d11-453d-a403-e96b0029c9fe' 21 | scope: subscription() 22 | } 23 | 24 | @description('Built-in Azure RBAC role that is applied to a Storage account to grant "Storage Queue Data Contributor" privileges.') 25 | resource storageQueueDataContributorRole 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = { 26 | name: '974c5e8b-45b9-4653-ba55-5f855dd0fb88' 27 | scope: subscription() 28 | } 29 | 30 | /*** NEW RESOURCES ***/ 31 | 32 | @description('The Azure Storage account which will be where authorized clients upload large blobs to. The Azure Function will hand out scoped, time-limited SaS tokens for this blobs in this account.') 33 | resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = { 34 | name: 'st${namePrefix}cc' 35 | location: location 36 | sku: { 37 | name: 'Standard_LRS' 38 | } 39 | kind: 'StorageV2' 40 | properties: { 41 | accessTier: 'Hot' 42 | allowBlobPublicAccess: false 43 | allowCrossTenantReplication: false 44 | allowSharedKeyAccess: false // Only allowed access by Managed Identity 45 | isLocalUserEnabled: false 46 | isHnsEnabled: false 47 | isNfsV3Enabled: false 48 | isSftpEnabled: false 49 | largeFileSharesState: 'Disabled' 50 | minimumTlsVersion: 'TLS1_2' 51 | publicNetworkAccess: 'Enabled' // Enabled to test the sample code from a local machine 52 | supportsHttpsTrafficOnly: true 53 | defaultToOAuthAuthentication: true 54 | allowedCopyScope: 'PrivateLink' 55 | networkAcls: { 56 | defaultAction: 'Allow' // For this sample, public Internet access is expected 57 | bypass: 'None' 58 | virtualNetworkRules: [] 59 | ipRules: [] 60 | } 61 | } 62 | 63 | resource blobContainers 'blobServices' = { 64 | name: 'default' 65 | 66 | @description('The blob container to serve as the large payloads data store.') 67 | resource payloadsContainer 'containers' = { 68 | name: 'payloads' 69 | } 70 | } 71 | 72 | resource QueueServices 'queueServices' = { 73 | name: 'default' 74 | 75 | @description('The queue to serve as the message queue for the sample.') 76 | resource claimcheckqueue 'queues' = { 77 | name: 'claimcheckqueue' 78 | } 79 | } 80 | } 81 | 82 | @description('Set permissions to give the user principal access to Storage Blob from the sample applications') 83 | resource blobContributorRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 84 | name: guid(storageAccount::blobContainers::payloadsContainer.id, storageBlobDataContributorRole.id, principalId) 85 | scope: storageAccount::blobContainers::payloadsContainer 86 | properties: { 87 | principalId: principalId 88 | roleDefinitionId: storageBlobDataContributorRole.id 89 | principalType: 'User' // 'ServicePrincipal' if this was a managed identity 90 | description: 'Allows this Microsoft Entra principal to access blobs in this storage blob container.' 91 | } 92 | } 93 | 94 | @description('Set permissions to give the user principal access to Storage Queues from the sample applications') 95 | resource queueContributorRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 96 | name: guid(storageAccount::QueueServices::claimcheckqueue.id, storageQueueDataContributorRole.id, principalId) 97 | scope: storageAccount::QueueServices::claimcheckqueue 98 | properties: { 99 | principalId: principalId 100 | roleDefinitionId: storageQueueDataContributorRole.id 101 | principalType: 'User' // 'ServicePrincipal' if this was a managed identity 102 | description: 'Allows this Microsoft Entra principal to access messages in this storage queue.' 103 | } 104 | } 105 | 106 | @description('Event Grid system topic to subscribe to blob created events.') 107 | resource eventGridStorageBlobTopic 'Microsoft.EventGrid/systemTopics@2023-12-15-preview' = { 108 | name: '${storageAccount.name}${guid(namePrefix, 'storage')}' 109 | location: location 110 | identity: { 111 | type: 'SystemAssigned' 112 | } 113 | properties: { 114 | source: storageAccount.id 115 | topicType: 'microsoft.storage.storageaccounts' 116 | } 117 | } 118 | 119 | @description('Event Grid system topic subscription to queue blob created events.') 120 | resource eventGridBlobCreatedQueueSubscription 'Microsoft.EventGrid/systemTopics/eventSubscriptions@2023-12-15-preview' = { 121 | parent: eventGridStorageBlobTopic 122 | name: 'storagequeue' 123 | properties: { 124 | destination: { 125 | properties: { 126 | resourceId: storageAccount.id 127 | queueName: 'claimcheckqueue' 128 | } 129 | endpointType: 'StorageQueue' 130 | } 131 | filter: { 132 | includedEventTypes: [ 133 | 'Microsoft.Storage.BlobCreated' 134 | ] 135 | } 136 | eventDeliverySchema: 'EventGridSchema' 137 | } 138 | } 139 | 140 | -------------------------------------------------------------------------------- /claim-check/code-samples/sample-1/images/sample-1-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mspnp/cloud-design-patterns/ec4c70c30df8012150ac4fe02486a7508bf7ed3a/claim-check/code-samples/sample-1/images/sample-1-diagram.png -------------------------------------------------------------------------------- /claim-check/code-samples/sample-1/readme.md: -------------------------------------------------------------------------------- 1 | # Sample 1: Automatic claim check token generation with Event Grid, Azure Queue Storage as messaging system 2 | 3 | ## Technologies used: Azure Blob Storage, Azure Event Grid, Azure Functions, Azure Queue Storage, .NET 9.0 4 | 5 | This example uses Azure Blob Storage to store the payload, but any service that supports [Event Grid](https://azure.microsoft.com/services/event-grid/) integration can be used. A client application uploads the payload to Azure Blob Store and Event Grid automatically generates an event, with a reference to the blob, that can be used as a claim check token. The event is forwarded to an Azure Queue Storage queue from where it can be retrieved by the consumer sample app. 6 | 7 | This approach allows a client application to poll the queue, get the message, extract the reference to the payload blob, and use it to download the payload directly from Azure Blob Storage. Azure Functions can also consume the Event Grid message directly. 8 | 9 | > This example uses [`DefaultAzureCredential`](https://learn.microsoft.com/dotnet/azure/sdk/authentication/#defaultazurecredential) for authentication while accessing Azure resources. the user principal must be provided as a parameter to the included Bicep script. The Bicep script is responsible for assigning the necessary RBAC (Role-Based Access Control) permissions for accessing the various Azure resources. While the principal can be the account associated with the interactive user, there are alternative [configurations](https://learn.microsoft.com/dotnet/azure/sdk/authentication/?tabs=command-line#exploring-the-sequence-of-defaultazurecredential-authentication-methods) available. 10 | 11 | ![A diagram showing Event Grid connected to Azure Blob Storage. As blobs are created, Event Grid forwards a message, containing the reference to the blob, to Azure Queue Storage. A consumer Function receives the message from the queue, extracts the reference, and dowloads the blob from the storage account.](images/sample-1-diagram.png) 12 | 13 | 1. The payload uploaded to Azure Blob Storage. 14 | 1. A Blob Created event is generated by Event Grid. 15 | 1. Event Grid forwards the event, containing a reference to the payload blob, to Azure Queue Storage. 16 | 1. An Azure Function connects to the queue and reads messages as they arrive. 17 | 1. The Function extracts the reference to the payload blob from the message and downloads the blob directly from storage. 18 | 19 | ## :rocket: Deployment guide 20 | 21 | Install the prerequisites and follow the steps to deploy and run the examples. 22 | 23 | ### Prerequisites 24 | 25 | - Permission to create a new resource group and resources in an [Azure subscription](https://azure.com/free) 26 | - Unix-like shell. Also available in: 27 | - [Azure Cloud Shell](https://shell.azure.com/) 28 | - [Windows Subsystem for Linux (WSL)](https://learn.microsoft.com/windows/wsl/install) 29 | - [Git](https://git-scm.com/downloads) 30 | - [.NET 9 SDK](https://dotnet.microsoft.com/download/dotnet/9.0) 31 | - [Azure Functions Core Tools](https://learn.microsoft.com/azure/azure-functions/functions-run-local#install-the-azure-functions-core-tools) 32 | - [Azurite](/azure/storage/common/storage-use-azurite) 33 | - [Azure CLI](https://learn.microsoft.com/cli/azure/install-azure-cli) 34 | - Optionally, an IDE, like [Visual Studio](https://visualstudio.microsoft.com/downloads/) or [Visual Studio Code](https://code.visualstudio.com/). 35 | 36 | ### Steps 37 | 38 | 1. Clone this repository to your workstation and navigate to the working directory. 39 | 40 | ```shell 41 | git clone https://github.com/mspnp/cloud-design-patterns 42 | cd cloud-design-patterns/claim-check/code-samples/sample-1 43 | ``` 44 | 45 | 1. Log into Azure and create an empty resource group. 46 | 47 | ```azurecli 48 | az login 49 | az account set -s 50 | 51 | NAME_PREFIX= 52 | LOCATION=eastus2 53 | RESOURCE_GROUP_NAME="rg-${NAME_PREFIX}-${LOCATION}" 54 | 55 | az group create -n "${RESOURCE_GROUP_NAME}" -l ${LOCATION} 56 | ``` 57 | 58 | 1. Deploy the supporting Azure resources. 59 | 60 | ```azurecli 61 | CURRENT_USER_OBJECT_ID=$(az ad signed-in-user show -o tsv --query id) 62 | 63 | # This could take a few minutes 64 | az deployment group create -n deploy-claim-check -f bicep/main.bicep -g "${RESOURCE_GROUP_NAME}" -p namePrefix=$NAME_PREFIX principalId=$CURRENT_USER_OBJECT_ID 65 | ``` 66 | 67 | 1. Configure the sample consumer to use the created Azure resources. 68 | 69 | ```shell 70 | sed "s/{STORAGE_ACCOUNT_NAME}/st${NAME_PREFIX}cc/g" FunctionConsumer1/local.settings.json.template > FunctionConsumer1/local.settings.json 71 | ``` 72 | 73 | 1. [Run Azurite](https://learn.microsoft.com/azure/storage/common/storage-use-azurite#run-azurite) blob storage emulation service. 74 | 75 | > The local storage emulator is required as an Azure Storage account is a required "backing resource" for Azure Functions. 76 | 77 | 1. Launch the consumer sample application to receive and process claim check messages from Azure Queue Storage. 78 | 79 | The message consumer sample application for this scenario is implemented as an Azure Function, showcasing the serveless approach. Run the sample application to connect to the the queue and process messages as they arrive. 80 | 81 | ```bash 82 | cd sample-1\FunctionConsumer1 83 | func start 84 | ``` 85 | 86 | > Please note: For demo purposes, the sample consumer application will write the payload content to the the screen. Keep that in mind before you try sending really large payloads. 87 | 88 | ### :checkered_flag: Try it out 89 | 90 | To generate a claim check message you just have to drop a file in the created Azure Storage account. You can use Azure **Storage browser** to do that. [Refer this to know how to upload blobs to a container using Storage Explorer](https://learn.microsoft.com/azure/storage/blobs/quickstart-storage-explorer#upload-blobs-to-the-container). 91 | 92 | ### :broom: Clean up 93 | 94 | Remove the resource group that you created when you are done with this sample. 95 | 96 | ```azurecli 97 | az group delete -n "${RESOURCE_GROUP_NAME}" -y 98 | ``` 99 | -------------------------------------------------------------------------------- /claim-check/code-samples/sample-1/sample-1.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.9.34723.18 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FunctionConsumer1", "FunctionConsumer1\FunctionConsumer1.csproj", "{6022107B-519D-4B28-937E-DFC348933E3F}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{5260176F-A29C-479D-AEB7-876E233403EE}" 9 | ProjectSection(SolutionItems) = preProject 10 | readme.md = readme.md 11 | EndProjectSection 12 | EndProject 13 | Global 14 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 15 | Debug|Any CPU = Debug|Any CPU 16 | Release|Any CPU = Release|Any CPU 17 | EndGlobalSection 18 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 19 | {6022107B-519D-4B28-937E-DFC348933E3F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 20 | {6022107B-519D-4B28-937E-DFC348933E3F}.Debug|Any CPU.Build.0 = Debug|Any CPU 21 | {6022107B-519D-4B28-937E-DFC348933E3F}.Release|Any CPU.ActiveCfg = Release|Any CPU 22 | {6022107B-519D-4B28-937E-DFC348933E3F}.Release|Any CPU.Build.0 = Release|Any CPU 23 | EndGlobalSection 24 | GlobalSection(SolutionProperties) = preSolution 25 | HideSolutionNode = FALSE 26 | EndGlobalSection 27 | GlobalSection(ExtensibilityGlobals) = postSolution 28 | SolutionGuid = {1B1FEA88-54B9-477B-927B-F3542A3AA1B6} 29 | EndGlobalSection 30 | EndGlobal 31 | -------------------------------------------------------------------------------- /claim-check/code-samples/sample-2/ClientConsumer2/ClientConsumer2.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net9.0 6 | enable 7 | enable 8 | Pnp.Samples.ClaimCheckPattern 9 | latest 10 | 11 | 12 | 13 | 14 | PreserveNewest 15 | 16 | 17 | 18 | 19 | Never 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /claim-check/code-samples/sample-2/ClientConsumer2/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | using Microsoft.Extensions.Logging; 3 | using Pnp.Samples.ClaimCheckPattern; 4 | 5 | var configuration = new ConfigurationBuilder() 6 | .AddJsonFile("appsettings.json", false, false).Build(); 7 | 8 | using ILoggerFactory loggerFactory = LoggerFactory.Create(builder => builder.AddConsole()); 9 | 10 | Console.WriteLine("Begin sample 2 for Claim Check pattern"); 11 | Console.WriteLine("Uses the Event Hubs EventProcessorClient to receive claim-check messages, auto-generated with event-grid when a blob is uploaded to Storage Blob"); 12 | Console.WriteLine("Initializing..."); 13 | Console.WriteLine(); 14 | 15 | configuration.ThrowIfMissingSettings([ 16 | "AppSettings:EventHubsFullyQualifiedNamespace", 17 | "AppSettings:EventHubName", 18 | "AppSettings:EventProcessorStorageBlobUrl", 19 | "AppSettings:EventProcessorStorageContainer" 20 | ]); 21 | 22 | Console.WriteLine("Receiving messages..."); 23 | var eventHubsConsumer = new EventHubsConsumer(configuration, loggerFactory); 24 | var cts = new CancellationTokenSource(); 25 | var task = eventHubsConsumer.StartAsync(cts.Token); 26 | 27 | Console.WriteLine("Press any key to terminate the application..."); 28 | Console.ReadKey(true); 29 | cts.Cancel(); 30 | 31 | Console.WriteLine("Exiting..."); 32 | await task; 33 | await eventHubsConsumer.StopAsync(); 34 | Console.WriteLine("Done."); 35 | 36 | -------------------------------------------------------------------------------- /claim-check/code-samples/sample-2/ClientConsumer2/SampleBlobDataMover.cs: -------------------------------------------------------------------------------- 1 | using Azure.Identity; 2 | using Azure.Storage.Blobs; 3 | using Azure.Storage.Blobs.Specialized; 4 | using Microsoft.Extensions.Logging; 5 | using System.Text; 6 | 7 | namespace Pnp.Samples.ClaimCheckPattern 8 | { 9 | /// 10 | /// A sample Storage Blob uploader/downloader. Uses Azure Entra ID for authentication. 11 | /// 12 | public class SampleBlobDataMover(ILoggerFactory loggerFactory) 13 | { 14 | readonly ILogger _logger = loggerFactory.CreateLogger(); 15 | 16 | /// 17 | /// Downloads the referenced payload from Azure Blobs and returns the content. 18 | /// Optionally deletes the blob after download 19 | /// 20 | public async Task DownloadAsync(Uri blobUri, bool deleteAfter = true) 21 | { 22 | var blobClient = new BlockBlobClient(blobUri, new DefaultAzureCredential()); 23 | if (!await blobClient.ExistsAsync()) 24 | { 25 | _logger.LogError("Blob {BlobUri} does not exist.", blobUri.AbsoluteUri); 26 | return string.Empty; 27 | } 28 | var response = await blobClient.DownloadContentAsync(); 29 | if (deleteAfter) 30 | { 31 | await blobClient.DeleteIfExistsAsync(); 32 | } 33 | return Encoding.UTF8.GetString(response.Value.Content); 34 | } 35 | 36 | /// 37 | /// Upload sample text content to Azure Blob Storage and returns Url to the newly created blob 38 | /// 39 | public async Task UploadAsync(string blobUri, string containerName, string content) 40 | { 41 | var serviceClient = new BlobServiceClient(new Uri(blobUri), new DefaultAzureCredential()); 42 | 43 | var containerClient = serviceClient.GetBlobContainerClient(containerName); 44 | await containerClient.CreateIfNotExistsAsync(); 45 | 46 | var blobClient = containerClient.GetBlobClient(Guid.NewGuid().ToString()); 47 | await blobClient.UploadAsync(new BinaryData(content), overwrite: true); 48 | _logger.LogInformation("Uploaded content to {Uri}", blobClient.Uri.AbsoluteUri); 49 | return blobClient.Uri.AbsoluteUri; 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /claim-check/code-samples/sample-2/ClientConsumer2/SampleSettingsValidator.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.Extensions.Configuration; 2 | 3 | public static class SampleSettingsValidator 4 | { 5 | /// 6 | /// Ensure the required app settings are present 7 | /// 8 | /// 9 | /// 10 | public static void ThrowIfMissingSettings(this IConfiguration configuration, List settings) 11 | { 12 | if (settings.Any(option => string.IsNullOrWhiteSpace(configuration.GetSection(option).Value))) 13 | { 14 | throw new ApplicationException($"Configure options {string.Join(", ", settings)} in appsettings.json and run again."); 15 | } 16 | } 17 | 18 | } -------------------------------------------------------------------------------- /claim-check/code-samples/sample-2/ClientConsumer2/appsettings.json.template: -------------------------------------------------------------------------------- 1 | { 2 | "AppSettings": { 3 | "EventHubsFullyQualifiedNamespace": "{EVENT_HUBS_NAMESPACE}.servicebus.windows.net", 4 | "EventHubName": "evh-claimcheck", 5 | 6 | "EventProcessorStorageBlobUrl": "https://{EVENT_PROCESSOR_STORAGE_ACCOUNT_NAME}.blob.core.windows.net", 7 | "EventProcessorStorageContainer": "eventprocessor" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /claim-check/code-samples/sample-2/images/sample-2-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mspnp/cloud-design-patterns/ec4c70c30df8012150ac4fe02486a7508bf7ed3a/claim-check/code-samples/sample-2/images/sample-2-diagram.png -------------------------------------------------------------------------------- /claim-check/code-samples/sample-2/readme.md: -------------------------------------------------------------------------------- 1 | # Sample 2: Automatic claim check token generation with Event Grid, Event Hubs as messaging system 2 | 3 | ## Technologies used: Azure Blob Storage, Azure Event Grid, Azure Event Hub, .NET 9.0 4 | 5 | Like in Sample 1, a reference message is automatically generated by [Event Grid](https://azure.microsoft.com/services/event-grid/) when the payload is uploaded to Azure Blob Storage. In this sample, [Event Hubs](https://learn.microsoft.com/azure/event-hubs) is used as the messaging system. 6 | 7 | Using Event Hubs enable integration with a existing Event Hub-based solutions and to take advantage of its unique capabilities, like schema registry support and Apache Kafka compatibility. And Event Hub can be configured to [automatically archive](https://learn.microsoft.com/azure/event-hubs/event-hubs-capture-overview) received messages to make then available as an Avro files for integration with tools like Apache Spark, Stream Analytics, and Azure Data Factory. 8 | 9 | > This example uses [`DefaultAzureCredential`](https://learn.microsoft.com/dotnet/azure/sdk/authentication/#defaultazurecredential) for authentication while accessing Azure resources. the user principal must be provided as a parameter to the included Bicep script. The Bicep script is responsible for assigning the necessary RBAC (Role-Based Access Control) permissions for accessing the various Azure resources. While the principal can be the account associated with the interactive user, there are alternative [configurations](https://learn.microsoft.com/dotnet/azure/sdk/authentication/?tabs=command-line#exploring-the-sequence-of-defaultazurecredential-authentication-methods) available. 10 | 11 | ![A diagram showing Event Grid connected to Azure Blob Storage. As blobs are created, Event Grid forwards a message, containing the reference to the blob, to an Event Hub. A consumer CLI application receives the message from the queue, extracts the reference, and dowloads the blob from the storage account.](images/sample-2-diagram.png) 12 | 13 | 1. The payload uploaded to Azure Blob Storage. 14 | 1. A Blob Created event is generated by Event Grid. 15 | 1. Event Grid forwards the event, containing a reference to the payload blob, to an Azure Event Hub. 16 | 1. An net console program connects to the Event Hub and reads messages as they arrive. 17 | 1. The net console program extracts the reference to the payload blob from the message and downloads the blob directly from storage. 18 | 19 | ## :rocket: Deployment guide 20 | 21 | Install the prerequisites and follow the steps to deploy and run the examples. 22 | 23 | ### Prerequisites 24 | 25 | - Permission to create a new resource group and resources in an [Azure subscription](https://azure.com/free) 26 | - Unix-like shell. Also available in: 27 | - [Azure Cloud Shell](https://shell.azure.com/) 28 | - [Windows Subsystem for Linux (WSL)](https://learn.microsoft.com/windows/wsl/install) 29 | - [Git](https://git-scm.com/downloads) 30 | - [.NET 9 SDK](https://dotnet.microsoft.com/download/dotnet/9.0) 31 | - [Azure CLI](https://learn.microsoft.com/cli/azure/install-azure-cli) 32 | - Optionally, an IDE, like [Visual Studio](https://visualstudio.microsoft.com/downloads/) or [Visual Studio Code](https://code.visualstudio.com/). 33 | 34 | ### Steps 35 | 36 | 1. Clone this repository to your workstation and navigate to the working directory. 37 | 38 | ```bash 39 | git clone https://github.com/mspnp/cloud-design-patterns 40 | cd cloud-design-patterns/claim-check/code-samples/sample-2 41 | ``` 42 | 43 | 2. Log into Azure and create an empty resource group. 44 | 45 | ```bash 46 | az login 47 | az account set -s 48 | 49 | NAME_PREFIX="" 50 | LOCATION=eastus2 51 | RESOURCE_GROUP_NAME="rg-${NAME_PREFIX}-${LOCATION}" 52 | 53 | az group create -n "${RESOURCE_GROUP_NAME}" -l ${LOCATION} 54 | ``` 55 | 56 | 3. Deploy the supporting Azure resources. 57 | 58 | ```bash 59 | CURRENT_USER_OBJECT_ID=$(az ad signed-in-user show -o tsv --query id) 60 | 61 | # This could take a few minutes 62 | az deployment group create -n deploy-claim-check -f bicep/main.bicep -g "${RESOURCE_GROUP_NAME}" -p namePrefix=$NAME_PREFIX principalId=$CURRENT_USER_OBJECT_ID 63 | ``` 64 | 65 | 4. Configure the sample consumer to use the created Azure resources. 66 | 67 | ```bash 68 | sed "s/{EVENT_HUBS_NAMESPACE}/evhns-${NAME_PREFIX}/g" ClientConsumer2/appsettings.json.template >ClientConsumer2/appsettings.json 69 | sed -i "s/{EVENT_PROCESSOR_STORAGE_ACCOUNT_NAME}/st${NAME_PREFIX}ehub/g" ClientConsumer2/appsettings.json 70 | ``` 71 | 72 | 5. Launch the consumer sample application to receive and process claim check messages from Event Hubs. 73 | 74 | The message consumer sample application for this scenario is implemented as a Command Line Interface (CLI) application. Run the sample application to connect to Event Hubs and process messages as they arrive. 75 | 76 | ```bash 77 | dotnet run --project ClientConsumer2 78 | ``` 79 | 80 | > Please note: For demo purposes, the sample application will write the payload content to the the screen. Keep that in mind before you try sending really large payloads. 81 | 82 | ### :checkered_flag: Try it out 83 | 84 | To generate a claim check message you just have to drop a file in the created Azure Storage account. You can use Azure **Storage browser** to do that. [Refer this to know how to upload blobs to a container using Storage Explorer](https://learn.microsoft.com/azure/storage/blobs/quickstart-storage-explorer#upload-blobs-to-the-container). 85 | 86 | ### :broom: Clean up 87 | 88 | Remove the resource group that you created when you are done with this sample. 89 | 90 | ```bash 91 | az group delete -n "${RESOURCE_GROUP_NAME}" -y 92 | ``` 93 | -------------------------------------------------------------------------------- /claim-check/code-samples/sample-2/sample-2.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.9.34723.18 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ClientConsumer2", "ClientConsumer2\ClientConsumer2.csproj", "{325326E4-0909-4B3E-97E6-283900C84DF6}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{FC97FAC0-E4D1-43FF-869F-FB079BA5CB68}" 9 | ProjectSection(SolutionItems) = preProject 10 | readme.md = readme.md 11 | EndProjectSection 12 | EndProject 13 | Global 14 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 15 | Debug|Any CPU = Debug|Any CPU 16 | Release|Any CPU = Release|Any CPU 17 | EndGlobalSection 18 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 19 | {325326E4-0909-4B3E-97E6-283900C84DF6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 20 | {325326E4-0909-4B3E-97E6-283900C84DF6}.Debug|Any CPU.Build.0 = Debug|Any CPU 21 | {325326E4-0909-4B3E-97E6-283900C84DF6}.Release|Any CPU.ActiveCfg = Release|Any CPU 22 | {325326E4-0909-4B3E-97E6-283900C84DF6}.Release|Any CPU.Build.0 = Release|Any CPU 23 | EndGlobalSection 24 | GlobalSection(SolutionProperties) = preSolution 25 | HideSolutionNode = FALSE 26 | EndGlobalSection 27 | GlobalSection(ExtensibilityGlobals) = postSolution 28 | SolutionGuid = {1B1FEA88-54B9-477B-927B-F3542A3AA1B6} 29 | EndGlobalSection 30 | EndGlobal 31 | -------------------------------------------------------------------------------- /claim-check/code-samples/sample-3/FunctionConsumer3/FunctionConsumer3.cs: -------------------------------------------------------------------------------- 1 | using Azure.Messaging.ServiceBus; 2 | using Microsoft.Azure.Functions.Worker; 3 | using Microsoft.Extensions.Logging; 4 | using System.Text; 5 | using System.Text.Json; 6 | 7 | namespace Pnp.Samples.ClaimCheckPattern 8 | { 9 | /// 10 | /// Sample function illustrating the processing of Service Bus messages containing claim-check style events forwarded by Event Grid 11 | /// 12 | public class FunctionConsumer3(ILoggerFactory loggerFactory, ISampleBlobDataMover sampleBlobDataMover) 13 | { 14 | readonly ILogger _logger = loggerFactory.CreateLogger(); 15 | readonly ISampleBlobDataMover _downloader = sampleBlobDataMover; 16 | 17 | /// 18 | /// Function that processes Service Bus events auto-generated using Event Grid when files are uploaded to a storage blob container. 19 | /// 20 | [Function(nameof(FunctionConsumer3))] 21 | public async Task RunAsync( 22 | [ServiceBusTrigger("%ServiceBusQueue%", Connection = "ServiceBusConnection", IsBatched = false, AutoCompleteMessages = true)] 23 | ServiceBusReceivedMessage receivedMessage 24 | ) 25 | { 26 | _logger.LogInformation("Service Bus message received: {MessageId}", receivedMessage.MessageId); 27 | 28 | var messageText = Encoding.UTF8.GetString(receivedMessage.Body); 29 | var jsonMessage = JsonDocument.Parse(messageText).RootElement; 30 | var payloadUri = new Uri(jsonMessage.GetProperty("data").GetProperty("url").GetString()!); 31 | _logger.LogInformation("Message received. Payload Url: {Uri}", payloadUri); 32 | 33 | // download the payload 34 | var payload = await _downloader.DownloadAsync(payloadUri); 35 | _logger.LogInformation("Payload content\n {Payload}", payload); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /claim-check/code-samples/sample-3/FunctionConsumer3/FunctionConsumer3.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net9.0 4 | v4 5 | Exe 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | PreserveNewest 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /claim-check/code-samples/sample-3/FunctionConsumer3/ISampleBlobDataMover.cs: -------------------------------------------------------------------------------- 1 |  2 | namespace Pnp.Samples.ClaimCheckPattern 3 | { 4 | public interface ISampleBlobDataMover 5 | { 6 | Task DownloadAsync(Uri blobUri, bool deleteAfter = true); 7 | Task UploadAsync(string blobUri, string containerName, string content); 8 | } 9 | } -------------------------------------------------------------------------------- /claim-check/code-samples/sample-3/FunctionConsumer3/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Azure.Functions.Worker; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Microsoft.Extensions.Hosting; 4 | using Pnp.Samples.ClaimCheckPattern; 5 | 6 | var host = new HostBuilder() 7 | .ConfigureFunctionsWebApplication() 8 | .ConfigureServices(services => 9 | { 10 | services.AddApplicationInsightsTelemetryWorkerService(); 11 | services.ConfigureFunctionsApplicationInsights(); 12 | services.AddScoped(); 13 | }) 14 | .Build(); 15 | 16 | host.Run(); 17 | -------------------------------------------------------------------------------- /claim-check/code-samples/sample-3/FunctionConsumer3/SampleBlobDataMover.cs: -------------------------------------------------------------------------------- 1 | using Azure.Identity; 2 | using Azure.Storage.Blobs; 3 | using Azure.Storage.Blobs.Specialized; 4 | using Microsoft.Extensions.Logging; 5 | using System.Text; 6 | 7 | namespace Pnp.Samples.ClaimCheckPattern 8 | { 9 | /// 10 | /// A sample Storage Blob uploader/downloader. Uses Azure Entra ID for authentication. 11 | /// 12 | public class SampleBlobDataMover(ILoggerFactory loggerFactory) : ISampleBlobDataMover 13 | { 14 | readonly ILogger _logger = loggerFactory.CreateLogger(); 15 | 16 | /// 17 | /// Downloads the referenced payload from Azure Blobs and returns the content. 18 | /// Optionally deletes the blob after download 19 | /// 20 | public async Task DownloadAsync(Uri blobUri, bool deleteAfter = true) 21 | { 22 | var blobClient = new BlockBlobClient(blobUri, new DefaultAzureCredential()); 23 | if (!await blobClient.ExistsAsync()) 24 | { 25 | _logger.LogError("Blob {BlobUri} does not exist.", blobUri.AbsoluteUri); 26 | return string.Empty; 27 | } 28 | var response = await blobClient.DownloadContentAsync(); 29 | if (deleteAfter) 30 | { 31 | await blobClient.DeleteIfExistsAsync(); 32 | } 33 | return Encoding.UTF8.GetString(response.Value.Content); 34 | } 35 | 36 | /// 37 | /// Upload sample text content to Azure Blob Storage and returns Url to the newly created blob 38 | /// 39 | public async Task UploadAsync(string blobUri, string containerName, string content) 40 | { 41 | var serviceClient = new BlobServiceClient(new Uri(blobUri), new DefaultAzureCredential()); 42 | 43 | var containerClient = serviceClient.GetBlobContainerClient(containerName); 44 | await containerClient.CreateIfNotExistsAsync(); 45 | 46 | var blobClient = containerClient.GetBlobClient(Guid.NewGuid().ToString()); 47 | await blobClient.UploadAsync(new BinaryData(content), overwrite: true); 48 | _logger.LogInformation("Uploaded content to {Uri}", blobClient.Uri.AbsoluteUri); 49 | return blobClient.Uri.AbsoluteUri; 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /claim-check/code-samples/sample-3/FunctionConsumer3/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "logging": { 4 | "applicationInsights": { 5 | "samplingSettings": { 6 | "isEnabled": true, 7 | "excludedTypes": "Request" 8 | } 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /claim-check/code-samples/sample-3/FunctionConsumer3/local.settings.json.template: -------------------------------------------------------------------------------- 1 | { 2 | "IsEncrypted": false, 3 | "Values": { 4 | "AzureWebJobsStorage": "UseDevelopmentStorage=true", 5 | "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated", 6 | 7 | "AzureFunctionsJobHost__logging__applicationInsights__samplingSettings__isEnabled": "false", 8 | 9 | "ServiceBusConnection:fullyQualifiedNamespace": "{SERVICE_BUS_NAMESPACE}.servicebus.windows.net", 10 | "ServiceBusQueue": "esbq-claimcheck" 11 | } 12 | } -------------------------------------------------------------------------------- /claim-check/code-samples/sample-3/images/sample-3-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mspnp/cloud-design-patterns/ec4c70c30df8012150ac4fe02486a7508bf7ed3a/claim-check/code-samples/sample-3/images/sample-3-diagram.png -------------------------------------------------------------------------------- /claim-check/code-samples/sample-3/sample-3.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.9.34723.18 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FunctionConsumer3", "FunctionConsumer3\FunctionConsumer3.csproj", "{67AC55F4-7647-466F-8A3E-83E2878347AF}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{D8D14A53-FEC2-4CA6-BB0D-E916D6AAAE69}" 9 | ProjectSection(SolutionItems) = preProject 10 | readme.md = readme.md 11 | EndProjectSection 12 | EndProject 13 | Global 14 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 15 | Debug|Any CPU = Debug|Any CPU 16 | Release|Any CPU = Release|Any CPU 17 | EndGlobalSection 18 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 19 | {67AC55F4-7647-466F-8A3E-83E2878347AF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 20 | {67AC55F4-7647-466F-8A3E-83E2878347AF}.Debug|Any CPU.Build.0 = Debug|Any CPU 21 | {67AC55F4-7647-466F-8A3E-83E2878347AF}.Release|Any CPU.ActiveCfg = Release|Any CPU 22 | {67AC55F4-7647-466F-8A3E-83E2878347AF}.Release|Any CPU.Build.0 = Release|Any CPU 23 | EndGlobalSection 24 | GlobalSection(SolutionProperties) = preSolution 25 | HideSolutionNode = FALSE 26 | EndGlobalSection 27 | GlobalSection(ExtensibilityGlobals) = postSolution 28 | SolutionGuid = {1B1FEA88-54B9-477B-927B-F3542A3AA1B6} 29 | EndGlobalSection 30 | EndGlobal 31 | -------------------------------------------------------------------------------- /claim-check/code-samples/sample-4/ClientProducer4/ClientProducer4.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Exe 4 | net9.0 5 | enable 6 | enable 7 | Pnp.Samples.ClaimCheckPattern 8 | latest 9 | 10 | 11 | 12 | 13 | PreserveNewest 14 | 15 | 16 | 17 | 18 | Never 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /claim-check/code-samples/sample-4/ClientProducer4/ISampleBlobDataMover.cs: -------------------------------------------------------------------------------- 1 |  2 | namespace Pnp.Samples.ClaimCheckPattern 3 | { 4 | public interface ISampleBlobDataMover 5 | { 6 | Task DownloadAsync(Uri blobUri, bool deleteAfter = true); 7 | Task UploadAsync(string blobUri, string containerName, string content); 8 | } 9 | } -------------------------------------------------------------------------------- /claim-check/code-samples/sample-4/ClientProducer4/ISampleKafkaProducer.cs: -------------------------------------------------------------------------------- 1 |  2 | namespace Pnp.Samples.ClaimCheckPattern 3 | { 4 | public interface ISampleKafkaProducer 5 | { 6 | Task SendMessageAsync(string message); 7 | } 8 | } -------------------------------------------------------------------------------- /claim-check/code-samples/sample-4/ClientProducer4/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | using Microsoft.Extensions.Logging; 3 | using Pnp.Samples.ClaimCheckPattern; 4 | using System.Text.Json; 5 | 6 | Console.WriteLine("Begin sample 4 for Claim Check pattern"); 7 | Console.WriteLine("Uploads a sample file to Storage Blob and sends a claim-check message via Event Hubs using the Kafka Api"); 8 | Console.WriteLine("Initializing..."); 9 | Console.WriteLine(); 10 | 11 | var configuration = new ConfigurationBuilder() 12 | .AddJsonFile("appsettings.json", false, false).Build(); 13 | using ILoggerFactory loggerFactory = LoggerFactory.Create(builder => builder.AddConsole()); 14 | 15 | configuration.ThrowIfMissingSettings([ 16 | "AppSettings:eventHubEndpoint", 17 | "AppSettings:eventHubFqdn", 18 | "AppSettings:eventHubName", 19 | "AppSettings:storageBlobUri", 20 | "AppSettings:payloadContainerName" 21 | ]); 22 | 23 | Console.WriteLine("Uploading sample file to Azure Blob Storage..."); 24 | var blobMover = new SampleBlobDataMover(loggerFactory); 25 | var newBlobUri = await blobMover.UploadAsync( 26 | configuration.GetSection("AppSettings:StorageBlobUri").Value!, 27 | configuration.GetSection("AppSettings:PayloadContainerName").Value!, 28 | "Hello, World! This is a sample text file to illustrate using the claim check cloud pattern"); 29 | Console.WriteLine(); 30 | 31 | Console.WriteLine("Sending Claim Check message..."); 32 | var kafkaWorker = new SampleKafkaProducer(configuration); 33 | 34 | await kafkaWorker.SendMessageAsync(JsonSerializer.Serialize(new { payloadUri = newBlobUri })); 35 | Console.WriteLine(); 36 | 37 | Console.WriteLine("Done."); 38 | Console.WriteLine("Press any key to exit the sample application."); 39 | Console.ReadKey(); 40 | -------------------------------------------------------------------------------- /claim-check/code-samples/sample-4/ClientProducer4/SampleBlobDataMover.cs: -------------------------------------------------------------------------------- 1 | using Azure.Identity; 2 | using Azure.Storage.Blobs; 3 | using Azure.Storage.Blobs.Specialized; 4 | using Microsoft.Extensions.Logging; 5 | using System.Text; 6 | 7 | namespace Pnp.Samples.ClaimCheckPattern 8 | { 9 | /// 10 | /// A sample Storage Blob uploader/downloader. Uses Azure Entra ID for authentication. 11 | /// 12 | public class SampleBlobDataMover(ILoggerFactory loggerFactory) : ISampleBlobDataMover 13 | { 14 | readonly ILogger _logger = loggerFactory.CreateLogger(); 15 | 16 | /// 17 | /// Downloads the referenced payload from Azure Blobs and returns the content. 18 | /// Optionally deletes the blob after download 19 | /// 20 | public async Task DownloadAsync(Uri blobUri, bool deleteAfter = true) 21 | { 22 | var blobClient = new BlockBlobClient(blobUri, new DefaultAzureCredential()); 23 | if (!await blobClient.ExistsAsync()) 24 | { 25 | _logger.LogError("Blob {BlobUri} does not exist.", blobUri.AbsoluteUri); 26 | return string.Empty; 27 | } 28 | var response = await blobClient.DownloadContentAsync(); 29 | if (deleteAfter) 30 | { 31 | await blobClient.DeleteIfExistsAsync(); 32 | } 33 | return Encoding.UTF8.GetString(response.Value.Content); 34 | } 35 | 36 | /// 37 | /// Upload sample text content to Azure Blob Storage and returns Url to the newly created blob 38 | /// 39 | public async Task UploadAsync(string blobUri, string containerName, string content) 40 | { 41 | var serviceClient = new BlobServiceClient(new Uri(blobUri), new DefaultAzureCredential()); 42 | 43 | var containerClient = serviceClient.GetBlobContainerClient(containerName); 44 | await containerClient.CreateIfNotExistsAsync(); 45 | 46 | var blobClient = containerClient.GetBlobClient(Guid.NewGuid().ToString()); 47 | await blobClient.UploadAsync(new BinaryData(content), overwrite: true); 48 | _logger.LogInformation("Uploaded content to {Uri}", blobClient.Uri.AbsoluteUri); 49 | return blobClient.Uri.AbsoluteUri; 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /claim-check/code-samples/sample-4/ClientProducer4/SampleKafkaProducer.cs: -------------------------------------------------------------------------------- 1 | using Azure.Identity; 2 | using Confluent.Kafka; 3 | using Microsoft.Extensions.Configuration; 4 | 5 | namespace Pnp.Samples.ClaimCheckPattern 6 | { 7 | /// 8 | /// Implements the Kafka producer to send messages to a Kafka-enabled Event Hub namespace 9 | /// Uses Azure Entra ID to authenticate. 10 | /// 11 | public class SampleKafkaProducer(IConfiguration configuration) : ISampleKafkaProducer 12 | { 13 | readonly string _eventHubEndpoint = configuration.GetSection("AppSettings:eventHubEndpoint").Value!; 14 | readonly string _kafkaFqdn = configuration.GetSection("AppSettings:eventHubFqdn").Value!; 15 | readonly string _topic = configuration.GetSection("AppSettings:eventHubName").Value!; 16 | 17 | public async Task SendMessageAsync(string message) 18 | { 19 | try 20 | { 21 | var config = new ProducerConfig 22 | { 23 | BootstrapServers = _kafkaFqdn, 24 | SecurityProtocol = SecurityProtocol.SaslSsl, 25 | SaslMechanism = SaslMechanism.OAuthBearer, 26 | //Debug = "security,broker,protocol", //Uncomment for librdkafka debugging information 27 | }; 28 | 29 | using var producer = new ProducerBuilder(config) 30 | .SetOAuthBearerTokenRefreshHandler(async (client, cfg) => 31 | { 32 | var credential = new DefaultAzureCredential(); 33 | var accessToken = await credential.GetTokenAsync(new Azure.Core.TokenRequestContext([_eventHubEndpoint])); 34 | client.OAuthBearerSetToken(accessToken.Token, accessToken.ExpiresOn.ToUnixTimeMilliseconds(), credential.GetUserPrincipal()); 35 | }) 36 | .Build(); 37 | try 38 | { 39 | var deliveryResult = await producer.ProduceAsync(_topic, new Message { Value = message }); 40 | Console.WriteLine($"Delivered '{deliveryResult.Value}' to '{deliveryResult.TopicPartitionOffset}'"); 41 | } 42 | catch (ProduceException e) 43 | { 44 | Console.WriteLine($"Delivery failed: {e.Error.Reason}"); 45 | } 46 | } 47 | catch (Exception e) 48 | { 49 | Console.WriteLine("!!! Exception Occurred - {0}", e.Message); 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /claim-check/code-samples/sample-4/ClientProducer4/SampleSettingsValidator.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.Extensions.Configuration; 2 | 3 | public static class SampleSettingsValidator 4 | { 5 | /// 6 | /// Ensure the required app settings are present 7 | /// 8 | /// 9 | /// 10 | public static void ThrowIfMissingSettings(this IConfiguration configuration, List settings) 11 | { 12 | if (settings.Any(option => string.IsNullOrWhiteSpace(configuration.GetSection(option).Value))) 13 | { 14 | throw new ApplicationException($"Configure options {string.Join(", ", settings)} in appsettings.json and run again."); 15 | } 16 | } 17 | 18 | } -------------------------------------------------------------------------------- /claim-check/code-samples/sample-4/ClientProducer4/SampleTokenCredentialHelper.cs: -------------------------------------------------------------------------------- 1 | using Azure.Core; 2 | using System.IdentityModel.Tokens.Jwt; 3 | 4 | namespace Azure.Identity; 5 | 6 | internal static class SampleTokenCredentialHelper 7 | { 8 | 9 | /// 10 | /// Extract the user principal from the token credential. 11 | /// 12 | public static string GetUserPrincipal(this DefaultAzureCredential azureCredential) 13 | { 14 | const string DefaultEntraIdCredentialContext = "https://management.azure.com/.default"; 15 | 16 | var token = azureCredential.GetToken(new TokenRequestContext([DefaultEntraIdCredentialContext]), CancellationToken.None); 17 | var handler = new JwtSecurityTokenHandler(); 18 | var jwtSecurityToken = handler.ReadJwtToken(token.Token); 19 | return jwtSecurityToken.Subject; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /claim-check/code-samples/sample-4/ClientProducer4/appsettings.json.template: -------------------------------------------------------------------------------- 1 | { 2 | "AppSettings": { 3 | "EventHubEndpoint": "https://{EVENT_HUBS_NAMESPACE}.servicebus.windows.net", 4 | "EventHubFqdn": "{EVENT_HUBS_NAMESPACE}.servicebus.windows.net:9093", 5 | "EventHubName": "evh-claimcheck", 6 | 7 | "StorageBlobUri": "https://{STORAGE_ACCOUNT_NAME}.blob.core.windows.net", 8 | "PayloadContainerName": "payloads" 9 | } 10 | } 11 | 12 | -------------------------------------------------------------------------------- /claim-check/code-samples/sample-4/FunctionConsumer4/FunctionConsumer4.cs: -------------------------------------------------------------------------------- 1 | using Azure.Messaging.EventHubs; 2 | using Microsoft.Azure.Functions.Worker; 3 | using Microsoft.Extensions.Logging; 4 | using System.Text; 5 | using System.Text.Json; 6 | 7 | namespace Pnp.Samples.ClaimCheckPattern 8 | { 9 | /// 10 | /// Sample function illustrating the processing of Event Hub events containing claim-check style events. 11 | /// 12 | public class FunctionConsumer4(ILoggerFactory loggerFactory, ISampleBlobDataMover sampleBlobDataMover) 13 | { 14 | readonly ILogger _logger = loggerFactory.CreateLogger(); 15 | readonly ISampleBlobDataMover _downloader = sampleBlobDataMover; 16 | 17 | [Function(nameof(FunctionConsumer4))] 18 | public async Task RunAsync( 19 | [EventHubTrigger("%EventHubName%", Connection = "EventHubConnectionString", IsBatched = false)] EventData receivedMessage 20 | ) 21 | { 22 | _logger.LogInformation("EventHub message received: {SequenceNumber}", receivedMessage.SequenceNumber); 23 | 24 | // retrieve the paylod Uri from the message 25 | var jsonMessage = JsonDocument.Parse(Encoding.UTF8.GetString(receivedMessage.EventBody)).RootElement; 26 | var payloadUri = new Uri(jsonMessage.GetProperty("payloadUri").GetString()!); 27 | _logger.LogInformation("Message received. Payload Url: {Uri}", payloadUri); 28 | 29 | // download the payload 30 | var payload = await _downloader.DownloadAsync(payloadUri); 31 | _logger.LogInformation("Payload content (Url {Uri)}\n {Payload}", payloadUri, payload); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /claim-check/code-samples/sample-4/FunctionConsumer4/FunctionConsumer4.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net9.0 4 | v4 5 | Exe 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | PreserveNewest 26 | 27 | 28 | PreserveNewest 29 | 30 | 31 | Never 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /claim-check/code-samples/sample-4/FunctionConsumer4/ISampleBlobDataMover.cs: -------------------------------------------------------------------------------- 1 |  2 | namespace Pnp.Samples.ClaimCheckPattern 3 | { 4 | public interface ISampleBlobDataMover 5 | { 6 | Task DownloadAsync(Uri blobUri, bool deleteAfter = true); 7 | Task UploadAsync(string blobUri, string containerName, string content); 8 | } 9 | } -------------------------------------------------------------------------------- /claim-check/code-samples/sample-4/FunctionConsumer4/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Azure.Functions.Worker; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Microsoft.Extensions.Hosting; 4 | using Pnp.Samples.ClaimCheckPattern; 5 | 6 | var host = new HostBuilder() 7 | .ConfigureFunctionsWebApplication() 8 | .ConfigureServices(services => 9 | { 10 | services.AddApplicationInsightsTelemetryWorkerService(); 11 | services.ConfigureFunctionsApplicationInsights(); 12 | services.AddScoped(); 13 | }) 14 | .Build(); 15 | 16 | host.Run(); 17 | -------------------------------------------------------------------------------- /claim-check/code-samples/sample-4/FunctionConsumer4/SampleBlobDataMover.cs: -------------------------------------------------------------------------------- 1 | using Azure.Identity; 2 | using Azure.Storage.Blobs; 3 | using Azure.Storage.Blobs.Specialized; 4 | using Microsoft.Extensions.Logging; 5 | using System.Text; 6 | 7 | namespace Pnp.Samples.ClaimCheckPattern 8 | { 9 | /// 10 | /// A sample Storage Blob uploader/downloader. Uses Azure Entra ID for authentication. 11 | /// 12 | public class SampleBlobDataMover(ILoggerFactory loggerFactory) : ISampleBlobDataMover 13 | { 14 | readonly ILogger _logger = loggerFactory.CreateLogger(); 15 | 16 | /// 17 | /// Downloads the referenced payload from Azure Blobs and returns the content. 18 | /// Optionally deletes the blob after download 19 | /// 20 | public async Task DownloadAsync(Uri blobUri, bool deleteAfter = true) 21 | { 22 | var blobClient = new BlockBlobClient(blobUri, new DefaultAzureCredential()); 23 | if (!await blobClient.ExistsAsync()) 24 | { 25 | _logger.LogError("Blob {BlobUri} does not exist.", blobUri.AbsoluteUri); 26 | return string.Empty; 27 | } 28 | var response = await blobClient.DownloadContentAsync(); 29 | if (deleteAfter) 30 | { 31 | await blobClient.DeleteIfExistsAsync(); 32 | } 33 | return Encoding.UTF8.GetString(response.Value.Content); 34 | } 35 | 36 | /// 37 | /// Upload sample text content to Azure Blob Storage and returns Url to the newly created blob 38 | /// 39 | public async Task UploadAsync(string blobUri, string containerName, string content) 40 | { 41 | var serviceClient = new BlobServiceClient(new Uri(blobUri), new DefaultAzureCredential()); 42 | 43 | var containerClient = serviceClient.GetBlobContainerClient(containerName); 44 | await containerClient.CreateIfNotExistsAsync(); 45 | 46 | var blobClient = containerClient.GetBlobClient(Guid.NewGuid().ToString()); 47 | await blobClient.UploadAsync(new BinaryData(content), overwrite: true); 48 | _logger.LogInformation("Uploaded content to {Uri}", blobClient.Uri.AbsoluteUri); 49 | return blobClient.Uri.AbsoluteUri; 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /claim-check/code-samples/sample-4/FunctionConsumer4/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "logging": { 4 | "applicationInsights": { 5 | "samplingSettings": { 6 | "excludedTypes": "Request", 7 | "isEnabled": true 8 | } 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /claim-check/code-samples/sample-4/FunctionConsumer4/local.settings.json.template: -------------------------------------------------------------------------------- 1 | { 2 | "IsEncrypted": false, 3 | "Values": { 4 | "AzureWebJobsStorage": "UseDevelopmentStorage=true", 5 | "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated", 6 | "UseDevelopmentStorage": true, 7 | "AzureFunctionsJobHost__logging__applicationInsights__samplingSettings__isEnabled": "false", 8 | 9 | "EventHubConnectionString:fullyQualifiedNamespace": "{EVENT_HUBS_NAMESPACE}.servicebus.windows.net", 10 | "EventHubName": "evh-claimcheck" 11 | } 12 | } 13 | 14 | 15 | -------------------------------------------------------------------------------- /claim-check/code-samples/sample-4/images/sample-4-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mspnp/cloud-design-patterns/ec4c70c30df8012150ac4fe02486a7508bf7ed3a/claim-check/code-samples/sample-4/images/sample-4-diagram.png -------------------------------------------------------------------------------- /claim-check/code-samples/sample-4/sample-4.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.9.34723.18 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FunctionConsumer4", "FunctionConsumer4\FunctionConsumer4.csproj", "{5A7E5D39-BF09-4A3B-A7A1-7084A5359BAA}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ClientProducer4", "ClientProducer4\ClientProducer4.csproj", "{CA4D4A68-55B4-496F-B976-D3464076FC62}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{D704D686-1E69-4DF7-B224-B4E5BBF9773B}" 11 | ProjectSection(SolutionItems) = preProject 12 | readme.md = readme.md 13 | EndProjectSection 14 | EndProject 15 | Global 16 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 17 | Debug|Any CPU = Debug|Any CPU 18 | Release|Any CPU = Release|Any CPU 19 | EndGlobalSection 20 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 21 | {5A7E5D39-BF09-4A3B-A7A1-7084A5359BAA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 22 | {5A7E5D39-BF09-4A3B-A7A1-7084A5359BAA}.Debug|Any CPU.Build.0 = Debug|Any CPU 23 | {5A7E5D39-BF09-4A3B-A7A1-7084A5359BAA}.Release|Any CPU.ActiveCfg = Release|Any CPU 24 | {5A7E5D39-BF09-4A3B-A7A1-7084A5359BAA}.Release|Any CPU.Build.0 = Release|Any CPU 25 | {CA4D4A68-55B4-496F-B976-D3464076FC62}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 26 | {CA4D4A68-55B4-496F-B976-D3464076FC62}.Debug|Any CPU.Build.0 = Debug|Any CPU 27 | {CA4D4A68-55B4-496F-B976-D3464076FC62}.Release|Any CPU.ActiveCfg = Release|Any CPU 28 | {CA4D4A68-55B4-496F-B976-D3464076FC62}.Release|Any CPU.Build.0 = Release|Any CPU 29 | EndGlobalSection 30 | GlobalSection(SolutionProperties) = preSolution 31 | HideSolutionNode = FALSE 32 | EndGlobalSection 33 | GlobalSection(ExtensibilityGlobals) = postSolution 34 | SolutionGuid = {1B1FEA88-54B9-477B-927B-F3542A3AA1B6} 35 | EndGlobalSection 36 | EndGlobal 37 | -------------------------------------------------------------------------------- /leader-election/DistributedMutex/BlobLeaseManager.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | namespace DistributedMutex 4 | { 5 | using System; 6 | using System.Diagnostics; 7 | using System.Net; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | using Azure; 11 | using Azure.Identity; 12 | using Azure.Storage.Blobs; 13 | using Azure.Storage.Blobs.Specialized; 14 | 15 | public struct BlobSettings 16 | { 17 | public readonly string Container; 18 | public readonly string BlobName; 19 | public BlobServiceClient BlobServiceClient; 20 | 21 | public BlobSettings(string blobUri, string container, string blobName) 22 | { 23 | var blobClientOptions = new BlobClientOptions(); 24 | blobClientOptions.Retry.Delay = TimeSpan.FromSeconds(5); 25 | blobClientOptions.Retry.MaxRetries = 3; 26 | 27 | BlobServiceClient = new BlobServiceClient(new Uri(blobUri), new DefaultAzureCredential(), blobClientOptions); 28 | Container = container; 29 | BlobName = blobName; 30 | } 31 | } 32 | 33 | /// 34 | /// Wrapper around a Windows Azure Blob Lease 35 | /// 36 | internal class BlobLeaseManager 37 | { 38 | private readonly BlobContainerClient leaseContainerClient; 39 | private readonly PageBlobClient leaseBlobClient; 40 | 41 | public BlobLeaseManager(BlobSettings settings) 42 | : this(settings.BlobServiceClient, settings.Container, settings.BlobName) 43 | { 44 | } 45 | 46 | public BlobLeaseManager(BlobServiceClient blobServiceClient, string leaseContainerName, string leaseBlobName) 47 | { 48 | leaseContainerClient = blobServiceClient.GetBlobContainerClient(leaseContainerName); 49 | leaseBlobClient = leaseContainerClient.GetPageBlobClient(leaseBlobName); 50 | leaseContainerClient.CreateIfNotExists(); 51 | leaseBlobClient.CreateIfNotExists(512); 52 | } 53 | 54 | public void ReleaseLease(string leaseId) 55 | { 56 | try 57 | { 58 | var leaseClient = leaseBlobClient.GetBlobLeaseClient(leaseId); 59 | leaseClient.Release(); 60 | } 61 | catch (RequestFailedException e) 62 | { 63 | // Lease will eventually be released. 64 | Trace.TraceError(e.ErrorCode); 65 | } 66 | } 67 | 68 | public async Task AcquireLeaseAsync(CancellationToken token) 69 | { 70 | bool blobNotFound = false; 71 | try 72 | { 73 | var leaseClient = leaseBlobClient.GetBlobLeaseClient(); 74 | var lease = await leaseClient.AcquireAsync(TimeSpan.FromSeconds(15), null, token); 75 | return lease.Value.LeaseId; 76 | } 77 | catch (RequestFailedException storageException) 78 | { 79 | Trace.TraceError(storageException.ErrorCode); 80 | 81 | var status = storageException.Status; 82 | if (status == (int) HttpStatusCode.NotFound) 83 | { 84 | blobNotFound = true; 85 | } 86 | 87 | if (status == (int)HttpStatusCode.Conflict) 88 | { 89 | return null; 90 | } 91 | } 92 | catch (Exception e) 93 | { 94 | // If the storage account is unavailable or we fail for any other reason we still want to keep retrying 95 | Trace.TraceError(e.Message); 96 | return null; 97 | } 98 | 99 | if (blobNotFound) 100 | { 101 | await CreateBlobAsync(token); 102 | return await AcquireLeaseAsync(token); 103 | } 104 | 105 | return null; 106 | } 107 | 108 | public async Task RenewLeaseAsync(string leaseId, CancellationToken token) 109 | { 110 | try 111 | { 112 | var leaseClient = leaseBlobClient.GetBlobLeaseClient(leaseId); 113 | await leaseClient.RenewAsync(cancellationToken: token); 114 | return true; 115 | } 116 | catch (RequestFailedException storageException) 117 | { 118 | Trace.TraceError(storageException.ErrorCode); 119 | 120 | return false; 121 | } 122 | } 123 | 124 | private async Task CreateBlobAsync(CancellationToken token) 125 | { 126 | await leaseContainerClient.CreateIfNotExistsAsync(cancellationToken: token); 127 | await leaseBlobClient.CreateIfNotExistsAsync(0, cancellationToken: token); 128 | } 129 | } 130 | } -------------------------------------------------------------------------------- /leader-election/DistributedMutex/DistributedMutex.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /leader-election/LeaderElection.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31903.59 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DistributedMutex", "DistributedMutex\DistributedMutex.csproj", "{75FCC9BC-48D0-4D86-871E-087386F8B602}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LeaderElectionConsoleWorker", "LeaderElectionConsoleWorker\LeaderElectionConsoleWorker.csproj", "{164AD311-21A1-401D-AF26-E2DE1CD68675}" 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(SolutionProperties) = preSolution 16 | HideSolutionNode = FALSE 17 | EndGlobalSection 18 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 19 | {75FCC9BC-48D0-4D86-871E-087386F8B602}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 20 | {75FCC9BC-48D0-4D86-871E-087386F8B602}.Debug|Any CPU.Build.0 = Debug|Any CPU 21 | {75FCC9BC-48D0-4D86-871E-087386F8B602}.Release|Any CPU.ActiveCfg = Release|Any CPU 22 | {75FCC9BC-48D0-4D86-871E-087386F8B602}.Release|Any CPU.Build.0 = Release|Any CPU 23 | {164AD311-21A1-401D-AF26-E2DE1CD68675}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 24 | {164AD311-21A1-401D-AF26-E2DE1CD68675}.Debug|Any CPU.Build.0 = Debug|Any CPU 25 | {164AD311-21A1-401D-AF26-E2DE1CD68675}.Release|Any CPU.ActiveCfg = Release|Any CPU 26 | {164AD311-21A1-401D-AF26-E2DE1CD68675}.Release|Any CPU.Build.0 = Release|Any CPU 27 | EndGlobalSection 28 | EndGlobal 29 | -------------------------------------------------------------------------------- /leader-election/LeaderElectionConsoleWorker/LeaderElectionConsoleWorker.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | DistributedMutex 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /leader-election/LeaderElectionConsoleWorker/Program.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | namespace LeaderElectionConsoleWorker 5 | { 6 | using System; 7 | using System.Configuration; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | using DistributedMutex; 11 | 12 | class Program 13 | { 14 | static async Task Main(string[] args) 15 | { 16 | // Create a new shared cancellation token source 17 | CancellationTokenSource source = new CancellationTokenSource(); 18 | CancellationToken token = source.Token; 19 | 20 | // Get the connection string from app settings 21 | var storageUri = ConfigurationManager.AppSettings["StorageUri"]; 22 | if (string.IsNullOrEmpty(storageUri)) 23 | { 24 | Console.Error.WriteLine("A connection string must be set in the app.config file."); 25 | return; 26 | } 27 | // Create a BlobSettings object with the connection string and the name of the blob to use for the lease 28 | BlobSettings blobSettings = new BlobSettings( 29 | storageUri, 30 | "leases", 31 | "leader"); 32 | 33 | // Get the current process ID for output 34 | var pid = System.Diagnostics.Process.GetCurrentProcess().Id; 35 | 36 | // Start an async task that will wait for a keypress and cancel the token when a key is pressed 37 | var uiTask = Task.Run(async () => 38 | { 39 | while (!token.IsCancellationRequested) 40 | { 41 | if (Console.KeyAvailable) 42 | { 43 | Console.ReadKey(true); 44 | Console.WriteLine($"[{DateTime.Now.ToString("HH:mm:ss")}] Requesting shutdown."); 45 | source.Cancel(); 46 | } 47 | await Task.Delay(500); 48 | } 49 | Console.WriteLine($"[{DateTime.Now.ToString("HH:mm:ss")}] This process ({pid}) is shutting down."); 50 | 51 | }); 52 | 53 | // Create a new BlobDistributedMutex object with the BlobSettings object and a task 54 | // to run when the lease is acquired, and an action to run when the lease is not acquired. 55 | BlobDistributedMutex distributedMutex = new BlobDistributedMutex( 56 | blobSettings, 57 | async (CancellationToken token) => 58 | { 59 | while (!token.IsCancellationRequested) 60 | { 61 | Console.WriteLine($"[{DateTime.Now.ToString("HH:mm:ss")}] This process ({pid}) is currently the leader. Press any key to exit."); 62 | await Task.Delay(15000); 63 | } 64 | }, () => { 65 | Console.WriteLine($"[{DateTime.Now.ToString("HH:mm:ss")}] This process ({pid}) could not acquire lease. Retrying in 20 seconds. Press any key to exit."); 66 | }); 67 | 68 | // Wait for completion of the DistributedMutex and the UI task before exiting 69 | await distributedMutex.RunTaskWhenMutexAcquired(token); 70 | await uiTask; 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /leader-election/LeaderElectionConsoleWorker/app.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /pipes-and-filters/.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/dotnet 3 | { 4 | "name": "C# (.NET)", 5 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile 6 | "image": "mcr.microsoft.com/devcontainers/dotnet:8.0-bookworm", 7 | "features": { 8 | "ghcr.io/devcontainers/features/azure-cli:1": { 9 | "version": "latest" 10 | } 11 | }, 12 | "customizations": { 13 | "vscode": { 14 | "extensions": [ 15 | "ms-dotnettools.csdevkit", 16 | "ms-azuretools.vscode-azurefunctions", 17 | "ms-vscode.azurecli", 18 | "ms-azuretools.vscode-bicep", 19 | "DavidAnson.vscode-markdownlint" 20 | ] 21 | } 22 | } 23 | 24 | // Features to add to the dev container. More info: https://containers.dev/features. 25 | // "features": {}, 26 | 27 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 28 | // "forwardPorts": [5000, 5001], 29 | // "portsAttributes": { 30 | // "5001": { 31 | // "protocol": "https" 32 | // } 33 | // } 34 | 35 | // Use 'postCreateCommand' to run commands after the container is created. 36 | // "postCreateCommand": "dotnet restore", 37 | 38 | // Configure tool-specific properties. 39 | // "customizations": {}, 40 | 41 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. 42 | // "remoteUser": "root" 43 | } 44 | -------------------------------------------------------------------------------- /pipes-and-filters/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "ms-dotnettools.csdevkit", 4 | "ms-azuretools.vscode-azurefunctions", 5 | "ms-vscode.azurecli", 6 | "ms-azuretools.vscode-bicep", 7 | "DavidAnson.vscode-markdownlint" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /pipes-and-filters/.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 | } 12 | -------------------------------------------------------------------------------- /pipes-and-filters/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "azureFunctions.deploySubpath": "ImageProcessingPipeline/bin/Release/net8.0/publish", 3 | "azureFunctions.projectLanguage": "C#", 4 | "azureFunctions.projectRuntime": "~4", 5 | "debug.internalConsoleOptions": "neverOpen", 6 | "azureFunctions.preDeployTask": "publish (functions)" 7 | } 8 | -------------------------------------------------------------------------------- /pipes-and-filters/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "clean (functions)", 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}/ImageProcessingPipeline" 16 | } 17 | }, 18 | { 19 | "label": "build (functions)", 20 | "command": "dotnet", 21 | "args": [ 22 | "build", 23 | "/property:GenerateFullPaths=true", 24 | "/consoleloggerparameters:NoSummary" 25 | ], 26 | "type": "process", 27 | "dependsOn": "clean (functions)", 28 | "group": { 29 | "kind": "build", 30 | "isDefault": true 31 | }, 32 | "problemMatcher": "$msCompile", 33 | "options": { 34 | "cwd": "${workspaceFolder}/ImageProcessingPipeline" 35 | } 36 | }, 37 | { 38 | "label": "clean release (functions)", 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}/ImageProcessingPipeline" 51 | } 52 | }, 53 | { 54 | "label": "publish (functions)", 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 (functions)", 65 | "problemMatcher": "$msCompile", 66 | "options": { 67 | "cwd": "${workspaceFolder}/ImageProcessingPipeline" 68 | } 69 | }, 70 | { 71 | "type": "func", 72 | "dependsOn": "build (functions)", 73 | "options": { 74 | "cwd": "${workspaceFolder}/ImageProcessingPipeline/bin/Debug/net8.0" 75 | }, 76 | "command": "host start", 77 | "isBackground": true, 78 | "problemMatcher": "$func-dotnet-watch" 79 | } 80 | ] 81 | } 82 | -------------------------------------------------------------------------------- /pipes-and-filters/ImageProcessingPipeline/.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 -------------------------------------------------------------------------------- /pipes-and-filters/ImageProcessingPipeline/ImageProcessingPipeline.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net9.0 4 | v4 5 | Exe 6 | enable 7 | enable 8 | true 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | PreserveNewest 22 | 23 | 24 | PreserveNewest 25 | Never 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | Always 35 | 36 | 37 | -------------------------------------------------------------------------------- /pipes-and-filters/ImageProcessingPipeline/ImageProcessingPipeline.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.5.002.0 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImageProcessingPipeline", "ImageProcessingPipeline.csproj", "{9E2EFA90-60A5-409A-960F-3C2A5A184264}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {9E2EFA90-60A5-409A-960F-3C2A5A184264}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {9E2EFA90-60A5-409A-960F-3C2A5A184264}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {9E2EFA90-60A5-409A-960F-3C2A5A184264}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {9E2EFA90-60A5-409A-960F-3C2A5A184264}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {B7A172BA-FA5D-45FD-B091-E7F55399D039} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /pipes-and-filters/ImageProcessingPipeline/Program.cs: -------------------------------------------------------------------------------- 1 | using Azure.Identity; 2 | using Microsoft.Extensions.Azure; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Microsoft.Extensions.FileProviders; 5 | using Microsoft.Extensions.Hosting; 6 | 7 | var host = new HostBuilder() 8 | .ConfigureFunctionsWorkerDefaults() 9 | .ConfigureServices((hostContext, services) => 10 | { 11 | services.AddAzureClients(c => 12 | { 13 | c.UseCredential(new DefaultAzureCredential()); 14 | c.AddBlobServiceClient(hostContext.Configuration.GetSection("output")).WithName("processed"); 15 | }); 16 | services.AddSingleton(new ManifestEmbeddedFileProvider(typeof(Program).Assembly)); 17 | }) 18 | .Build(); 19 | 20 | host.Run(); 21 | -------------------------------------------------------------------------------- /pipes-and-filters/ImageProcessingPipeline/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "ImageProcessingPipeline": { 4 | "commandName": "Project", 5 | "commandLineArgs": "--port 7253", 6 | "launchBrowser": false 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /pipes-and-filters/ImageProcessingPipeline/PublishFinal.cs: -------------------------------------------------------------------------------- 1 | using Azure.Storage.Blobs; 2 | using Azure.Storage.Blobs.Models; 3 | using Azure.Storage.Blobs.Specialized; 4 | using Microsoft.Azure.Functions.Worker; 5 | using Microsoft.Extensions.Azure; 6 | using Microsoft.Extensions.Logging; 7 | 8 | namespace ImageProcessingPipeline 9 | { 10 | public class PublishFinal(ILogger logger, IAzureClientFactory blobClientFactory) 11 | { 12 | private readonly ILogger _logger = logger; 13 | private readonly BlobContainerClient _destinationContainerClient = blobClientFactory.CreateClient("processed").GetBlobContainerClient("processed"); 14 | 15 | [Function(nameof(PublishFinal))] 16 | public async Task RunAsync( 17 | [QueueTrigger("pipe-yhrb", Connection = "pipe")] string imageFilePath, 18 | [BlobInput("{QueueTrigger}", Connection = "pipe")] BlockBlobClient imageBlob, 19 | CancellationToken cancellationToken) 20 | { 21 | _logger.LogDebug("Starting copying {source} into {destination}.", imageBlob.Uri, _destinationContainerClient.Uri); 22 | 23 | // Copy blob and delete orginal 24 | var newBlobClient = _destinationContainerClient.GetBlobClient(imageBlob.Name); 25 | await newBlobClient.UploadAsync(await imageBlob.OpenReadAsync(null, cancellationToken), overwrite: true, cancellationToken); 26 | await imageBlob.DeleteAsync(DeleteSnapshotsOption.None, null, cancellationToken); 27 | 28 | _logger.LogInformation("Copied {source} into {destination} and deleted original.", imageBlob.Uri, _destinationContainerClient.Uri); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /pipes-and-filters/ImageProcessingPipeline/Resize.cs: -------------------------------------------------------------------------------- 1 | using Azure.Storage.Blobs.Models; 2 | using Azure.Storage.Blobs.Specialized; 3 | using Microsoft.Azure.Functions.Worker; 4 | using Microsoft.Extensions.Logging; 5 | using SixLabors.ImageSharp; 6 | using SixLabors.ImageSharp.Processing; 7 | 8 | namespace ImageProcessingPipeline 9 | { 10 | 11 | public class Resize(ILogger logger) 12 | { 13 | private readonly ILogger _logger = logger; 14 | 15 | [Function(nameof(Resize))] 16 | [QueueOutput("pipe-fjur", Connection = "pipe")] 17 | public async Task RunAsync( 18 | [QueueTrigger("pipe-xfty", Connection = "pipe")] string imageFilePath, 19 | [BlobInput("{QueueTrigger}", Connection = "pipe")] BlockBlobClient imageBlob, 20 | CancellationToken cancellationToken) 21 | { 22 | _logger.LogInformation("Processing image {uri} for resizing.", imageBlob.Uri); 23 | 24 | // Download image and resize it 25 | using BlobDownloadStreamingResult imageBlobContents = await imageBlob.DownloadStreamingAsync(null, cancellationToken); 26 | var image = await Image.LoadAsync(imageBlobContents.Content, cancellationToken); 27 | image.Mutate(i => 28 | { 29 | i.Resize(new ResizeOptions 30 | { 31 | Mode = ResizeMode.Max, 32 | Size = new Size(600, 600) 33 | }); 34 | }); 35 | 36 | // Write modified image back to storage 37 | _logger.LogDebug("Writing resized image back to storage: {uri}.", imageBlob.Uri); 38 | using (var blobStream = await imageBlob.OpenWriteAsync(overwrite: true, null, cancellationToken)) 39 | { 40 | await image.SaveAsync(blobStream, image.Metadata.DecodedImageFormat!, cancellationToken); 41 | } 42 | 43 | _logger.LogInformation("Image resizing done. Adding image \"{filePath}\" into the next pipe.", imageFilePath); 44 | return imageFilePath; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /pipes-and-filters/ImageProcessingPipeline/Watermark.cs: -------------------------------------------------------------------------------- 1 | using Azure.Storage.Blobs.Models; 2 | using Azure.Storage.Blobs.Specialized; 3 | using Microsoft.Azure.Functions.Worker; 4 | using Microsoft.Extensions.FileProviders; 5 | using Microsoft.Extensions.Logging; 6 | using SixLabors.ImageSharp; 7 | using SixLabors.ImageSharp.Processing; 8 | 9 | namespace ImageProcessingPipeline 10 | { 11 | public class Watermark(ILogger logger, IFileProvider files) 12 | { 13 | private readonly ILogger _logger = logger; 14 | private readonly IFileProvider _files = files; 15 | 16 | [Function(nameof(Watermark))] 17 | [QueueOutput("pipe-yhrb", Connection = "pipe")] 18 | public async Task RunAsync( 19 | [QueueTrigger("pipe-fjur", Connection = "pipe")] string imageFilePath, 20 | [BlobInput("{QueueTrigger}", Connection = "pipe")] BlockBlobClient imageBlob, 21 | CancellationToken cancellationToken) 22 | { 23 | _logger.LogInformation("Processing image {uri} for watermarking.", imageBlob.Uri); 24 | 25 | // Download image and watermark it 26 | using BlobDownloadStreamingResult imageBlobContents = await imageBlob.DownloadStreamingAsync(null, cancellationToken); 27 | var image = await Image.LoadAsync(imageBlobContents.Content, cancellationToken); 28 | 29 | var resources = _files.GetDirectoryContents("/"); 30 | using var watermarkStream = resources.First(resource=>resource.Name.Equals("resources/watermark.png")).CreateReadStream(); 31 | var watermarkImage = await Image.LoadAsync(watermarkStream, cancellationToken); 32 | 33 | image.Mutate(i => 34 | { 35 | i.DrawImage(watermarkImage, new Point((image.Width - watermarkImage.Width) / 2, (image.Height - watermarkImage.Height) / 2), 0.5f); 36 | }); 37 | 38 | // Write modified image back to storage 39 | _logger.LogDebug("Writing watermarked image back to storage: {uri}.", imageBlob.Uri); 40 | using (var blobStream = await imageBlob.OpenWriteAsync(overwrite: true, null, cancellationToken)) 41 | { 42 | await image.SaveAsync(blobStream, image.Metadata.DecodedImageFormat!, cancellationToken); 43 | } 44 | 45 | _logger.LogInformation("Watermarking done. Adding image \"{filePath}\" into the next pipe.", imageFilePath); 46 | return imageFilePath; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /pipes-and-filters/ImageProcessingPipeline/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "logging": { 4 | "applicationInsights": { 5 | "enableDependencyTracking": true, 6 | "enableLiveMetrics": true, 7 | "enablePerformanceCountersCollection": true, 8 | "samplingSettings": { 9 | "isEnabled": true, 10 | "excludedTypes": "Request" 11 | }, 12 | "enableLiveMetricsFilters": true 13 | }, 14 | "logLevel": { 15 | "default": "Warning", 16 | "Host.Aggregator": "Information", 17 | "Host.Results": "Information", 18 | "Function": "Information", 19 | "Azure.Storage.Blobs": "Critical", 20 | "Azure.Core": "Critical", 21 | "Azure.Identity": "Critical" 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /pipes-and-filters/ImageProcessingPipeline/local.settings.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "IsEncrypted": false, 3 | "Values": { 4 | "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated", 5 | "AzureWebJobsStorage__accountName": "STORAGE_ACCOUNT_NAME", 6 | "pipe__accountName": "STORAGE_ACCOUNT_NAME", 7 | "output__serviceUri": "https://STORAGE_ACCOUNT_NAME.blob.core.windows.net" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /pipes-and-filters/ImageProcessingPipeline/resources/watermark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mspnp/cloud-design-patterns/ec4c70c30df8012150ac4fe02486a7508bf7ed3a/pipes-and-filters/ImageProcessingPipeline/resources/watermark.png -------------------------------------------------------------------------------- /pipes-and-filters/bicep/main.bicep: -------------------------------------------------------------------------------- 1 | targetScope = 'resourceGroup' 2 | 3 | @minLength(3) 4 | @description('The name of the storage account. Must be globally unique.') 5 | param storageAccountName string 6 | 7 | @minLength(36) 8 | @description('The Object ID (GUID) the user your Azure CLI session is logged in as.') 9 | param userObjectId string 10 | 11 | @minLength(5) 12 | @description('Location of the resources. Defaults to resource group location') 13 | param location string = resourceGroup().location 14 | 15 | /*** EXISTING RESOURCES ***/ 16 | 17 | @description('Built-in Azure RBAC role that is applied to a Storage account to grant "Storage Blob Data Contributor" privileges. Granted to the user provided in the paramters.') 18 | resource storageBlobDataContributorRole 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = { 19 | name: 'ba92f5b4-2d11-453d-a403-e96b0029c9fe' 20 | scope: subscription() 21 | } 22 | 23 | @description('Built-in Azure RBAC role that is applied to a Storage account to grant "Storage Queue Data Contributor" privileges. Granted to the user provided in the paramters.') 24 | resource storageQueueDataContributorRole 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = { 25 | name: '974c5e8b-45b9-4653-ba55-5f855dd0fb88' 26 | scope: subscription() 27 | } 28 | 29 | /*** NEW RESOURCES ***/ 30 | 31 | @description('The Azure Storage account which will contain the pipes (queues) and the images to be sent through the filters (Azure Functions).') 32 | resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = { 33 | name: storageAccountName 34 | location: location 35 | sku: { 36 | name: 'Standard_LRS' 37 | } 38 | kind: 'StorageV2' 39 | properties: { 40 | accessTier: 'Hot' 41 | allowBlobPublicAccess: true 42 | allowCrossTenantReplication: false 43 | allowSharedKeyAccess: false // This storage account is configured to only work with Microsoft Entra ID authentication 44 | isLocalUserEnabled: false 45 | isHnsEnabled: false 46 | isNfsV3Enabled: false 47 | isSftpEnabled: false 48 | largeFileSharesState: 'Disabled' 49 | minimumTlsVersion: 'TLS1_2' 50 | publicNetworkAccess: 'Enabled' // This sample does not use private networking, but could be configured to use Private Link connections if fully deploy to Azure 51 | supportsHttpsTrafficOnly: true 52 | } 53 | 54 | resource blobContainers 'blobServices' = { 55 | name: 'default' 56 | 57 | resource images 'containers' = { 58 | name: 'images' 59 | } 60 | 61 | resource processed 'containers' = { 62 | name: 'processed' 63 | } 64 | } 65 | 66 | resource queueContainers 'queueServices' = { 67 | name: 'default' 68 | 69 | resource pipexfty 'queues' = { 70 | name: 'pipe-xfty' 71 | } 72 | } 73 | } 74 | 75 | @description('Grant Storage Blob Data Contributor to the user.') 76 | resource blobContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 77 | name: guid(userObjectId, storageAccount.id, storageBlobDataContributorRole.id) 78 | scope: storageAccount 79 | properties: { 80 | principalId: userObjectId 81 | roleDefinitionId: storageBlobDataContributorRole.id 82 | principalType: 'User' 83 | } 84 | } 85 | 86 | @description('Grant Storage Queue Data Contributor to the user.') 87 | resource queueContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 88 | name: guid(userObjectId, storageAccount.id, storageQueueDataContributorRole.id) 89 | scope: storageAccount 90 | properties: { 91 | principalId: userObjectId 92 | roleDefinitionId: storageQueueDataContributorRole.id 93 | principalType: 'User' 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /pipes-and-filters/images/clouds.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mspnp/cloud-design-patterns/ec4c70c30df8012150ac4fe02486a7508bf7ed3a/pipes-and-filters/images/clouds.png -------------------------------------------------------------------------------- /priority-queue/PriorityQueue.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.31205.134 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{5EDEDBDF-7C69-489E-A9E0-AB1E5B2AD481}" 7 | ProjectSection(SolutionItems) = preProject 8 | Readme.md = Readme.md 9 | EndProjectSection 10 | EndProject 11 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PriorityQueueConsumerHigh", "PriorityQueueConsumerHigh\PriorityQueueConsumerHigh.csproj", "{17291F2C-2F43-4910-A1C5-A81B4C05CB7E}" 12 | EndProject 13 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PriorityQueueConsumerLow", "PriorityQueueConsumerLow\PriorityQueueConsumerLow.csproj", "{3FEC510A-6AE0-4A49-BB3E-B337814F7F43}" 14 | EndProject 15 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PriorityQueueSender", "PriorityQueueSender\PriorityQueueSender.csproj", "{8A281D7D-4481-4333-92DD-271F7A78AA6E}" 16 | EndProject 17 | Global 18 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 19 | Debug|Any CPU = Debug|Any CPU 20 | Release|Any CPU = Release|Any CPU 21 | EndGlobalSection 22 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 23 | {17291F2C-2F43-4910-A1C5-A81B4C05CB7E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 24 | {17291F2C-2F43-4910-A1C5-A81B4C05CB7E}.Debug|Any CPU.Build.0 = Debug|Any CPU 25 | {17291F2C-2F43-4910-A1C5-A81B4C05CB7E}.Release|Any CPU.ActiveCfg = Release|Any CPU 26 | {17291F2C-2F43-4910-A1C5-A81B4C05CB7E}.Release|Any CPU.Build.0 = Release|Any CPU 27 | {3FEC510A-6AE0-4A49-BB3E-B337814F7F43}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 28 | {3FEC510A-6AE0-4A49-BB3E-B337814F7F43}.Debug|Any CPU.Build.0 = Debug|Any CPU 29 | {3FEC510A-6AE0-4A49-BB3E-B337814F7F43}.Release|Any CPU.ActiveCfg = Release|Any CPU 30 | {3FEC510A-6AE0-4A49-BB3E-B337814F7F43}.Release|Any CPU.Build.0 = Release|Any CPU 31 | {8A281D7D-4481-4333-92DD-271F7A78AA6E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 32 | {8A281D7D-4481-4333-92DD-271F7A78AA6E}.Debug|Any CPU.Build.0 = Debug|Any CPU 33 | {8A281D7D-4481-4333-92DD-271F7A78AA6E}.Release|Any CPU.ActiveCfg = Release|Any CPU 34 | {8A281D7D-4481-4333-92DD-271F7A78AA6E}.Release|Any CPU.Build.0 = Release|Any CPU 35 | EndGlobalSection 36 | GlobalSection(SolutionProperties) = preSolution 37 | HideSolutionNode = FALSE 38 | EndGlobalSection 39 | GlobalSection(ExtensibilityGlobals) = postSolution 40 | SolutionGuid = {E162A7B2-849B-4840-B9D9-D0E10FBE0814} 41 | EndGlobalSection 42 | EndGlobal 43 | -------------------------------------------------------------------------------- /priority-queue/PriorityQueueConsumerHigh/.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 -------------------------------------------------------------------------------- /priority-queue/PriorityQueueConsumerHigh/PriorityQueueConsumerHigh.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net6.0 4 | v4 5 | 6 | 7 | true 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | PreserveNewest 16 | 17 | 18 | PreserveNewest 19 | Never 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /priority-queue/PriorityQueueConsumerHigh/PriorityQueueConsumerHighFn.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Azure.WebJobs; 2 | using Microsoft.Extensions.Logging; 3 | 4 | namespace PriorityQueueConsumerHigh 5 | { 6 | public static class PriorityQueueConsumerHighFn 7 | { 8 | [FunctionName("HighPriorityQueueConsumerFunction")] 9 | public static void Run([ServiceBusTrigger("messages", "highPriority", Connection = "ServiceBusConnection")]string highPriorityMessage, ILogger log) 10 | { 11 | log.LogInformation($"C# ServiceBus topic trigger function processed message: {highPriorityMessage}"); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /priority-queue/PriorityQueueConsumerHigh/Properties/serviceDependencies.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "appInsights1": { 4 | "type": "appInsights" 5 | }, 6 | "storage1": { 7 | "type": "storage", 8 | "connectionId": "AzureWebJobsStorage" 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /priority-queue/PriorityQueueConsumerHigh/Properties/serviceDependencies.local.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "appInsights1": { 4 | "type": "appInsights.sdk" 5 | }, 6 | "storage1": { 7 | "type": "storage.emulator", 8 | "connectionId": "AzureWebJobsStorage" 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /priority-queue/PriorityQueueConsumerHigh/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "logging": { 4 | "applicationInsights": { 5 | "samplingSettings": { 6 | "isEnabled": true, 7 | "excludedTypes": "Request" 8 | } 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /priority-queue/PriorityQueueConsumerHigh/local.settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "IsEncrypted": false, 3 | "Values": { 4 | "AzureWebJobsStorage": "UseDevelopmentStorage=true", 5 | "FUNCTIONS_WORKER_RUNTIME": "dotnet", 6 | "ServiceBusConnection__fullyQualifiedNamespace": ".servicebus.windows.net" 7 | } 8 | } -------------------------------------------------------------------------------- /priority-queue/PriorityQueueConsumerLow/.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 -------------------------------------------------------------------------------- /priority-queue/PriorityQueueConsumerLow/PriorityQueueConsumerLow.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net6.0 4 | v4 5 | 736bb6a2-68b4-463b-a8fb-3a90cba7cd4f 6 | 7 | 8 | true 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | PreserveNewest 17 | 18 | 19 | PreserveNewest 20 | Never 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /priority-queue/PriorityQueueConsumerLow/PriorityQueueConsumerLowFn.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Azure.WebJobs; 2 | using Microsoft.Extensions.Logging; 3 | 4 | namespace PriorityQueueConsumerLow 5 | { 6 | public static class PriorityQueueConsumerLowFn 7 | { 8 | [FunctionName("LowPriorityQueueConsumerFunction")] 9 | public static void Run([ServiceBusTrigger("messages", "lowPriority", Connection = "ServiceBusConnection")]string lowPriorityMessage, ILogger log) 10 | { 11 | log.LogInformation($"C# ServiceBus topic trigger function processed message: {lowPriorityMessage}"); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /priority-queue/PriorityQueueConsumerLow/Properties/serviceDependencies.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "appInsights1": { 4 | "type": "appInsights" 5 | }, 6 | "storage1": { 7 | "type": "storage", 8 | "connectionId": "AzureWebJobsStorage" 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /priority-queue/PriorityQueueConsumerLow/Properties/serviceDependencies.local.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "appInsights1": { 4 | "type": "appInsights.sdk" 5 | }, 6 | "storage1": { 7 | "type": "storage.emulator", 8 | "connectionId": "AzureWebJobsStorage" 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /priority-queue/PriorityQueueConsumerLow/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "logging": { 4 | "applicationInsights": { 5 | "samplingSettings": { 6 | "isEnabled": true, 7 | "excludedTypes": "Request" 8 | } 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /priority-queue/PriorityQueueConsumerLow/local.settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "IsEncrypted": false, 3 | "Values": { 4 | "AzureWebJobsStorage": "UseDevelopmentStorage=true", 5 | "FUNCTIONS_WORKER_RUNTIME": "dotnet", 6 | "ServiceBusConnection__fullyQualifiedNamespace": ".servicebus.windows.net" 7 | } 8 | } -------------------------------------------------------------------------------- /priority-queue/PriorityQueueSender/.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 -------------------------------------------------------------------------------- /priority-queue/PriorityQueueSender/Priority.cs: -------------------------------------------------------------------------------- 1 | namespace PriorityQueueSender 2 | { 3 | public static class Priority 4 | { 5 | public static readonly string High = "highpriority"; 6 | 7 | public static readonly string Low = "lowpriority"; 8 | } 9 | } -------------------------------------------------------------------------------- /priority-queue/PriorityQueueSender/PriorityQueueSender.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net6.0 4 | v4 5 | 6 | 7 | true 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | PreserveNewest 16 | 17 | 18 | PreserveNewest 19 | Never 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /priority-queue/PriorityQueueSender/PriorityQueueSenderFn.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Azure.Messaging.ServiceBus; 4 | using Microsoft.Azure.WebJobs; 5 | 6 | namespace PriorityQueueSender 7 | { 8 | public static class PriorityQueueSenderFn 9 | { 10 | [FunctionName("PriorityQueueSenderFunction")] 11 | public static async Task Run( 12 | [TimerTrigger("0,30 * * * * *")] TimerInfo myTimer, 13 | [ServiceBus("messages", Connection = "ServiceBusConnection")] IAsyncCollector collector ) 14 | { 15 | for (int i = 0; i < 10; i++) 16 | { 17 | var messageId = Guid.NewGuid().ToString(); 18 | var lpMessage = new ServiceBusMessage() { MessageId = messageId }; 19 | lpMessage.ApplicationProperties["Priority"] = Priority.Low; 20 | lpMessage.Body = BinaryData.FromString($"Low priority message with Id: {messageId}"); 21 | await collector.AddAsync(lpMessage); 22 | 23 | messageId = Guid.NewGuid().ToString(); 24 | var hpMessage = new ServiceBusMessage() { MessageId = messageId }; 25 | hpMessage.ApplicationProperties["Priority"] = Priority.High; 26 | hpMessage.Body = BinaryData.FromString($"High priority message with Id: {messageId}"); 27 | await collector.AddAsync(hpMessage); 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /priority-queue/PriorityQueueSender/Properties/serviceDependencies.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "appInsights1": { 4 | "type": "appInsights" 5 | }, 6 | "storage1": { 7 | "type": "storage", 8 | "connectionId": "AzureWebJobsStorage" 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /priority-queue/PriorityQueueSender/Properties/serviceDependencies.local.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "appInsights1": { 4 | "type": "appInsights.sdk" 5 | }, 6 | "storage1": { 7 | "type": "storage.emulator", 8 | "connectionId": "AzureWebJobsStorage" 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /priority-queue/PriorityQueueSender/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "logging": { 4 | "applicationInsights": { 5 | "samplingSettings": { 6 | "isEnabled": true, 7 | "excludedTypes": "Request" 8 | } 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /priority-queue/PriorityQueueSender/local.settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "IsEncrypted": false, 3 | "Values": { 4 | "AzureWebJobsStorage": "UseDevelopmentStorage=true", 5 | "FUNCTIONS_WORKER_RUNTIME": "dotnet", 6 | "ServiceBusConnection__fullyQualifiedNamespace": "mgransb1.servicebus.windows.net" 7 | } 8 | } -------------------------------------------------------------------------------- /sharding/README.md: -------------------------------------------------------------------------------- 1 | # Sharding pattern example 2 | 3 | The [Sharding cloud design pattern](https://learn.microsoft.com/azure/architecture/patterns/sharding) does not have code associated with it. You'll find an example of this pattern in the [Example section](https://learn.microsoft.com/azure/architecture/patterns/sharding#example) of that article. 4 | 5 | The scenario presented is how code running in an Azure Web App can use a *lookup* against a managed shard list database to select the correct Azure SQL Database instance for queries. The scenario uses a book cataloging system, with the shard being based off of the books' ISBNs. 6 | 7 | ![A diagram showing an Azure Web App using a shard mapping database to decide which database instance to use for a given shard key. It also shows Azure AI Search for pre-aggregated data that the site uses that likely would have come from multiple shards.](sharding-example.png) 8 | 9 | ## Related documentation 10 | 11 | - [Scaling out with Azure SQL Database](https://learn.microsoft.com/azure/azure-sql/database/elastic-scale-introduction) 12 | - [Sharding models in Azure CosmosDB for PostgreSQL](https://learn.microsoft.com/azure/cosmos-db/postgresql/concepts-sharding-models) 13 | 14 | ## Contributions 15 | 16 | Please see our [Contributor guide](../CONTRIBUTING.md). 17 | 18 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact with any additional questions or comments. 19 | 20 | With :heart: from Azure patterns & practices, [Azure Architecture Center](https://azure.com/architecture). 21 | -------------------------------------------------------------------------------- /sharding/sharding-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mspnp/cloud-design-patterns/ec4c70c30df8012150ac4fe02486a7508bf7ed3a/sharding/sharding-example.png -------------------------------------------------------------------------------- /static-content-hosting/bicep/main.bicep: -------------------------------------------------------------------------------- 1 | targetScope = 'resourceGroup' 2 | 3 | @minLength(5) 4 | @description('Location of the resources. Defaults to resource group location.') 5 | param location string = resourceGroup().location 6 | 7 | @minLength(5) 8 | @description('The globally unique name for the storage account.') 9 | param storageAccountName string 10 | 11 | @minLength(5) 12 | @description('The user assignee object id to be granted with Storage Blob Data Contributor permissions.') 13 | param assigneeObjectId string 14 | 15 | 16 | /*** EXISTING SUBSCRIPTION RESOURCES ***/ 17 | 18 | resource storageBlobDataContributorRole 'Microsoft.Authorization/roleDefinitions@2022-05-01-preview' existing = { 19 | name: 'ba92f5b4-2d11-453d-a403-e96b0029c9fe' 20 | scope: subscription() 21 | } 22 | 23 | /*** EXISTING RESOURCES ***/ 24 | 25 | /*** NEW RESOURCES ***/ 26 | 27 | @description('The Azure Storage account with static website support enabled and where operations upload static content to.') 28 | resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = { 29 | name: storageAccountName 30 | location: location 31 | sku: { 32 | name: 'Standard_LRS' 33 | } 34 | kind: 'StorageV2' 35 | properties: { 36 | accessTier: 'Hot' 37 | allowBlobPublicAccess: false 38 | allowCrossTenantReplication: false 39 | allowSharedKeyAccess: false // This storage account is not configured to allow SaS tokens, since all content in this static content hosting example is public. For content that need to be protected, this field could be enabled. 40 | isLocalUserEnabled: false 41 | isHnsEnabled: false 42 | isNfsV3Enabled: false 43 | isSftpEnabled: false 44 | largeFileSharesState: 'Disabled' 45 | minimumTlsVersion: 'TLS1_2' 46 | publicNetworkAccess: 'Enabled' // In a static content hosting scenario, typically clients are not hosted in your virtual network. However if they were, then you could disable this. In this sample, you'll be accessing this from your workstation. 47 | supportsHttpsTrafficOnly: true 48 | defaultToOAuthAuthentication: true 49 | allowedCopyScope: 'PrivateLink' 50 | networkAcls: { 51 | defaultAction: 'Allow' // For this sample, public Internet access is expected since content is delivered to anonymous client 52 | bypass: 'None' 53 | virtualNetworkRules: [] 54 | ipRules: [] 55 | } 56 | } 57 | } 58 | 59 | resource storageAccountUserStorageBlobDataContributorRole_roleAssignment 'Microsoft.Authorization/roleAssignments@2020-10-01-preview' = { 60 | scope: storageAccount 61 | name: guid(storageAccount.id, storageBlobDataContributorRole.id, assigneeObjectId) 62 | properties: { 63 | roleDefinitionId: storageBlobDataContributorRole.id 64 | description: 'Allows cluster identity to join the nodepool vmss resources to this subnet.' 65 | principalId: assigneeObjectId 66 | principalType: 'User' 67 | } 68 | } 69 | 70 | -------------------------------------------------------------------------------- /static-content-hosting/src/index.template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |

Welcome to contoso.com

8 | 9 | -------------------------------------------------------------------------------- /static-content-hosting/src/static/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

Page Not Found

7 | 8 | -------------------------------------------------------------------------------- /static-content-hosting/src/static/style.css: -------------------------------------------------------------------------------- 1 | h1 { 2 | color: blue; 3 | text-align: center; 4 | } -------------------------------------------------------------------------------- /static-content-hosting/static-content-hosting-pattern.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mspnp/cloud-design-patterns/ec4c70c30df8012150ac4fe02486a7508bf7ed3a/static-content-hosting/static-content-hosting-pattern.png -------------------------------------------------------------------------------- /valet-key/.gitignore: -------------------------------------------------------------------------------- 1 | # Azurite 2 | __blobstorage__/ 3 | __azurite* 4 | .mono/ 5 | -------------------------------------------------------------------------------- /valet-key/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "ms-azuretools.vscode-azurefunctions", 4 | "ms-dotnettools.csharp", 5 | "azurite.azurite", 6 | "ms-azuretools.vscode-bicep" 7 | ] 8 | } -------------------------------------------------------------------------------- /valet-key/ValetKey.Client/Program.cs: -------------------------------------------------------------------------------- 1 | namespace ValetKey.Client 2 | { 3 | using Azure; 4 | using Azure.Storage.Blobs; 5 | using Microsoft.Extensions.Configuration; 6 | using System.Net.Http.Json; 7 | 8 | public class Program 9 | { 10 | private static readonly HttpClient httpClient = new(); 11 | 12 | public static async Task Main() 13 | { 14 | Console.WriteLine("Press any key to run the sample..."); 15 | Console.ReadKey(); 16 | 17 | var configuration = new ConfigurationBuilder() 18 | .AddJsonFile("appsettings.json", false, false).Build(); 19 | 20 | // Get the valet key endpoint from config 21 | var tokenServiceEndpoint = configuration.GetSection("AppSettings:ServiceEndpointUrl").Value; 22 | if (string.IsNullOrWhiteSpace(tokenServiceEndpoint)) throw new InvalidOperationException("Configure AppSettings:ServiceEndpointUrl and run again."); 23 | 24 | // Get the valet key 25 | var blobSas = await GetBlobSasAsync(new Uri(tokenServiceEndpoint)); 26 | 27 | var sasUri = new UriBuilder(blobSas!.BlobUri!) 28 | { 29 | Query = blobSas.Signature 30 | }; 31 | 32 | var blob = new BlobClient(sasUri.Uri); 33 | try 34 | { 35 | using (var stream = await GetFileToUploadAsync(10)) 36 | { 37 | await blob.UploadAsync(stream); 38 | } 39 | 40 | Console.WriteLine("Blob uploaded successful: {0}", blob.Name); 41 | } 42 | catch (RequestFailedException e) 43 | { 44 | // Check for a 403 (Forbidden) error. If the SAS is invalid, 45 | // Azure Storage returns this error. 46 | if (e.Status == 403) 47 | { 48 | Console.WriteLine("Write operation failed for SAS {0}", sasUri); 49 | Console.WriteLine("Additional error information: " + e.Message); 50 | Console.WriteLine(); 51 | } 52 | else 53 | { 54 | Console.WriteLine(e.Message); 55 | throw; 56 | } 57 | } 58 | 59 | Console.WriteLine(); 60 | Console.WriteLine("Done. Press any key to exit..."); 61 | Console.ReadKey(); 62 | } 63 | 64 | private static async Task GetBlobSasAsync(Uri tokenUri) 65 | { 66 | return await httpClient.GetFromJsonAsync(tokenUri); 67 | } 68 | 69 | /// 70 | /// Create a sample file containing random bytes of data 71 | /// 72 | private static async Task GetFileToUploadAsync(int sizeMb) 73 | { 74 | var stream = new MemoryStream(); 75 | 76 | var rnd = new Random(); 77 | var buffer = new byte[1024 * 1024]; 78 | 79 | for (int i = 0; i < sizeMb; i++) 80 | { 81 | rnd.NextBytes(buffer); 82 | await stream.WriteAsync(buffer.AsMemory(0, buffer.Length)); 83 | } 84 | 85 | stream.Position = 0; 86 | 87 | return stream; 88 | } 89 | 90 | public class StorageEntitySas 91 | { 92 | public Uri? BlobUri { get; set; } 93 | public string? Signature { get; set; } 94 | } 95 | } 96 | } -------------------------------------------------------------------------------- /valet-key/ValetKey.Client/ValetKey.Client.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net9.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | PreserveNewest 17 | true 18 | PreserveNewest 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /valet-key/ValetKey.Client/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "AppSettings": { 3 | "ServiceEndpointUrl": "http://localhost:7071/api/file-services/access" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /valet-key/ValetKey.Web/.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 | # Azurite 14 | __blobstorage__/ 15 | __azurite* 16 | .mono/ 17 | 18 | # User-specific files (MonoDevelop/Xamarin Studio) 19 | *.userprefs 20 | 21 | # Build results 22 | [Dd]ebug/ 23 | [Dd]ebugPublic/ 24 | [Rr]elease/ 25 | [Rr]eleases/ 26 | x64/ 27 | x86/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | 33 | # Visual Studio 2015 cache/options directory 34 | .vs/ 35 | # Uncomment if you have tasks that create the project's static files in wwwroot 36 | #wwwroot/ 37 | 38 | # MSTest test Results 39 | [Tt]est[Rr]esult*/ 40 | [Bb]uild[Ll]og.* 41 | 42 | # NUNIT 43 | *.VisualState.xml 44 | TestResult.xml 45 | 46 | # Build Results of an ATL Project 47 | [Dd]ebugPS/ 48 | [Rr]eleasePS/ 49 | dlldata.c 50 | 51 | # DNX 52 | project.lock.json 53 | project.fragment.lock.json 54 | artifacts/ 55 | 56 | *_i.c 57 | *_p.c 58 | *_i.h 59 | *.ilk 60 | *.meta 61 | *.obj 62 | *.pch 63 | *.pdb 64 | *.pgc 65 | *.pgd 66 | *.rsp 67 | *.sbr 68 | *.tlb 69 | *.tli 70 | *.tlh 71 | *.tmp 72 | *.tmp_proj 73 | *.log 74 | *.vspscc 75 | *.vssscc 76 | .builds 77 | *.pidb 78 | *.svclog 79 | *.scc 80 | 81 | # Chutzpah Test files 82 | _Chutzpah* 83 | 84 | # Visual C++ cache files 85 | ipch/ 86 | *.aps 87 | *.ncb 88 | *.opendb 89 | *.opensdf 90 | *.sdf 91 | *.cachefile 92 | *.VC.db 93 | *.VC.VC.opendb 94 | 95 | # Visual Studio profiler 96 | *.psess 97 | *.vsp 98 | *.vspx 99 | *.sap 100 | 101 | # TFS 2012 Local Workspace 102 | $tf/ 103 | 104 | # Guidance Automation Toolkit 105 | *.gpState 106 | 107 | # ReSharper is a .NET coding add-in 108 | _ReSharper*/ 109 | *.[Rr]e[Ss]harper 110 | *.DotSettings.user 111 | 112 | # JustCode is a .NET coding add-in 113 | .JustCode 114 | 115 | # TeamCity is a build add-in 116 | _TeamCity* 117 | 118 | # DotCover is a Code Coverage Tool 119 | *.dotCover 120 | 121 | # NCrunch 122 | _NCrunch_* 123 | .*crunch*.local.xml 124 | nCrunchTemp_* 125 | 126 | # MightyMoose 127 | *.mm.* 128 | AutoTest.Net/ 129 | 130 | # Web workbench (sass) 131 | .sass-cache/ 132 | 133 | # Installshield output folder 134 | [Ee]xpress/ 135 | 136 | # DocProject is a documentation generator add-in 137 | DocProject/buildhelp/ 138 | DocProject/Help/*.HxT 139 | DocProject/Help/*.HxC 140 | DocProject/Help/*.hhc 141 | DocProject/Help/*.hhk 142 | DocProject/Help/*.hhp 143 | DocProject/Help/Html2 144 | DocProject/Help/html 145 | 146 | # Click-Once directory 147 | publish/ 148 | 149 | # Publish Web Output 150 | *.[Pp]ublish.xml 151 | *.azurePubxml 152 | # TODO: Comment the next line if you want to checkin your web deploy settings 153 | # but database connection strings (with potential passwords) will be unencrypted 154 | #*.pubxml 155 | *.publishproj 156 | 157 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 158 | # checkin your Azure Web App publish settings, but sensitive information contained 159 | # in these scripts will be unencrypted 160 | PublishScripts/ 161 | 162 | # NuGet Packages 163 | *.nupkg 164 | # The packages folder can be ignored because of Package Restore 165 | **/packages/* 166 | # except build/, which is used as an MSBuild target. 167 | !**/packages/build/ 168 | # Uncomment if necessary however generally it will be regenerated when needed 169 | #!**/packages/repositories.config 170 | # NuGet v3's project.json files produces more ignoreable files 171 | *.nuget.props 172 | *.nuget.targets 173 | 174 | # Microsoft Azure Build Output 175 | csx/ 176 | *.build.csdef 177 | 178 | # Microsoft Azure Emulator 179 | ecf/ 180 | rcf/ 181 | 182 | # Windows Store app package directories and files 183 | AppPackages/ 184 | BundleArtifacts/ 185 | Package.StoreAssociation.xml 186 | _pkginfo.txt 187 | 188 | # Visual Studio cache files 189 | # files ending in .cache can be ignored 190 | *.[Cc]ache 191 | # but keep track of directories ending in .cache 192 | !*.[Cc]ache/ 193 | 194 | # Others 195 | ClientBin/ 196 | ~$* 197 | *~ 198 | *.dbmdl 199 | *.dbproj.schemaview 200 | *.jfm 201 | *.pfx 202 | *.publishsettings 203 | node_modules/ 204 | orleans.codegen.cs 205 | 206 | # Since there are multiple workflows, uncomment next line to ignore bower_components 207 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 208 | #bower_components/ 209 | 210 | # RIA/Silverlight projects 211 | Generated_Code/ 212 | 213 | # Backup & report files from converting an old project file 214 | # to a newer Visual Studio version. Backup files are not needed, 215 | # because we have git ;-) 216 | _UpgradeReport_Files/ 217 | Backup*/ 218 | UpgradeLog*.XML 219 | UpgradeLog*.htm 220 | 221 | # SQL Server files 222 | *.mdf 223 | *.ldf 224 | 225 | # Business Intelligence projects 226 | *.rdl.data 227 | *.bim.layout 228 | *.bim_*.settings 229 | 230 | # Microsoft Fakes 231 | FakesAssemblies/ 232 | 233 | # GhostDoc plugin setting file 234 | *.GhostDoc.xml 235 | 236 | # Node.js Tools for Visual Studio 237 | .ntvs_analysis.dat 238 | 239 | # Visual Studio 6 build log 240 | *.plg 241 | 242 | # Visual Studio 6 workspace options file 243 | *.opt 244 | 245 | # Visual Studio LightSwitch build output 246 | **/*.HTMLClient/GeneratedArtifacts 247 | **/*.DesktopClient/GeneratedArtifacts 248 | **/*.DesktopClient/ModelManifest.xml 249 | **/*.Server/GeneratedArtifacts 250 | **/*.Server/ModelManifest.xml 251 | _Pvt_Extensions 252 | 253 | # Paket dependency manager 254 | .paket/paket.exe 255 | paket-files/ 256 | 257 | # FAKE - F# Make 258 | .fake/ 259 | 260 | # JetBrains Rider 261 | .idea/ 262 | *.sln.iml 263 | 264 | # CodeRush 265 | .cr/ 266 | 267 | # Python Tools for Visual Studio (PTVS) 268 | __pycache__/ 269 | *.pyc -------------------------------------------------------------------------------- /valet-key/ValetKey.Web/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "ms-azuretools.vscode-azurefunctions", 4 | "ms-dotnettools.csharp", 5 | "azurite.azurite", 6 | ] 7 | } -------------------------------------------------------------------------------- /valet-key/ValetKey.Web/.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 | } -------------------------------------------------------------------------------- /valet-key/ValetKey.Web/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "azureFunctions.deploySubpath": "bin/Release/net8.0/publish", 3 | "azureFunctions.projectLanguage": "C#", 4 | "azureFunctions.projectRuntime": "~4", 5 | "debug.internalConsoleOptions": "neverOpen", 6 | "azureFunctions.preDeployTask": "publish (functions)" 7 | } -------------------------------------------------------------------------------- /valet-key/ValetKey.Web/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "clean (functions)", 6 | "command": "dotnet", 7 | "args": [ 8 | "clean", 9 | "/property:GenerateFullPaths=true", 10 | "/consoleloggerparameters:NoSummary" 11 | ], 12 | "type": "process", 13 | "problemMatcher": "$msCompile" 14 | }, 15 | { 16 | "label": "build (functions)", 17 | "command": "dotnet", 18 | "args": [ 19 | "build", 20 | "/property:GenerateFullPaths=true", 21 | "/consoleloggerparameters:NoSummary" 22 | ], 23 | "type": "process", 24 | "dependsOn": "clean (functions)", 25 | "group": { 26 | "kind": "build", 27 | "isDefault": true 28 | }, 29 | "problemMatcher": "$msCompile" 30 | }, 31 | { 32 | "label": "clean release (functions)", 33 | "command": "dotnet", 34 | "args": [ 35 | "clean", 36 | "--configuration", 37 | "Release", 38 | "/property:GenerateFullPaths=true", 39 | "/consoleloggerparameters:NoSummary" 40 | ], 41 | "type": "process", 42 | "problemMatcher": "$msCompile" 43 | }, 44 | { 45 | "label": "publish (functions)", 46 | "command": "dotnet", 47 | "args": [ 48 | "publish", 49 | "--configuration", 50 | "Release", 51 | "/property:GenerateFullPaths=true", 52 | "/consoleloggerparameters:NoSummary" 53 | ], 54 | "type": "process", 55 | "dependsOn": "clean release (functions)", 56 | "problemMatcher": "$msCompile" 57 | }, 58 | { 59 | "type": "func", 60 | "dependsOn": "build (functions)", 61 | "options": { 62 | "cwd": "${workspaceFolder}/bin/Debug/net8.0" 63 | }, 64 | "command": "host start", 65 | "isBackground": true, 66 | "problemMatcher": "$func-dotnet-watch" 67 | } 68 | ] 69 | } -------------------------------------------------------------------------------- /valet-key/ValetKey.Web/FileServices.cs: -------------------------------------------------------------------------------- 1 | using Azure.Storage.Blobs; 2 | using Azure.Storage.Blobs.Specialized; 3 | using Azure.Storage.Sas; 4 | using Microsoft.Azure.Functions.Worker; 5 | using Microsoft.Azure.Functions.Worker.Http; 6 | using Microsoft.Extensions.Logging; 7 | 8 | namespace ValetKey.Web 9 | { 10 | public class FileServices(ILoggerFactory loggerFactory) 11 | { 12 | private readonly ILogger _logger = loggerFactory.CreateLogger(); 13 | 14 | // WARNING: This route would normally require its own AuthZ so that you are handing out valet keys 15 | // to only authorized clients. For example, using App Service authentication integrated with 16 | // the IdP requirements aligned with your clients. 17 | [Function(nameof(FileServices))] 18 | public async Task RunAsync( 19 | [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "file-services/access")] HttpRequestData req, 20 | [BlobInput("uploads", Connection = "UploadStorage")] BlobContainerClient blobContainerClient, 21 | CancellationToken cancellationToken 22 | ) 23 | { 24 | _logger.LogInformation("Processing new request for a valet key."); 25 | 26 | return await GetSharedAccessReferenceForUploadAsync(blobContainerClient, Guid.NewGuid().ToString(), cancellationToken); 27 | } 28 | 29 | /// 30 | /// Return an access key that allows the caller to upload a file to this specific destination for defined period of time (~three minutes). 31 | /// 32 | private async Task GetSharedAccessReferenceForUploadAsync(BlobContainerClient blobContainerClient, string blobName, CancellationToken cancellationToken) 33 | { 34 | var blobServiceClient = blobContainerClient.GetParentBlobServiceClient(); 35 | var blobClient = blobContainerClient.GetBlockBlobClient(blobName); 36 | 37 | var userDelegationKey = await blobServiceClient.GetUserDelegationKeyAsync(DateTimeOffset.UtcNow.AddMinutes(-3), 38 | DateTimeOffset.UtcNow.AddMinutes(3), cancellationToken); 39 | 40 | // Limit the scope of this SaS token to the following: 41 | // - The specific blob 42 | // - Create permissions only 43 | // - In the next ~three minutes 44 | // - Over HTTPs 45 | var blobSasBuilder = new BlobSasBuilder 46 | { 47 | BlobContainerName = blobContainerClient.Name, 48 | BlobName = blobClient.Name, 49 | Resource = "b", 50 | StartsOn = DateTimeOffset.UtcNow.AddMinutes(-3), 51 | ExpiresOn = DateTimeOffset.UtcNow.AddMinutes(3), 52 | Protocol = SasProtocol.Https 53 | }; 54 | blobSasBuilder.SetPermissions(BlobSasPermissions.Create); 55 | 56 | var sas = blobSasBuilder.ToSasQueryParameters(userDelegationKey, blobServiceClient.AccountName).ToString(); 57 | 58 | _logger.LogInformation("Generated user delegated SaS token for {uri} that expires at {expiresOn}.", blobClient.Uri, blobSasBuilder.ExpiresOn); 59 | 60 | return new StorageEntitySas 61 | { 62 | BlobUri = blobClient.Uri, 63 | Signature = sas 64 | }; 65 | } 66 | 67 | public class StorageEntitySas 68 | { 69 | public Uri? BlobUri { get; internal set; } 70 | public string? Signature { get; internal set; } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /valet-key/ValetKey.Web/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Azure.Functions.Worker; 2 | using Microsoft.Extensions.Hosting; 3 | 4 | var host = new HostBuilder() 5 | .ConfigureFunctionsWorkerDefaults(appBuilder => 6 | { 7 | appBuilder.ConfigureBlobStorageExtension(); 8 | }) 9 | .Build(); 10 | 11 | await host.RunAsync(); 12 | -------------------------------------------------------------------------------- /valet-key/ValetKey.Web/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "ValetKey_Web": { 4 | "commandName": "Project", 5 | "commandLineArgs": "--port 7071", 6 | "launchBrowser": false 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /valet-key/ValetKey.Web/ValetKey.Web.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net9.0 4 | v4 5 | Exe 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | PreserveNewest 18 | 19 | 20 | PreserveNewest 21 | Never 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /valet-key/ValetKey.Web/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "logging": { 4 | "applicationInsights": { 5 | "samplingSettings": { 6 | "isEnabled": true, 7 | "excludedTypes": "Request" 8 | }, 9 | "enableLiveMetricsFilters": true 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /valet-key/ValetKey.Web/local.settings.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "IsEncrypted": false, 3 | "Values": { 4 | "AzureWebJobsStorage": "UseDevelopmentStorage=true", 5 | "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated", 6 | "UploadStorage__blobServiceUri": "https://STORAGE_ACCOUNT_NAME.blob.core.windows.net" 7 | } 8 | } -------------------------------------------------------------------------------- /valet-key/bicep/main.bicep: -------------------------------------------------------------------------------- 1 | targetScope = 'resourceGroup' 2 | 3 | @minLength(5) 4 | @description('Location of the resources. Defaults to resource group location.') 5 | param location string = resourceGroup().location 6 | 7 | @minLength(36) 8 | @description('The guid of the principal running the valet key generation code. In Azure this would be replaced with the managed identity of the Azure Function, when running locally it will be your user.') 9 | param principalId string 10 | 11 | @minLength(5) 12 | @description('The globally unique name for the storage account.') 13 | param storageAccountName string 14 | 15 | /*** EXISTING RESOURCES ***/ 16 | 17 | @description('Built-in Azure RBAC role that is applied to a Storage account to grant "Storage Blob Data Contributor" privileges. Used by the managed identity of the valet key Azure Function as for being able to delegate permissions to create blobs.') 18 | resource storageBlobDataContributorRole 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = { 19 | name: 'ba92f5b4-2d11-453d-a403-e96b0029c9fe' 20 | scope: subscription() 21 | } 22 | 23 | @description('Built-in Azure RBAC role that is applied to a Storage account to grant "Storage Blob Delegator" privileges. Used by the managed identity of the valet key Azure Function to manage generate SaS tokens.') 24 | resource storageBlobDelegatorRole 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = { 25 | name: 'db58b8e5-c6ad-4a2a-8342-4190687cbf4a' 26 | scope: subscription() 27 | } 28 | 29 | /*** NEW RESOURCES ***/ 30 | 31 | @description('Workload logs.') 32 | resource workloadLogs 'Microsoft.OperationalInsights/workspaces@2023-09-01' = { 33 | name: 'la-logs' 34 | location: location 35 | properties: { 36 | publicNetworkAccessForIngestion: 'Enabled' 37 | publicNetworkAccessForQuery: 'Enabled' 38 | sku: { 39 | name: 'PerGB2018' 40 | } 41 | retentionInDays: 30 42 | features: { 43 | enableLogAccessUsingOnlyResourcePermissions: true 44 | } 45 | workspaceCapping: { 46 | dailyQuotaGb: -1 47 | } 48 | } 49 | } 50 | 51 | @description('The Azure Storage account which will be where authorized clients upload large blobs to. The Azure Function will hand out scoped, time-limited SaS tokens for this blobs in this account.') 52 | resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = { 53 | name: storageAccountName 54 | location: location 55 | sku: { 56 | name: 'Standard_LRS' 57 | } 58 | kind: 'StorageV2' 59 | properties: { 60 | accessTier: 'Hot' 61 | allowBlobPublicAccess: false 62 | allowCrossTenantReplication: false 63 | allowSharedKeyAccess: false // Only managed identity allowed, we needed to change the way to generate SAS token using UserDelegationKey 64 | isLocalUserEnabled: false 65 | isHnsEnabled: false 66 | isNfsV3Enabled: false 67 | isSftpEnabled: false 68 | largeFileSharesState: 'Disabled' 69 | minimumTlsVersion: 'TLS1_2' 70 | publicNetworkAccess: 'Enabled' // In a valet key scenario, typically clients are not hosted in your virtual network. However if they were, then you could disable this. In this sample, you'll be accessing this from your workstation. 71 | supportsHttpsTrafficOnly: true 72 | defaultToOAuthAuthentication: true 73 | allowedCopyScope: 'PrivateLink' 74 | sasPolicy: { 75 | expirationAction: 'Log' 76 | sasExpirationPeriod: '00.00:10:00' // Log the creation of SaS tokens over 10 minutes long 77 | } 78 | keyPolicy: { 79 | keyExpirationPeriodInDays: 10 // Storage account key isn't used, require agressive rotation 80 | } 81 | networkAcls: { 82 | defaultAction: 'Allow' // For this sample, public Internet access is expected 83 | bypass: 'None' 84 | virtualNetworkRules: [] 85 | ipRules: [] 86 | } 87 | } 88 | 89 | resource blobContainers 'blobServices' = { 90 | name: 'default' 91 | 92 | @description('The blob container that SaS tokens will be generated for.') 93 | resource uploadsContainer 'containers' = { 94 | name: 'uploads' 95 | } 96 | } 97 | } 98 | 99 | @description('Enable access logs on blob storage.') 100 | resource azureStorageBlobAccessLogs 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = { 101 | name: 'default' 102 | scope: storageAccount::blobContainers 103 | properties: { 104 | logs: [ 105 | { 106 | categoryGroup: 'allLogs' 107 | enabled: true 108 | } 109 | ] 110 | workspaceId: workloadLogs.id 111 | } 112 | } 113 | 114 | @description('SaS tokens are created at the account level, allow our identified principal the permissions necessary to create those.') 115 | resource blobUploadStorageDelegator 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 116 | name: guid(storageAccount.id, storageBlobDelegatorRole.id, principalId) 117 | scope: storageAccount 118 | properties: { 119 | principalId: principalId 120 | roleDefinitionId: storageBlobDelegatorRole.id 121 | principalType: 'User' // 'ServicePrincipal' if this was a managed identity 122 | description: 'Allows this Microsoft Entra principal to create Entra ID-signed SaS tokens for this storage account.' 123 | } 124 | } 125 | 126 | @description('User delegation requires the user doing the delegating to SaS to also have the permissions being delgated. So scoping a Data Contributor to the container. In this scenario, technically this principal only needs permissions to create blobs.') 127 | resource blobContributorUploadStorage 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 128 | name: guid(storageAccount::blobContainers::uploadsContainer.id, storageBlobDataContributorRole.id, principalId) 129 | scope: storageAccount::blobContainers::uploadsContainer 130 | properties: { 131 | principalId: principalId 132 | roleDefinitionId: storageBlobDataContributorRole.id 133 | principalType: 'User' // 'ServicePrincipal' if this was a managed identity 134 | description: 'Allows this Microsoft Entra principal to manage blobs in this storage container.' 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /valet-key/valet-key-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mspnp/cloud-design-patterns/ec4c70c30df8012150ac4fe02486a7508bf7ed3a/valet-key/valet-key-example.png -------------------------------------------------------------------------------- /valet-key/valet-key.sln: -------------------------------------------------------------------------------- 1 | Microsoft Visual Studio Solution File, Format Version 12.00 2 | # Visual Studio Version 17 3 | VisualStudioVersion = 17.5.2.0 4 | MinimumVisualStudioVersion = 10.0.40219.1 5 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ValetKey.Client", "ValetKey.Client\ValetKey.Client.csproj", "{E04AE64E-BF28-F956-5423-989F1F266BD9}" 6 | EndProject 7 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ValetKey.Web", "ValetKey.Web\ValetKey.Web.csproj", "{576BFECC-F315-A089-0A93-BF9D5845EB04}" 8 | EndProject 9 | Global 10 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 11 | Debug|Any CPU = Debug|Any CPU 12 | Release|Any CPU = Release|Any CPU 13 | EndGlobalSection 14 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 15 | {E04AE64E-BF28-F956-5423-989F1F266BD9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 16 | {E04AE64E-BF28-F956-5423-989F1F266BD9}.Debug|Any CPU.Build.0 = Debug|Any CPU 17 | {E04AE64E-BF28-F956-5423-989F1F266BD9}.Release|Any CPU.ActiveCfg = Release|Any CPU 18 | {E04AE64E-BF28-F956-5423-989F1F266BD9}.Release|Any CPU.Build.0 = Release|Any CPU 19 | {576BFECC-F315-A089-0A93-BF9D5845EB04}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 20 | {576BFECC-F315-A089-0A93-BF9D5845EB04}.Debug|Any CPU.Build.0 = Debug|Any CPU 21 | {576BFECC-F315-A089-0A93-BF9D5845EB04}.Release|Any CPU.ActiveCfg = Release|Any CPU 22 | {576BFECC-F315-A089-0A93-BF9D5845EB04}.Release|Any CPU.Build.0 = Release|Any CPU 23 | EndGlobalSection 24 | GlobalSection(SolutionProperties) = preSolution 25 | HideSolutionNode = FALSE 26 | EndGlobalSection 27 | GlobalSection(ExtensibilityGlobals) = postSolution 28 | SolutionGuid = {FE8353B1-C363-4C0D-8F2D-64ECAA853FDD} 29 | EndGlobalSection 30 | EndGlobal 31 | --------------------------------------------------------------------------------