├── .vscode ├── settings.json ├── launch.json └── tasks.json ├── Program.cs ├── Services ├── CDN.sh ├── Redis.sh ├── AD.http ├── CosmoDB.sql ├── Blobs.http ├── CosmoDB.sh ├── QueueStorage.sh ├── AZ CLI.md ├── ServiceBus.sh ├── EventGrid.ARM.jsonc ├── AD.cs ├── Dockerfile ├── Functions.sh ├── ApiManagement.sh ├── SAS.sh ├── CosmoDB.js ├── AD.sh ├── Blobs.ts ├── EventGrid.ts ├── KeyVault.sh ├── Functions_host.jsonc ├── ContainerInstance.yml ├── EventHub.sh ├── Redis.cs ├── ContainerInstance.sh ├── CDN.cs ├── EventGrid.sh ├── ManagedIdentities.cs ├── KeyVault.cs ├── QueueStorage.cs ├── Application Insights.cs ├── Graph.cs ├── ContainerRegistry.sh ├── Blobs.sh ├── ContainerApp.sh ├── EventGrid.cs ├── ServiceBus.cs ├── CosmoDB.cs ├── EventHub.cs ├── AppService.sh └── Blobs.cs ├── Training.sln ├── Project.csproj └── .gitignore /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.formatOnPaste": true 4 | } 5 | -------------------------------------------------------------------------------- /Program.cs: -------------------------------------------------------------------------------- 1 | // See https://aka.ms/new-console-template for more information 2 | Console.WriteLine("Hello, World!"); 3 | -------------------------------------------------------------------------------- /Services/CDN.sh: -------------------------------------------------------------------------------- 1 | az cdn endpoint purge \ 2 | --content-paths '/css/*' '/js/app.js' \ 3 | --name ContosoEndpoint \ 4 | --profile-name DemoProfile \ 5 | --resource-group $resourceGroup 6 | 7 | az cdn endpoint load \ 8 | --content-paths '/img/*' '/js/module.js' \ 9 | --name ContosoEndpoint \ 10 | --profile-name DemoProfile \ 11 | --resource-group $resourceGroup -------------------------------------------------------------------------------- /Services/Redis.sh: -------------------------------------------------------------------------------- 1 | az redis create 2 | --name $name # must be globally unique 3 | --location $location # region where you want to create the cache 4 | --sku # pricing tier 5 | --vm-size # depends on the chosen tier 6 | --shard-count # number of shards to be used for clustering (Premium and Enterprise only). Max: 10 7 | --resource-group $resourceGroup -------------------------------------------------------------------------------- /Services/AD.http: -------------------------------------------------------------------------------- 1 | Requesting individual user consent: 2 | 3 | ```http 4 | GET https://login.microsoftonline.com/common/oauth2/v2.0/authorize? 5 | client_id=6731de76-14a6-49ae-97bc-6eba6914391e 6 | &response_type=code 7 | &redirect_uri=http%3A%2F%2Flocalhost%2Fmyapp%2F 8 | &response_mode=query 9 | &scope=https%3A%2F%2Fgraph.microsoft.com%2Fcalendars.read%20https%3A%2F%2Fgraph.microsoft.com%2Fmail.send 10 | &state=12345 11 | ``` 12 | 13 | Scope is full path -------------------------------------------------------------------------------- /Services/CosmoDB.sql: -------------------------------------------------------------------------------- 1 | -- composite index for more than 1 order by 2 | 3 | SELECT 4 | c.name, 5 | c.age, 6 | { 7 | "phoneNumber": p.number, 8 | "phoneType": p.type 9 | } AS phoneInfo 10 | FROM c 11 | JOIN p IN c.phones 12 | JOIN (SELECT VALUE t FROM t IN p.tags WHERE t.name IN ("winter", "fall")) 13 | WHERE c.age > 21 AND ARRAY_CONTAINS(c.tags, 'student') AND STARTSWITH(p.number, '123') 14 | ORDER BY c.age DESC 15 | OFFSET 10 LIMIT 20 16 | 17 | 18 | SELECT c.id, udf.GetMaxNutritionValue(c.nutrients) AS MaxNutritionValue FROM c 19 | 20 | 21 | SELECT VALUE COUNT(1) FROM models -------------------------------------------------------------------------------- /Services/Blobs.http: -------------------------------------------------------------------------------- 1 | PUT https://myaccount.blob.core.windows.net/mycontainer/myblob?comp=lease 2 | Request Headers: 3 | x-ms-version: 2015-02-21 4 | x-ms-lease-action: acquire 5 | x-ms-lease-duration: -1 # In seconds. -1 is infinite 6 | x-ms-proposed-lease-id: 1f812371-a41d-49e6-b123-f4b542e851c5 7 | x-ms-date: 8 | 9 | # Working with leased blob 10 | PUT https://myaccount.blob.core.windows.net/mycontainer/myblob?comp=metadata 11 | Request Headers: 12 | x-ms-meta-name:string-value 13 | x-ms-lease-id:[lease_id] 14 | 15 | PUT https://myaccount.blob.core.windows.net/mycontainer?comp=metadata?restype=container 16 | x-ms-meta-name:string-value -------------------------------------------------------------------------------- /Services/CosmoDB.sh: -------------------------------------------------------------------------------- 1 | # Create a Cosmos DB account 2 | az cosmosdb create --name $account --kind GlobalDocumentDB ... 3 | 4 | # Create a database 5 | az cosmosdb sql database create --account-name $account --name $database # throughput 6 | 7 | # Create a container 8 | az cosmosdb sql container create --account-name $account --database-name $database --name $container --partition-key-path "/mypartitionkey" 9 | 10 | # Create an item 11 | az cosmosdb sql container item create --account-name $account --database-name $database --container-name $container --value "{\"id\": \"1\", \"mypartitionkey\": \"mypartitionvalue\", \"description\": \"mydescription\"}" -------------------------------------------------------------------------------- /Services/QueueStorage.sh: -------------------------------------------------------------------------------- 1 | az storage account create --name mystorageaccount --resource-group $resourceGroup --location eastus --sku Standard_LRS 2 | az storage queue create --name myqueue --account-name mystorageaccount 3 | az storage queue list --account-name mystorageaccount --output table 4 | az storage message put --queue-name myqueue --account-name mystorageaccount --content "Hello, World!" 5 | az storage message peek --queue-name myqueue --account-name mystorageaccount 6 | az storage message get --queue-name myqueue --account-name mystorageaccount 7 | az storage message delete --queue-name myqueue --account-name mystorageaccount --message-id --pop-receipt 8 | az storage queue delete --name myqueue --account-name mystorageaccount -------------------------------------------------------------------------------- /Services/AZ CLI.md: -------------------------------------------------------------------------------- 1 | # AZ CLI 2 | 3 | ## Prerequisites 4 | 5 | ```sh 6 | # Upgrade the Azure CLI to the latest version 7 | az upgrade 8 | ``` 9 | 10 | ### Extensions 11 | 12 | ```sh 13 | az extension add --name --upgrade 14 | ``` 15 | 16 | - `containerapp` 17 | - `storage-preview` 18 | 19 | ### Providers 20 | 21 | ```sh 22 | # Only needed on subscriptions that haven't previously used it (takes some time for changes to propagate) 23 | az provider register --namespace 24 | 25 | # Check status 26 | az provider show --namespace --query "registrationState" 27 | ``` 28 | 29 | - `Microsoft.App` (App Services - hosting APIs) 30 | - `Microsoft.EventGrid` 31 | - `Microsoft.CDN` 32 | - `Microsoft.OperationalInsights` (telemetry) 33 | - `Microsoft.OperationsManagement` 34 | -------------------------------------------------------------------------------- /Services/ServiceBus.sh: -------------------------------------------------------------------------------- 1 | # 1) Create resource group 2 | # 2) Create namespace 3 | # 3) Create queue 4 | 5 | az group create --name $resourceGroup --location $location 6 | 7 | az servicebus namespace create --name mynamespace --resource-group $resourceGroup --location $location 8 | 9 | az servicebus queue create --name myqueue --namespace-name mynamespace --resource-group $resourceGroup 10 | 11 | az servicebus queue list --namespace-name mynamespace --resource-group $resourceGroup 12 | 13 | az servicebus namespace authorization-rule keys list --name RootManageSharedAccessKey --namespace-name mynamespace --resource-group $resourceGroup --query primaryConnectionString 14 | 15 | az servicebus queue delete --name myqueue --namespace-name mynamespace --resource-group $resourceGroup 16 | az servicebus namespace delete --name mynamespace --resource-group $resourceGroup -------------------------------------------------------------------------------- /Services/EventGrid.ARM.jsonc: -------------------------------------------------------------------------------- 1 | // ARM Template 2 | { 3 | "filter": { 4 | "subjectBeginsWith": "/blobServices/default/containers/mycontainer/log", 5 | "subjectEndsWith": ".jpg", 6 | "includedEventTypes": [ 7 | "Microsoft.Resources.ResourceWriteFailure", 8 | "Microsoft.Resources.ResourceWriteSuccess" 9 | ], 10 | 11 | // enableAdvancedFilteringOnArrays: true // Allow array keys 12 | "advancedFilters": [ 13 | // AND operation 14 | { 15 | "operatorType": "NumberGreaterThanOrEquals", 16 | "key": "Data.Key1", // The field in the event data that you're using for filtering (number, boolean, string) 17 | "value": 5 18 | }, 19 | { 20 | "operatorType": "StringContains", 21 | "key": "Subject", 22 | "values": ["container1", "container2"] // OR operation 23 | } 24 | ] 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Services/AD.cs: -------------------------------------------------------------------------------- 1 | // You'll need to provide the ClientID and Secret when you're setting up an application to authenticate against Azure AD 2 | // When registering, you need to setup identity provider (ex. Microsoft) and then click "Add" 3 | 4 | // Ensure AzureEventSourceListener is in scope and active while using the client library for log collection. 5 | // Create it as a top-level member of the class using the Event Hubs client. 6 | // using AzureEventSourceListener listener = AzureEventSourceListener.CreateConsoleLogger(); 7 | 8 | // DefaultAzureCredentialOptions options = new DefaultAzureCredentialOptions 9 | // { 10 | // Diagnostics = 11 | // { 12 | // LoggedHeaderNames = { "x-ms-request-id" }, 13 | // LoggedQueryParameters = { "api-version" }, 14 | // IsAccountIdentifierLoggingEnabled = true, // enable logging of sensitive information 15 | // IsLoggingContentEnabled = true // log details about the account that was used to attempt authentication and authorization 16 | // } 17 | // }; -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | // Use IntelliSense to find out which attributes exist for C# debugging 6 | // Use hover for the description of the existing attributes 7 | // For further information visit https://github.com/dotnet/vscode-csharp/blob/main/debugger-launchjson.md 8 | "name": ".NET Core Launch (console)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | // If you have changed target frameworks, make sure to update the program path. 13 | "program": "${workspaceFolder}/bin/Debug/net7.0/Project.dll", 14 | "args": [], 15 | "cwd": "${workspaceFolder}", 16 | // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console 17 | "console": "internalConsole", 18 | "stopAtEntry": false 19 | }, 20 | { 21 | "name": ".NET Core Attach", 22 | "type": "coreclr", 23 | "request": "attach" 24 | } 25 | ] 26 | } -------------------------------------------------------------------------------- /Training.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}") = "Project", "Project.csproj", "{3FF06231-9422-4E10-B7A6-FB68339BA5DA}" 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 | {3FF06231-9422-4E10-B7A6-FB68339BA5DA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {3FF06231-9422-4E10-B7A6-FB68339BA5DA}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {3FF06231-9422-4E10-B7A6-FB68339BA5DA}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {3FF06231-9422-4E10-B7A6-FB68339BA5DA}.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 = {158368C8-160D-4DDD-B20D-0658AA5B3070} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/Project.csproj", 11 | "/property:GenerateFullPaths=true", 12 | "/consoleloggerparameters:NoSummary" 13 | ], 14 | "problemMatcher": "$msCompile" 15 | }, 16 | { 17 | "label": "publish", 18 | "command": "dotnet", 19 | "type": "process", 20 | "args": [ 21 | "publish", 22 | "${workspaceFolder}/Project.csproj", 23 | "/property:GenerateFullPaths=true", 24 | "/consoleloggerparameters:NoSummary" 25 | ], 26 | "problemMatcher": "$msCompile" 27 | }, 28 | { 29 | "label": "watch", 30 | "command": "dotnet", 31 | "type": "process", 32 | "args": [ 33 | "watch", 34 | "run", 35 | "--project", 36 | "${workspaceFolder}/Project.csproj" 37 | ], 38 | "problemMatcher": "$msCompile" 39 | } 40 | ] 41 | } -------------------------------------------------------------------------------- /Services/Dockerfile: -------------------------------------------------------------------------------- 1 | # - Compile Stage: 2 | # - Choose a base image suitable for compiling the code. 3 | # - Set the working directory. 4 | # - Copy the source code. 5 | # - Compile the code. 6 | # - Runtime Stage: 7 | # - Choose a base image suitable for running the application. 8 | # - Copy compiled binaries or artifacts from the compile stage (--from=build). 9 | # - Set the command to run the application. 10 | 11 | FROM mcr.microsoft.com/dotnet/core/sdk:3.0 AS build 12 | WORKDIR /app 13 | 14 | # copy csproj and restore as distinct layers 15 | COPY *.sln . 16 | COPY aspnetapp/*.csproj ./aspnetapp/ 17 | RUN dotnet restore 18 | 19 | # copy everything else and build app 20 | COPY aspnetapp/. ./aspnetapp/ 21 | WORKDIR /app/aspnetapp 22 | RUN dotnet publish -c Release -o out 23 | 24 | FROM mcr.microsoft.com/dotnet/core/aspnet:3.0 AS runtime 25 | WORKDIR /app 26 | COPY --from=build /app/aspnetapp/out ./ 27 | ENTRYPOINT ["dotnet", "aspnetapp.dll"] 28 | 29 | 30 | 31 | # Serving both secure and non-secure web traffic: 32 | 33 | FROM mcr.microsoft.com/dotnet/core/aspnet:3.1 AS base 34 | WORKDIR /app 35 | EXPOSE 80 36 | EXPOSE 443 37 | 38 | # copy csproj and restore as distinct layers 39 | FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS build 40 | WORKDIR /src 41 | COPY ["WebApplication1/WebApplication1.csproj", "WebApplication1/"] 42 | RUN dotnet restore "WebApplication1/WebApplication1.csproj" 43 | 44 | # copy everything else and build app 45 | COPY . . 46 | WORKDIR "/src/WebApplication1" 47 | RUN dotnet build "WebApplication1.csproj" -c Release -o /app/build 48 | 49 | FROM build AS publish 50 | RUN dotnet publish "WebApplication1.csproj" -c Release -o /app/publish 51 | 52 | FROM base AS final 53 | WORKDIR /app 54 | COPY --from=publish /app/publish . 55 | ENTRYPOINT ["dotnet", "WebApplication1.dll"] -------------------------------------------------------------------------------- /Services/Functions.sh: -------------------------------------------------------------------------------- 1 | az functionapp plan create 2 | --name 3 | --resource-group 4 | --sku # F1(Free), D1(Shared), B1(Basic Small), B2(Basic Medium), B3(Basic Large), S1(Standard Small), P1V2(Premium V2 Small), I1 (Isolated Small), I2 (Isolated Medium), I3 (Isolated Large), K1 (Kubernetes) 5 | [--is-linux {false, true}] 6 | [--location] 7 | [--max-burst] 8 | [--min-instances] 9 | [--tags] 10 | [--zone-redundant] # Cannot be changed after plan creation. Minimum instance count is 3. 11 | # az functionapp plan create -g $resourceGroup -n MyPlan --min-instances 1 --max-burst 10 --sku EP1 12 | 13 | # Microsoft Defender: Basic 14 | # Consumption: Serverless 15 | # Dedicated: Predictable 16 | 17 | # function.json Connection does not contain connection string 18 | 19 | # List the existing application settings 20 | az functionapp config appsettings list --name $name --resource-group $resourceGroup 21 | 22 | # Add or update an application setting 23 | az functionapp config appsettings set --settings CUSTOM_FUNCTION_APP_SETTING=12345 --name $name --resource-group $resourceGroup 24 | 25 | # Create a new function app (Consumption) 26 | az functionapp create --resource-group $resourceGroup --name $consumptionFunctionName --consumption-plan-location $regionName --runtime dotnet --functions-version 3 --storage-account $storageName 27 | 28 | # Get the default (host) key that can be used to access any HTTP triggered function in the function app 29 | subName='' 30 | resGroup=AzureFunctionsContainers-rg 31 | appName=glengagtestdocker 32 | path=/subscriptions/$subName/resourceGroups/$resGroup/providers/Microsoft.Web/sites/$appName/host/default/listKeys?api-version=2018-11-01 33 | az rest --method POST --uri $path --query functionKeys.default --output tsv 34 | 35 | az functionapp config appsettings set --settings SCALE_CONTROLLER_LOGGING_ENABLED=AppInsights:Verbose 36 | az functionapp config appsettings delete --setting-names SCALE_CONTROLLER_LOGGING_ENABLED -------------------------------------------------------------------------------- /Services/ApiManagement.sh: -------------------------------------------------------------------------------- 1 | # ## Versions and Revisions 2 | # - Revisions: non-breaking changes; no need to publish 3 | # - Versions: breaking changes, requiring publishing and potentially requiring users to update their applications. 4 | 5 | az apim create --name MyAPIMInstance --resource-group $resourceGroup --location eastus --publisher-name "My Publisher" --publisher-email publisher@example.com --sku-name Developer 6 | 7 | # Add a secret (nv - named value) 8 | az apim nv create --resource-group $resourceGroup \ 9 | --display-name "named_value_01" --named-value-id named_value_01 \ 10 | --secret true --service-name apim-hello-world --value test 11 | 12 | # To use a named value in a policy, place its display name inside a double pair of braces like `{{ContosoHeader}}`. 13 | # If the value is an expression, it will be evaluated. If the value is the name of another named value - not. 14 | 15 | 16 | # Calling API with Subscription Key 17 | # Ocp-Apim-Subscription-Key header with subscription-key 18 | # Add the API to a product in Azure Portal 19 | curl --header "Ocp-Apim-Subscription-Key: " https://.azure-api.net/api/path 20 | curl https://.azure-api.net/api/path?subscription-key= 21 | 22 | 23 | # _Header-based versioning_ if the _URL has to stay the same_. Revisions and other types of versioning schemas require modified URL. 24 | 25 | # Creating separate gateways or web APIs would force users to access a different endpoint. A separate gateway provides complete isolation. 26 | az apim api release create --resource-group $resourceGroup \ 27 | --api-id demo-conference-api --api-revision 2 --service-name apim-hello-world \ 28 | --notes 'Testing revisions. Added new "test" operation.' 29 | az group deployment create --resource-group $resourceGroup --template-file ./apis.json --parameters apiRevision="20191206" apiVersion="v1" serviceName= apiVersionSetName= apiName= apiDisplayName= -------------------------------------------------------------------------------- /Services/SAS.sh: -------------------------------------------------------------------------------- 1 | # Best practices: 2 | # - Always use HTTPS. 3 | # - Use Azure Monitor and Azure Storage logs to monitor the application. 4 | # - Use user delegation SAS wherever possible. 5 | # - Set your expiration time to the smallest useful value. 6 | # - Only grant the access that's required. 7 | # - Create a middle-tier service to manage users and their access to storage when there's an unacceptable risk of using a SAS. 8 | 9 | # All need --account-name 10 | # Service and Account Level SAS need --account-key 11 | # Service SAS needs --resource-types 12 | # User Level SAS: --auth-mode login 13 | 14 | az storage container policy create \ 15 | --name \ 16 | --container-name \ 17 | --start \ 18 | --expiry \ 19 | --permissions <(a)dd, (c)reate, (d)elete, (l)ist, (r)ead, or (w)rite> \ 20 | --account-key \ 21 | --account-name 22 | 23 | az role assignment create \ 24 | --role "Storage Blob Data Contributor" \ 25 | --assignee \ 26 | --scope "/subscriptions//resourceGroups//providers/Microsoft.Storage/storageAccounts/" 27 | 28 | # Generate a user delegation SAS for a container 29 | az storage container generate-sas \ 30 | --account-name \ 31 | --name \ 32 | --permissions acdlrw \ 33 | --expiry \ 34 | --auth-mode login \ 35 | --as-user 36 | 37 | # Generate a user delegation SAS for a blob 38 | az storage blob generate-sas \ 39 | --account-name \ 40 | --container-name \ 41 | --name \ 42 | --permissions acdrw \ 43 | --expiry \ 44 | --auth-mode login \ 45 | --as-user \ 46 | --full-uri 47 | 48 | # Revoke all user delegation keys for the storage account 49 | az storage account revoke-delegation-keys \ 50 | --name \ 51 | --resource-group $resourceGroup 52 | ```` -------------------------------------------------------------------------------- /Services/CosmoDB.js: -------------------------------------------------------------------------------- 1 | function validateToDoItemTimestamp() { 2 | var context = getContext(); 3 | var request = context.getRequest(); 4 | 5 | // item to be created in the current operation 6 | var itemToCreate = request.getBody(); 7 | 8 | // validate properties 9 | if (!("timestamp" in itemToCreate)) { 10 | var ts = new Date(); 11 | itemToCreate["timestamp"] = ts.getTime(); 12 | } 13 | 14 | // update the item that will be created 15 | request.setBody(itemToCreate); 16 | } 17 | 18 | // Posttrigger 19 | function updateMetadata() { 20 | var context = getContext(); 21 | var container = context.getCollection(); 22 | var response = context.getResponse(); 23 | 24 | // item that was created 25 | var createdItem = response.getBody(); 26 | 27 | // query for metadata document 28 | var filterQuery = 'SELECT * FROM root r WHERE r.id = "_metadata"'; 29 | var accept = container.queryDocuments( 30 | container.getSelfLink(), 31 | filterQuery, 32 | updateMetadataCallback 33 | ); 34 | if (!accept) throw "Unable to update metadata, abort"; 35 | 36 | function updateMetadataCallback(err, items, responseOptions) { 37 | if (err) throw new Error("Error" + err.message); 38 | if (items.length != 1) throw "Unable to find metadata document"; 39 | 40 | var metadataItem = items[0]; 41 | 42 | // update metadata 43 | metadataItem.createdItems += 1; 44 | metadataItem.createdNames += " " + createdItem.id; 45 | var accept = container.replaceDocument( 46 | metadataItem._self, 47 | metadataItem, 48 | function (err, itemReplaced) { 49 | if (err) throw "Unable to update metadata, abort"; 50 | } 51 | ); 52 | if (!accept) throw "Unable to update metadata, abort"; 53 | return; 54 | } 55 | } 56 | 57 | function tax(income) { 58 | if (income == undefined) throw "no input"; 59 | 60 | if (income < 1000) return income * 0.1; 61 | else if (income < 10000) return income * 0.2; 62 | else return income * 0.4; 63 | } 64 | -------------------------------------------------------------------------------- /Services/AD.sh: -------------------------------------------------------------------------------- 1 | # Creating a resource (like a VM or any other service that supports it) with a system-assigned identity 2 | az create --resource-group $resourceGroup --name myResource --assign-identity '[system]' 3 | 4 | # Assigning a system-assigned identity to an existing resource 5 | az identity assign --resource-group $resourceGroup --name myResource --identities '[system]' 6 | 7 | # First, create the identity 8 | az identity create --resource-group $resourceGroup --name identityName 9 | 10 | # Creating a resource (like a VM or any other service that supports it) with a user-assigned identity 11 | az create --assign-identity $identityName --resource-group $resourceGroup --name $resourceName 12 | #az create --assign-identity '/subscriptions//resourcegroups/$resourceGroup/providers/Microsoft.ManagedIdentity/userAssignedIdentities/myIdentity' --resource-group $resourceGroup --name $resourceName 13 | 14 | # Assigning a user-assigned identity to an existing resource 15 | az identity assign --identities $identityName --resource-group $resourceGroup --name $resourceName 16 | # az identity assign --identities '/subscriptions//resourcegroups/$resourceGroup/providers/Microsoft.ManagedIdentity/userAssignedIdentities/myIdentity' --resource-group $resourceGroup --name $resourceName 17 | 18 | # Here ROLE is about permissions what to do, SCOPE is where these permissions apply to 19 | az role assignment create --assignee --role --scope 20 | 21 | # To protect an API in Azure API Management, register both the backend API and web app, configure permissions to allow the web app to call the backend API 22 | az ad app permission add --id --api --api-permissions =Scope # delegated permissions (user) 23 | az ad app permission add --id --api --api-permissions =Role # application permission -------------------------------------------------------------------------------- /Services/Blobs.ts: -------------------------------------------------------------------------------- 1 | type RelesType = { 2 | rules: [ 3 | { 4 | enabled: boolean; 5 | name: string; 6 | type: "Lifecycle"; 7 | definition: { 8 | actions: { 9 | // NOTE: Delete is the only action available for all blob types; snapshots cannot auto set to hot 10 | version?: RuleAction; 11 | /* blobBlock */ baseBlob?: RuleAction; 12 | snapshopt?: Omit; 13 | appendBlob?: { delete: ActionRunCondition }; // only one lifecycle policy 14 | }; 15 | filters?: { 16 | blobTypes: Array<"appendBlob" | "blockBlob">; 17 | // A prefix string must start with a container name. 18 | // To match the container or blob name exactly, include the trailing forward slash ('/'), e.g., 'sample-container/' or 'sample-container/blob1/' 19 | // To match the container or blob name pattern (wildcard), omit the trailing forward slash, e.g., 'sample-container' or 'sample-container/blob1' 20 | prefixMatch?: string[]; 21 | // Each rule can define up to 10 blob index tag conditions. 22 | // example, if you want to match all blobs with `Project = Contoso``: `{"name": "Project","op": "==","value": "Contoso"}`` 23 | // https://learn.microsoft.com/en-us/azure/storage/blobs/storage-manage-find-blobs?tabs=azure-portal 24 | blobIndexMatch?: Record; 25 | }; 26 | }; 27 | } 28 | ]; 29 | }; 30 | 31 | type RuleAction = { 32 | tierToCool?: ActionRunCondition; 33 | tierToArchive?: { 34 | daysAfterModificationGreaterThan: number; 35 | daysAfterLastTierChangeGreaterThan: number; 36 | }; 37 | enableAutoTierToHotFromCool?: ActionRunCondition; 38 | delete?: ActionRunCondition; 39 | }; 40 | 41 | type ActionRunCondition = { 42 | daysAfterModificationGreaterThan: number; 43 | daysAfterCreationGreaterThan: number; 44 | daysAfterLastAccessTimeGreaterThan: number; // requires last access time tracking 45 | }; -------------------------------------------------------------------------------- /Services/EventGrid.ts: -------------------------------------------------------------------------------- 1 | type EventGridEvent = { 2 | // Full resource path to the event source. 3 | // If not included, Event Grid stamps onto the event. 4 | // If included, it must match the Event Grid topic Azure Resource Manager ID exactly. 5 | topic?: string; 6 | // Publisher-defined path to the event subject. 7 | subject: string; 8 | // One of the registered event types for this event source. 9 | eventType: string; 10 | // The time the event is generated based on the provider's UTC time. 11 | eventTime: string; 12 | // Unique identifier for the event. 13 | id: string; 14 | // Event data specific to the resource provider. 15 | data?: { 16 | // Object unique to each publisher. 17 | // Place your properties specific to the resource provider here. 18 | }; 19 | // The schema version of the data object. The publisher defines the schema version. 20 | // If not included, it is stamped with an empty value. 21 | dataVersion?: string; 22 | // The schema version of the event metadata. Event Grid defines the schema of the top-level properties. 23 | // If not included, Event Grid will stamp onto the event. 24 | // If included, must match the metadataVersion exactly (currently, only 1) 25 | metadataVersion?: string; 26 | }; 27 | 28 | interface CloudEvent { 29 | // Identifies the event. Producers must ensure it's unique. Consumers can assume same source+id means duplicates. 30 | id: string; 31 | 32 | // Identifies the context in which an event happened. 33 | // Syntax defined by the producer, preferably an absolute URI 34 | source: string; 35 | 36 | // The version of the CloudEvents specification used. Compliant producers MUST use value "1.0". 37 | specversion: string; 38 | 39 | // Describes the type of event related to the originating occurrence. 40 | // Should be prefixed with a reverse-DNS name. 41 | type: string; 42 | 43 | subject?: string; // Required in EventSchema, but optional here 44 | 45 | // eventType is now "type" 46 | // eventTime is now "time" and is optional 47 | 48 | // ... 49 | } -------------------------------------------------------------------------------- /Services/KeyVault.sh: -------------------------------------------------------------------------------- 1 | az keyvault set-policy --name --upn user@domain.com \ 2 | # Using Customer-Managed Keys for Encryption: Standard tier app config, soft-delete + purge protection 3 | --key-permissions # To access: GET, WRAP, UNWRAP \ 4 | --secret-permissions \ 5 | --certificate-permissions # delete get list create purge 6 | 7 | 8 | 9 | az login 10 | 11 | # A resource group is a logical container into which Azure resources are deployed and managed. 12 | az group create --name $resourceGroup --location eastus 13 | 14 | # Create a key vault in the same region and tenant as the VMs to be encrypted. 15 | # The key vault will be used to control and manage disk encryption keys and secrets. 16 | az keyvault create --name "" --resource-group $resourceGroup --location "eastus" 17 | 18 | # Update the key vault's advanced access policies 19 | az keyvault update --name "" --resource-group $resourceGroup --enabled-for-disk-encryption "true" 20 | # Enables the Microsoft.Compute resource provider to retrieve secrets from this key vault when this key vault is referenced in resource creation, for example when creating a virtual machine. 21 | az keyvault update --name "" --resource-group $resourceGroup --enabled-for-deployment "true" 22 | # Allow Resource Manager to retrieve secrets from the vault. 23 | az keyvault update --name "" --resource-group $resourceGroup --enabled-for-template-deployment "true" 24 | 25 | # This step is optional. When a key encryption key (KEK) is specified, Azure Disk Encryption uses that key to wrap the encryption secrets before writing to Key Vault. 26 | az keyvault key create --name "myKEK" --vault-name "" --kty RSA --size 4096 27 | 28 | # Enable disk encryption: 29 | ## Optionally use KEK by name 30 | az vm encryption enable -g $resourceGroup --name "myVM" --disk-encryption-keyvault "" --key-encryption-key "myKEK" 31 | ## Optionally use KEK by url 32 | ## Obtain 33 | ## az keyvault key show --vault-name "" --name "myKEK" --query "key.kid" 34 | ## az vm encryption enable -g $resourceGroup --name "MyVM" --disk-encryption-keyvault "" --key-encryption-key-url --volume-type All -------------------------------------------------------------------------------- /Services/Functions_host.jsonc: -------------------------------------------------------------------------------- 1 | // Reduce telemetry: adjust log levels and enable sampling 2 | { 3 | "version": "2.0", 4 | "logging": { 5 | "applicationInsights": { 6 | "samplingSettings": { 7 | "isEnabled": true, 8 | "excludedTypes": "Request" 9 | }, 10 | "snapshotConfiguration": { 11 | "isEnabled": true 12 | } 13 | }, 14 | "logLevel": { 15 | "default": "Information", 16 | "Function": "Error", 17 | "Host.Results": "Critical", 18 | "Function.MyFunction.User": "Trace" 19 | }, 20 | "categoryFilter": { 21 | "defaultLevel": "Information", 22 | "categoryLevels": { 23 | "Host": "Error", 24 | "Function": "Error", 25 | "Host.Aggregator": "Information" 26 | } 27 | }, 28 | "console": { 29 | "isEnabled": true 30 | } 31 | }, 32 | "customHandler": { 33 | "description": { 34 | "defaultExecutablePath": "CustomHandlerExecutable", // 60 sec; In `local.settings.json`, set `FUNCTIONS_WORKER_RUNTIME` to "custom" 35 | "workingDirectory": "./CustomHandlerDir", 36 | "arguments": ["arg1", "arg2"] 37 | }, 38 | "enableForwardingHttpRequest": true 39 | }, 40 | "extensionBundle": { 41 | "id": "Microsoft.Azure.Functions.ExtensionBundle", 42 | "version": "[1.*, 2.0.0)" 43 | }, 44 | "extensions": { 45 | "http": { 46 | "routePrefix": "api", 47 | "maxOutstandingRequests": 200, 48 | "maxConcurrentRequests": 100, 49 | "dynamicThrottlesEnabled": false, 50 | "hsts": { 51 | "isEnabled": true, 52 | "maxAge": "10", 53 | "includeSubDomains": true, 54 | "preload": true 55 | } 56 | }, 57 | "queues": { 58 | "batchSize": 4, 59 | "newBatchThreshold": 8, 60 | "maxDequeueCount": 5, 61 | "visibilityTimeout": "00:00:10" 62 | }, 63 | "blobs": { 64 | "centralizedPoisonQueue": true 65 | } 66 | }, 67 | "functionTimeout": "00:05:00", // 5 min Consumption, 30 Premium 68 | "watchDirectories": ["Shared"], 69 | "healthMonitor": { 70 | "enabled": true, 71 | "healthCheckInterval": "00:00:10", 72 | "healthCheckWindow": "00:02:00", 73 | "healthCheckThreshold": 6, 74 | "counterThreshold": 0.8 75 | }, 76 | "functionWorkerRuntime": "dotnet", 77 | "managedDependency": { 78 | "enabled": true 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Services/ContainerInstance.yml: -------------------------------------------------------------------------------- 1 | apiVersion: "2019-12-01" 2 | location: eastus 3 | name: containerName 4 | properties: 5 | # Container groups: https://learn.microsoft.com/en-us/azure/container-instances/container-instances-container-groups 6 | # Containers use a single host machine, sharing lifecycle, resources, network (share an external IP, ports. DNS), and storage volumes 7 | # For Windows containers, only single-instance deployment are allowed (NOTE: Here we use two!) 8 | # The resources allocated for the host are sum of all resources requested (In this case: 2 CPUs and 2.5 GB RAM) 9 | containers: 10 | - name: helloworld 11 | properties: 12 | environmentVariables: 13 | - name: "PUBLIC_ENV_VAR" 14 | value: "my-exposed-value" 15 | 16 | - name: "SECRET_ENV_VAR" 17 | secureValue: "my-secret-value" 18 | image: mcr.microsoft.com/hello-world 19 | ports: 20 | - port: 443 21 | resources: 22 | requests: 23 | cpu: 1.0 24 | memoryInGB: 1 25 | volumeMounts: 26 | - mountPath: /mnt/secrets 27 | name: secretvolume 28 | - name: hellofiles 29 | properties: 30 | environmentVariables: [] 31 | image: mcr.microsoft.com/azuredocs/aci-hellofiles 32 | ports: 33 | - port: 80 34 | resources: 35 | requests: 36 | cpu: 1.0 37 | memoryInGB: 1.5 38 | volumeMounts: 39 | - mountPath: /aci/logs/ 40 | name: filesharevolume 41 | osType: Linux # or Windows (for single containers) 42 | restartPolicy: Always 43 | ipAddress: 44 | type: Public 45 | ports: 46 | - port: 443 47 | - port: 80 48 | dnsNameLabel: containerName 49 | volumes: 50 | - name: filesharevolume 51 | # Can only be mounted to Linux containers running as root! 52 | azureFile: # No blob storage support 53 | sharename: acishare 54 | storageAccountName: 55 | storageAccountKey: 56 | - name: secretvolume 57 | secret: 58 | # NB: The secret values must be Base64-encoded! 59 | mysecret1: TXkgZmlyc3Qgc2VjcmV0IEZPTwo= # "My first secret FOO" 60 | mysecret2: TXkgc2Vjb25kIHNlY3JldCBCQVIK # "My second secret BAR" 61 | tags: {} 62 | type: Microsoft.ContainerInstance/containerGroups 63 | -------------------------------------------------------------------------------- /Services/EventHub.sh: -------------------------------------------------------------------------------- 1 | # 1) Create resource group 2 | # 2) Create event hub namespace 3 | # 3) Create an Event Hub inside the namespace 4 | # 4) Create consumer group 5 | # 5) (Optional) Enable event hub capture with storage account SAS url and container name 6 | # NOTE: Everything starts with "az eventhubs ..." 7 | 8 | # Create a resource group 9 | az group create --name $resourceGroup --location $location 10 | 11 | # Create an Event Hubs namespace 12 | # Throughput units are specified here 13 | az eventhubs namespace create --name $eventHubNamespace --sku Standard --location $location --resource-group $resourceGroup 14 | 15 | # Get the connection string for a namespace 16 | az eventhubs namespace authorization-rule keys list --namespace-name $eventHubNamespace --name RootManageSharedAccessKey --resource-group $resourceGroup 17 | 18 | # Create an Event Hub inside the namespace 19 | # Partition count and retention days are specified here 20 | az eventhubs eventhub create --partition-count 2 --message-retention 1 --name $eventHub --namespace-name $eventHubNamespace --resource-group $resourceGroup 21 | 22 | # Create a Consumer Group 23 | az eventhubs eventhub consumer-group create --name MyConsumerGroup --eventhub-name $eventHub --namespace-name $eventHubNamespace --resource-group $resourceGroup 24 | 25 | # Capture Event Data (Event Hubs Capture) - Requires Standard+ 26 | # Enable capture and specify the storage account and container 27 | az eventhubs eventhub update --enable-capture True --storage-account sasurl --blob-container containerName --name $eventHub --namespace-name $eventHubNamespace --resource-group $resourceGroup 28 | 29 | # Scale the throughput units (Throughput Units) 30 | az eventhubs namespace update --name $eventHubNamespace --sku Standard --capacity 2 --resource-group $resourceGroup 31 | 32 | # Get the connection string for a specific event hub within a namespace 33 | az eventhubs eventhub authorization-rule keys list --eventhub-name $eventHubName --namespace-name $eventHubNamespace --name MyAuthRuleName --resource-group $resourceGroup 34 | 35 | # Get Event Hub details (Partitions, Consumer Groups) 36 | az eventhubs eventhub show --name $eventHub --namespace-name $eventHubNamespace --resource-group $resourceGroup 37 | 38 | # Delete the Event Hub Namespace (this will delete the Event Hub and Consumer Groups within it) 39 | az eventhubs namespace delete --name $eventHubNamespace --resource-group $resourceGroup -------------------------------------------------------------------------------- /Project.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net7.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.sln.docstates 8 | 9 | # Build results 10 | 11 | [Dd]ebug/ 12 | [Rr]elease/ 13 | x64/ 14 | [Bb]in/ 15 | [Oo]bj/ 16 | 17 | # MSTest test Results 18 | [Tt]est[Rr]esult*/ 19 | [Bb]uild[Ll]og.* 20 | 21 | *_i.c 22 | *_p.c 23 | *_i.h 24 | *.ilk 25 | *.meta 26 | *.obj 27 | *.pch 28 | *.pdb 29 | *.pgc 30 | *.pgd 31 | *.rsp 32 | *.sbr 33 | *.tlb 34 | *.tli 35 | *.tlh 36 | *.tmp 37 | *.tmp_proj 38 | *.log 39 | *.vspscc 40 | *.vssscc 41 | .builds 42 | *.pidb 43 | *.log 44 | *.svclog 45 | *.scc 46 | 47 | # Visual C++ cache files 48 | ipch/ 49 | *.aps 50 | *.ncb 51 | *.opensdf 52 | *.sdf 53 | *.cachefile 54 | 55 | # Visual Studio profiler 56 | *.psess 57 | *.vsp 58 | *.vspx 59 | 60 | # Guidance Automation Toolkit 61 | *.gpState 62 | 63 | # ReSharper is a .NET coding add-in 64 | _ReSharper*/ 65 | *.[Rr]e[Ss]harper 66 | *.DotSettings.user 67 | 68 | # Click-Once directory 69 | publish/ 70 | 71 | # Publish Web Output 72 | *.Publish.xml 73 | *.pubxml 74 | *.azurePubxml 75 | 76 | # NuGet Packages Directory 77 | ## TODO: If you have NuGet Package Restore enabled, uncomment the next line 78 | packages/ 79 | ## TODO: If the tool you use requires repositories.config, also uncomment the next line 80 | !packages/repositories.config 81 | 82 | # Windows Azure Build Output 83 | csx/ 84 | *.build.csdef 85 | 86 | # Windows Store app package directory 87 | AppPackages/ 88 | 89 | # Others 90 | sql/ 91 | *.Cache 92 | ClientBin/ 93 | [Ss]tyle[Cc]op.* 94 | ![Ss]tyle[Cc]op.targets 95 | ~$* 96 | *~ 97 | *.dbmdl 98 | *.[Pp]ublish.xml 99 | 100 | *.publishsettings 101 | 102 | # RIA/Silverlight projects 103 | Generated_Code/ 104 | 105 | # Backup & report files from converting an old project file to a newer 106 | # Visual Studio version. Backup files are not needed, because we have git ;-) 107 | _UpgradeReport_Files/ 108 | Backup*/ 109 | UpgradeLog*.XML 110 | UpgradeLog*.htm 111 | 112 | # SQL Server files 113 | App_Data/*.mdf 114 | App_Data/*.ldf 115 | 116 | # ========================= 117 | # Windows detritus 118 | # ========================= 119 | 120 | # Windows image file caches 121 | Thumbs.db 122 | ehthumbs.db 123 | 124 | # Folder config file 125 | Desktop.ini 126 | 127 | # Recycle Bin used on file shares 128 | $RECYCLE.BIN/ 129 | 130 | # Mac desktop service store files 131 | .DS_Store 132 | 133 | _NCrunch* -------------------------------------------------------------------------------- /Services/Redis.cs: -------------------------------------------------------------------------------- 1 | // - Data cache: Cache databases for fast access; syncs with data changes. 2 | // - Content cache: In-memory cache for static web content. 3 | // - Session store: Cache user history for quick retrieval. 4 | // - Job & message queuing: Defer time-consuming tasks for sequential processing. 5 | // - Distributed transactions: Atomic operations via Azure Cache for Redis. 6 | // - Cache-Aside Pattern: On-demand data caching; updates invalidate cache. Use for unpredictable demand, not for web farm session state. 7 | 8 | // - Private Caching: Local, fast, not scalable, can be inconsistent, simple, for single-user data. 9 | // - Shared Caching: Common source, slower, scalable, consistent, complex, for multi-user data. 10 | 11 | // Eviction Policies: 12 | // - Most-Recently-Used (LIFO) 13 | // - First-In-First-Out 14 | // - Explicit Removal: Based on triggered events like data modification. 15 | 16 | // Tiers: 17 | // - Standard 18 | // - Enterprise: redis modules, hosting replica nodes in different availability zones 19 | // - Enterprise Flash: nonvolatile memory, hosting replica nodes in different availability zones 20 | 21 | // Session State Providers 22 | // - In Memory: Simple and fast. Not scalable, as it's not distributed. 23 | // - SQL Server: Allows for scalability and persistent storage. Can affect performance, though In-Memory OLTP can improve it. 24 | // - Distributed In Memory (e.g., Azure Cache for Redis): MS self-ad 25 | 26 | // TTL (1ms precision): EXPIRE key seconds [NX | XX | GT | LT] 27 | 28 | // Key eviction (ex: maxmemory 100mb): allkeys, volatile (has ttl); lru, lfu 29 | 30 | // Data persistence 31 | // - RDB: Creates binary snapshots, stored in Azure Storage. Restores cache from latest snapshot. 32 | // - AOF: Logs write operations (negatively affects performance/throughput), saved at least once per second in Azure Storage. 33 | 34 | // Supports string and byte[] data 35 | 36 | using Newtonsoft.Json; 37 | using StackExchange.Redis; 38 | 39 | class RedisService 40 | { 41 | class MyEntity { } 42 | async Task CacheAsidePattern(int id) 43 | { 44 | using var redis = ConnectionMultiplexer.Connect("your-redis-connection-string"); 45 | var key = $"MyEntity:{id}"; 46 | var cache = redis.GetDatabase(); 47 | var json = await cache.StringGetAsync(key); 48 | var value = string.IsNullOrWhiteSpace(json) ? default : JsonConvert.DeserializeObject(json!); 49 | if (value == null) // Cache miss 50 | { 51 | // value = ...; // Retrieve from data store 52 | if (value != null) 53 | { 54 | await cache.StringSetAsync(key, JsonConvert.SerializeObject(value)); 55 | await cache.KeyExpireAsync(key, TimeSpan.FromMinutes(5)); 56 | } 57 | } 58 | 59 | RedisResult ping = cache.Execute("ping"); // PONG 60 | RedisResult clients = await cache.ExecuteAsync("client", "list"); // All the connected clients 61 | 62 | return value!; 63 | } 64 | } -------------------------------------------------------------------------------- /Services/ContainerInstance.sh: -------------------------------------------------------------------------------- 1 | # No scaling 2 | 3 | # Login to manage resources 4 | az login 5 | 6 | # Create a resource group 7 | az group create --name $resourceGroup --location eastus 8 | 9 | # (Optional) 10 | 11 | # Deployment 12 | ## 13 | ## NOTE: If using managed identities with ACR, you'll also need --asign-identity param 14 | ## or az container identity assign --identities $identityName --resource-group $resourceGroup --name $containerName 15 | ## 16 | ## From image - simple scenarios 17 | ### 18 | ### Azure File share: https://learn.microsoft.com/en-us/azure/container-instances/container-instances-volume-azure-files 19 | ### Can only be mounted to Linux containers running as root! 20 | ### --os-type Linux 21 | ### --azure-file-volume-account-name # Azure File Share requires existing storage account and account key 22 | ### --azure-file-volume-account-key 23 | ### --azure-file-volume-mount-path 24 | ### --azure-file-volume-share-name 25 | ### NOTE: No direct integration Blob Storage because it lacks SMB support 26 | ### 27 | ### Public DNS name (must be unique) - accessible from $dnsLabel..azurecontainer.io 28 | ### --dns-name-label $dnsLabel 29 | ### --ip-address public 30 | ### 31 | ### [--restart-policy {Always, Never, OnFailure}] # Default: Always. Never if you only want to run once. Status when stopped: Terminated 32 | ### 33 | ### Environment variables: https://learn.microsoft.com/en-us/azure/container-instances/container-instances-environment-variables 34 | #### NOTE: Format can be 'key'='value', key=value, 'key=value' 35 | ### --environment-variables # ex: 'PUBLIC_ENV_VAR'='my-exposed-value' 36 | ### --secure-environment-variables # ex: 'SECRET_ENV_VAR'='my-secret-value' - not visible in your container's properties 37 | ### 38 | ### Mount secret volumes: https://learn.microsoft.com/en-us/azure/container-instances/container-instances-volume-secret 39 | ### --secrets mysecret1="My first secret FOO" mysecret2="My second secret BAR" 40 | ### --secrets-mount-path /mnt/secrets 41 | ### NB: Restricted to Linux containers 42 | ### NOTE: This creates mysecret1 and mysecret2 files in /mnt/secrets with value the content of the secret 43 | ### 44 | az container create --name $containerName --image $imageName:$tag --resource-group $resourceGroup 45 | ## 46 | ## From YAML file - deployment includes only container instances 47 | ### Same options as from simple deployment, but in a YAML file. Includes container groups. 48 | az container create --name $containerName --file deploy.yml --resource-group $resourceGroup 49 | ## 50 | ## ARM template - deploy additional Azure service resources (for example, an Azure Files share) 51 | ### No example, but it's good to know this fact 52 | 53 | # Verify container is running 54 | az container show --name $containerName --resource-group $resourceGroup --query "{FQDN:ipAddress.fqdn,ProvisioningState:provisioningState}" --out table 55 | 56 | # Logging 57 | az container attach # Connects your local console to a container's output and error streams in real time (example: to debug startup issue). 58 | az container logs # Displays logs (when no real time monitoring is needed) -------------------------------------------------------------------------------- /Services/CDN.cs: -------------------------------------------------------------------------------- 1 | // User requests file via special URL. It directs to nearest POP. 2 | // If not cached, fetched from origin server (Azure or web server). 3 | // Sent to user and cached at POP for faster delivery to others. 4 | 5 | // ETag and Last-Modified control cache behavior. 6 | // Content is cached based on TTL, determined by `Cache-Control` header 7 | // - Generalized: 7 days 8 | // - Large files: 1 day 9 | // - Media streaming: one year 10 | 11 | // Caching behavior settings: 12 | // - Bypass cache: No caching; ignore origin headers. 13 | // - Override: Use provided duration, except for cache-control: no-cache. 14 | // - Set if missing: Use origin headers or provided duration if absent. 15 | 16 | // Purging clears main servers, not browser caches. To update users, rename files or use caching methods. 17 | // Recreating a CDN endpoint also purges content from edge servers. (don't!) 18 | 19 | // To ensure users receive the latest version of a file, include a version string in the URL or purge cached content 20 | 21 | // Compression (Front Door): specific type, 1kb-8mb; gzip and brotli 22 | 23 | // Search: "cdn features" for sku 24 | 25 | using Microsoft.Azure.Management.Cdn; 26 | using Microsoft.Azure.Management.Cdn.Models; 27 | using Microsoft.Rest; 28 | 29 | class CDNService 30 | { 31 | // You need to configure Azure Active Directory to provide authentication for the application 32 | public static void ManageCdnEndpoint(string subscriptionId, TokenCredentials authResult, string resourceGroupName, string profileName, string endpointName, string resourceLocation) 33 | { 34 | // Create CDN client 35 | CdnManagementClient cdn = new CdnManagementClient(authResult) { SubscriptionId = subscriptionId }; 36 | 37 | // List all the CDN profiles in this resource group 38 | var profileList = cdn.Profiles.ListByResourceGroup(resourceGroupName); 39 | foreach (Profile p in profileList) 40 | { 41 | // List all the CDN endpoints on this CDN profile 42 | var endpointList = cdn.Endpoints.ListByProfile(p.Name, resourceGroupName); 43 | foreach (Endpoint e in endpointList) { } 44 | } 45 | 46 | // Create a new CDN profile (check if not exist first!) 47 | var profileParms = new Profile() { Location = resourceLocation, Sku = new Sku(SkuName.StandardVerizon) }; 48 | cdn.Profiles.Create(resourceGroupName, profileName, profileParms); 49 | 50 | // Create a new CDN endpoint (check if not exist first!) 51 | var endpoint = new Endpoint() 52 | { 53 | Origins = new List() { new DeepCreatedOrigin("Contoso", "www.contoso.com") }, 54 | IsHttpAllowed = true, 55 | IsHttpsAllowed = true, 56 | Location = resourceLocation 57 | }; 58 | cdn.Endpoints.BeginCreateWithHttpMessagesAsync(resourceGroupName, profileName, endpointName, endpoint); 59 | 60 | // Purge content from the endpoint 61 | cdn.Endpoints.PurgeContent(resourceGroupName, profileName, endpointName, new List() { "/*" }); 62 | } 63 | } -------------------------------------------------------------------------------- /Services/EventGrid.sh: -------------------------------------------------------------------------------- 1 | # Create resource group 2 | # Create topic 3 | # (Optional) Create a supported service 4 | # Create subscription with topic id as source and service id as endpoint (az eventgrid event-subscription create) 5 | # 6 | # Subscription (az eventgrid event-subscription) 7 | # Dead lettering: point to storage account 8 | 9 | az provider register --namespace Microsoft.EventGrid 10 | 11 | # Create a resource group 12 | az group create --name $resourceGroup --location $myLocation 13 | 14 | # Create a custom topic - the endpoint where publishers send events. Used for related events 15 | az eventgrid topic create --name $topicName --location $location --resource-group $resourceGroup 16 | 17 | # Get resource IDs 18 | ## Topic 19 | topicId=$(az eventgrid topic show --name $topicName --resource-group $resourceGroup --query "id" --output tsv) 20 | ## EventHub (for example) 21 | serviceId=$(az eventhubs eventhub show --name $eventHubName --namespace-name $namespaceName --resource-group $resourceGroup --query "id" --output tsv) 22 | 23 | # Link the Event Grid Topic to service 24 | az eventgrid event-subscription create --name $name --resource-group $resourceGroup \ 25 | --source-resource-id $topicId \ 26 | --endpoint-type webhook # {eventhub,storagequeue,servicebusqueue} \ 27 | --endpoint $serviceId # resourceId of the endpoint type; or url: https://contoso.azurewebsites.net/api/f1?code=code 28 | # [--expiration-date] - for temporary needs; no need to cleanup afterwards 29 | # Batching 30 | # [--max-events-per-batch] 31 | # [--preferred-batch-size-in-kilobytes] Events bigger than the size will be sent as their own batch 32 | 33 | # Dead lettering (set empty to disable) 34 | storageid=$(az storage account show --name demoStorage --resource-group gridResourceGroup --query id --output tsv) 35 | az eventgrid event-subscription update --name $name \ 36 | --deadletter-endpoint $storageid/blobServices/default/containers/$containername 37 | 38 | # Filters 39 | az eventgrid event-subscription update --name $name \ 40 | --advanced-filter data.url StringBeginsWith https://myaccount.blob.core.windows.net # Can have multiple --advanced-filter (up to 25) \ 41 | --subject-case-sensitive {false, true} \ 42 | --subject-begins-with mysubject_prefix # ex: /blobServices/default/containers/ \ 43 | --subject-ends-with mysubject_suffix # ex: .txt 44 | 45 | # Alt: System topic for storage account 46 | az eventgrid system-topic create --name $name --resource-group $resourceGroup \ 47 | --source $storageid \ 48 | --topic-type microsoft.storage.storageaccounts 49 | 50 | # Send an event to the custom topic 51 | ## Need to pass key as aeg-sas-key header 52 | topicEndpoint=$(az eventgrid topic show --name $topicName -g $resourceGroup --query "endpoint" --output tsv) 53 | key=$(az eventgrid topic key list --name $topicName -g $resourceGroup --query "key1" --output tsv) 54 | event='[ {"id": "'"$RANDOM"'", "eventType": "recordInserted", "subject": "myapp/vehicles/motorcycles", "eventTime": "'`date +%Y-%m-%dT%H:%M:%S%z`'", "data":{ "make": "Contoso", "model": "Monster"},"dataVersion": "1.0"} ]' 55 | curl -X POST -H "aeg-sas-key: $key" -d "$event" $topicEndpoint -------------------------------------------------------------------------------- /Services/ManagedIdentities.cs: -------------------------------------------------------------------------------- 1 | // Token caching via TokenCachePersistenceOptions() 2 | // - memory (default): Managed identities 3 | // - disk 4 | 5 | // Public client applications: User-facing apps without the ability to securely store secrets. They interact with web APIs on the user's behalf. 6 | // Confidential client applications: Server-based apps and daemons that can securely handle secrets. Each instance maintains a unique configuration, including identifiers and secrets. 7 | 8 | // Changes to your application object also affect its service principals in the home tenant only. 9 | // Deleting the application also deletes its home tenant service principal, 10 | // but restoring that application object won't recover its service principals. 11 | 12 | // Search: identity platform 13 | /* 14 | 1. Register in Azure AD ✓ ✓ ✓ 15 | 2. Configure app with code sample ✕ ✓ ✕ 16 | 3. Validate token ID Access ✕ 17 | 4. Configure secrets & certificates ✓ ✓ ✓ 18 | 5. Configure permission & call API of choice ✓ ✓ ✓ 19 | 6. Control access (authorization) ✓ ✓ (add validate-jwt policy to validate the OAuth token) ✕ 20 | 7. Store token cache ✓ ✓ ✓ 21 | */ 22 | 23 | // Check for transitive membership in a list of groups (add checkMemberGroups): 24 | // POST /me/checkMemberGroups 25 | // POST /users/{id | userPrincipalName}/checkMemberGroups 26 | 27 | // - Azure AD B2C: social media or user/pass 28 | // - Azure AD B2B: share apps with external users. 29 | // - Azure AD Application Proxy: secure remote access to on-premises applications. 30 | // - Azure AD Connect: synchronize an AD tenant with an on-premises AD domain. 31 | // - Azure AD Enterprise Application: integrate other applications with Azure AD, including your own apps. 32 | 33 | // Search: auth flows 34 | // - Authorization code: code for token 35 | // - Client credentials: Confidential App Secret/Certificate 36 | // - On-behalf-of: existing token to get another 37 | // - Device code: Polls the endpoint until user auth 38 | // - Implicit: Token in URI fragment 39 | 40 | // Search: app manifest overview 41 | 42 | using Azure.Identity; 43 | using Microsoft.Identity.Client; 44 | 45 | class AuthService 46 | { 47 | // Search: msal dev (2) 48 | async Task Public() 49 | { 50 | IPublicClientApplication app = PublicClientApplicationBuilder.Create("your_client_id") 51 | .WithAuthority(AzureCloudInstance.AzurePublic, "your_tenant_id") 52 | .WithRedirectUri("http://localhost") 53 | .Build(); 54 | 55 | var scopes = new[] { "User.Read" }; 56 | 57 | AuthenticationResult result = await app.AcquireTokenInteractive(scopes).ExecuteAsync(); 58 | 59 | Console.WriteLine($"Token:\n{result.AccessToken}"); 60 | } 61 | 62 | async Task Confidential() 63 | { 64 | IConfidentialClientApplication app = ConfidentialClientApplicationBuilder.Create("your_client_id") 65 | .WithClientSecret("your_client_secret") 66 | .WithAuthority(new Uri("https://login.microsoftonline.com/your_tenant_id")) // public too 67 | .Build(); 68 | 69 | var scopes = new[] { "https://graph.microsoft.com/.default" }; 70 | 71 | AuthenticationResult result = await app.AcquireTokenForClient(scopes).ExecuteAsync(); 72 | 73 | Console.WriteLine($"Token:\n{result.AccessToken}"); 74 | } 75 | } 76 | 77 | class TokenStorageService 78 | { 79 | void UseDisk() 80 | { 81 | var persistenceOptions = new TokenCachePersistenceOptions 82 | { 83 | Name = "my_cache_name", // specify a cache name 84 | UnsafeAllowUnencryptedStorage = true // opt-in for unencrypted storage 85 | }; 86 | 87 | var credential = new InteractiveBrowserCredential( 88 | new InteractiveBrowserCredentialOptions { TokenCachePersistenceOptions = persistenceOptions } 89 | ); 90 | } 91 | 92 | void UseMemory() 93 | { 94 | new DefaultAzureCredential(); 95 | } 96 | } -------------------------------------------------------------------------------- /Services/KeyVault.cs: -------------------------------------------------------------------------------- 1 | // Perfect Forward Secrecy (PFS): protects connections between customer and cloud services by unique keys 2 | 3 | // Auth 4 | // PUT https://.vault.azure.net/keys/?api-version=7.2 HTTP/1.1 5 | // Authorization: Bearer # token obtained from Azure Active Directory 6 | 7 | // ## Best Practices 8 | // - Use a separate vault for each application and environment (production, test, staging). 9 | // - Restrict vault access to authorized applications and users. (`az keyvault set-policy --name --object-id --secret-permissions get list`) 10 | // - Regularly backup your vault. (`az keyvault key backup --vault-name --name --file `) 11 | // - Enable logging and alerts. 12 | // - Enable **soft-delete** and **purge protection** to keep secrets for 7-90 days and prevent forced deletion. Charges apply for HSM-keys in the last 30 days of use. Operations are disabled on deleted objects, and no charges apply. (NOTE: _soft-delete_ increased security, but also _increases storage cost_!) 13 | 14 | using System.Text; 15 | using Azure.Identity; 16 | using Azure.Messaging.EventGrid; 17 | using Azure.Security.KeyVault.Certificates; 18 | using Azure.Security.KeyVault.Keys; 19 | using Azure.Security.KeyVault.Keys.Cryptography; 20 | using Azure.Security.KeyVault.Secrets; 21 | using Microsoft.AspNetCore.Http; 22 | using Microsoft.AspNetCore.Mvc; 23 | using Microsoft.Azure.WebJobs; 24 | using Microsoft.Azure.WebJobs.Extensions.Http; 25 | using Microsoft.Extensions.Logging; 26 | 27 | class KeyVaultService 28 | { 29 | async Task Crypt() 30 | { 31 | var vaultUrl = "https://.vault.azure.net/"; 32 | 33 | // Fetching a secret 34 | var secretClient = new SecretClient(vaultUri: new Uri(vaultUrl), credential: new DefaultAzureCredential()); 35 | KeyVaultSecret secret = await secretClient.GetSecretAsync("YourSecretName"); 36 | Console.WriteLine($"Fetched Secret: {secret.Value}"); 37 | 38 | var keyClient = new KeyClient(vaultUri: new Uri(vaultUrl), credential: new DefaultAzureCredential()); 39 | // Creating a new key 40 | KeyVaultKey key = await keyClient.GetKeyAsync("YourKeyName"); 41 | // Encrypting and decrypting data using the key via CryptographyClient 42 | CryptographyClient cryptoClient = keyClient.GetCryptographyClient(key.Name, key.Properties.Version); 43 | EncryptResult encryptResult = cryptoClient.Encrypt(EncryptionAlgorithm.RsaOaep, Encoding.UTF8.GetBytes("plaintext")); 44 | DecryptResult decryptResult = cryptoClient.Decrypt(EncryptionAlgorithm.RsaOaep, encryptResult.Ciphertext); 45 | } 46 | 47 | async Task Cert() 48 | { 49 | var client = new CertificateClient(new Uri("https://.vault.azure.net"), new DefaultAzureCredential()); 50 | 51 | // Create certificate 52 | var operation = await client.StartCreateCertificateAsync("certificateName", CertificatePolicy.Default); 53 | await operation.WaitForCompletionAsync(); 54 | 55 | // Retrieve 56 | var certificate = await client.GetCertificateAsync("certificateName"); 57 | } 58 | } 59 | 60 | class KeyVaultFunctions 61 | { 62 | // Portal > All Services > Key Vaults > key vault > Events > Event Grid Subscriptions > + Event Subscription 63 | [FunctionName("KeyVaultMonitoring")] 64 | public static async Task Run( 65 | [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req, ILogger log) 66 | { 67 | var requestBody = await new StreamReader(req.Body).ReadToEndAsync(); 68 | var eventGridEvent = EventGridEvent.Parse(new BinaryData(requestBody)); 69 | 70 | switch (eventGridEvent.EventType) 71 | { 72 | case SystemEventNames.KeyVaultCertificateNewVersionCreated: 73 | case SystemEventNames.KeyVaultSecretNewVersionCreated: 74 | log.LogInformation($"New Key Vault secret/certificate version created event. Data: {eventGridEvent.Data}"); break; 75 | case SystemEventNames.KeyVaultKeyNewVersionCreated: 76 | log.LogInformation($"New Key Vault key version created event. Data: {eventGridEvent.Data}"); break; 77 | default: 78 | log.LogInformation($"Event Grid Event of type {eventGridEvent.EventType} occurred, but it's not processed."); break; 79 | } 80 | 81 | return new OkResult(); 82 | } 83 | } -------------------------------------------------------------------------------- /Services/QueueStorage.cs: -------------------------------------------------------------------------------- 1 | using System.Configuration; 2 | using Azure.Storage.Queues; 3 | using Azure.Storage.Queues.Models; 4 | using Microsoft.AspNetCore.Http; 5 | using Microsoft.Azure.WebJobs; 6 | using Microsoft.Azure.WebJobs.Extensions.Http; 7 | using Microsoft.Extensions.Logging; 8 | 9 | // Endpoint: `https://queue.core.windows.net` 10 | 11 | // - May contain millions of messages, up to the total capacity limit of a storage account. 12 | // - Commonly used to create a backlog of work to process asynchronously. 13 | // - Max size: 64KB 14 | // - TTL: 7 days(⏺️), -1 to never expire. 15 | // - Applications can scale indefinitely to meet demand. 16 | // - Receive mode: Peek & Lease 17 | 18 | class StorageQueueService 19 | { 20 | string connectionString = ConfigurationManager.AppSettings["StorageConnectionString"] ?? throw new Exception(); 21 | string queueName = "storagequeue"; 22 | 23 | async Task Handle() 24 | { 25 | // Instantiate a QueueClient which will be used to create and manipulate the queue 26 | QueueClient queueClient = new QueueClient(connectionString, queueName); 27 | 28 | // Create the queue if it doesn't already exist 29 | await queueClient.CreateIfNotExistsAsync(); 30 | 31 | if (await queueClient.ExistsAsync()) 32 | { 33 | await queueClient.SendMessageAsync("message"); 34 | await queueClient.SendMessageAsync(new BinaryData(new { Name = "John", Age = 30 })); 35 | 36 | // Peek at the next message 37 | // If you don't pass a value for the `maxMessages` parameter, the default is to peek at one message. 38 | PeekedMessage[] peekedMessages = await queueClient.PeekMessagesAsync(); 39 | 40 | // Peek single message (no await) 41 | PeekedMessage peekedMessage = queueClient.PeekMessage(); 42 | 43 | // Change the contents of a message in-place 44 | // This code saves the work state and grants the client an extra minute to continue their message (default is 30 sec). 45 | // Removes the messages 46 | QueueMessage[] message = await queueClient.ReceiveMessagesAsync(); 47 | // PopReceipt must be provided when performing operations to the message 48 | // in order to prove that the client has the right to do so when locked 49 | queueClient.UpdateMessage(message[0].MessageId, 50 | message[0].PopReceipt, 51 | "Updated contents", 52 | TimeSpan.FromSeconds(60.0) // Make it invisible for another 60 seconds 53 | ); 54 | 55 | // Dequeue the next message 56 | QueueMessage[] retrievedMessage = await queueClient.ReceiveMessagesAsync(); 57 | Console.WriteLine($"Dequeued message: '{retrievedMessage[0].Body}'"); 58 | await queueClient.DeleteMessageAsync(retrievedMessage[0].MessageId, retrievedMessage[0].PopReceipt); 59 | 60 | // Get the queue length 61 | QueueProperties properties = await queueClient.GetPropertiesAsync(); 62 | int cachedMessagesCount = properties.ApproximateMessagesCount; // >= of actual messages count 63 | Console.WriteLine($"Number of messages in queue: {cachedMessagesCount}"); 64 | 65 | // Delete the queue 66 | await queueClient.DeleteAsync(); 67 | } 68 | } 69 | } 70 | 71 | class QueueStorageFunctions 72 | { 73 | [FunctionName(nameof(RunQueue))] 74 | public static void RunQueue([QueueTrigger("queue", Connection = "StorageConnectionAppSetting")] string myQueueItem, ILogger log) 75 | { 76 | log.LogInformation($"Queue trigger function processed: {myQueueItem}"); 77 | } 78 | 79 | // No Input binding 80 | 81 | [FunctionName(nameof(QueueStorageOutputBinding))] 82 | [return: Queue("queue")] 83 | public static string QueueStorageOutputBinding( 84 | [HttpTrigger(AuthorizationLevel.Function, "post", Route = "queue/{message}")] HttpRequest req, string message, 85 | ILogger log) 86 | { 87 | // Sends a message to Azure Queue Storage 88 | log.LogInformation($"Message sent: {message}"); 89 | return message; 90 | } 91 | 92 | [FunctionName(nameof(AddMessages))] 93 | public static void AddMessages( 94 | [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req, 95 | [Queue("outqueue"), StorageAccount("AzureWebJobsStorage")] ICollector msg, 96 | ILogger log) 97 | { 98 | msg.Add("First"); 99 | msg.Add("Second"); 100 | } 101 | } -------------------------------------------------------------------------------- /Services/Application Insights.cs: -------------------------------------------------------------------------------- 1 | // Extension of Azure Monitor and provides Application Performance Monitoring (APM) 2 | 3 | // Metrics 4 | // - Log-based metrics: thorough, complete sets of events. 5 | // - Standard: pre-aggregated, use backend for better accuracy. For real time, sampling/filtering 6 | // - Sampling: 7 | // - Adaptive: adjust in certain limits; used in functions 8 | // - Fixed-rate: for syncing client and server data to investigations of related events. 9 | // - Ingestion sampling: discard data to stay in monthly limit 10 | 11 | // Group custom events by same OperationId value 12 | 13 | // Usage analysis 14 | // - Users tool: Counts unique app users per browser/machine. 15 | // - Sessions tool: Tracks feature usage per session; resets after 30min inactivity or 24hr use. 16 | // - Events tool: Measures page views and custom events like clicks. 17 | // - Funnels: For linear, step-by-step processes 18 | // - User Flows: For understanding complex, branching user behavior 19 | // - Cohorts: Things in common 20 | // - Impact: Performance effects 21 | // - Retention: Returning users 22 | 23 | // instrumentation key: provide authorized access - send telemetry data from app to Application Insights 24 | 25 | // Monitor an app (Instrumentation) 26 | // - Auto: Through config, no app code. OpenCensus for tracking metrics across services and technologies 27 | // - Manual: Coding against the Application Insights or OpenTelemetry API. Supports Azure AD and Complex Tracing (collect data that is not available in Application Insights) 28 | 29 | // Availability test 30 | // - URL ping test: Checks endpoint, measures performance, customizable. Uses public DNS. 31 | // - Standard test: Single request, covers SSL, HTTP verbs, custom headers. 32 | // - Custom TrackAvailability: For multi-request/authentication tests. Use TrackAvailability() in code editor. 33 | // Create an alert that will notify you via email if the web app becomes unresponsive: 34 | // Portal > Application Insights resource > Availability > Add Test option > Rules (Alerts) > set action group for availability alert > Configure notifications (email, SMS) 35 | 36 | // Azure Monitor: Infrastructure and multi-resource; hybrid and multi-cloud environments. 37 | // - Activity Log: subscription-level events 38 | // - Log Analytics: Kusto 39 | // - Azure Storage account: audit, static analysis, or backup 40 | // - Azure Event Hubs: external systems 41 | // Application Insights: App-level monitoring, focuses on web apps/services. 42 | 43 | using System.Diagnostics; 44 | using Microsoft.ApplicationInsights; 45 | using Microsoft.ApplicationInsights.DataContracts; 46 | using Microsoft.ApplicationInsights.Extensibility; 47 | 48 | class ApplicationInsightsService 49 | { 50 | void Telemetry() 51 | { 52 | TelemetryConfiguration configuration = TelemetryConfiguration.CreateDefault(); 53 | configuration.InstrumentationKey = "your-instrumentation-key-here"; 54 | var telemetry = new TelemetryClient(configuration); 55 | // This information is attached to all events that the instance sends. 56 | telemetry.Context.User.Id = "..."; 57 | telemetry.Context.Device.Id = "..."; 58 | 59 | // For multi-request/authentication tests. 60 | telemetry.TrackAvailability("testName", DateTimeOffset.Now, TimeSpan.FromSeconds(30), "runLocation", true); 61 | 62 | telemetry.TrackEvent("WinGame"); // Custom events 63 | 64 | telemetry.GetMetric("metricId"); // pre-aggregation; lowers cost; no sampling 65 | 66 | // Prefer GetMetric() 67 | telemetry.TrackMetric(new MetricTelemetry() { Name = "queueLength", Sum = 42.3 }); 68 | 69 | telemetry.TrackPageView("GameReviewPage"); 70 | 71 | // Send a "breadcrumb trail" to Application Insights 72 | // Lets you send longer data such as POST information. 73 | telemetry.TrackTrace("Some message", SeverityLevel.Warning); 74 | 75 | // Event log: use ILogger or a class inheriting EventSource. 76 | 77 | // Track the response times and success rates of calls to an external piece of code 78 | var success = false; 79 | var startTime = DateTime.UtcNow; 80 | var timer = Stopwatch.StartNew(); 81 | try { success = true; } 82 | catch (Exception ex) 83 | { 84 | // Send exceptions to Application Insights 85 | telemetry.TrackException(ex); 86 | 87 | // Log exceptions to a diagnostic trace listener (Trace.aspx). 88 | Trace.TraceError(ex.Message); 89 | } 90 | finally 91 | { 92 | timer.Stop(); 93 | // Send data to Dependency Tracking in Application Insights 94 | telemetry.TrackDependency("DependencyType", "myDependency", "myCall", startTime, timer.Elapsed, success); 95 | } 96 | 97 | telemetry.Flush(); 98 | 99 | 100 | } 101 | } -------------------------------------------------------------------------------- /Services/Graph.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http.Headers; 2 | using Azure.Identity; 3 | using Microsoft.Graph; 4 | using Microsoft.Graph.Models; 5 | using Microsoft.Identity.Client; 6 | 7 | namespace Services; 8 | 9 | // Connectors: deliver external to Graph 10 | // Data Connect: deliver to other Azure Services 11 | 12 | // Headers always returned: request-id 13 | 14 | // Auth with Bearer 15 | 16 | // Full set of HTTP operations 17 | 18 | // Pagination via @odata.nextLink 19 | // Metadata: https://graph.microsoft.com/v1.0/$metadata 20 | // My photo: https://graph.microsoft.com/v1.0/me/photo/$value 21 | // My photo metadata: https://graph.microsoft.com/v1.0/me/photo/ 22 | // Filter: ?filter= eq '' 23 | // Limit: ?top=5 24 | 25 | class GraphService 26 | { 27 | async Task MSAL() 28 | { 29 | var authority = "https://login.microsoftonline.com/" + "tenantId"; 30 | var scopes = new[] { "https://graph.microsoft.com/.default" }; 31 | 32 | var app = ConfidentialClientApplicationBuilder.Create("clientId") 33 | .WithAuthority(authority) 34 | .WithClientSecret("clientSecret") 35 | .Build(); 36 | 37 | var result = await app.AcquireTokenForClient(scopes).ExecuteAsync(); 38 | 39 | var httpClient = new HttpClient(); 40 | var request = new HttpRequestMessage(HttpMethod.Get, "https://graph.microsoft.com/v1.0/me"); 41 | request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken); 42 | 43 | var response = await httpClient.SendAsync(request); 44 | var content = await response.Content.ReadAsStringAsync(); 45 | } 46 | 47 | async Task SDK() 48 | { 49 | var scopes = new[] { "User.Read" }; 50 | 51 | // Multi-tenant apps can use "common", 52 | // single-tenant apps must use the tenant ID from the Azure portal 53 | var tenantId = "common"; 54 | 55 | // Value from app registration 56 | var clientId = "YOUR_CLIENT_ID"; 57 | 58 | var options = new TokenCredentialOptions 59 | { 60 | AuthorityHost = AzureAuthorityHosts.AzurePublicCloud 61 | }; 62 | 63 | // Using device code: https://learn.microsoft.com/dotnet/api/azure.identity.devicecodecredential 64 | var deviceOptions = new DeviceCodeCredentialOptions 65 | { 66 | AuthorityHost = AzureAuthorityHosts.AzurePublicCloud, 67 | ClientId = clientId, 68 | TenantId = tenantId, 69 | // Callback function that receives the user prompt 70 | // Prompt contains the generated device code that user must 71 | // enter during the auth process in the browser 72 | DeviceCodeCallback = (code, cancellation) => 73 | { 74 | Console.WriteLine(code.Message); 75 | return Task.FromResult(0); 76 | }, 77 | }; 78 | var credential = new DeviceCodeCredential(deviceOptions); 79 | // var credential = new DeviceCodeCredential(callback, tenantId, clientId, options); 80 | 81 | // Using a client certificate: https://learn.microsoft.com/dotnet/api/azure.identity.clientcertificatecredential 82 | // var clientCertificate = new X509Certificate2("MyCertificate.pfx"); 83 | // var credential = new ClientCertificateCredential(tenantId, clientId, clientCertificate, options); 84 | 85 | // Using a client secret: https://learn.microsoft.com/dotnet/api/azure.identity.clientsecretcredential 86 | // var credential = new ClientSecretCredential(tenantId, clientId, clientSecret, options); 87 | 88 | // On-behalf-of provider 89 | // var oboToken = "JWT_TOKEN_TO_EXCHANGE"; 90 | // var onBehalfOfCredential = new OnBehalfOfCredential(tenantId, clientId, clientSecret, oboToken, options); 91 | 92 | var graphClient = new GraphServiceClient(credential, scopes); 93 | 94 | var user = await graphClient.Me.GetAsync(); 95 | 96 | var messages = await graphClient.Me.Messages 97 | .GetAsync(requestConfig => 98 | { 99 | requestConfig.QueryParameters.Select = 100 | new string[] { "subject", "sender" }; 101 | requestConfig.QueryParameters.Filter = 102 | "subject eq 'Hello world'"; 103 | 104 | requestConfig.Headers.Add( 105 | "Prefer", @"outlook.timezone=""Pacific Standard Time"""); 106 | }); 107 | 108 | var message = await graphClient.Me.Messages["messageId"].GetAsync(); 109 | 110 | var newCalendar = await graphClient.Me.Calendars 111 | .PostAsync(new Calendar { Name = "Volunteer" }); // new 112 | 113 | await graphClient.Teams["teamId"] 114 | .PatchAsync(new Team { }); // update 115 | 116 | await graphClient.Me.Messages["messageId"] 117 | .DeleteAsync(); 118 | } 119 | } -------------------------------------------------------------------------------- /Services/ContainerRegistry.sh: -------------------------------------------------------------------------------- 1 | # Login to manage resources 2 | az login 3 | 4 | # Create a resource group 5 | az group create --name $resourceGroup --location eastus 6 | 7 | # Create Azure Container Registry 8 | ## https://learn.microsoft.com/en-us/azure/container-registry/container-registry-skus 9 | ## --sku {Basic,Standard,Premium} # 10, 100, 500GB; 💎: Concurrent operations, High volumes (⚡), Customer-Managed Key, Content trust for image tag signing, Private link 10 | ## Throttling: May happen if you exceed the registry's limits, causing temporary `HTTP 429` errors and requiring retry logic or reducing the request rate. 11 | ## 12 | ## [--default-action {Allow, Deny}] # 💎: Default action when no rules apply 13 | ## 14 | ## https://learn.microsoft.com/en-us/azure/container-registry/zone-redundancy 15 | ## [--zone-redundancy {Disabled, Enabled}] # 💎: Min 3 separate zones in each enabled region. The environment must include a virtual network (VNET) with an available subnet. 16 | az acr create --resource-group $resourceGroup --name $registryName --sku Standard # ⭐: Production 17 | # NOTE: High numbers of repositories and tags can impact the performance. Periodically delete unused. 18 | 19 | # ACR Login: https://learn.microsoft.com/en-us/azure/container-registry/container-registry-authentication 20 | ## - Interactive: Individual AD login, Admin Account 21 | ## - Unatended / Headless: AD Service Principal, Managed Identity for Azure Resources 22 | ## Roles: https://learn.microsoft.com/en-us/azure/container-registry/container-registry-roles?tabs=azure-cli 23 | ## 24 | ## 1) Individual login with Azure AD: Interactive push/pull by developers, testers. 25 | ## az login - provides the token. It has to be renewed every 3 hours 26 | az acr login --name "$registryName" # Token must be renewed every 3 hours. 27 | ## 28 | ## 2) AD Service Principal: Unattended push/pull in CI/CD pipelines 29 | ### Create service principal 30 | #### Method 1: Short version that will setup and return appId and password in JSON format 31 | az ad sp create-for-rbac --name $ServicePrincipalName --role AcrPush,AcrPull,AcrDelete --scopes /subscriptions/$subscriptionId/resourceGroups/$resourceGroup/providers/Microsoft.ContainerRegistry/registries/$registryName 32 | #### Method 2: Create a service principal and configure roles separately 33 | az ad sp create --id $ServicePrincipalName 34 | az role assignment create --assignee $appId --role AcrPush,AcrPull,AcrDelete --scope /subscriptions/$subscriptionId/resourceGroups/$resourceGroup/providers/Microsoft.ContainerRegistry/registries/$registryName 35 | az ad sp credential reset --name $appId # for method 2 password is not explicitly created, so we need to create (reset) it 36 | #### Note: Password expires in 1 year. 37 | az acr login --name $registryName --username $appId --password $password 38 | ## 39 | ## 3) Managed identities 40 | az role assignment create --assignee $managedIdentityId --scope $registryName --role AcrPush,AcrPull,AcrDelete 41 | ## Now container instances / apps must use that managed identity to access this ACR (pull or push images) 42 | ## 43 | ## 4) Admin User: ❌. Interactive push/pull by individual developers. 44 | ### The admin account is provided with two passwords, both of which can be regenerated 45 | az acr update -n $registryName --admin-enabled true # this is disabled by default 46 | docker login $registryName.azurecr.io 47 | 48 | # Tasks: https://learn.microsoft.com/en-us/azure/container-registry/container-registry-tasks-overview 49 | ## [--platform Linux {Linux, Windows}] # Linux supports all architectures (ex: Linux/arm), Windows: only amd64 (ex: Windows/amd64) - arch is optional 50 | ## 51 | ## - Quick task 52 | az acr build --registry $registryName --image $imageName:$tag . # docker build, docker push 53 | az acr run --registry $registryName --cmd '$registryName/$repository/$imageName:$tag' /dev/null # Run image (last param is source location, optional for non-image building tasks) 54 | ## 55 | ## - Automatically Triggered Task 56 | ### [---trigger-enabled true] # CI on commit or pull-request 57 | ### [--schedule] # CRON schedule (⭐: OS/framework patching): https://learn.microsoft.com/en-us/azure/container-registry/container-registry-tasks-scheduled 58 | az acr task create --name ciTask --registry $registryName --image $imageName:{{.Run.ID}} --context https://github.com/myuser/myrepo.git --file Dockerfile --git-access-token $GIT_ACCESS_TOKEN 59 | az acr task create --name cmdTask --registry $registryName --cmd mcr.microsoft.com/hello-world --context /dev/null 60 | ### az acr task run --name mytask --registry $registryName # manually run task 61 | ## 62 | ## - Multi-step Task: granular control (build, push, when, cmd defined as steps) - https://learn.microsoft.com/en-us/azure/container-registry/container-registry-tasks-reference-yaml 63 | ### NOTE: --file is used for both multi-step task and Dockerfile 64 | az acr run --file multi-step.yaml https://github.com/Azure-Samples/acr-tasks.git 65 | az acr task create --file multi-step.yaml --name ciTask --registry $registryName --image $imageName:{{.Run.ID}} --context https://github.com/myuser/myrepo.git --git-access-token $GIT_ACCESS_TOKEN 66 | 67 | # List images and tags 68 | az acr repository list --name $registryName --output table 69 | az acr repository show-tags --name $registryName --repository $repository --output table -------------------------------------------------------------------------------- /Services/Blobs.sh: -------------------------------------------------------------------------------- 1 | az storage account create 2 | --name # valid DNS name, 3-24 chars 3 | --resource-group 4 | 5 | # Pricing tiers (_) 6 | # Changing type: Copy to another account. 7 | # Changing redundancy: Instantly applied 8 | [--sku {Standard_GRS, Standard_GZRS, Standard_LRS, Standard_RAGRS, Standard_ZRS, Standard_RAGZRS, Premium_LRS, Premium_ZRS}] 9 | # Type 🧊: 10 | # - Standard: ⏺️⭐ 11 | # - Premium: ⚡💲 (SSD). ⭐: using smaller objects 12 | # Redundancy: 13 | # - LRS: 🏷️, ❌: 🙋‍♂️. 14 | # ⭐: your application can reconstruct lost data, requires regional replication (perhaps due to governance reasons), or uses Azure unmanaged disks. 15 | # - ZRS: Data write operations are confirmed successful once all the available zones have received the data. This even includes zones that are temporarily unavailable. 16 | # ⭐: 🙋‍♂️, regional data replication, Azure Files workloads. 17 | # - GRS: LRS + async copy to a secondary region. 18 | # - GZRS: ZRS + async copy to a secondary region. 🦺 19 | # Read Access (RA): 🙋‍♂️ Allow read-only from `https://{accountName}-secondary.` 20 | # Failover: manually initialized, swaps primary and secondary regions. 21 | # - C#: BlobClientOptions.GeoRedundantSecondaryUri (will not attempt again if 404). 22 | # - Alt: Copy data. 23 | # - ❌: Azure Files, BlockBlobStorage 24 | 25 | [--access-tier {Cool, Hot, Premium}] # Premium is inherited by SKU 26 | 27 | [--kind {BlobStorage, BlockBlobStorage, FileStorage, Storage, StorageV2}] 28 | # - BlobStorage: Simple blob-only scenarios. 29 | # - BlockBlobStorage: ⚡💎 30 | # - FileStorage: High-scale or high IOPS file shares. 💎 31 | # - Storage (General-purpose v1): Legacy. ⭐: classic deployment model or 🏋🏿 apps 32 | # - StorageV2: ⏺️⭐ 33 | 34 | [--dns-endpoint-type {AzureDnsZone, Standard}] # Requires storage-preview extension 35 | # In one subscription, you can have accounts with both 36 | # - Standard: 250 accounts (500 with quota increase) 37 | # - AzureDnsZone: 5000 accounts 38 | # https://.z[00-50]..core.windows.net 39 | # Retrieve endpoints: GET https://management.azure.com/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Storage/storageAccounts/{accountName}?api-version=2022-09-01 40 | 41 | [--enable-hierarchical-namespace {false, true}] 42 | # Filesystem semantics. StorageV2 only. ❌ failover 43 | 44 | az storage container create 45 | --name # Valid lowercase DNS name (3-63) with no double dashes 46 | [--resource-group] 47 | [--metadata] 48 | [--public-access {blob, container, off}] 49 | 50 | az storage account management-policy create \ 51 | #--account-name "" \ 52 | #--resource-group $resourceGroup 53 | --policy @policy.json 54 | 55 | az storage account 56 | [--encryption-key-source {Microsoft.Keyvault, Microsoft.Storage}] 57 | [--encryption-services {blob, file, queue, table}] # queue / table with customer-managed keys = 💲 58 | [--encryption-key-type-for-queue {Account, Service}] 59 | [--encryption-key-type-for-table {Account, Service}] 60 | # When using Microsoft.Keyvault: 61 | # [--encryption-key-name] 62 | # [--encryption-key-vault] # URL 63 | # [--encryption-key-version] 64 | # 🧊 Optionally encrypt infrastructure with separate Microsoft managed key. StorageV2 or BlockBlobStorage only. 65 | [--require-infrastructure-encryption {false, true}] # false 66 | 67 | az storage account encryption-scope create 68 | --account-name 69 | --name "" 70 | [--key-source {Microsoft.KeyVault, Microsoft.Storage}] # Same rules like encryption at account level 71 | [--key-uri] # For KeyVault 72 | [--require-infrastructure-encryption {false, true}] # Inherited from storage account level, if set 73 | 74 | # Optional 75 | az storage container create 76 | --default-encryption-scope "" 77 | --prevent-encryption-scope-override true # force all blobs in a container to use the container's default scope 78 | 79 | az storage 80 | --encryption-scope "" # if not set, inherited from container or storage account 81 | # EncryptionScope property for BlobOptions in C# 82 | 83 | az storage blob 84 | # Authenticate: 85 | ## By Storage Account Key 86 | --account-key # az storage account keys list -g $resourcegroup -n $accountname --query '[0].value' -o tsv 87 | ## By AD Login 88 | --auth-mode login # Use credentials from az login 89 | ## By Connection String 90 | --connection-string 91 | ## By SAS token 92 | --sas-token 93 | 94 | # Select target blob 95 | ## By name 96 | [--blob-endpoint] # https://.blob.core.windows.net 97 | [--account-name] # When using storage account key or a SAS token 98 | --container-name 99 | --name # Case sensitive, cannot end with dot (.) or dash (-) 100 | ## By URL 101 | --blob-url "https://.blob.core.windows.net//?" # Use only if unauthenticated. 102 | 103 | # 104 | ## upload 105 | --file "/path/to/file" # for file uploads 106 | --data "some data" # for text uploads 107 | ## copy start-batch 108 | ## use -source- and --destination- 109 | 110 | # Example 111 | az storage blob upload --file /path/to/file --container mycontainer --name MyBlob 112 | az storage container list --account-name $storageaccountname # get containers -------------------------------------------------------------------------------- /Services/ContainerApp.sh: -------------------------------------------------------------------------------- 1 | # No root, linux/amd64 container images 2 | # auth runs as a sidecar 3 | # State doesn't persist inside a container. Use external cache. 4 | # A webhook can notify Azure Container Apps of a new image in ACR, triggering its automatic deployment. 5 | 6 | # Scopes: 7 | # - Revision-scope: Changing properties.template creates a new revision. Example: version, config, scaling. 8 | # - Application-scope: Changies to properties.configuration are applied to all revisions. Example: secrets, mode, ingress, credentials. 9 | 10 | # Logs: 11 | # - System Logs (at the container app level) 12 | # - Console Logs (from the `stderr` and `stdout` messages inside container app) 13 | 14 | # Upgrade Azure CLI version on the workstation 15 | az upgrade 16 | 17 | # Add and upgrade the containerapp extension for managing containerized services 18 | az extension add --name containerapp --upgrade 19 | 20 | # Login to Azure 21 | az login 22 | 23 | # Register providers for Azure App Services (for hosting APIs) and Azure Operational Insights (for telemetry) 24 | az provider register --namespace Microsoft.App 25 | az provider register --namespace Microsoft.OperationalInsights 26 | 27 | # Create an environment 'prod' in Azure Container Apps 28 | az containerapp env create --resource-group $resourceGroup --name prod 29 | 30 | # Deploy the API service to the 'prod' environment, using the source code from a repository 31 | # https://learn.microsoft.com/en-us/azure/container-apps/quickstart-code-to-cloud 32 | function deploy_repo() { 33 | az containerapp up \ 34 | --name MyAPI \ 35 | --resource-group $resourceGroup \ 36 | --location eastus \ 37 | --environment prod \ 38 | --context-path ./src \ 39 | --repo myuser/myrepo \ 40 | --ingress 'external' 41 | 42 | # Display the Fully Qualified Domain Name (FQDN) of the app after it's deployed. This is the URL you would use to access your application. 43 | az containerapp show --name MyAPI --resource-group $resourceGroup --query properties.configuration.ingress.fqdn 44 | } 45 | 46 | # Deploy a containerized application in Azure Container Apps, using an existing public Docker image 47 | # https://learn.microsoft.com/en-us/azure/container-apps/get-started 48 | function deploy_image() { 49 | az containerapp up \ 50 | --name MyContainerApp \ 51 | --resource-group $resourceGroup \ 52 | --environment prod \ 53 | --image mcr.microsoft.com/azuredocs/containerapps-helloworld:latest \ 54 | --target-port 80 \ 55 | --ingress 'external' \ # allows the application to be accessible from the internet. 56 | # Display the Fully Qualified Domain Name (FQDN) of the app after it's deployed. This is the URL you would use to access your application. 57 | --query properties.configuration.ingress.fqdn 58 | 59 | # Alt: Deploy from a Docker Image in Azure Container Registry (ACR) 60 | # --image myAcr.azurecr.io/myimage:latest \ 61 | # --registry-username myAcrUsername \ 62 | # --registry-password myAcrPassword \ 63 | } 64 | 65 | ####################### 66 | 67 | function using_secrets() { 68 | # restart to reflect updates 69 | 70 | # Key vault 71 | az keyvault create --name MyKeyVault --resource-group $resourceGroup --location eastus 72 | az containerapp create \ 73 | --resource-group $resourceGroup \ 74 | --name queuereader \ 75 | --environment prod \ 76 | --image demos/queuereader:v1 \ 77 | --user-assigned "" \ 78 | --secrets ""api-key=$API_KEY" queue-connection-string=keyvaultref:,identityref:" \ 79 | --secret-volume-mount "/mnt/secrets" \ # Mounting in a volume 80 | # Referencing: `secretref:`` 81 | --env-vars "QueueName=myqueue" "ConnectionString=secretref:queue-connection-string" 82 | } 83 | 84 | function scale_by_servicebus() { 85 | # Custom: annot scale to 0 86 | az containerapp create \ 87 | --name \ 88 | --resource-group \ 89 | --environment \ 90 | --image 91 | --min-replicas 0 \ 92 | --max-replicas 5 \ 93 | --secrets "connection-string-secret=" \ 94 | --scale-rule-name azure-servicebus-queue-rule \ 95 | --scale-rule-type azure-servicebus \ 96 | --scale-rule-metadata "queueName=my-queue" \ 97 | "namespace=service-bus-namespace" \ 98 | "messageCount=5" \ 99 | --scale-rule-auth "connection=connection-string-secret" # No secretref because it's not env var 100 | } 101 | 102 | function scaling() { 103 | # Adding or editing scaling rules creates a new revision of the container app 104 | az containerapp create \ 105 | # Revisions 106 | # - Single Mode: Old revision stays until new is ready. 107 | # - Multi Mode: Control lifecycle and traffic via ingress; switches to latest when ready. 108 | # Labels: Route traffic to specific revisions via unique URLs. 109 | -revision-mode "Single|Multiple" 110 | 111 | --min-replicas 0 \ 112 | --max-replicas 5 \ 113 | 114 | # HTTP Scaling Rule 115 | # Based on the number of concurrent HTTP requests to your revision. 116 | --scale-rule-name http-rule-name \ 117 | --scale-rule-type http \ 118 | --scale-rule-http-concurrency 100 119 | 120 | # TCP Scaling Rule 121 | # Based on the number of concurrent TCP connections to your revision. 122 | --scale-rule-name tcp-rule-name \ 123 | --scale-rule-type tcp \ 124 | --scale-rule-tcp-concurrency 100 125 | } 126 | 127 | function dapr() { 128 | # To load components only for the right apps, application scopes are used (or all will be loaded). 129 | az containerapp create --dapr-enabled 130 | # Pub/sub: implement event-driven architectures 131 | # Observability: Sends tracing information to an Application Insights backend. 132 | # Bindings: Communicate with external systems. 133 | } -------------------------------------------------------------------------------- /Services/EventGrid.cs: -------------------------------------------------------------------------------- 1 | using Azure; 2 | using Azure.Messaging; 3 | using Azure.Messaging.EventGrid; 4 | using Microsoft.AspNetCore.Http; 5 | using Microsoft.AspNetCore.Mvc; 6 | using Microsoft.Azure.WebJobs; 7 | using Microsoft.Azure.WebJobs.Extensions.EventGrid; 8 | using Microsoft.Azure.WebJobs.Extensions.Http; 9 | using Microsoft.Extensions.Logging; 10 | using Newtonsoft.Json; 11 | 12 | namespace Services; 13 | 14 | // Focuses on event, not payload 15 | // No event order guarantee 16 | // Messages as array, 1MB max total, charged per 64KB 17 | // Webhooks: New event triggers HTTP POST to endpoint. 18 | 19 | // Schemas: 20 | // - EventGridEvent: specific to Azure only; `"content-type":"application/json;` 21 | // - CloudSchema (recommended): can be used across different cloud providers and platforms; supports bidirectional event transformation; `"content-type":"application/cloudevents+json;` 22 | 23 | // Targets: 24 | // - Webhooks 25 | // - Azure Service Bus topics and queues (up to 80GB in total, FIFO per session) 26 | // - Azure Storage Queue (up to 64KB per message, hiding while processing) 27 | // - Azure Event Hubs (checkpointing, FIFO per partition) 28 | // - Azure Functions 29 | 30 | // Retry policies: Dead-lettering on 4XX response (set/unset with --deadletter-endpoint). 31 | // - Webhooks are retried until 200 - OK 32 | // - Azure Storage Queue retries until successful processing 33 | // Exponentially delaying delivery attempts for unhealty endpoints 34 | 35 | // Batching: All or None only. 36 | // Optimistic Batching: Batching is at best effort, not strictly 37 | 38 | // Subscriptions roles don't grant access for actions such as creating topics. 39 | // Permissions needed to subscribe to event handlers (except WebHooks): Microsoft.EventGrid/EventSubscriptions/Write 40 | 41 | // Validation: endpoint must return 200 and: 42 | // - validationCode (sync) 43 | // - validationUrl (async) 44 | // Automatically handled for: 45 | // - Azure Functions with Event Grid Trigger 46 | // - Azure Automation via webhook 47 | // - Azure Logic Apps with Event Grid Connector 48 | 49 | // No self-signed certificates, only commercial 50 | 51 | class EventGridService 52 | { 53 | async Task SendMessage() 54 | { 55 | Uri endpoint = new Uri("https://..eventgrid.azure.net/api/events"); 56 | 57 | var credential = new AzureKeyCredential(""); // key for Event Grid topic, which you can find in the Azure Portal 58 | // var credential = new DefaultAzureCredential(); 59 | 60 | var data = new object(); 61 | 62 | var client = new EventGridPublisherClient(endpoint, credential); 63 | 64 | await client.SendEventAsync(new EventGridEvent( 65 | subject: "Object.New", // mandatory in schema 66 | eventType: "EventGridEvent.New", 67 | dataVersion: "1.0", 68 | data: data 69 | )); 70 | 71 | await client.SendEventAsync(new CloudEvent( 72 | source: "Object.New", // mandatory in schema 73 | type: "CloudEvent.New", // mandatory in schema 74 | jsonSerializableData: JsonConvert.SerializeObject(data) 75 | )); 76 | } 77 | } 78 | 79 | class EventGridFunctions 80 | { 81 | // Instead of TopicEndpointUri and TopicKeySetting, simply pass "__topicEndpointUri" to TopicEndpointUri 82 | [FunctionName("WithTopicEndpointUriOnly")] 83 | [return: EventGrid(TopicEndpointUri = "EventGridTopicKeyAppSetting__topicEndpointUri")] 84 | public static EventGridEvent WithTopicEndpointUriOnly( 85 | [HttpTrigger(AuthorizationLevel.Function, "post", Route = null)] HttpRequest req, string subject, 86 | ILogger log) => new EventGridEvent(subject: subject, eventType: "HttpEvent", dataVersion: "1.0", data: "{}"); 87 | 88 | // Instead of TopicEndpointUri and TopicKeySetting, use Connection 89 | [FunctionName("WithConnectionOnly")] 90 | [return: EventGrid(Connection = "EventGridConnectionAppSetting")] 91 | public static EventGridEvent WithConnectionOnly( 92 | [HttpTrigger(AuthorizationLevel.Function, "post", Route = "event/{subject}")] HttpRequest req, string subject, 93 | ILogger log) => new EventGridEvent(subject: subject, eventType: "HttpEvent", dataVersion: "1.0", data: "{}"); 94 | 95 | [FunctionName("ReturnEventGridEvent")] 96 | [return: EventGrid(TopicEndpointUri = "EventGridTopicUriAppSetting", TopicKeySetting = "EventGridTopicKeyAppSetting")] 97 | public static async Task ReturnEventGridEvent( 98 | [HttpTrigger(AuthorizationLevel.Function, "post", Route = "event/{subject}")] HttpRequest req, string subject, 99 | ILogger log) 100 | { 101 | string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); 102 | var eventGridEvent = new EventGridEvent(subject: subject, eventType: "HttpEvent", dataVersion: "1.0", data: requestBody); 103 | log.LogInformation($"Event sent: {subject}\n{requestBody}"); 104 | return eventGridEvent; 105 | } 106 | 107 | [FunctionName("CollectMultipleEventGridEvents")] 108 | public static async Task CollectMultipleEventGridEvents( 109 | [HttpTrigger(AuthorizationLevel.Function, "post", Route = null)] HttpRequest req, 110 | [EventGrid(TopicEndpointUri = "EventGridEndpoint", TopicKeySetting = "EventGridKey")] IAsyncCollector eventCollector) 111 | { 112 | var ev = new EventGridEvent(subject: "IncomingRequest", eventType: "IncomingRequest", dataVersion: "1.0", data: await req.ReadAsStringAsync()); 113 | await eventCollector.AddAsync(ev); 114 | return new OkResult(); 115 | } 116 | 117 | [FunctionName("CollectMultipleCloudEvents")] 118 | public static async Task CollectMultipleCloudEvents( 119 | [HttpTrigger(AuthorizationLevel.Function, "post", Route = null)] HttpRequest req, 120 | [EventGrid(TopicEndpointUri = "EventGridEndpoint", TopicKeySetting = "EventGridKey")] IAsyncCollector eventCollector) 121 | { 122 | var ev = new CloudEvent(source: "IncomingRequest", type: "IncomingRequest", jsonSerializableData: await req.ReadAsStringAsync()); 123 | await eventCollector.AddAsync(ev); 124 | return new OkResult(); 125 | } 126 | 127 | [FunctionName("GetSingleEventGridEvent")] 128 | public static IActionResult GetSingleEventGridEvent( 129 | [HttpTrigger(AuthorizationLevel.Function, "post", Route = null)] HttpRequest req, 130 | [EventGrid(TopicEndpointUri = "EventGridEndpoint", TopicKeySetting = "EventGridKey")] out EventGridEvent ev) 131 | { 132 | ev = new EventGridEvent(subject: "IncomingRequest", eventType: "IncomingRequest", dataVersion: "1.0", data: "Data"); 133 | return new OkResult(); 134 | } 135 | 136 | [FunctionName("GetSingleCloudEvent")] 137 | public static IActionResult GetSingleCloudEvent( 138 | [HttpTrigger(AuthorizationLevel.Function, "post", Route = null)] HttpRequest req, 139 | [EventGrid(TopicEndpointUri = "EventGridEndpoint", TopicKeySetting = "EventGridKey")] out CloudEvent ev) 140 | { 141 | ev = new CloudEvent(source: "IncomingRequest", type: "IncomingRequest", jsonSerializableData: "Data"); 142 | return new OkResult(); 143 | } 144 | } -------------------------------------------------------------------------------- /Services/ServiceBus.cs: -------------------------------------------------------------------------------- 1 | using Azure.Messaging.ServiceBus; 2 | using Azure.Messaging.ServiceBus.Administration; 3 | using Microsoft.AspNetCore.Http; 4 | using Microsoft.Azure.WebJobs; 5 | using Microsoft.Azure.WebJobs.Extensions.Http; 6 | using Microsoft.Extensions.Logging; 7 | 8 | namespace Services; 9 | 10 | // ServiceBusClient 11 | // ServiceBusSender - (publishers) 12 | // ServiceBusProcessor - (subscribers) for consuming messages (via handlers) 13 | 14 | // Publishers send messages to a topic (1:n), and each message is distributed to all subscriptions registered with the topic. 15 | // - Broadcast Pattern: Every subscription gets a copy of each message. 16 | // - Partitioning Pattern: Distributes messages across subscriptions in a mutually exclusive manner. 17 | // - Routing Pattern: When you need to route messages based on their content or some attributes. 18 | 19 | // Message Routing and Correlation 20 | // - Simple request/reply: Queue-based, uses ReplyTo and MessageId for replies. 21 | // - Multicast request/reply: Topic-based, multiple subscribers, optional topic in ReplyTo. 22 | // - Multiplexing: Single queue or subscription, groups messages via SessionId. 23 | // - Multiplexed request/reply: Shared reply queue, guided by ReplyToSessionId and SessionId. 24 | // Applications can also use user properties for routing, as long as they don't use the reserved `To` property. 25 | 26 | // Subscriptions act like virtual queues and can apply filters to receive specific messages. 27 | // Subscribers receive messages from the topic. Only one can receives and processes each message at a time from queues (Point-to-Point connection) 28 | // Receive modes: 29 | // - Receive and delete: message immediately removed from queue 30 | // - Peek lock: message will be removed after being marked as complete (ProcessMessageEventArgs.CompleteMessageAsync()), or after timeout 31 | 32 | // Message ordering: FIFO per session (sessionId) 33 | // - sessionId also allows processing messages as parallel, long-running streams 34 | 35 | // Namespace: .servicebus.windows.net. 36 | 37 | // Protocols: HTTPS and HTTPS (see Event Hub) 38 | 39 | // Multiple message factories boost throughput; use one if senders/receivers are imbalanced. 40 | 41 | // Prefetch count: 20x max processing rate for all receivers; adjust for large receiver count or low-latency. 42 | 43 | // ## TTL 44 | // - Message-level TTL cannot be higher than topic's (queue) TTL. If not set, queue's TTL is used. 45 | // - When a message is locked, its expiration is halted until the lock expires or the message is abandoned. 46 | 47 | // Premium plan: Up to 100MB messages (compared to 256KB), fixed pricing (compared to "pay as you go"), high throughput. 48 | 49 | // Filters 50 | // - SQL: SQL-like, complex conditions (not, comparison, etc). Lower throughput. System properties must be prefixed with `sys.` 51 | // - Boolean: All or none messages. 52 | // - Correlation: Match on specific properties, like Subject & CorrelationId. Higher efficiency. 53 | // Actions: Modify properties post-match. 54 | 55 | // Load-leveling: System is optimized to manage the average load, instead of peaks. 56 | // Autoforwarding: Transfers messages within namespace 57 | // Dead-letter queue: Stores undeliverable messages (automatic). Can store expired messages as well 58 | // Scheduled delivery: Delays messages until set time using ScheduledEnqueueTimeUtc property 59 | // Message deferral: Sets aside messages for later retrieval 60 | // Batching: Better throughput, worse latency 61 | // Transactions: Groups operations for single entity 62 | // Autodelete on idle 63 | // Duplicate detection 64 | // Geo-disaster recovery: Switches to alternate region during downtime 65 | 66 | class ServiceBusService 67 | { 68 | static string serviceBusEndpoint = "example-namespace.servicebus.windows.net/"; 69 | string connectionString = $"Endpoint=sb://{serviceBusEndpoint};SharedAccessKeyName=KeyName;SharedAccessKey=AccessKey"; 70 | string queueName = "az204-queue"; 71 | 72 | // ServiceBusClient ManagedIdentityServiceBus() => new(serviceBusEndpoint, new DefaultAzureCredential()); 73 | // ServiceBusClient ConnectionStringServiceBus() => new(connectionString); 74 | // ServiceBusClient NamedKeyCredentialServiceBus() => new(serviceBusEndpoint, new AzureNamedKeyCredential("sharedAccessKeyName", "sharedAccessKey")); 75 | 76 | async Task SendBatch() 77 | { 78 | await using ServiceBusClient client = new ServiceBusClient(connectionString); 79 | await using ServiceBusSender sender = client.CreateSender(queueName); 80 | using ServiceBusMessageBatch messageBatch = await sender.CreateMessageBatchAsync(); 81 | for (int i = 1; i <= 3; i++) 82 | if (!messageBatch.TryAddMessage(new ServiceBusMessage($"Message {i}"))) 83 | throw new Exception($"Exception {i} has occurred."); 84 | await sender.SendMessagesAsync(messageBatch); 85 | } 86 | 87 | async Task SendMessage() 88 | { 89 | await using ServiceBusClient client = new ServiceBusClient(connectionString); 90 | await using ServiceBusSender sender = client.CreateSender(queueName); 91 | await sender.SendMessageAsync(new ServiceBusMessage("Message")); 92 | await sender.SendMessageAsync(new ServiceBusMessage(new BinaryData(new Person { Name = "John", Age = 30 }))); 93 | } 94 | 95 | async Task ReceiveMessage() 96 | { 97 | await using ServiceBusClient client = new ServiceBusClient(connectionString); 98 | await using ServiceBusReceiver receiver = client.CreateReceiver(queueName); 99 | var receivedMessage = await receiver.ReceiveMessageAsync(); 100 | var receivedPerson = receivedMessage.Body.ToObjectFromJson(); 101 | } 102 | 103 | async Task UseProcessor() // Note: No checkpointing here, only completion 104 | { 105 | await using ServiceBusClient client = new ServiceBusClient(connectionString); 106 | await using ServiceBusSender sender = client.CreateSender(queueName); 107 | await using ServiceBusProcessor processor = client.CreateProcessor(queueName, new ServiceBusProcessorOptions()); 108 | 109 | processor.ProcessMessageAsync += async (ProcessMessageEventArgs args) => 110 | { 111 | string body = args.Message.Body.ToString();// payload is an opaque binary block, format described in `ContentType` property 112 | Console.WriteLine($"Received: {body}"); 113 | await args.CompleteMessageAsync(args.Message); 114 | }; 115 | 116 | processor.ProcessErrorAsync += (ProcessErrorEventArgs args) => 117 | { 118 | Console.WriteLine(args.Exception.ToString()); 119 | return Task.CompletedTask; 120 | }; 121 | 122 | await processor.StartProcessingAsync(); 123 | try { await Task.Delay(Timeout.Infinite, new CancellationTokenSource(TimeSpan.FromSeconds(45)).Token); } catch (TaskCanceledException) { } 124 | await processor.StopProcessingAsync(); 125 | } 126 | 127 | async Task FiltersAndActions() 128 | { 129 | var adminClient = new ServiceBusAdministrationClient(connectionString); 130 | 131 | await adminClient.CreateSubscriptionAsync( 132 | new CreateSubscriptionOptions("topicName", "subscriptionName"), 133 | new CreateRuleOptions("BlueSize10Orders", new SqlRuleFilter("color='blue' AND quantity=10")) 134 | ); 135 | 136 | await adminClient.CreateRuleAsync("topicName", "subscriptionName", new CreateRuleOptions 137 | { 138 | Name = "RedOrdersWithAction", 139 | Filter = new SqlRuleFilter("user.color='red'"), 140 | Action = new SqlRuleAction("SET quantity = quantity / 2;") 141 | }); 142 | 143 | await adminClient.CreateSubscriptionAsync( 144 | new CreateSubscriptionOptions("topicName", "subscriptionName"), 145 | new CreateRuleOptions("AllOrders", new TrueRuleFilter()) 146 | ); 147 | 148 | await adminClient.CreateSubscriptionAsync( 149 | new CreateSubscriptionOptions("topicName", "subscriptionName"), 150 | new CreateRuleOptions("HighPriorityRedOrdersRule", new CorrelationRuleFilter() 151 | { 152 | Subject = "red", 153 | CorrelationId = "high" 154 | }) 155 | ); 156 | } 157 | 158 | class Person 159 | { 160 | public string? Name { get; set; } 161 | public int? Age { get; set; } 162 | } 163 | } 164 | 165 | class ServiceBusFunctions 166 | { 167 | [FunctionName("ServiceBusOutputBinding")] 168 | [return: ServiceBus("queue", Connection = "ServiceBusConnectionAppSetting")] 169 | public static string RunServiceBusOutputBinding( 170 | [HttpTrigger(AuthorizationLevel.Function, "post", Route = "servicebus/{message}")] HttpRequest req, string message, 171 | ILogger log) 172 | { 173 | // Sends a message to Service Bus Queue 174 | log.LogInformation($"Message sent: {message}"); 175 | return message; 176 | } 177 | } 178 | 179 | -------------------------------------------------------------------------------- /Services/CosmoDB.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection.Metadata; 2 | using Azure.Messaging.EventGrid; 3 | using Microsoft.AspNetCore.Http; 4 | using Microsoft.Azure.Cosmos; 5 | using Microsoft.Azure.Cosmos.Linq; 6 | using Microsoft.Azure.WebJobs; 7 | using Microsoft.Azure.WebJobs.Extensions.EventGrid; 8 | using Microsoft.Azure.WebJobs.Extensions.Http; 9 | using Microsoft.Extensions.Logging; 10 | 11 | namespace Services; 12 | 13 | // Database: consistency 14 | // Container: partition key 15 | 16 | // Throughput: 17 | // - container - dedicated, 18 | // - database - shared 19 | // - serverless - pay as you go 20 | // - Autoscale - scales to meet a target 21 | 22 | // Stored procedures: get context; getResponse() or getCollection(); response.getBody() or container.createDocument(); 23 | 24 | // Composite index for 2+ order by 25 | 26 | // Conflict resolution: auto(last wins), custom 27 | 28 | // Change feed: order per partition key 29 | // - Monitored container: Holds data for change feed. Reflects inserts/updates. 30 | // - Lease container: State storage, coordinates feed processing across workers. Can be in same/different account as monitored container. 31 | // - Delegate component: Custom logic for processing changes. 32 | // - Compute Instance: Hosts processor; can be VM, Kubernetes pod, Azure App Service, or physical machine. 33 | 34 | // Best practices: 35 | // - Latest SDK 36 | // - Use single instance of `CosmosClient` 37 | // - `Direct` mode for ⚡ 38 | // - Retry logic for handling transient errors 39 | // - Read 🏋🏿: `Stream API` and `FeedIterator` 40 | // - Write 🏋🏿: Enable bulk support, set `EnableContentResponseOnWrite` to false, exclude unused paths from indexing and keep the size of your documents minimal 41 | 42 | class CosmosDBService 43 | { 44 | async Task Query() 45 | { 46 | var client = new CosmosClient("AccountEndpoint=https://.documents.azure.com:443/;AccountKey=;Database=;"); 47 | 48 | // var database = await client.CreateDatabaseAsync("db", ThroughputProperties.CreateAutoscaleThroughput(autoscaleMaxThroughput: 700)); 49 | var database = client.GetDatabase("db"); 50 | 51 | Container container = await database.CreateContainerIfNotExistsAsync(id: "", partitionKeyPath: "/Group", throughput: 700); // Note: This returns ContainerResponse object, but we go around it 52 | // var container = await database.CreateContainerAsync(new ContainerProperties(id: "container", partitionKeyPath: "/name")); 53 | // var container = database.GetContainer(""); 54 | 55 | var created = await container.CreateItemAsync(new Item { Name = "1", Group = "MyPartitionValue" }, new PartitionKey("Group")); // No slash 56 | 57 | string queryText = "select * from items s where s.Name = @NameInput "; 58 | QueryDefinition query = new QueryDefinition(queryText) 59 | .WithParameter("@NameInput", "Account1"); 60 | FeedIterator feedIterator = container.GetItemQueryIterator( 61 | query // Note: you can pass queryText directly here 62 | // Optional: 63 | // requestOptions: new QueryRequestOptions() 64 | // { 65 | // PartitionKey = new PartitionKey("Account1"), 66 | // MaxItemCount = 1 67 | // } 68 | ); 69 | while (feedIterator.HasMoreResults) 70 | { 71 | FeedResponse response = await feedIterator.ReadNextAsync(); 72 | foreach (var item in response) Console.WriteLine(item.Name); 73 | } 74 | 75 | var queryable = container 76 | .GetItemLinqQueryable() 77 | .Where(item => item.Age > 25 && item.Group == "MyPartitionValue") 78 | .ToFeedIterator(); 79 | while (queryable.HasMoreResults) { } 80 | } 81 | 82 | class Item 83 | { 84 | public string Name { get; set; } = ""; 85 | public int Age { get; set; } = 0; 86 | public string Group { get; set; } = ""; // Partition key 87 | } 88 | 89 | async Task StartChangeFeedProcessorAsync(CosmosClient cosmosClient) 90 | { 91 | Container monitoredContainer = cosmosClient.GetContainer("databaseName", "monitoredContainerName"); 92 | Container leaseContainer = cosmosClient.GetContainer("databaseName", "leaseContainerName"); 93 | 94 | ChangeFeedProcessor changeFeedProcessor = monitoredContainer 95 | .GetChangeFeedProcessorBuilder(processorName: "changeFeedSample", onChangesDelegate: DelagateHandleChangesAsync) 96 | .WithInstanceName("consoleHost") // Compute Instance 97 | .WithLeaseContainer(leaseContainer) 98 | .Build(); 99 | 100 | Console.WriteLine("Starting Change Feed Processor..."); 101 | await changeFeedProcessor.StartAsync(); 102 | Console.WriteLine("Change Feed Processor started."); 103 | return changeFeedProcessor; 104 | } 105 | 106 | static async Task DelagateHandleChangesAsync( 107 | ChangeFeedProcessorContext context, 108 | IReadOnlyCollection changes, 109 | CancellationToken cancellationToken) 110 | { 111 | Console.WriteLine($"Started handling changes for lease {context.LeaseToken}..."); 112 | Console.WriteLine($"Change Feed request consumed {context.Headers.RequestCharge} RU."); 113 | // SessionToken if needed to enforce Session consistency on another client instance 114 | Console.WriteLine($"SessionToken ${context.Headers.Session}"); 115 | 116 | foreach (ToDoItem item in changes) 117 | { 118 | Console.WriteLine($"Detected operation for item with id {item.id}."); 119 | await Task.Delay(10); 120 | } 121 | } 122 | } 123 | 124 | class CosmoDBFunctions 125 | { 126 | [FunctionName("LogItems")] 127 | public static void LogItems( 128 | [CosmosDBTrigger( 129 | databaseName: "ecommerceDB", 130 | containerName: "orders", 131 | Connection = "CosmosDBConnection", 132 | LeaseContainerName = "leases", 133 | CreateLeaseContainerIfNotExists = true)] 134 | IReadOnlyList input, // Can be 135 | ILogger log) 136 | { 137 | if (input == null) return; 138 | 139 | log.LogInformation("Documents modified " + input.Count); 140 | foreach (var todo in input) 141 | { 142 | log.LogInformation("First document Id " + todo.id); 143 | } 144 | } 145 | 146 | [FunctionName("SyncToAnotherContainer")] 147 | public static async Task SyncToAnotherContainer( 148 | [CosmosDBTrigger( 149 | databaseName: "sourceDB", 150 | containerName: "sourceContainer", 151 | Connection = "CosmosDBConnection", 152 | LeaseContainerName = "leases")] 153 | IReadOnlyList input, 154 | [CosmosDB( 155 | databaseName: "destinationDB", 156 | containerName: "destinationContainer", 157 | Connection = "CosmosDBConnection")] 158 | IAsyncCollector output, 159 | ILogger log) 160 | { 161 | if (input == null || input.Count == 0) return; 162 | 163 | foreach (var document in input) 164 | { 165 | await output.AddAsync(document); 166 | } 167 | } 168 | 169 | [FunctionName("SendNotificationOnUpdate")] 170 | public static void Run( 171 | [CosmosDBTrigger( 172 | databaseName: "notificationDB", 173 | containerName: "events", 174 | Connection = "CosmosDBConnection", 175 | LeaseContainerName = "leases")] 176 | IReadOnlyList input, 177 | [EventGrid(TopicEndpointUri = "EventGridTopicUri", TopicKeySetting = "EventGridTopicKey")] 178 | ICollector outputEvents, 179 | ILogger log) 180 | { 181 | if (input != null && input.Count > 0) 182 | { 183 | foreach (var document in input) 184 | { 185 | var eventGridEvent = new EventGridEvent(subject: "New Event", eventType: "CosmosDB.ItemUpdated", dataVersion: "1.0", data: document); 186 | outputEvents.Add(eventGridEvent); 187 | } 188 | } 189 | } 190 | 191 | [FunctionName("EventGridOutputBinding")] 192 | [return: EventGrid(TopicEndpointUri = "EventGridTopicUriAppSetting", TopicKeySetting = "EventGridTopicKeyAppSetting")] 193 | public static async Task RunEventGridOutputBinding( 194 | [HttpTrigger(AuthorizationLevel.Function, "post", Route = "event/{subject}")] HttpRequest req, string subject, 195 | ILogger log) 196 | { 197 | // Sends an event to Event Grid Topic 198 | string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); 199 | var eventGridEvent = new EventGridEvent(subject, "MyEventType", requestBody, "1.0"); 200 | log.LogInformation($"Event sent: {subject}"); 201 | return eventGridEvent; 202 | } 203 | } 204 | 205 | public class ToDoItem 206 | { 207 | public string id { get; set; } = ""; 208 | public string Description { get; set; } = ""; 209 | } -------------------------------------------------------------------------------- /Services/EventHub.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using Azure.Messaging.EventHubs; 3 | using Azure.Messaging.EventHubs.Consumer; 4 | using Azure.Messaging.EventHubs.Producer; 5 | using Azure.Messaging.EventHubs.Processor; 6 | using Azure.Storage.Blobs; 7 | using Microsoft.AspNetCore.Http; 8 | using Microsoft.Azure.WebJobs; 9 | using Microsoft.Azure.WebJobs.Extensions.Http; 10 | using Microsoft.Extensions.Logging; 11 | using Newtonsoft.Json; 12 | 13 | // EventHubProducerClient: Source of various types of data such as telemetry, diagnostics, logs, etc. 14 | // EventProcessorClient: 15 | // - Handle data from several partitions (distributed ownership). 16 | // - Multiple instances can be used to scale/balance load. 17 | // - Marks the last processed event within a partition (checkpointing) - resume after failure. Requires storage account. 18 | // EventHubConsumerClient 19 | // - Read EventData from a specific consumer group 20 | // - Exlusive (Epoch) - only one reader; Non-inclusive (Non-Epoch) - allow multiple consumers from the same consumer group. 21 | 22 | // Message ordering: FIFO per partition 23 | 24 | // Partitioning: Divides the message stream into smaller, ordered sequences for parallel data processing and increased throughput. Increases processing time 25 | // - New events added in the order they were received 26 | 27 | // Namespace: .servicebus.windows.net. Throughput unit are specified here. 28 | 29 | // Consumer Groups: Allows multiple applications to read the event stream independently. 30 | 31 | // Batch Processing: Uses partitioned consumer model to process streams concurrently and control processing speed. 32 | 33 | // Event Receivers: Entities that read event data through: 34 | // - HTTPS: faster initialization, slower (use for less frequent publisher) 35 | // - AMQP: higher throughput and lower latency for frequent publishers 36 | 37 | // Max event retention: 7 days (standard), 90 days (premium and dedicated) 38 | // - Use Event Hubs Capture to store for longer 39 | 40 | // Event Hubs Capture 41 | // - Streaming data into Azure Blob storage or Azure Data Lake Storage (in any region) 42 | // - Format (avro): https://{storageAccount}.blob.core.windows.net/{containerName}/{eventHubNamespace}/{eventHubName}/{partitionId}/{year}/{month}/{day}/{hour}/{minute}/{second}.avro 43 | // - Log Compaction: use key-based retention, instead of time 44 | // - Capture windowing: minimum size and time window for capturing data 45 | // - First wins policy: initiated by the first trigger encountered 46 | // - Each partition independently captures data and names a block blob after the capture interval is reached. 47 | 48 | // Roles: 49 | // - Azure Event Hubs Data Owner: complete access 50 | // - Azure Event Hubs Data Sender: send access 51 | // - Azure Event Hubs Data Receiver: receiving access 52 | 53 | class EventHubService 54 | { 55 | static string serviceBusEndpoint = "example-namespace.servicebus.windows.net/"; 56 | string connectionString = $"Endpoint=sb://{serviceBusEndpoint};SharedAccessKeyName=KeyName;SharedAccessKey=AccessKey"; 57 | string eventHubName = "example-event-hub"; 58 | string consumerGroup = EventHubConsumerClient.DefaultConsumerGroupName; 59 | 60 | string queueName = "az204-queue"; 61 | 62 | // EventHubProducerClient ManagedIdentityEventHubProducerClient() => new(serviceBusEndpoint, eventHubName, new DefaultAzureCredential()); 63 | // EventHubProducerClient ConnectionStringEventHubProducerClient() => new(connectionString: $"Endpoint=sb://{serviceBusEndpoint};SharedAccessKeyName=KeyName;SharedAccessKey=AccessKey", eventHubName); 64 | // EventHubProducerClient NamedKeyCredentialEventHubProducerClient() => new(serviceBusEndpoint, eventHubName, new AzureNamedKeyCredential("sharedAccessKeyName", "sharedAccessKey")); 65 | 66 | async Task SendBatch() 67 | { 68 | await using (var producerClient = new EventHubProducerClient(connectionString, eventHubName)) 69 | { 70 | using EventDataBatch eventBatch = await producerClient.CreateBatchAsync(); 71 | eventBatch.TryAdd(new EventData(Encoding.UTF8.GetBytes("First event"))); 72 | eventBatch.TryAdd(new EventData("Second event")); 73 | await producerClient.SendAsync(eventBatch); 74 | } 75 | } 76 | 77 | async Task Partitioning() 78 | { 79 | await using (var producerClient = new EventHubProducerClient(connectionString, eventHubName)) 80 | { 81 | string[] partitionIds = await producerClient.GetPartitionIdsAsync(); // Query partition IDs 82 | 83 | using EventDataBatch eventBatch = await producerClient.CreateBatchAsync(new CreateBatchOptions { PartitionKey = partitionIds[0] }); 84 | eventBatch.TryAdd(new EventData(Encoding.UTF8.GetBytes("First event"))); 85 | eventBatch.TryAdd(new EventData("Second event")); 86 | await producerClient.SendAsync(eventBatch); 87 | } 88 | } 89 | 90 | async Task UsingBuffer() 91 | { 92 | await using (var bufferedProducerClient = new EventHubBufferedProducerClient(connectionString, eventHubName)) 93 | { 94 | await bufferedProducerClient.EnqueueEventAsync(new EventData(Encoding.UTF8.GetBytes("First event"))); 95 | await bufferedProducerClient.EnqueueEventAsync(new EventData(Encoding.UTF8.GetBytes("Second event"))); 96 | await bufferedProducerClient.EnqueueEventsAsync(new[] { new EventData("Third Event") }); 97 | } 98 | } 99 | 100 | async Task ConsumeEvents() 101 | { 102 | await using (var consumer = new EventHubConsumerClient(consumerGroup, connectionString, eventHubName)) 103 | { 104 | // All events 105 | await foreach (PartitionEvent receivedEvent in consumer.ReadEventsAsync()) { } // Wait for events 106 | 107 | // Events from partition - needs partition and starting position 108 | EventPosition startingPosition = EventPosition.Earliest; 109 | string partitionId = (await consumer.GetPartitionIdsAsync()).First(); 110 | await foreach (PartitionEvent receivedEvent in consumer.ReadEventsFromPartitionAsync(partitionId, startingPosition)) // Wait for events in partition 111 | { 112 | string readFromPartition = receivedEvent.Partition.PartitionId; 113 | byte[] eventBody = receivedEvent.Data.EventBody.ToArray(); 114 | } 115 | } 116 | } 117 | 118 | async Task UseEventProcessor() 119 | { 120 | // You need Blob Storage for checkpointing 121 | string storageConnectionString = "DefaultEndpointsProtocol=https;AccountName=exampleaccount;AccountKey=examplekey;EndpointSuffix=core.windows.net"; 122 | string blobContainerName = "example-container"; 123 | var storageClient = new BlobContainerClient(storageConnectionString, blobContainerName); 124 | 125 | var processor = new EventProcessorClient(storageClient, consumerGroup, connectionString, eventHubName); 126 | 127 | processor.ProcessEventAsync += async (ProcessEventArgs eventArgs) => 128 | { 129 | // Checkpointing: Update checkpoint in the blob storage so that you can resume from this point if the processor restarts 130 | await eventArgs.UpdateCheckpointAsync(); 131 | }; 132 | 133 | processor.ProcessErrorAsync += (ProcessErrorEventArgs eventArgs) => Task.CompletedTask; 134 | 135 | await processor.StartProcessingAsync(); 136 | try { await Task.Delay(Timeout.Infinite, new CancellationTokenSource(TimeSpan.FromSeconds(45)).Token); } catch (TaskCanceledException) { } 137 | await processor.StopProcessingAsync(); 138 | } 139 | } 140 | 141 | class EventHubFunctions 142 | { 143 | [FunctionName(nameof(ReceiveMessagesAsBatch))] 144 | public static void ReceiveMessagesAsBatch([EventHubTrigger(eventHubName: "hub", Connection = "EventHubConnectionAppSetting")] EventData[] events, ILogger log) 145 | { 146 | foreach (EventData message in events) 147 | { 148 | log.LogInformation($"Message: {Encoding.UTF8.GetString(message.EventBody)}"); 149 | log.LogInformation($"System Properties: {JsonConvert.SerializeObject(message.SystemProperties)}"); 150 | } 151 | } 152 | 153 | [FunctionName(nameof(OutputEventHubMessage))] 154 | [return: EventHub(eventHubName: "hub", Connection = "EventHubConnectionAppSetting")] 155 | public static string OutputEventHubMessage( 156 | [HttpTrigger(AuthorizationLevel.Function, "post", Route = "event/{message}")] HttpRequest req, string message, 157 | ILogger log) 158 | { 159 | log.LogInformation($"Event sent: {message}"); 160 | return message; 161 | } 162 | 163 | [FunctionName(nameof(RedirectMessagesToAnotherHubWithPartitioning))] 164 | public static async Task RedirectMessagesToAnotherHubWithPartitioning( 165 | [EventHubTrigger("source", Connection = "EventHubConnectionAppSetting")] EventData[] events, 166 | [EventHub("dest", Connection = "EventHubConnectionAppSetting")] IAsyncCollector outputEvents, 167 | ILogger log) 168 | { 169 | foreach (EventData message in events) 170 | { 171 | string newMessage = Encoding.UTF8.GetString(message.EventBody); 172 | await outputEvents.AddAsync(new EventData(newMessage)); 173 | 174 | // Group events together by partition key 175 | await outputEvents.AddAsync(new EventData(newMessage), partitionKey: "sample-key"); 176 | } 177 | } 178 | } -------------------------------------------------------------------------------- /Services/AppService.sh: -------------------------------------------------------------------------------- 1 | ## Deploying apps 2 | # - Create resource group: `az group create` 3 | # - Create App Service plan: `az appservice plan create --location $location` 4 | # - Create web app: `az webapp create --runtime "DOTNET|6.0"` 5 | # - (optinal) Use managed identity for ACR: 6 | # - Assign managed identity to the web app 7 | # - Assign `AcrPull` role: `az role assignment create --assignee $principalId --scope $registry_resource_id --role "AcrPull"` 8 | # - Set generic config to `{acrUseManagedIdentityCreds:true}` for system identity and `{acrUserManagedIdentityID:id}` for user identity: `az webapp config set --generic-configurations ''` 9 | # - (optional) Create deployment slot (staging) (Standard+): `az webapp deployment slot create` 10 | # - Deploy app (add `--slot staging` to use deployment slot): 11 | # - Git: `az webapp deployment source config --repo-url $gitrepo --branch master --manual-integration` 12 | # - Docker: `az webapp config container set --docker-custom-image-name` 13 | # - Compose (skip step 3): `az webapp create --multicontainer-config-type compose --multicontainer-config-file $dockerComposeFile` 14 | # - Local ZIP file: `az webapp deploy --src-path "path/to/zip"` 15 | # - Remote ZIP file: `az webapp deploy --src-url ""` 16 | # - (optional) Set some settings: `az webapp config appsettings set --settings` (ex: `DEPLOYMENT_BRANCH='main'` for git, `SCM_DO_BUILD_DURING_DEPLOYMENT=true` for build automation) 17 | 18 | # Source Settings: 19 | # Key vault: 20 | # - @Microsoft.KeyVault(SecretUri=https://myvault.vault.azure.net/secrets/mysecret/) 21 | # - @Microsoft.KeyVault(VaultName=myvault;SecretName=mysecret) 22 | # App Configuration: @Microsoft.AppConfiguration(Endpoint=https://myAppConfigStore.azconfig.io; Key=myAppConfigKey; Label=myKeysLabel) 23 | 24 | # Managed identities for App Service and Azure Functions: App Service / Database and Service Connection / Connect using app identity 25 | # GET {IDENTITY_ENDPOINT}?resource=https://vault.azure.net&api-version=2019-08-01&client_id=XXX 26 | # X-IDENTITY-HEADER: {IDENTITY_HEADER} # Mitigate SSRF attacks 27 | 28 | # Authentication flows: App Service / Auth / Built in 29 | # Server vs Client Sign-in: Server uses redirects for authentication, while client uses the provider's SDK and validates with the server. 30 | # Session Management: Post-authentication, the server either sets a cookie or returns a token (client). This is used in subsequent requests for authenticated access. 31 | 32 | # TLS mutual authentication: Basic+ 33 | # X-ARR-ClientCert header; HttpRequest.ClientCertificate 34 | 35 | # Scaling 36 | # - Manual scaling (Basic+) - one time events (example: doing X on this date) 37 | # - Autoscale (Standard+) - for predictable changes of application load, based on schedules (every X days/weeks/months) or resources 38 | # - Automatic scaling (PremiumV2+) - pre-warmed, always-ready; avoid cold start 39 | 40 | # Deployment slots 41 | # Best practices: Deploy to staging, then swap slots to warm up instances and eliminate downtime. 42 | # - Swapped: Settings that define the application's _behavior_. Includes connection strings, authentication settings, public certificates, path mappings, CDN, hybrid connections. 43 | # - Not Swapped: Settings that define the application's _environment and security_. They are less about the application itself and more about how it interacts with the external world. Examples: Private certificates, managed identities, publishing endpoints, diagnostic logs settings, CORS. 44 | 45 | # The _App Service file system_ option is for temporary debugging purposes, and turns itself off in 12 hours. 46 | # _The Blob_ option is for long-term logging, includes additional information. .Net apps only. 47 | # Linux can only have App loging in blobs, Windows: server too 48 | az webapp log config --application-logging {azureblobstorage, filesystem, off} --name MyWebapp --resource-group $resourceGroup 49 | 50 | # Private Health checks: x-ms-auth-internal-token request header must equals the hashed value of WEBSITE_AUTH_ENCRYPTION_KEY 51 | 52 | # Local Cache: “WEBSITE_LOCAL_CACHE_OPTION”: “Always”, “WEBSITE_LOCAL_CACHE_SIZEINMB”: “1500” 53 | # Search: local cache app service 54 | 55 | # Move App Service plan by cloning it. Source plan and destination plan must be in the same resource group, geographical region, same OS type, and supports the currently used features. 56 | # New-AzResourceGroup -Name DestinationAzureResourceGroup -Location $destinationLocation 57 | # New-AzAppServicePlan -Location $destinationLocation -ResourceGroupName DestinationAzureResourceGroup -Name DestinationAppServicePlan -Tier Standard 58 | # $srcapp = Get-AzWebApp -Name MyAppService -ResourceGroupName SourceAzureResourceGroup 59 | # $destapp = New-AzWebApp -SourceWebApp $srcapp -AppServicePlan DestinationAppServicePlan -Location $destinationLocation -ResourceGroupName DestinationAzureResourceGroup -Name MyAppService2 60 | 61 | # Configuration: 62 | # az webapp config - identities 63 | # az webapp config appsettings 64 | # az webapp config container - docker and compose deployment 65 | # az webapp log config - where to log 66 | 67 | # Stream HTTP logs 68 | az webapp log tail --provider http --name $app --resource-group $resourceGroup 69 | # Stream errors 70 | az webapp log tail --filter Error --name $app --resource-group $resourceGroup # filter by word Error 71 | az webapp log tail --only-show-errors --name $app --resource-group $resourceGroup 72 | 73 | # Metrics: measured at App Service plan level, not web app or resource group 74 | az monitor metrics list --resource $app_service_plan_resource_id --metric "Percentage CPU" --time-grain PT1M --output table 75 | 76 | ############################ 77 | 78 | let "randomIdentifier=$RANDOM*$RANDOM" 79 | location="East US" 80 | resourceGroup="app-service-rg-$randomIdentifier" 81 | tag="deploy-github.sh" 82 | appServicePlan="app-service-plan-$randomIdentifier" 83 | webapp="web-app-$randomIdentifier" 84 | gitrepo="https://github.com/Azure-Samples/dotnet-core-sample" 85 | 86 | az group create --name $resourceGroup --location "$location" --tag $tag 87 | 88 | az appservice plan create --name $appServicePlan --resource-group $resourceGroup --location $location # --sku B1 89 | # az appservice plan create --name $appServicePlan --resource-group $resourceGroup --sku S1 --is-linux 90 | 91 | az webapp create --name $webapp --plan $appServicePlan --runtime "DOTNET|6.0" --resource-group $resourceGroup 92 | 93 | # https://learn.microsoft.com/en-us/azure/app-service/scripts/cli-deploy-github 94 | github_deployment() { 95 | echo "Deploying from GitHub" 96 | az webapp deployment source config --name $webapp --repo-url $gitrepo --branch master --manual-integration --resource-group $resourceGroup 97 | 98 | # Change deploiment branch to "main" 99 | # az webapp config appsettings set --name $webapp --settings DEPLOYMENT_BRANCH='main' --resource-group $resourceGroup 100 | } 101 | 102 | # https://learn.microsoft.com/en-us/azure/app-service/scripts/cli-deploy-staging-environment 103 | # Use it to avoid locking files 104 | staging_deployment() { 105 | # Deployment slots require Standard tier, default is Basic (B1) 106 | az appservice plan update --name $appServicePlan --sku S1 --resource-group $resourceGroup 107 | 108 | echo "Creating a deployment slot" 109 | az webapp deployment slot create --name $webapp --slot staging --resource-group $resourceGroup 110 | 111 | echo "Deploying to Staging Slot" 112 | az webapp deployment source config --name $webapp --resource-group $resourceGroup \ 113 | --slot staging \ 114 | --repo-url $gitrepo \ 115 | --branch master --manual-integration \ 116 | 117 | 118 | echo "Swapping staging slot into production" 119 | az webapp deployment slot swap --slot staging --name $webapp --resource-group $resourceGroup 120 | } 121 | 122 | # https://learn.microsoft.com/en-us/azure/app-service/configure-custom-container?tabs=debian&pivots=container-linux#change-the-docker-image-of-a-custom-container 123 | docker_deployment() { 124 | # (Optional) Use managed identity: https://learn.microsoft.com/en-us/azure/app-service/configure-custom-container?tabs=debian&pivots=container-linux#change-the-docker-image-of-a-custom-container 125 | ## Enable the system-assigned managed identity for the web app 126 | az webapp identity assign --name $webapp --resource-group $resourceGroup 127 | ## Grant the managed identity permission to access the container registry 128 | az role assignment create --assignee $principalId --scope $registry_resource_id --role "AcrPull" 129 | ## Configure your app to use the system managed identity to pull from Azure Container Registry 130 | az webapp config set --generic-configurations '{"acrUseManagedIdentityCreds": true}' --name $webapp --resource-group $resourceGroup 131 | ## (OR) Set the user-assigned managed identity ID for your app 132 | az webapp config set --generic-configurations '{"acrUserManagedIdentityID": "$principalId"}' --name $webapp --resource-group $resourceGroup 133 | 134 | echo "Deploying from DockerHub" # Custom container 135 | az webapp config container set --name $webapp --resource-group $resourceGroup \ 136 | --docker-custom-image-name / \ 137 | # Private registry: https://learn.microsoft.com/en-us/azure/app-service/configure-custom-container?tabs=debian&pivots=container-linux#use-an-image-from-a-private-registry 138 | --docker-registry-server-url \ 139 | --docker-registry-server-user \ 140 | --docker-registry-server-password 141 | 142 | # NOTE: Another version of it, using 143 | # az webapp create --deployment-container-image-name .azurecr.io/$image:$tag 144 | # https://learn.microsoft.com/en-us/azure/app-service/tutorial-custom-container 145 | } 146 | 147 | # https://learn.microsoft.com/en-us/azure/app-service/tutorial-multi-container-app 148 | compose_deployment() { 149 | echo "Creating webapp with Docker Compose configuration" 150 | $dockerComposeFile=docker-compose-wordpress.yml 151 | # Note that az webapp create is different 152 | az webapp create --resource-group $resourceGroup --plan $appServicePlan --name wordpressApp --multicontainer-config-type compose --multicontainer-config-file $dockerComposeFile 153 | 154 | echo "Setup database" 155 | az mysql server create --resource-group $resourceGroup --name wordpressDb --location $location --admin-user adminuser --admin-password letmein --sku-name B_Gen5_1 --version 5.7 156 | az mysql db create --resource-group $resourceGroup --server-name --name wordpress 157 | 158 | echo "Setting app settings for WordPress" 159 | az webapp config appsettings set \ 160 | --settings WORDPRESS_DB_HOST=".mysql.database.azure.com" WORDPRESS_DB_USER="adminuser" WORDPRESS_DB_PASSWORD="letmein" WORDPRESS_DB_NAME="wordpress" MYSQL_SSL_CA="BaltimoreCyberTrustroot.crt.pem" \ 161 | --resource-group $resourceGroup \ 162 | --name wordpressApp 163 | } 164 | 165 | # https://learn.microsoft.com/en-us/azure/app-service/deploy-zip?tabs=cli 166 | # uses the same Kudu service that powers continuous integration-based deployments 167 | zip_archive() { 168 | az webapp deploy --src-path "path/to/zip" --name $webapp --resource-group $resourceGroup 169 | # Zip from url 170 | # az webapp deploy --src-url "https://storagesample.blob.core.windows.net/sample-container/myapp.zip?sv=2021-10-01&sb&sig=slk22f3UrS823n4kSh8Skjpa7Naj4CG3" --name $webapp --resource-group $resourceGroup 171 | 172 | # (Optional) Enable build automation 173 | # az webapp config appsettings set --settings SCM_DO_BUILD_DURING_DEPLOYMENT=true --name $webapp --resource-group $resourceGroup 174 | } -------------------------------------------------------------------------------- /Services/Blobs.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using Azure.Core; 3 | using Azure.Identity; 4 | using Azure.Storage; 5 | using Azure.Storage.Blobs; 6 | using Azure.Storage.Blobs.Models; 7 | using Azure.Storage.Blobs.Specialized; 8 | using Azure.Storage.Sas; 9 | using Microsoft.AspNetCore.Http; 10 | using Microsoft.AspNetCore.Mvc; 11 | using Microsoft.Azure.WebJobs; 12 | using Microsoft.Azure.WebJobs.Extensions.Http; 13 | using Microsoft.Extensions.Logging; 14 | 15 | namespace Services; 16 | 17 | // Cool: 30 18 | // Archive: 180; offline. Copy or change tier. Min 1 hour for premium. Only for individual blob block. 19 | 20 | // Lifecycle: 21 | // - Blob blocks: all 22 | // - Version: all 23 | // - Snapshots: no cool 24 | // - Append: only delete 25 | 26 | // Filters: 27 | // - prefixMatch: / at the end to match exactly, otherwise it's treated as startsWith 28 | // - blobIndexMatch: json { name, op, value } 29 | 30 | // Version / Snapshots 31 | // Creation: Manual / Auto if enabled 32 | // Immut: Always / Only last version 33 | 34 | // Replication: requires change feed and versioning 35 | 36 | // All HTTP operations are PUT 37 | 38 | // restype=container 39 | // comp=block 40 | // comp=appendblock 41 | // comp=page 42 | // comp=lease 43 | // comp=metadata 44 | // comp=list 45 | 46 | // Use OAuth access tokens for authentication 47 | // - Delegation Scope: Use `user_impersonation` to allow applications to perform actions permitted by the user. 48 | // - Resource ID: Use `https://storage.azure.com/` to request tokens. 49 | 50 | // Anonymous public read access: If allowed at the storage account level; then depends per container/blob 51 | 52 | // Properties and meta: x-ms- and x-ms-meta- 53 | 54 | // StorageSharedKeyCredential (storage account key) 55 | // DefaultAzureCredential 56 | // ClientSecretCredential (AD through app registration) 57 | 58 | class BlobService 59 | { 60 | async Task CreateSnapshot() 61 | { 62 | var options = new BlobClientOptions(); 63 | var blobClient = new BlobClient(new Uri("..."), options); 64 | BlobSnapshotInfo snapshotInfo = await blobClient.CreateSnapshotAsync(); 65 | // If you attempt to delete a blob that has snapshots, the operation will fail unless you explicitly specify that you also want to delete the snapshots 66 | await blobClient.DeleteIfExistsAsync(DeleteSnapshotsOption.IncludeSnapshots); 67 | } 68 | } 69 | 70 | class BlobFunctions 71 | { 72 | // If Connection parameter is not specified in [Blob(Connection=?)], then value of "AzureWebJobsStorage" is used 73 | // You can set it from Azure portal > Configurations > AzureWebJobsStorage 74 | // or local.settings.json > Values.AzureWebJobsStorage 75 | 76 | // Login 77 | // Storage Account Key: new StorageSharedKeyCredential(accountName, ""); 78 | // AD Login: new DefaultAzureCredential(); 79 | // App registration: new ClientSecretCredential("", "", ""); 80 | 81 | // BlobSasBuilder uses DateTimeOffset instead of DateTime 82 | 83 | private static BlobServiceClient GetBlobServiceClient(HttpRequest req, BlobClientOptions? options = null) 84 | { 85 | if (req.Headers.TryGetValue("AccountName", out var accountName)) 86 | { 87 | TokenCredential credential; 88 | if (req.Headers.TryGetValue("ManagedIdentityId", out var managedIdentityId)) 89 | credential = new ManagedIdentityCredential(managedIdentityId); 90 | else 91 | credential = new DefaultAzureCredential(); 92 | return new BlobServiceClient(new Uri($"https://${accountName}.blob.core.windows.net"), credential, options); 93 | } 94 | 95 | var connectionString = Environment.GetEnvironmentVariable("AzureWebJobsStorage") ?? throw new Exception(); 96 | return new BlobServiceClient(connectionString, options); 97 | } 98 | 99 | private static BlobContainerClient GetBlobContainerClient(HttpRequest req, string containerName, BlobClientOptions? options = null) 100 | { 101 | if (req.Headers.TryGetValue("AccountName", out var accountName)) 102 | { 103 | TokenCredential credential; 104 | if (req.Headers.TryGetValue("ManagedIdentityId", out var managedIdentityId)) 105 | credential = new ManagedIdentityCredential(managedIdentityId); 106 | else 107 | credential = new DefaultAzureCredential(); 108 | return new BlobContainerClient(new Uri($"https://${accountName}.blob.core.windows.net/{containerName}"), credential, options); 109 | } 110 | 111 | var connectionString = Environment.GetEnvironmentVariable("AzureWebJobsStorage") ?? throw new Exception(); 112 | return new BlobContainerClient(connectionString, containerName, options); 113 | } 114 | 115 | [FunctionName("ListContainers")] 116 | public static async Task ListContainers( 117 | [HttpTrigger(AuthorizationLevel.Function, "get", Route = "containers")] HttpRequest req, 118 | ILogger log) 119 | { 120 | var containerClient = GetBlobServiceClient(req); 121 | 122 | if (req.Query.ContainsKey("metadata")) 123 | { 124 | var metadata = new List>(); 125 | await foreach (var container in containerClient.GetBlobContainersAsync()) 126 | metadata.Add(container.Properties.Metadata); 127 | return new OkObjectResult(metadata); 128 | } 129 | 130 | if (req.Query.ContainsKey("properties")) 131 | { 132 | var properties = new List(); 133 | await foreach (var container in containerClient.GetBlobContainersAsync()) 134 | properties.Add(container.Properties); 135 | return new OkObjectResult(properties); 136 | } 137 | 138 | var names = new List(); 139 | await foreach (var container in containerClient.GetBlobContainersAsync()) 140 | names.Add(container.Name); 141 | return new OkObjectResult(names); 142 | } 143 | 144 | [FunctionName("ListBlobs")] 145 | public static async Task ListBlobs( 146 | [HttpTrigger(AuthorizationLevel.Function, "get", Route = "containers/{name}")] HttpRequest req, string name, 147 | [Blob("{name}", FileAccess.Read)] BlobContainerClient containerClient, 148 | ILogger log) 149 | { 150 | if (containerClient == null) 151 | { 152 | log.LogError($"Container {name} not found!"); 153 | return new NotFoundResult(); 154 | } 155 | 156 | var items = new List(); 157 | await foreach (var blob in containerClient.GetBlobsAsync()) 158 | items.Add($"{blob.Name}"); 159 | 160 | return new OkObjectResult(items); 161 | } 162 | 163 | [FunctionName("CreateContainer")] 164 | public static async Task CreateContainer( 165 | [HttpTrigger(AuthorizationLevel.Function, "post", Route = "containers/{name}")] HttpRequest req, string name, 166 | ILogger log) 167 | { 168 | var containerClient = GetBlobContainerClient(req, name); 169 | await containerClient.CreateIfNotExistsAsync(); 170 | 171 | if (req.Query.ContainsKey("metadata")) 172 | { 173 | var metadata = new Dictionary(); 174 | containerClient.SetMetadata(metadata); 175 | } 176 | var properties = containerClient.GetProperties(); 177 | 178 | return new OkObjectResult(properties); 179 | } 180 | 181 | [FunctionName("GetBlob")] 182 | public static IActionResult GetBlob( 183 | [HttpTrigger(AuthorizationLevel.Function, "get", Route = "containers/{container}/{name}")] HttpRequest req, string container, string name, 184 | [Blob("{container}/{name}", FileAccess.Read/*, Connection = "AzureWebJobsStorage" */)] Stream blob, 185 | ILogger log 186 | ) 187 | { 188 | if (blob == null) 189 | { 190 | log.LogError($"Blob /{container}/{name} not found!"); 191 | return new NotFoundResult(); 192 | } 193 | 194 | using StreamReader reader = new StreamReader(blob); 195 | string content = reader.ReadToEnd(); 196 | log.LogInformation($"Blob Content: {content}"); 197 | return new OkObjectResult(content); 198 | } 199 | 200 | // Azure Functions runtime will automatically create the blob in target container for you when you try to write to it 201 | [FunctionName("UploadBlob")] 202 | public static async Task UploadBlob( 203 | [HttpTrigger(AuthorizationLevel.Function, "post", Route = "containers/{container}/{name}")] HttpRequest req, string container, string name, 204 | [Blob("{container}/{name}", FileAccess.Write)] Stream blob, 205 | ILogger log 206 | ) 207 | { 208 | var content = await new StreamReader(req.Body).ReadToEndAsync(); 209 | using StreamWriter writer = new StreamWriter(blob); 210 | writer.Write(content); 211 | return new OkObjectResult(content); 212 | } 213 | 214 | [FunctionName("SyncAccounts")] 215 | public static async Task SyncAccounts( 216 | [HttpTrigger(AuthorizationLevel.Admin, "get", "post", Route = "sync")] HttpRequest req, 217 | ILogger log 218 | ) 219 | { 220 | var sourceName = req.Query["source"]; 221 | var tatgetName = req.Query["tatget"]; 222 | 223 | TokenCredential credential; 224 | if (req.Headers.TryGetValue("ManagedIdentityId", out var managedIdentityId)) 225 | credential = new ManagedIdentityCredential(managedIdentityId); 226 | else 227 | credential = new DefaultAzureCredential(); 228 | 229 | var source = new BlobServiceClient(new Uri($"https://${sourceName}.blob.core.windows.net"), credential); 230 | var target = new BlobServiceClient(new Uri($"https://${tatgetName}.blob.core.windows.net"), credential); 231 | 232 | await foreach (var sourceContainer in source.GetBlobContainersAsync()) 233 | { 234 | var targetContainer = target.GetBlobContainerClient(sourceContainer.Name); 235 | await targetContainer.CreateIfNotExistsAsync(); 236 | 237 | var sourceBlobContainerClient = source.GetBlobContainerClient(sourceContainer.Name); 238 | 239 | await foreach (var blobItem in sourceBlobContainerClient.GetBlobsAsync()) 240 | { 241 | log.LogInformation($"Blob name: {blobItem.Name}"); 242 | 243 | var sourceBlobClient = sourceBlobContainerClient.GetBlobClient(blobItem.Name); 244 | var targetBlobClient = targetContainer.GetBlobClient(blobItem.Name); 245 | 246 | // Sync blob to target container 247 | var sourceBlobUri = sourceBlobClient.Uri; 248 | await targetBlobClient.StartCopyFromUriAsync(sourceBlobUri); 249 | } 250 | } 251 | 252 | return new OkResult(); 253 | } 254 | 255 | [FunctionName("CreateUserDelegatedSas")] 256 | public static async Task CreateUserDelegatedSas( 257 | [HttpTrigger(AuthorizationLevel.Admin, "get", Route = "sas/user/{container}/{name}")] HttpRequest req, string container, string name, 258 | ILogger log 259 | ) 260 | { 261 | var accountName = req.Headers["AccountName"]; 262 | var credential = new DefaultAzureCredential(); 263 | var serviceClient = new BlobServiceClient(new Uri($"https://${accountName}.blob.core.windows.net"), credential); 264 | var blobClient = serviceClient.GetBlobContainerClient(container).GetBlobClient(name); 265 | 266 | var sasBuilder = new BlobSasBuilder() 267 | { 268 | BlobContainerName = container, 269 | BlobName = name, 270 | Resource = "b", 271 | StartsOn = DateTimeOffset.UtcNow, 272 | ExpiresOn = DateTimeOffset.UtcNow.AddDays(1) 273 | }; 274 | sasBuilder.SetPermissions(BlobSasPermissions.Read | BlobSasPermissions.Write); 275 | 276 | UserDelegationKey userDelegationKey = await serviceClient.GetUserDelegationKeyAsync( 277 | DateTimeOffset.UtcNow, 278 | DateTimeOffset.UtcNow.AddDays(1)); 279 | // Add the SAS token to the blob URI 280 | BlobUriBuilder uriBuilder = new BlobUriBuilder(blobClient.Uri) 281 | { 282 | // Specify the user delegation key 283 | Sas = sasBuilder.ToSasQueryParameters(userDelegationKey, serviceClient.AccountName) 284 | }; 285 | 286 | return new OkObjectResult(uriBuilder.ToUri().ToString()); 287 | } 288 | 289 | [FunctionName("CreateServiceContainerSas")] 290 | public static IActionResult CreateServiceContainerSas( 291 | [HttpTrigger(AuthorizationLevel.Admin, "get", Route = "sas/service/{container}/{name}")] HttpRequest req, string container, 292 | ILogger log 293 | ) 294 | { 295 | var accountName = req.Headers["AccountName"]; 296 | var accountKey = req.Headers["AccountKey"]; 297 | var credential = new StorageSharedKeyCredential(accountName, accountKey); 298 | var containerClient = new BlobContainerClient(new Uri($"https://${accountName}.blob.core.windows.net/{container}"), credential); 299 | 300 | var sasBuilder = new BlobSasBuilder() 301 | { 302 | BlobContainerName = container, 303 | Resource = "c", 304 | StartsOn = DateTimeOffset.UtcNow, 305 | ExpiresOn = DateTimeOffset.UtcNow.AddDays(1) 306 | }; 307 | sasBuilder.SetPermissions(BlobSasPermissions.Read | BlobSasPermissions.Write); 308 | 309 | var serviceSasUri = containerClient.GenerateSasUri(sasBuilder); 310 | 311 | return new OkObjectResult(serviceSasUri.ToString()); 312 | } 313 | 314 | [FunctionName("CreateServiceBlobSas")] 315 | public static IActionResult CreateServiceBlobSas( 316 | [HttpTrigger(AuthorizationLevel.Admin, "get", Route = "sas/service/{container}/{name}")] HttpRequest req, string container, string name, 317 | ILogger log 318 | ) 319 | { 320 | var accountName = req.Headers["AccountName"]; 321 | var accountKey = req.Headers["AccountKey"]; 322 | var credential = new StorageSharedKeyCredential(accountName, accountKey); 323 | var containerClient = new BlobContainerClient(new Uri($"https://${accountName}.blob.core.windows.net/{container}"), credential); 324 | var blobClient = containerClient.GetBlobClient(name); 325 | 326 | var sasBuilder = new BlobSasBuilder() 327 | { 328 | BlobContainerName = container, 329 | BlobName = name, 330 | Resource = "b", 331 | StartsOn = DateTimeOffset.UtcNow, 332 | ExpiresOn = DateTimeOffset.UtcNow.AddDays(1) 333 | }; 334 | sasBuilder.SetPermissions(BlobSasPermissions.Read | BlobSasPermissions.Write); 335 | 336 | var serviceSasUri = blobClient.GenerateSasUri(sasBuilder); 337 | 338 | return new OkObjectResult(serviceSasUri.ToString()); 339 | } 340 | 341 | [FunctionName("CreateAccountSas")] 342 | public static IActionResult CreateAccountSas( 343 | [HttpTrigger(AuthorizationLevel.Admin, "get", Route = "sas/account")] HttpRequest req, 344 | ILogger log 345 | ) 346 | { 347 | var accountName = req.Headers["AccountName"]; 348 | var accountKey = req.Headers["AccountKey"]; 349 | var credential = new StorageSharedKeyCredential(accountName, accountKey); 350 | 351 | var sasBuilder = new AccountSasBuilder() 352 | { 353 | Services = AccountSasServices.Blobs, 354 | ResourceTypes = AccountSasResourceTypes.Service, 355 | ExpiresOn = DateTimeOffset.UtcNow.AddDays(1), 356 | Protocol = SasProtocol.Https 357 | }; 358 | sasBuilder.SetPermissions(AccountSasPermissions.Read | AccountSasPermissions.Write); 359 | 360 | var sasToken = sasBuilder.ToSasQueryParameters(credential).ToString(); 361 | var blobServiceURI = $"https://{accountName}.blob.core.windows.net?{sasToken}"; 362 | 363 | return new OkObjectResult(blobServiceURI); 364 | } 365 | 366 | [FunctionName("CreateSharedAccessContainerPolicy")] 367 | public static IActionResult CreateSharedAccessContainerPolicy( 368 | [HttpTrigger(AuthorizationLevel.Admin, "get", Route = "sap/container/{container}")] HttpRequest req, string container, 369 | [Blob("{name}", FileAccess.Write)] BlobContainerClient containerClient, 370 | ILogger log 371 | ) 372 | { 373 | var identifier = new BlobSignedIdentifier() 374 | { 375 | Id = Guid.NewGuid().ToString(), 376 | AccessPolicy = new BlobAccessPolicy() 377 | { 378 | ExpiresOn = DateTimeOffset.UtcNow.AddDays(1), 379 | Permissions = "r" 380 | } 381 | }; 382 | containerClient.SetAccessPolicy(permissions: new BlobSignedIdentifier[] { identifier }); 383 | 384 | return new OkResult(); 385 | } 386 | 387 | [FunctionName("CreateLease")] 388 | public static async Task CreateLease( 389 | [HttpTrigger(AuthorizationLevel.Admin, "get", Route = "sap/container/{container}/{name}")] HttpRequest req, string container, string name, 390 | [Blob("{container}", FileAccess.Write)] BlobContainerClient containerClient, 391 | ILogger log 392 | ) 393 | { 394 | var leaseId = Guid.NewGuid().ToString(); 395 | var blobLeaseClient = containerClient.GetBlobClient(name).GetBlobLeaseClient(leaseId); 396 | await blobLeaseClient.AcquireAsync(TimeSpan.FromHours(1)); 397 | 398 | return new OkObjectResult(leaseId); 399 | } 400 | 401 | [FunctionName("AccessLeasedBlob")] 402 | public static IActionResult AccessLeasedBlob( 403 | [HttpTrigger(AuthorizationLevel.Admin, "get", Route = "sap/container/{container}/{name}")] HttpRequest req, string container, string name, 404 | [Blob("{container}", FileAccess.Write)] BlobContainerClient containerClient, 405 | ILogger log 406 | ) 407 | { 408 | var leaseId = req.Headers["LeaseId"]; 409 | var metadata = new Dictionary(); 410 | var options = new BlobRequestConditions() { LeaseId = leaseId }; 411 | var blobClient = containerClient.GetBlobClient(name); 412 | blobClient.SetMetadata(metadata, options); 413 | 414 | return new OkResult(); 415 | } 416 | 417 | [FunctionName("BlobTrigger")] 418 | public static async Task RunBlob( 419 | [BlobTrigger("{container}/{name}.conf")] string myBlob, string container, string name, 420 | [Blob("{container}/events.log", FileAccess.Write)] AppendBlobClient blobClient, 421 | ILogger log) 422 | { 423 | var logMessage = $"{DateTime.UtcNow}: {container}/{name}.conf changed \n Data: {myBlob}\n"; 424 | using MemoryStream stream = new MemoryStream(Encoding.UTF8.GetBytes(logMessage)); 425 | await blobClient.AppendBlockAsync(stream); 426 | return new OkResult(); 427 | } 428 | } 429 | 430 | --------------------------------------------------------------------------------