l

Request A Quote

ASP.NET Core – MVC Web Application – Integrate:

  • Azure Active Directory
  • Azure App Configuration
  • Azure Key Vault
  • Azure Functions and
  • Azure Storage (Tables, Blobs, and Queues)
  • Publish with Continuous Integration and Continuous Delivery using GitHub Actions

    The purpose of this document is to go over the steps that are necessary to build a development environment that will host an ASP.NET Core MVC Web Application, Azure Functions, and Azurite for Azure Storage Emulation.

    Introduction — This post will be a standard “Todo” / “Task” application where Users of our application will store list of tasks on our site.

    The following ASP.NET Core MVC Web Application will integrate with the following technologies for the following purposes:

        • Azure Active Directory – We will be using the Single Tenant Authentication Option for user Authentication.
        • Azure App Configuration – We will be using this to store information that we would commonly use in appsetting.json to be retrieved via this cloud service at runtime.
        • Azure Key Vault – We will be using this to store secret information that we would commonly use in appsettings.json to be retrieved via this cloud service at runtime.
        • Azure Storage Tables – We will be using this to store basic information about Users, and their Todos.
        • Azure Storage Blobs – We will be using this to store lists of Tasks.
        • Azure Storage Queues – We will be using this as a messaging system to let the Azure Functions know that a Todo has been modified, and that the lists should be rebuilt.
        • Azure Functions – We will be using this to listen to messages sent to the Storage Queue and perform the list rebuilding.
        • Azure App Service – We will be using this to host our MVC Web Application.
        • GitHub Actions – We will be using this to Continuously Build and Delivery our MVC Web Application to Azure App Service, and Azure Functions using a mono repo approach.

    There are many steps and integrations. We have previously published other articles that cover some of these topics in smaller bites, and while not necessary to follow along with this tutorial, it may be helpful to go through or review these articles:

    To accomplish all of this we will be taking the following steps:

        • Create a Development Container
        • Confirm Connectivity to Azurite
        • Create a new C# Solution
        • Create a MVC Application
        • Create an App Registration in Azure Active Directory Tenant and Configure the MVC Application
        • Create Class Libraries that the MVC Application and Azure Functions can share
        • Create an Azure Functions Project
        • Set up debuggers for the MVC Application and Azure Function Projects
        • Build the Entity Models
        • Build the Repository Layer
        • Build the Service Layer
        • Build the Functions Project
        • Build the Web Application
        • Test the application locally
        • Create Azure Resources:
        • – Create an Azure Resource Group

          – Create an Azure App Configuration

          – Create an Azure Key Vault

          – Create an Azure Storage Account

          – Create an Azure Application App Service Plan

          – Create an Azure App Service

          – Create an Azure Functions App

        • Copy settings to Azure App Configuration and Azure Key Vault
        • Sign-in to Azure CLI, Setup Container Environment Variables and Setup Web App and Functions Project to read from Azure App Configuration and Key Vault
        • Verify App Configuration is being downloaded locally and that your application still runs locally
        • Add Environment Variables to the Web App and Functions app in Azure
        • Add Configuration and Secrets for production to the App Configuration and Key Vault
        • Create and Configure Web Application and Function Application identities to read App Configuration and Key Vault
        • Publish to GitHub
        • Build GitHub Actions to Deploy to Azure App Services and Functions App from GitHub repository

    Create a Development Container

    To begin, let’s… 

      1 – Create a new folder on your computer somewhere. I will use the name of “azure-storage-todos-demo.”
      2 – Open the empty folder in Visual Studio Code.
      3 – Create a new folder labeled “.devcontrainer”.

    In that folder create a new file labeled “Dockerfile”.

    Give it the following code:

    # [Choice] .NET version: 6.0-focal, 3.1-focal
    ARG VARIANT="6.0-focal"
    FROM mcr.microsoft.com/vscode/devcontainers/dotnet:0-${VARIANT}
    
    # [Choice] Node.js version: none, lts/*, 18, 16, 14
    ARG NODE_VERSION="lts/*"
    RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi
    
    # [Optional] Uncomment this section to install additional OS packages.
    # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
    #     && apt-get -y install --no-install-recommends <your-package-list-here>
    
    # [Optional] Uncomment this line to install global node packages.
    RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g azure-functions-core-tools@4 --unsafe-perm true" 2>&1
    
    Some things to note. This means that we are using the 6.0 version of .NET SDK, the LTS version of NodeJs. We are also installing the azure-functions-core-tools to the development container to make it possible for us to create Azure functions in our development container.

    So, next let’s create a docker-compose.yml file in that .devcontainer folder.

    Give it the following code:

    version: '3'
    
    services:
      app:
        build: 
          context: .
          dockerfile: Dockerfile
          args:
            # Update 'VARIANT' to pick a version of .NET: 3.1-focal, 6.0-focal
            VARIANT: "6.0-focal"
            # Optional version of Node.js
            NODE_VERSION: "lts/*"
    
        volumes:
          - ..:/workspace:cached
    
        # Overrides default command so things don't shut down after the process ends.
        command: sleep infinity
    
        # Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function.
        network_mode: service:azurite
    
        # Uncomment the next line to use a non-root user for all processes.
        # user: vscode
    
        # Use "forwardPorts" in **devcontainer.json** to forward an app port locally. 
        # (Adding the "ports" property to this file will not forward from a Codespace.)
    
      azurite:
        image: mcr.microsoft.com/azure-storage/azurite
        restart: unless-stopped
        command: "azurite -d /opt/azurite/azurite_logs.txt"
    
        # Add "forwardPorts": ["1433"] to **devcontainer.json** to forward MSSQL locally.
        # (Adding the "ports" property to this file will not forward from a Codespace.)
    
    Take note that we are using the mcr.microsoft.com/azure-storage/azurite docker image, and that we put the app in the same network as that image so that we can use it in our MVC application, and functions applications.

    Now, create a devcontainer.json file in that .devcontainer folder.

    Give it the following code:

    // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
    // https://github.com/microsoft/vscode-dev-containers/tree/v0.238.0/containers/dotnet-mssql
    {
        "name": "C# (.NET) and Azurite",
        "dockerComposeFile": "docker-compose.yml",
        "service": "app",
        "workspaceFolder": "/workspace",
    
        // Configure tool-specific properties.
        "customizations": {
            // Configure properties specific to VS Code.
            "vscode": {
                // Set *default* container specific settings.json values on container create.
                "settings": {},
                
                // Add the IDs of extensions you want installed when the container is created.
                "extensions": [
                    "ms-dotnettools.csharp",
                    "ms-azuretools.vscode-azurefunctions"
                ]
            }
        },
    
        // Use 'forwardPorts' to make a list of ports inside the container available locally.
        "forwardPorts": [10000, 10001, 10002],
    
        // [Optional] To reuse of your local HTTPS dev cert:
        //
        // 1. Export it locally using this command:
        //    * Windows PowerShell:
        //        dotnet dev-certs https --trust; dotnet dev-certs https -ep "$env:USERPROFILE/.aspnet/https/aspnetapp.pfx" -p "SecurePwdGoesHere"
        //    * macOS/Linux terminal:
        //        dotnet dev-certs https --trust; dotnet dev-certs https -ep "${HOME}/.aspnet/https/aspnetapp.pfx" -p "SecurePwdGoesHere"
        // 
        // 2. Uncomment these 'remoteEnv' lines:
        //    "remoteEnv": {
        //        "ASPNETCORE_Kestrel__Certificates__Default__Password": "SecurePwdGoesHere",
        //        "ASPNETCORE_Kestrel__Certificates__Default__Path": "/home/vscode/.aspnet/https/aspnetapp.pfx",
        //    },
        //
        // 3. Next, copy your certificate into the container:
        //      1. Start the container
        //      2. Drag ~/.aspnet/https/aspnetapp.pfx into the root of the file explorer
        //      3. Open a terminal in VS Code and run "mkdir -p /home/vscode/.aspnet/https && mv aspnetapp.pfx /home/vscode/.aspnet/https"
    
        // postCreateCommand.sh parameters: $1=SA password, $2=dacpac path, $3=sql script(s) path
        
        "features": {
            "github-cli": "latest",
            "azure-cli": "latest"
        }
    
    }
    
    A couple of things to note here:
      • We are adding the csharp plug-in (OmniSharp) as well as the AzureFunctions tool to allow us to be able to debug functions in our development environment
      • We are forwarding ports 10000, 10001, 10002 which are the Azureite services Blob, Queue, and Table services. Making it so that we can access those items from the Azure Storage Explorer Via our local computer
      • We have also installed the latest github and azure command line interfaces

    This should be sufficient to setup a development environment to run and debug our app as well as have Azurite run as a service in a separate container that both our local machine, and app container can access.

    Open the command pallet (CTRL+SHIFT+P) and select the “Remote-Containers: Rebuild and Reopen in Container” option.

    When it is done building, check your docker desktop and you should see something like this:
    And, if you check the “Ports” section in Visual Studio Code. You should see something like this:

    Confirm Connectivity to Azurite

    Launch the Azure Storage Explorer on your computer.

    According to https://docs.microsoft.com/en-us/azure/storage/common/storage-use-azurite the default HTTP Connection string is…

    DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;QueueEndpoint=http://127.0.0.1:10001/devstoreaccount1;TableEndpoint=http://127.0.0.1:10002/devstoreaccount1;
    First, right-click on Storage Accounts and click on Connect to Azure Storage

    Next, select that you want to connect to a Storage account or service

    Select connection string

    Give it a display name such as “Dev Container”, and then copy and paste the connection string from above.

    Click on Next, and then click on Connect.

    You will now see the storage account in your available connections.

    Double click on it and perform any experiments that you like.

    Create a new C# Solution

    We will now create a solution file to hold information about all of our projects.

    Open a new terminal in Visual Studio Code (CTRL+SHIFT+`) and execute the following commands:

    dotnet new sln
    mv workspace.sln azure-storage-todos-demo.sln
    

    Create an MVC Web Application

    The web application will use Azure Storage in the following ways:

      • Read Blobs to get lists of Todos
      • Add, Update, and Delete Todos in Tables
      • Send Messages to Queues to let the Queue know that a Table item has been modified, and that it is time to rebuild the list of Todos in the Blob Storage

    Execute the following commands, assuming that you are still in the /workspace directory:

    dotnet new mvc --auth SingleOrg --calls-graph -o AzureStorageTodos.Web
    dotnet sln add ./AzureStorageTodos.Web/
    dotnet dev-certs https --trust
    
    Open the AzureStorageTodos.Web/properties/launchSettings.json file as well as the AzureStorageTodos.Web/appsettings.json file since we will be needing some of the information in these files to create our Application Registration as well as updating the information in our appsetting.json file.

    Create an App Registration in Azure Active Directory Tenant and Configure the MVC Application

    In this example we will be creating an application registration using the azure portal @ https://portal.azure.com/

    NOTE: – Be sure that you are in a directory that you can manage, and then go to the “Azure Active Directory” resource.

    To start with, copy and paste the Domain and TenantId on the Overview page to the appropriate settings in the appsettings.json file.

    Select the “App Registrations” blade
    Click on “New Registration”
    Give the application registration something meaningful. For this example, I am going to use “Azure Storage Todos”.
    The supported account types will be: “Accounts in this organization only”.

    For Redirect URI select the “Web” option.

    For the URI section review the AzureStorageTodos.Web/properties/launchSettings.json file where it has an applicationUrl that will be like “https://localhost:7039;http://localhost:5179”.

    NOTE: Your actual settings will probably be different.

    Also review your appsettings.json file and the callback path. Combine the https portion of the application url with the callback path in appsettings.json and you will come up with the correct Redirect URI. For me it is https://localhost:7039/signin-oidc

    Click on Register button.

    Once the Application Registration has been created, you will be taken to the Overview page for the Application Registration. Copy and paste the Application (client) ID to your appsettings.json file.

    Next,
      1 – Click on the “Certificates & secrets” blade.
      2 – Click on the “New Client Secret” button.
      3 – For the description I will use “Debug Environment”. I will also select the default 6 months expiration.
      4 – Copy the “Value” to the ClientSecret in the appsettings.json file
    NOTE: – A common mistake that many people make is copying the Secret Id to this spot. That is not correct, be sure that you are copying the Value.

    NOTE: –This is also your only time to be able to see this value. If you navigate away from this page, then you will need to delete and recreate this secret.

    Create Class Libraries that the MVC Application and Azure Functions can share

    Before we go much further let’s create some class libraries that both the Web Application and Azure Functions project will be able to share. We will have a library of entity models, a storage layer, and a service layer.

    Assuming that the command prompt is still at /workspace, let’s execute the following commands:

    dotnet new classlib -o AzureStorageTodos.Models
    dotnet new classlib -o AzureStorageTodos.Repository
    dotnet new classlib -o AzureStorageTodos.Service
    
    dotnet sln add ./AzureStorageTodos.Models
    dotnet sln add ./AzureStorageTodos.Repository
    dotnet sln add ./AzureStorageTodos.Service
    
    Some of these class libraries rely on each other. Let’s add them as a reference.

    Assuming that the terminal is still at /workspace, the commands will be:

    cd AzureStorageTodos.Repository
    dotnet add reference ../AzureStorageTodos.Models/
    
    cd ..
    
    cd AzureStorageTodos.Service
    dotnet add reference ../AzureStorageTodos.Models/
    dotnet add reference ../AzureStorageTodos.Repository/
    
    cd ..
    
    cd AzureStorageTodos.Web
    dotnet add reference ../AzureStorageTodos.Models/
    dotnet add reference ../AzureStorageTodos.Repository/
    dotnet add reference ../AzureStorageTodos.Service/
    
    cd ..
    
    Furthermore, the Repository and Web projects will need additional Azure NuGet package to interact with Blobs, Tables, and Queues. Assuming that the command prompt is still at /workspace.

    Execute the following commands:

    cd AzureStorageTodos.Repository
    dotnet add package Azure.Storage.Blobs
    dotnet add package Azure.Data.Tables
    dotnet add package Azure.Storage.Queues
    
    cd ..
    
    cd AzureStorageTodos.Web
    dotnet add package Azure.Storage.Blobs
    dotnet add package Azure.Data.Tables
    dotnet add package Azure.Storage.Queues
    dotnet add package Microsoft.Extensions.Azure
    
    cd ..
    
    Now that we have that in place we are ready to create our Azure Functions Project.

    Create an Azure Functions Project

    The Azure Functions Project will be monitoring the Queue Storage and executing tasks when the messages come in.

    Execute the following commands, assuming that you are still in the /workspace directory:

    mkdir AzureStorageTodos.AzureFunctions
    cd AzureStorageTodos.AzureFunctions
    func init (select options 1, and 1)
    
    This will create the .csproj file that we need to create functions for Azure. Let us also go ahead and create a function that will trigger on a Queue Message.

    Execute the following commands, assuming that you are still in the /workspace/AzureStorageTodos.AzureFunctions folder.

    func new (select option 1, and then give it the name of TodoUpdated)
    This project will need a number of Project and Package references as well in order to interact with Azure Storage and our other class libraries that we created earlier.

    Execute the following commands, assuming that you are still in the /workspace/AzureStorageTodos.AzureFunctions folder

    dotnet add package Microsoft.Azure.Functions.Extensions
    dotnet add package Microsoft.Extensions.DependencyInjection
    dotnet add package Microsoft.Extensions.Azure
    dotnet add package Azure.Identity
    dotnet add package Azure.Storage.Blobs
    dotnet add reference ../AzureStorageTodos.Models/
    dotnet add reference ../AzureStorageTodos.Repository/
    dotnet add reference ../AzureStorageTodos.Service/
    
    This project should also be added to the solution. Change directories to the /workspace folder and execute the following command:
    dotnet sln add /workspace/AzureStorageTodos.AzureFunctions/

    Set up debuggers for the MVC Application and Azure Function Projects

      1 – Open “Program.cs” from within the AzureStorageTodos.Web project in Visual Studio Code.
      2 – CTRL+SHIFT+P to bring up the command pallet.
      3 – Select the option to Restart OmniSharp.
      4 – When prompted say “Yes” to add the Debug files.
    You have nows created a .vscode folder with a tasks.json and launch.json files.

      5 – Modify the tasks.json file as follows:

    {
        "version": "2.0.0",
        "tasks": [
            {
                "label": "build",
                "command": "dotnet",
                "type": "process",
                "args": [
                    "build",
                    "${workspaceFolder}/AzureStorageTodos.Web/AzureStorageTodos.Web.csproj",
                    "/property:GenerateFullPaths=true",
                    "/consoleloggerparameters:NoSummary"
                ],
                "problemMatcher": "$msCompile"
            },
            {
                "label": "publish",
                "command": "dotnet",
                "type": "process",
                "args": [
                    "publish",
                    "${workspaceFolder}/AzureStorageTodos.Web/AzureStorageTodos.Web.csproj",
                    "/property:GenerateFullPaths=true",
                    "/consoleloggerparameters:NoSummary"
                ],
                "problemMatcher": "$msCompile"
            },
            {
                "label": "watch",
                "command": "dotnet",
                "type": "process",
                "args": [
                    "watch",
                    "run",
                    "--project",
                    "${workspaceFolder}/AzureStorageTodos.Web/AzureStorageTodos.Web.csproj"
                ],
                "problemMatcher": "$msCompile"
            },
            {
                "label": "clean (functions)",
                "options": {
                    "cwd": "${workspaceFolder}/AzureStorageTodos.AzureFunctions"
                },
                "command": "dotnet",
                "args": [
                  "clean",
                  "/property:GenerateFullPaths=true",
                  "/consoleloggerparameters:NoSummary"
                ],
                "type": "process",
                "problemMatcher": "$msCompile"
              },
              {
                "label": "build (functions)",
                "options": {
                    "cwd": "${workspaceFolder}/AzureStorageTodos.AzureFunctions"
                },
                "command": "dotnet",
                "args": [
                  "build",
                  "/property:GenerateFullPaths=true",
                  "/consoleloggerparameters:NoSummary"
                ],
                "type": "process",
                "dependsOn": "clean (functions)",
                "group": {
                  "kind": "build",
                  "isDefault": true
                },
                "problemMatcher": "$msCompile"
              },
              {
                "type": "func",
                "dependsOn": "build (functions)",
                "options": {
                  "cwd": "${workspaceFolder}/AzureStorageTodos.AzureFunctions/bin/Debug/net6.0"
                },
                "command": "host start",
                "isBackground": true,
                "problemMatcher": "$func-dotnet-watch"
              }
        ]
    }
    
    A few things to note here. We now have setup ways to build and clean both the Web Application and the Functions Projects. We will now modify the launch.json file to launch the debuggers after these builds happen.
      6 – Modify the launch.json file as follows:
    {
        "version": "0.2.0",
        "configurations": [
            {
                // Use IntelliSense to find out which attributes exist for C# debugging
                // Use hover for the description of the existing attributes
                // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md
                "name": ".NET Core Launch (web)",
                "type": "coreclr",
                "request": "launch",
                "preLaunchTask": "build",
                // If you have changed target frameworks, make sure to update the program path.
                "program": "${workspaceFolder}/AzureStorageTodos.Web/bin/Debug/net6.0/AzureStorageTodos.Web.dll",
                "args": [],
                "cwd": "${workspaceFolder}/AzureStorageTodos.Web",
                "stopAtEntry": false,
                // Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser
                "serverReadyAction": {
                    "action": "openExternally",
                    "pattern": "\\bNow listening on:\\s+(https?://\\S+)"
                },
                "env": {
                    "ASPNETCORE_ENVIRONMENT": "Development"
                },
                "sourceFileMap": {
                    "/Views": "${workspaceFolder}/Views"
                }
            },
            {
                "name": ".NET Core Attach",
                "type": "coreclr",
                "request": "attach"
            },
            {
                "name": "Attach to .NET Functions",
                "type": "coreclr",
                "request": "attach",
                "processId": "${command:azureFunctions.pickProcess}"
            },
        ],
        "compounds": [
            {
                "name": "Attach to functions and launch web app",
                "configurations": [".NET Core Launch (web)", "Attach to .NET Functions"],
                "stopAll": true
            }
        ]
    }
    
    Go to the “Run and Debug” blade in Visual Studio Code. Next to the green play button you will notice that you now have a few launch options.
    One that will launch our web app, another that can connect to other .NET processes. One that can connect to .NET Functions, and another that will attach to function and launch the web app. The last one will be the preferred one for us to use. But of course, if we are ever troubleshooting one service and not the other, it may be more appropriate to only select one of those.

    For now, let’s go ahead and make sure that the Attach to function and launch web app is selected, and then press the green button or press F5.

    It will launch a browser, and likely ask you to give consent on behalf of your organization to allow the application to run. Check the box to consent on behalf of your organization and click on Accept.

    Back in Visual Studio Code you will notice that you have a few more ports being Auto Forwarded.
    Some additional terminals have been created as well and that we are on a terminal that shows that the function is running.
    After accepting consent, we should be able to notice that the browser shows something like this:
    If we review the template that was created for us in the TodoUpdate.cs file within the AzureStorageTodos.AzureFunctions project. We can see that it is going to be looking for items in “myqueue-items”. Let’s try it out and send a message there just to see what happens.

    Within your Azure Storage Explorer connect to your development container, and then create a queue called “myqueue-items.”

    Next, click on Add Message
    Then type anything you like and then click on Ok.
    Return to your Visual Studio Code and you should see something similar to this:
    In Azure Storage Explorer Click on Refresh and you should notice that your message is out of the queue now.
    This is because the message was processed and is no longer needed in the queue.

    To test out the debugger for the function go to line 13 within the TodoUpdated.cs file, and press F9 and send another message to the queue.

    You will notice that– your Visual Studio Code will go Orange, requesting your attention. Click on it, and you will see some debug information on the left.

    Next, press F10 or the continue button in the debugger floater

    To confirm that the debugger is working for the web project as well open the “HomeController.cs” file within the AzureStorageTodos.Web project. Go to line 27 and put a break point there by pressing F9.

    Now go to your browser where the web application is running, and press F5 to refresh the page. Visual Studio again should go orange, and you can see all of the debug information on the left.

    Press F10 or the continue button in the debugger floater

    Now that we have appropriate debuggers setup, let’s go ahead and create the application.

    Build the Entity Models

      1 – To the AzureStorageTodos.Models project, delete Class1.cs.
      2 – Add an “Interface” folder.
      3 – In the “Interface” folder create a new file called “IBaseAzureStorageEntityModel.cs”.

    Give it the following code:

    namespace AzureStorageTodos.Models.Interface;
    
    public interface IBaseAzureStorageEntityModel
    {
        public string Id {get; set;}
    }
    
    In the Root of the AzureStorageTodos.Models project create a new file called “BaseAzureStorageEntityModel.cs” to implement this interface. Give if the following code:
    using AzureStorageTodos.Models.Interface;
    
    namespace AzureStorageTodos.Models;
    
    public class BaseAzureStorageEntityModel : IBaseAzureStorageEntityModel
    {
        public BaseAzureStorageEntityModel()
        {
            this.Id = Guid.NewGuid().ToString();        
        }
    
        public BaseAzureStorageEntityModel(string id)
        {
            this.Id = id;        
        }
    
        public string Id {get; set;}
    }
    
    We will be dealing with three primary entities in this application:
      • The User entity
      • The Todo entity
      • List of Todos entity

    Users will be stored in Tables. Todos will be stored in Tables. List of Todos will be stored in blobs. Their classes will all inherit from our BaseAzureSTorageEntityModel class.

    Create a new file called “UserEntityModel.cs”. Give it the following code:

    namespace AzureStorageTodos.Models;
    
    public class UserEntityModel : BaseAzureStorageEntityModel
    {    
        public string DisplayName {get; set;} = null!;
        public string EmailAddress {get; set;} = null!;    
    }
    
    Create a new file called “TodoEntityModel.cs”. Give it the following code:
    namespace AzureStorageTodos.Models;
    
    public class TodoEntityModel : BaseAzureStorageEntityModel
    {
        public string OwnerUserId {get; set;} = null!;
        public string? Title { get; set; }
        public string? Description { get; set; }
        public bool Completed { get; set; } = false;    
    }
    
    Create a new file called “TodoListEntityModel.cs” Give it the following code:
    namespace AzureStorageTodos.Models;
    
    public class TodoListEntityModel : BaseAzureStorageEntityModel
    {
        public TodoListEntityModel(string ownerUserId, string listType)
        {
            this.Id = $"{ownerUserId}-{listType}";
            this.OwnerUserId = ownerUserId;
            this.Results = new List<TodoEntityModel>();
        }
    
        public string OwnerUserId {get; set;} = null!;
        public IEnumerable<TodoEntityModel> Results {get; set;} = null!;
    }
    
    We now have the models for our project. Let’s work on storing our models.

    Build the Repository Layer

      1 – To the AzureStorageTodos.Repository project delete Class1.cs.
      2 – Create an “Interface” folder.
      3 – In the “Interface” folder create a new file called “IAzureStorageRepository.cs”.

    Give it the following code:

    using AzureStorageTodos.Models.Interface;
    
    namespace AzureStorageTodos.Repository.Interface;
    
    public interface IAzureStorageRepository<T> where T : IBaseAzureStorageEntityModel
    {
        Task<T?> UpsertAsync(T entityDetails);
        Task<T?> GetOneAsync(string id);
        Task<List<T>?> GetAllAsync();
        Task DeleteAsync(string id);
    }
    
    As you can tell we are setting up this interface to also accept a type parameter which will need to be inherited from the IBaseAzureStorageEntityModel interface which all 3 of the entities that we created earlier are.
      4 – Create a new folder in the root of the AzureStorageTodos.Repository project called “BaseRepositories”

      5 – Create a new file in the “BaseRepositories” folder called “AzureTableStorageRepository.cs”.

    Give it the following code:

    using Azure;
    using Azure.Data.Tables;
    using System.Reflection;
    using AzureStorageTodos.Models.Interface;
    using AzureStorageTodos.Repository.Interface;
    
    namespace AzureStorageTodos.Repository.BaseRepositories;
    
    public abstract class AzureTableStorageRepository<T> : IAzureStorageRepository<T> where T : IBaseAzureStorageEntityModel
    {
        private readonly TableServiceClient _tableServiceClient;
        private readonly string _tableName;
        private readonly string _partitionKey;
    
        public AzureTableStorageRepository(TableServiceClient tableServiceClient, string tableName, string partitionKey)
        {
            _tableServiceClient = tableServiceClient;
            _tableName = tableName;
            _partitionKey = partitionKey;
        }
    
        public async Task DeleteAsync(string id)
        {
            var tableClient = await GetTableClientAsync();
    
            await tableClient.DeleteEntityAsync(_partitionKey, id);
        }
    
        public async Task<List<T>?> GetAllAsync()
        {
            var tableClient = await GetTableClientAsync();
    
            var results = new List<T>();
    
            Pageable<TableEntity> queryResults = tableClient.Query<TableEntity>(w => w.PartitionKey == _partitionKey);
    
            foreach (var qEntity in queryResults)
            {
                if (qEntity is not null)
                {
                    var result = TableEntityToEntity(qEntity);
                    if (result is not null)
                    {
                        results.Add(result);
                    }
                }
            }
    
            return results;
        }
    
        public async Task<T?> GetOneAsync(string id)
        {
            var tableClient = await GetTableClientAsync();
    
            var tableEntity = await tableClient.GetEntityAsync<TableEntity>(_partitionKey, id);
    
            if (tableEntity is not null)
            {
                var result = TableEntityToEntity(tableEntity);
    
                if (result is not null)
                {
                    return result;
                }
            }
    
            return default(T);
        }
    
        public async Task<T?> UpsertAsync(T entityDetails)
        {
            var tableClient = await GetTableClientAsync();
    
            var tableEntity = EntityToTableEntity(entityDetails);
    
            await tableClient.UpsertEntityAsync(tableEntity);
    
            return entityDetails;
        }
    
        private TableEntity EntityToTableEntity(T entity)
        {
            var result = new TableEntity();
            result.PartitionKey = _partitionKey;
    
            Type t = typeof(T);
            PropertyInfo[] entityProperties = t.GetProperties();
    
            foreach (var entityProperty in entityProperties)
            {
    
                var propertyName = entityProperty.Name;
    
                if (propertyName == "Id")
                {
                    result.RowKey = entity.Id;
                }
                else
                {
                    var propertyValue = entityProperty.GetValue(entity);
    
                    result[propertyName] = propertyValue;
                }
    
            }
    
            return result;
        }    
    
        private T TableEntityToEntity(TableEntity tableEntity)
        {
            var result = (T)Activator.CreateInstance<T>();
    
            Type t = typeof(T);
            PropertyInfo[] entityProperties = t.GetProperties();
    
            foreach (var entityProperty in entityProperties)
            {
    
                var propertyName = entityProperty.Name;
    
                if (propertyName == "Id")
                {
                    result.Id = tableEntity.RowKey;
                }
                else
                {
    
                    var propertyValue = tableEntity[propertyName];
    
                    entityProperty.SetValue(result, propertyValue);
                }
            }
    
            return result;
        }
    
        private async Task<TableClient> GetTableClientAsync()
        {
            var tableClient = _tableServiceClient.GetTableClient(_tableName);
            await tableClient.CreateIfNotExistsAsync();
    
            return tableClient;
        }
    }
    
    There is allot going on in this file and is partially at the heart of this topic. The most important parts are the constructor and last 3 private methods.

    This constructor will expect a tableServiceClient, tableName, and partitionKey to be passed into it. The tableServiceClient will be injected via Dependency injection in the Startup class of the Web project. We will cover that later. The table name will tell this repository which Azure Storage Table to use, and the partition key will be the default Partition Key to use for all of the T type entities. Since these are strings you can get creative. You may want each entity in its own table. You may want all of the entities in one table, but a different partition key. You may want some combination of both. The only thing that truly matters is that each entity has a unique combination of TableName and RowKey when this class is initialized.

    The GetTableClientAsync private method is used for every public method of this class. It is used to establish the connection to Azure Table Storage and returns a TableClient. The main responsibility of the TableClient is to interact with TableEntities. The EntityToTableEntity and TableEntityToEntity method use reflections to convert an Azure TableEntity to our own custom entities that we pass to this class as a Type Paramater. And as long as that type inherits from the IBaseStorageEntityModel interface it can be used in this abstract class. An important thing that happens during the conversion aside from copying all of the properties is that when the property is the Id property then it will be the RowKey property for the TableEntity. Similarly, when the TableEntity is being converted to the standard entity the Id is set to the RowKey from the TableEntity.

    We need a similar method to make a Blob repository.

    In the “BaseRepositories” folder create a new file called “BlobStorageService.cs”. This will give us additional abstraction to make it easier for us to interact with Blobs for the purpose of a repository later.

    Give it the following code:

    using Azure.Storage.Blobs;
    using AzureStorageTodos.Repository.Interface;
    
    namespace AzureStorageTodos.Repository.BaseRepositories;
    
    public class BlobStorageService : IBlobStorageService
    {
        private readonly BlobServiceClient _blobServiceClient;
        private readonly string _containerName;
    
        public BlobStorageService(BlobServiceClient blobServiceClient, string containerName)
        {
            _blobServiceClient = blobServiceClient;
            _containerName = containerName;
        }
    
        public async Task DeleteDocumentAsync(string blobName)
        {
            var containerClient = await GetContainerClientAsync();
    
            var blobClient = containerClient.GetBlobClient(blobName);
    
            await blobClient.DeleteAsync();
        }
    
        public async Task<Stream> GetBlobContentAsync(string blobName)
        {
            var containerClient = await GetContainerClientAsync();
    
            var blobClient = containerClient.GetBlobClient(blobName);
    
            return await blobClient.OpenReadAsync();
        }
    
        public async Task<List<string>?> GetListOfBlobsInFolderAsync(string folderName)
        {
            var results = new List<string>();
    
            var containerClient = await GetContainerClientAsync();
    
            var blobsInFolder = containerClient.GetBlobs(prefix: folderName);
    
            if(blobsInFolder is not null) {
                foreach(var blob in blobsInFolder) {
                    results.Add(blob.Name);
                }
    
                return results;
            }
    
            return null;
        }
    
        public async Task UpdateBlobContentAsync(string blobName, Stream content)
        {
            var containerClient = await GetContainerClientAsync();
    
            var blobClient = containerClient.GetBlobClient(blobName);
    
            await blobClient.UploadAsync(content, true);
        }
    
        private async Task<BlobContainerClient> GetContainerClientAsync()
        {
            var blobContainerClient = _blobServiceClient.GetBlobContainerClient(_containerName);
    
            await blobContainerClient.CreateIfNotExistsAsync();
    
            return blobContainerClient;
        }
    }
    
    Quite a bit is going on in this file and this is only half the battle. Basically, what we have done here is created some short cuts to make it easier for us to interact with Blobs. The most important parts of this code are the constructor and private method to get a BlobContainerClient. The constructor expects a BlobServiceClient which will be injected via dependency injection during the startup of the web application. The name of the container will be given when the class is initiated as well.

    Now to officially create the BlobStorageRepository create a file called “AzureBlobStorageRepository.cs” in the “BaseRepositories” folder.

    Give it the following code:

    using System.Text.Json;
    using Azure.Storage.Blobs;
    using AzureStorageTodos.Models.Interface;
    using AzureStorageTodos.Repository.Interface;
    
    namespace AzureStorageTodos.Repository.BaseRepositories;
    
    public abstract class AzureBlobStorageRepository<T> : IAzureStorageRepository<T> where T : IBaseAzureStorageEntityModel
    {
        private readonly IBlobStorageService _blobStorageService;    
        private readonly string _folderName;
    
        public AzureBlobStorageRepository(BlobServiceClient blobServiceClient, string containerName, string folderName)
        {
            _blobStorageService = new BlobStorageService(blobServiceClient, containerName);
            _folderName = folderName;
        }
    
        public async Task DeleteAsync(string id)
        {
            string filePath = GetFilePath(id);
    
            await _blobStorageService.DeleteDocumentAsync(filePath);
        }
    
        public async Task<List<T>?> GetAllAsync()
        {
            try {
                List<string>? filesInFolder = await _blobStorageService.GetListOfBlobsInFolderAsync(_folderName);
    
                if(filesInFolder is not null) {
                    var results = new List<T>();
    
                    foreach(var file in filesInFolder) {
                        var id = GetIdFromFilePath(file);
                        try {
                            var result = await this.GetOneAsync(id);
                            if(result is not null) {
                                results.Add(result);
                            }
                        }
                        catch {
                            // Do nothing, try to get the next one
                        }
                    }
    
                    return results;
                }
            }
            catch {
                // Do nothing, will return null
            }
    
            return null;
        }
    
        public async Task<T?> GetOneAsync(string id)
        {
            try {
                string filePath = GetFilePath(id);
    
                var blobContent = await _blobStorageService.GetBlobContentAsync(filePath);
    
                var contentAsObject = JsonSerializer.Deserialize<T>(blobContent);
    
                return contentAsObject;
            }
            catch {
                // Do nothing, will return null
            }
    
            return default(T);
        }
    
        public async Task<T?> UpsertAsync(T entityDetails)
        {
            try {
                string filePath = GetFilePath(entityDetails.Id);
    
                var entityDetailsAsString = JsonSerializer.Serialize(entityDetails, new JsonSerializerOptions { WriteIndented = true });
    
                if(entityDetails is not null) {
                    var entityDetailsAsStream = new MemoryStream();
                    var streamWriter = new StreamWriter(entityDetailsAsStream);
                    streamWriter.Write(entityDetailsAsString);
                    streamWriter.Flush();
                    entityDetailsAsStream.Position = 0;
    
                    await _blobStorageService.UpdateBlobContentAsync(filePath, entityDetailsAsStream);
    
                    return entityDetails;
                }
            }
            catch {
                // Do nothing, return null
            }
    
            return default(T);
        }
    
        private string GetFilePath(string id)
        {
            string filePath = string.Join('/', _folderName, $"{id}.json");
    
            return filePath;
        }
    
        private string GetIdFromFilePath(string filePath)
        {
            string[] fileParts = filePath.Split('/');
            string lastPart = fileParts.Last();
            string[] fileNameParts = lastPart.Split('.');
            string idPart = fileNameParts.First();
    
            return idPart;
        }
    }
    
    The most important part is the constructor and two private methods. The constructor is allowing a BlobServiceClient to be passed in, which we will initialize via dependence injection. Also, when we create an instance of this class, we will provide a container name and folder name for all of the records / files to be stored in to. The private methods retrieve the ID given the file name or gets the file path when provided an id. This gives the public methods common file names to use when reading and writing data to the blob storage. When the data is read and written it is serializing the string or object to or from Json.

    With all of this in place we are now ready to create repositories for our Entity Model Classes.

    In the “Interface” folder create a new file called “IUsersRepository.cs”.

    Give it the following code:

    using AzureStorageTodos.Models;
    
    namespace AzureStorageTodos.Repository.Interface;
    
    public interface IUsersRepository : IAzureStorageRepository<UserEntityModel>
    {
    }
    
    In the “Interface” folder create a new file called “ITodosRepository.cs”.

    Give it the following code:

    using AzureStorageTodos.Models;
    
    namespace AzureStorageTodos.Repository.Interface;
    
    public interface ITodosRepository : IAzureStorageRepository<TodoEntityModel>
    {
    }
    
    In the “Interface” folder create a new file called “ITodoListRepository.cs”.

    Give it the following code:

    using AzureStorageTodos.Models;
    
    namespace AzureStorageTodos.Repository.Interface;
    
    public interface ITodoListRepository : IAzureStorageRepository<TodoListEntityModel>
    {    
    }
    
    We will be using a number of different combinations of Blob Container Names, Blob Folder Names, Table Names, Row Keys, and Queue Names. Let’s create some static files to avoid “magic strings”.

    To the root of the AzureStorageTods.Repository project create a new file called “StaticBlobContainerNames.cs”.

    Give it the following code:

    namespace AzureStorageTodos.Repository;
    
    public static class StaticBlobContainerNames
    {
        public const string TodoLists = "todo-lists";
    }
    
    Create a file called “StaticBlobFolderNames.cs”.

    Give it the following code:

    namespace AzureStorageTodos.Repository;
    
    public static class StaticBlobFolderNames
    {
        public const string TodoListItems = "todolist-items";
    }
    
    Create a file called “StaticTableNames.cs”.

    Give it the following code:

    namespace AzureStorageTodos.Repository;
    
    public static class StaticTableNames
    {
        public const string TodoUsers = "TodoUsers";
        public const string Todos = "Todos";
    }
    
    Create a file called “StaticTableRowKeys.cs”.

    Give it the following code:

    namespace AzureStorageTodos.Repository;
    
    public static class StaticTableRowKeys
    {
        public const string UserItems = "UserItems";
        public const string TodoItems = "TodoItems";
    }
    
    Create a file called “StaticQueueNames.cs”.

    Give it the following code:

    namespace AzureStorageTodos.Repository;
    
    public static class StaticQueueNames
    {
        public const string TodoUpdated = "TodoUpdated";
    }
    
    Create a file called “StaticTodoListIds.cs”.

    Give it the following code:

    namespace AzureStorageTodos.Repository;
    
    public static class StaticTodoListIds
    {
        public const string All = "All";
        public const string Complete = "Complete";
        public const string Incomplete = "Incomplete";
    }
    
    Create a new folder to the root of the AzureStorageTodos.Repository Project called “Repositories”.

    To the “Repositories” folder create a new file called “UsersRepository.cs”.

    Give it the following code:

    using Azure.Data.Tables;
    using AzureStorageTodos.Models;
    using AzureStorageTodos.Repository.BaseRepositories;
    using AzureStorageTodos.Repository.Interface;
    
    namespace AzureStorageTodos.Repository.Repositories;
    
    public class UsersRepository : AzureTableStorageRepository<UserEntityModel>, IUsersRepository
    {
        public UsersRepository(TableServiceClient tableServiceClient) : base(tableServiceClient, StaticTableNames.TodoUsers, StaticTableRowKeys.UserItems)
        {        
        }
    }
    
    To the “Repositories” folder create a new file called “TodosRepository.cs”.

    Give it the following code:

    using Azure.Data.Tables;
    using AzureStorageTodos.Models;
    using AzureStorageTodos.Repository.BaseRepositories;
    using AzureStorageTodos.Repository.Interface;
    
    namespace AzureStorageTodos.Repository.Repositories;
    
    public class TodosRepository : AzureTableStorageRepository<TodoEntityModel>, ITodosRepository
    {
        public TodosRepository(TableServiceClient tableServiceClient) : base(tableServiceClient, StaticTableNames.Todos, StaticTableRowKeys.TodoItems)
        {        
        }
    }
    
    To the “Repositories” folder create a new file called “TodoListRepository.cs”.

    Give it the following code:

    using Azure.Storage.Blobs;
    using AzureStorageTodos.Models;
    using AzureStorageTodos.Repository.BaseRepositories;
    using AzureStorageTodos.Repository.Interface;
    
    namespace AzureStorageTodos.Repository.Repositories;
    
    public class TodoListRepository : AzureBlobStorageRepository<TodoListEntityModel>, ITodoListRepository
    {
        public TodoListRepository(BlobServiceClient blobServiceClient) : base(blobServiceClient, StaticBlobContainerNames.TodoLists, StaticBlobFolderNames.TodoListItems)
        {        
        }
    }
    
    
    Our repository layer is now ready to save, retrieve or delete our entities upon request. We just need an easy abstraction to use when utilizing the Storage Queue.

    To the “Interface” folder create a new file called “IAzureQueueStorageService.cs”.

    Give it the following code:

    namespace AzureStorageTodos.Repository.Interface;
    
    public interface IAzureQueueStorageService
    {
        public Task SendMessageAsync(string message);
    }
    
    To the “BaseRepositories” folder create a new file called “AzureQueueStorageService.cs”.

    Give it the following code:

    using Azure.Storage.Queues;
    using AzureStorageTodos.Repository.Interface;
    
    namespace AzureStorageTodos.Repository.BaseRepositories;
    
    public abstract class AzureQueueStorageService : IAzureQueueStorageService
    {
        private readonly QueueServiceClient _queueServiceClient;
        private readonly string _queueName;
    
        public AzureQueueStorageService(QueueServiceClient queueServiceClient, string queueName)
        {
            _queueServiceClient = queueServiceClient;
            _queueName = queueName;
        }
    
        public async Task SendMessageAsync(string message)
        {
            var queueClient = await GetQueueClientAsync();        
            await queueClient.SendMessageAsync(message);
        }
    
        private async Task<QueueClient> GetQueueClientAsync()
        {
            var queueClient = _queueServiceClient.GetQueueClient(_queueName);        
            await queueClient.CreateIfNotExistsAsync();
    
            return queueClient;                
        }
    }
    
    To the “Interface” folder create a new file called “ITodoUpdatedQueue.cs”.

    Give it the following code:

    namespace AzureStorageTodos.Repository.Interface;
    
    public interface ITodoUpdatedQueue : IAzureQueueStorageService
    {
    }
    
    To the “Repositories” folder create a new file called “TodoUpdatedQueue.cs”.

    Give it the following code:

    using Azure.Storage.Queues;
    using AzureStorageTodos.Repository.Interface;
    using AzureStorageTodos.Repository.BaseRepositories;
    
    namespace AzureStorageTodos.Repository.Repositories;
    
    public class TodoUpdatedQueue : AzureQueueStorageService, ITodoUpdatedQueue
    {
        public TodoUpdatedQueue(QueueServiceClient queueServiceClient) : base (queueServiceClient, StaticQueueNames.TodoUpdated)
        {        
        }
    }
    
    With this in place we not only have convenient access to store our Entities in Tables and Blobs, but we have a convenient way to send messages to the Storage Queue.

    Build the Service Layer

      1 – To the AzureStorageTodos.Service project, delete Class1.cs.
      2 – Create a new folder called “Interface”.
      3 – In the “Interface” folder create a new file called ” IAzureStorageRepositoryClient.cs”.

    Give it the following code.

    using AzureStorageTodos.Models.Interface;
    
    namespace AzureStorageTodos.Service.Interface;
    
    public interface IAzureStorageRepositoryClient<T> where T : IBaseAzureStorageEntityModel
    {
        Task<T?> UpsertAsync(T entityDetails);
        Task<T?> GetOneAsync(string id);
        Task<List<T>?> GetAllAsync();
        Task DeleteAsync(string id);
    }
    
    Create a new folder called “BaseRepositoryClients”.

    In the “BaseRepositoryClients” folder create a new file called “AzureStorageRepositoryClient.cs”.

    Give it the following code:

    using AzureStorageTodos.Models.Interface;
    using AzureStorageTodos.Repository.Interface;
    using AzureStorageTodos.Service.Interface;
    
    namespace AzureStorageTodos.Service.BaseRepositoryClients;
    
    public abstract class AzureStorageRepsoitoryClient<T> : IAzureStorageRepositoryClient<T> where T : IBaseAzureStorageEntityModel
    {
        private readonly IAzureStorageRepository<T> _repository;
    
        public AzureStorageRepsoitoryClient(IAzureStorageRepository<T> repository)
        {
            _repository = repository;
        }
    
        public virtual Task DeleteAsync(string id)
        {
            try {
                return _repository.DeleteAsync(id);
            }
            catch {
                // Do nothing
            }
            
            return Task.CompletedTask;
        }
    
        public virtual async Task<List<T>?> GetAllAsync()
        {
            try {
                return await _repository.GetAllAsync();
            }
            catch {
                // Do nothing, return null
            }
    
            return null;
        }
    
        public virtual async Task<T?> GetOneAsync(string id)
        {
            try {
                return await _repository.GetOneAsync(id);
            }
            catch {
                // Do nothing, return null
            }
    
            return default(T);
        }
    
        public virtual async Task<T?> UpsertAsync(T entityDetails)
        {
            try {
                var results = await _repository.UpsertAsync(entityDetails);
    
                return results;
            }
            catch {
                // Do nothing, return null
            }
    
            return default(T);
        }
    }
    
    This becomes a re-usable and overridable class for our Entity Models that will consume a Repository, whether that be a Table Repository or Blob Repository. It will try and use that repository and return null / default(T) if there are any errors that occur on the repository layer. Other than that, it pretty much does exactly what the repository layer is doing. Since this is overridable, that means any class instance we create from this base class can be overridden and we can add any custom logic if needed.

    We now need to create repository clients for each entity.

    To the “Interface” folder create a new file called “IUsersRepositoryClient.cs”.

    Give it the following code:

    using AzureStorageTodos.Models;
    
    namespace AzureStorageTodos.Service.Interface;
    
    public interface IUsersRepositoryClient : IAzureStorageRepositoryClient<UserEntityModel>
    {
    }
    
    To the “Interface” folder create a new file called “ITodosRepositoryClient.cs”.

    Give it the following code:

    using AzureStorageTodos.Models;
    
    namespace AzureStorageTodos.Service.Interface;
    
    public interface ITodosRepositoryClient : IAzureStorageRepositoryClient<TodoEntityModel>
    {    
    }
    
    To the “Interface” folder create a new file called “ITodoListRepositoryClient.cs”

    Give it the following code:

    using AzureStorageTodos.Models;
    
    namespace AzureStorageTodos.Service.Interface;
    
    public interface ITodoListRepositoryClient : IAzureStorageRepositoryClient<TodoListEntityModel>
    {
    }
    
      1 – Create a new folder called “RepositoryClients”.

      2 – Create a new file called “UsersRepositoryClient.cs”.

    Give it the following code:

    using AzureStorageTodos.Models;
    using AzureStorageTodos.Service.Interface;
    using AzureStorageTodos.Service.BaseRepositoryClients;
    using AzureStorageTodos.Repository.Interface;
    
    namespace AzureStorageTodos.Service.RepositoryClients;
    
    public class UsersRepositoryClient : AzureStorageRepsoitoryClient<UserEntityModel>, IUsersRepositoryClient
    {
        public UsersRepositoryClient(IUsersRepository usersRepository) : base(usersRepository)
        {        
        }
    }
    
    Create a new file called “TodosRepositoryClient.cs”.

    Give it the following code:

    using AzureStorageTodos.Models;
    using AzureStorageTodos.Service.Interface;
    using AzureStorageTodos.Service.BaseRepositoryClients;
    using AzureStorageTodos.Repository.Interface;
    
    namespace AzureStorageTodos.Service.RepositoryClients;
    
    public class TodosRepositoryClient : AzureStorageRepsoitoryClient<TodoEntityModel>, ITodosRepositoryClient
    {
        public TodosRepositoryClient(ITodosRepository todosRepository) : base(todosRepository)
        {        
        }
    }
    
    Create a new file called ” TodoListRepositoryClient.cs”.

    Give it the following code:

    using AzureStorageTodos.Models;
    using AzureStorageTodos.Service.Interface;
    using AzureStorageTodos.Service.BaseRepositoryClients;
    using AzureStorageTodos.Repository.Interface;
    
    namespace AzureStorageTodos.Service.RepositoryClients;
    
    public class TodoListRepositoryClient : AzureStorageRepsoitoryClient<TodoListEntityModel>, ITodoListRepositoryClient
    {
        public TodoListRepositoryClient(ITodoListRepository todoListRepository) : base(todoListRepository)
        {
        }
    }
    
    We will also need a custom service to refresh the TodoLists.

    In the “Interface” folder create a new file called “ITodoListManager.cs”.

    Give it the following code:

    namespace AzureStorageTodos.Service.Interface;
    
    public interface ITodoListManager
    {
        public Task RefreshTodoListsForUserAsync(string userId);
    }
    
    Create a new folder called “Services”.

    In the “Services” folder create a new file called “TodoListManager.cs”.

    Give it the following code:

    using AzureStorageTodos.Models;
    using AzureStorageTodos.Repository;
    using AzureStorageTodos.Service.Interface;
    
    namespace AzureStorageTodos.Service.Services;
    
    public class TodoListManager : ITodoListManager
    {
        private readonly ITodosRepositoryClient _todosRepositoryClient;
        private readonly ITodoListRepositoryClient _todoListRepositoryClient;
    
        public TodoListManager(ITodosRepositoryClient todosRepositoryClient, ITodoListRepositoryClient todoListRepositoryClient)
        {
            _todosRepositoryClient = todosRepositoryClient;
            _todoListRepositoryClient = todoListRepositoryClient;
        }
    
        public async Task RefreshTodoListsForUserAsync(string userId)
        {
            var allTodosInRepository = await _todosRepositoryClient.GetAllAsync();
    
            if(allTodosInRepository is not null) 
            {
                var allTodosForUser = allTodosInRepository.Where(w => w.OwnerUserId == userId).ToList();
    
                var allTodosListEntity = new TodoListEntityModel(userId, StaticTodoListIds.All);
                var completedTodosListEntity = new TodoListEntityModel(userId, StaticTodoListIds.Complete);
                var incompleteTodosListEntity = new TodoListEntityModel(userId, StaticTodoListIds.Incomplete);
    
                if(allTodosForUser is not null)
                {
                    allTodosListEntity.Results = allTodosForUser;
                    completedTodosListEntity.Results = allTodosForUser.Where(w => w.Completed == true).ToList();
                    incompleteTodosListEntity.Results = allTodosForUser.Where(w => w.Completed == false).ToList();
    
                    await _todoListRepositoryClient.UpsertAsync(allTodosListEntity);
                    await _todoListRepositoryClient.UpsertAsync(completedTodosListEntity);
                    await _todoListRepositoryClient.UpsertAsync(incompleteTodosListEntity);
                }
            }        
        }
    }
    
    Furthermore, we need to update the logic for when a Todo is upsertted that it sends a message to the queue letting the queue know that has happened.

    Modify the TodosRepositoryClient.cs file to be as such:

    using AzureStorageTodos.Models;
    using AzureStorageTodos.Service.Interface;
    using AzureStorageTodos.Service.BaseRepositoryClients;
    using AzureStorageTodos.Repository.Interface;
    
    namespace AzureStorageTodos.Service.RepositoryClients;
    
    public class TodosRepositoryClient : AzureStorageRepsoitoryClient<TodoEntityModel>, ITodosRepositoryClient
    {
        private readonly ITodosRepository _todosRepository;
        private readonly ITodoUpdatedQueue _todoUpdatedQueue;
    
        public TodosRepositoryClient(ITodosRepository todosRepository, ITodoUpdatedQueue todoUpdatedQueue) : base(todosRepository)
        {
            _todosRepository = todosRepository;
            _todoUpdatedQueue = todoUpdatedQueue;
        }
    
        public override async Task<TodoEntityModel?> UpsertAsync(TodoEntityModel entityDetails)
        {
            var results = await base.UpsertAsync(entityDetails);
    
            await _todoUpdatedQueue.SendMessageAsync($"Todo Updated For User: {entityDetails.OwnerUserId}");
    
            return results;
        }
    
        public override async Task DeleteAsync(string id)
        {
            var currentDetails = await this.GetOneAsync(id);
    
            if(currentDetails is not null) 
            {
                await base.DeleteAsync(id);
    
                await _todoUpdatedQueue.SendMessageAsync($"Todo Updated For User: {currentDetails.OwnerUserId}");
            }        
        }
    }
    

    Build the Functions Project

    The “TodoUpdated” queue trigger that we created earlier will need access to the TodoListManager service that we created earlier. In order for it to have access to that we need to implement dependency injection. Earlier we included the packages to allow us to do it. So, Let’s make it happen now.

    To the AzureStorageTodos.AzureFunctions project add a new file called “Startup.cs”.

    Give it the following code:

    using Azure.Identity;
    using Microsoft.Extensions.Azure;
    using Microsoft.Azure.Functions.Extensions.DependencyInjection;
    using Microsoft.Extensions.DependencyInjection;
    using Microsoft.Extensions.Configuration;
    using AzureStorageTodos.Repository.Interface;
    using AzureStorageTodos.Repository.Repositories;
    using AzureStorageTodos.Service.Interface;
    using AzureStorageTodos.Service.RepositoryClients;
    using AzureStorageTodos.Service.Services;
    
    [assembly: FunctionsStartup(typeof(AzureStorageTodos.AzureFunctions.Startup))]
    
    namespace AzureStorageTodos.AzureFunctions;
    
    public class Startup : FunctionsStartup
    {
        public override void Configure(IFunctionsHostBuilder builder)
        {
            var config = builder.GetContext().Configuration;
    
            string azureWebJobsStorage = config.GetValue<string>("AzureWebJobsStorage");
    
            // Register Azure Clients
            builder.Services.AddAzureClients(azureClientsBuilder => {
                azureClientsBuilder.AddTableServiceClient(azureWebJobsStorage);
                azureClientsBuilder.AddBlobServiceClient(azureWebJobsStorage);
                
                azureClientsBuilder.UseCredential(new DefaultAzureCredential());
            });
    
            // Register Repositories
            builder.Services.AddTransient<ITodosRepository, TodosRepository>();
            builder.Services.AddTransient<ITodoListRepository, TodoListRepository>();
    
            // Register Services
            builder.Services.AddTransient<ITodosRepositoryClient, TodosRepositoryClient>();
            builder.Services.AddTransient<ITodoListRepositoryClient, TodoListRepositoryClient>();
            builder.Services.AddTransient<ITodoListManager, TodoListManager>();
        }
    }
    
    First, we get the connection string for the AzureWebJobsStorage. Then we use that connection string to register the TableServiceClient and BlobService Client which the later registered repositories of TodosRepository and TodoListRepository depend on. Then we register the TodosRepositoryClient, TodoListRepositoryClient, which depend on the TodosRepository and TodoListRepository, and finally we register the TodoListManager which depends on the TodosRepositoryClient and TodoListRepositoryClient.

    Modify the TodoUpdated.cs file with the following code:

    using System;
    using System.Linq;
    using System.Threading.Tasks;
    using AzureStorageTodos.Repository;
    using AzureStorageTodos.Service.Interface;
    using Microsoft.Azure.WebJobs;
    using Microsoft.Azure.WebJobs.Host;
    using Microsoft.Extensions.Logging;
    
    namespace AzureStorageTodos.AzureFunctions
    {
        public class TodoUpdated
        {
            private readonly ITodoListManager _todoListManager;
    
            public TodoUpdated(ITodoListManager todoListManager)
            {
                _todoListManager = todoListManager;
            }
    
            [FunctionName("TodoUpdated")]
            public async Task Run([QueueTrigger(StaticQueueNames.TodoUpdated, Connection = "AzureWebJobsStorage")]string queueMessage, ILogger log)
            {
                string[] queueMessageParts = queueMessage.Split(": ");
                string updatedOwnerUserId = queueMessageParts.Last();
    
                await _todoListManager.RefreshTodoListsForUserAsync(updatedOwnerUserId);
            }
        }
    }
    
    With those services registered in the Startup class we can now inject them within the constructor of this Queue Trigger Class, and then make them available within the QueueTrigger function methods. This method only relies on the TodoListManager, but it was still necessary to register the other services that it relies on in the Startup, though they are not required here. They could be directly injected here as well, although that is not necessary at this time.

    When it comes to the new Run method, we converted it to an async function. As you may recall (from the TodosRepositoryClient.cs Service file) that we will be sending the message of “Todo Updated For User: SomeUserId”. So, we are parsing the string, and just grabbing the Id portion of that message, and then using the RefreshToDoListForUserAsync method from the TodoListManager service with the provided user id.

    Build the Web Application

    One of the first things that we want to do is create a user record any time someone logs in to our system. To do so begin by creating an WebAppOpenIdConnectEvents.cs file in the root of the AzureStorageTodos.Web project.

    Give it the following code:

    using Microsoft.AspNetCore.Authentication.OpenIdConnect;
    using AzureStorageTodos.Models;
    using AzureStorageTodos.Service.Interface;
    
    public class WebAppOpenIdConnectEvents : OpenIdConnectEvents
    {
        public WebAppOpenIdConnectEvents()
        {
            this.OnTicketReceived = async ctx => {
                if(ctx.Principal is not null) 
                {
                    var myPrincipal = ctx.Principal;
    
                    var uidClaim = myPrincipal.Claims.FirstOrDefault(w => w.Type == "uid");
                    var nameClaim = myPrincipal.Claims.FirstOrDefault(w => w.Type == "name");
                    var emailClaim = myPrincipal.Claims.FirstOrDefault(w => w.Type == "preferred_username");
    
                    if (uidClaim is not null && nameClaim is not null && emailClaim is not null)
                    {
                        var usersRepositoryClient = ctx.HttpContext.RequestServices.GetRequiredService<IUsersRepositoryClient>();
    
                        var userEntity = new UserEntityModel
                        {
                            Id = uidClaim.Value,
                            DisplayName = nameClaim.Value,
                            EmailAddress = emailClaim.Value
                        };
    
                        await usersRepositoryClient.UpsertAsync(userEntity);
                    }
                }
    
                await Task.Yield();
            };     
        }
    }
    
    In order to have this class executed, and to register our Azure Clients, Repositories and Services modify the “Program.cs” file as follows:
    using Microsoft.AspNetCore.Authentication;
    using Microsoft.AspNetCore.Authorization;
    using Microsoft.AspNetCore.Authentication.OpenIdConnect;
    using Microsoft.AspNetCore.Mvc.Authorization;
    using Microsoft.Identity.Web;
    using Microsoft.Identity.Web.UI;
    using Graph = Microsoft.Graph;
    using Microsoft.Extensions.Azure;
    using Azure.Identity;
    using AzureStorageTodos.Repository.Interface;
    using AzureStorageTodos.Repository.BaseRepositories;
    using AzureStorageTodos.Repository.Repositories;
    using AzureStorageTodos.Service.Interface;
    using AzureStorageTodos.Service.RepositoryClients;
    using AzureStorageTodos.Service.Services;
    
    var builder = WebApplication.CreateBuilder(args);
    
    // Add services to the container.
    var initialScopes = builder.Configuration["DownstreamApi:Scopes"]?.Split(' ');
    
    builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
        .AddMicrosoftIdentityWebApp(options => {
            options.Instance = builder.Configuration["AzureAd:Instance"];
            options.Domain = builder.Configuration["AzureAd:Domain"];
            options.TenantId = builder.Configuration["AzureAd:TenantId"];
            options.ClientId = builder.Configuration["AzureAd:ClientId"];
            options.ClientSecret = builder.Configuration["AzureAd:ClientSecret"];
            options.CallbackPath = builder.Configuration["AzureAd:CallbackPath"];
    
            options.Events = new WebAppOpenIdConnectEvents();
        })
        .EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
            .AddMicrosoftGraph(builder.Configuration.GetSection("DownstreamApi"))
            .AddInMemoryTokenCaches();
    
    builder.Services.AddControllersWithViews(options =>
    {
        var policy = new AuthorizationPolicyBuilder()
            .RequireAuthenticatedUser()
            .Build();
        options.Filters.Add(new AuthorizeFilter(policy));
    });
    builder.Services.AddRazorPages()
        .AddMicrosoftIdentityUI();
    
    // Register Azure Clients
    builder.Services.AddAzureClients(azureClientsBuilder => {
        string azureStorageConnectionString = builder.Configuration.GetConnectionString("AzureStorage");
    
        azureClientsBuilder.AddTableServiceClient(azureStorageConnectionString);
        azureClientsBuilder.AddBlobServiceClient(azureStorageConnectionString);
        azureClientsBuilder.AddQueueServiceClient(azureStorageConnectionString);
        
        azureClientsBuilder.UseCredential(new DefaultAzureCredential());
    });
    
    // Register Repositories
    builder.Services.AddTransient<IUsersRepository, UsersRepository>();
    builder.Services.AddTransient<ITodosRepository, TodosRepository>();
    builder.Services.AddTransient<ITodoListRepository, TodoListRepository>();
    builder.Services.AddTransient<ITodoUpdatedQueue, TodoUpdatedQueue>();
    
    // Register Services
    builder.Services.AddTransient<IUsersRepositoryClient, UsersRepositoryClient>();
    builder.Services.AddTransient<ITodosRepositoryClient, TodosRepositoryClient>();
    builder.Services.AddTransient<ITodoListRepositoryClient, TodoListRepositoryClient>();
    builder.Services.AddTransient<ITodoListManager, TodoListManager>();
    
    var app = builder.Build();
    
    // Configure the HTTP request pipeline.
    if (!app.Environment.IsDevelopment())
    {
        app.UseExceptionHandler("/Home/Error");
        // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
        app.UseHsts();
    }
    
    app.UseHttpsRedirection();
    app.UseStaticFiles();
    
    app.UseRouting();
    
    app.UseAuthentication();
    app.UseAuthorization();
    
    app.MapControllerRoute(
        name: "default",
        pattern: "{controller=Home}/{action=Index}/{id?}");
    app.MapRazorPages();
    
    app.Run();
    
    With this in place you should be able to Press F5 to start the application. Sign out and Sign Back in, then review your Azure Storage Explorer and the TodoUsers table should now appear with your user record in it.
    Stop debugging after you have verified that you can create your user account.

    Now let’s continue to build the web application. Create a “TodosHomePageViewModel.cs” file within the “Models” folder of the AzureStorageTodos.Web project.

    Give it the following code:

    using AzureStorageTodos.Models;
    
    namespace AzureStorageTodos.Web.Models;
    
    public class TodosHomePageViewModel
    {
        public IEnumerable<TodoEntityModel>? AllTodos {get; set;}
        public IEnumerable<TodoEntityModel>? CompletedTodos {get; set;}
        public IEnumerable<TodoEntityModel>? IncompleteTodos {get; set;}
    }
    
    Create a “TodosController.cs” file within the Controllers folder.

    Give it the following code:

    using AzureStorageTodos.Models;
    using AzureStorageTodos.Repository;
    using AzureStorageTodos.Service.Interface;
    using AzureStorageTodos.Web.Models;
    using Microsoft.AspNetCore.Authorization;
    using Microsoft.AspNetCore.Mvc;
    
    namespace AzureStorageTodos.Web.Controllers;
    
    [Authorize]
    public class TodosController : Controller
    {
        private readonly ITodoListRepositoryClient _todoListRepositoryClient;
        private readonly ITodosRepositoryClient _todosRepositoryClient;
    
        public TodosController(ITodoListRepositoryClient todoListRepositoryClient, ITodosRepositoryClient todosRepositoryClient)
        {
            _todoListRepositoryClient = todoListRepositoryClient;
            _todosRepositoryClient = todosRepositoryClient;
        }
    
        [HttpGet]
        public async Task<IActionResult> Index()
        {
            TodosHomePageViewModel todosHomePageViewModel = new TodosHomePageViewModel();
    
            
            var currentUser = this.HttpContext.User;
    
            var currentUserUserIdClaim = currentUser.Claims.Where(w => w.Type == "uid").FirstOrDefault();
    
            if(currentUserUserIdClaim is not null) 
            {
                var currentUserUserIdValue = currentUserUserIdClaim.Value;
    
                var allFileId = $"{currentUserUserIdValue}-{StaticTodoListIds.All}";
                var completedFileId = $"{currentUserUserIdValue}-{StaticTodoListIds.Complete}";
                var incompleteFileId = $"{currentUserUserIdValue}-{StaticTodoListIds.Incomplete}";
    
                var allTodos = await _todoListRepositoryClient.GetOneAsync(allFileId);
                var completedTods = await _todoListRepositoryClient.GetOneAsync(completedFileId);
                var incompleteTodos = await _todoListRepositoryClient.GetOneAsync(incompleteFileId);
    
                if(allTodos is not null) 
                {
                    todosHomePageViewModel.AllTodos = allTodos.Results;                
                }
    
                if(completedTods is not null) 
                {
                    todosHomePageViewModel.CompletedTodos = completedTods.Results;
                }
    
                if(incompleteTodos is not null) 
                {
                    todosHomePageViewModel.IncompleteTodos = incompleteTodos.Results;
                }
            }        
    
            return View(todosHomePageViewModel);
        }
    
        [HttpGet]
        public async Task<IActionResult> Details(string id)
        {
            var currentDetails = await _todosRepositoryClient.GetOneAsync(id);
    
            if(currentDetails is null)
            {
                return NotFound();
            }
    
            return View(currentDetails);
        }
    
        [HttpGet]
        public IActionResult Create()
        {
            TodoEntityModel newTodoEntityModel = new TodoEntityModel();
    
            var currentUser = this.HttpContext.User;
    
            var currentUserUserIdClaim = currentUser.Claims.Where(w => w.Type == "uid").FirstOrDefault();
    
            if(currentUserUserIdClaim is not null) 
            {
                var currentUserUserIdValue = currentUserUserIdClaim.Value;
    
                newTodoEntityModel.OwnerUserId = currentUserUserIdValue;            
            }
    
            return View(newTodoEntityModel);
        }
    
        [HttpPost]
        public async Task<IActionResult> Create(TodoEntityModel todoEntityModel)
        {
            if(ModelState.IsValid)
            {
                await _todosRepositoryClient.UpsertAsync(todoEntityModel);
    
                return RedirectToAction(nameof(Index));
            }
    
            return View(todoEntityModel);
        }
    
        [HttpGet]
        public async Task<IActionResult> Edit([FromRoute] string id)
        {
            var currentDetails = await _todosRepositoryClient.GetOneAsync(id);
    
            if(currentDetails is null)
            {
                return NotFound();            
            }
    
            return View(currentDetails);
        }
    
        [HttpPost]
        public async Task<IActionResult> Edit([FromForm] TodoEntityModel todoEntityModel)
        {
            if(ModelState.IsValid)
            {
                await _todosRepositoryClient.UpsertAsync(todoEntityModel);
    
                return RedirectToAction(nameof(Index));
            }
    
            return View(todoEntityModel);
        }
    
        [HttpGet]
        public async Task<IActionResult> Delete([FromRoute] string id)
        {
            var currentDetails = await _todosRepositoryClient.GetOneAsync(id);
    
            if(currentDetails is null)
            {
                return NotFound();            
            }
    
            return View(currentDetails);                
        }
    
        [HttpPost]
        public async Task<IActionResult> Delete([FromForm] TodoEntityModel todoEntityModel)
        {
            await _todosRepositoryClient.DeleteAsync(todoEntityModel.Id);
    
            return RedirectToAction(nameof(Index));
        }
    }
    
    Now let’s build out the DisplayTemplates and EditorTemplates for our TodoEntityModel.

    To the “View” folder and the “Shared” folder, create a new folder called “DisplayTemplates”.

    Create a new file called “ListOfTodosListItem.cshtml”.

    Give it the following code:

    @model AzureStorageTodos.Models.TodoEntityModel
    
    <tr>
        <td>@Html.DisplayFor(m => m.Title)</td>
        <td>@Html.DisplayFor(m => m.Description)</td>
        <td>@Html.DisplayFor(m => m.Completed)</td>
        <td>
            <a asp-action="Details" asp-controller="Todos" asp-route-id="@Model.Id">Details</a> | 
            <a asp-action="Edit" asp-controller="Todos" asp-route-id="@Model.Id">Edit</a> | 
            <a asp-action="Delete" asp-controller="Todos" asp-route-id="@Model.Id">Delete</a>
        </td>
    </tr>
    
    Create a new file called “ListOfTodos.cshtml”.

    Give it the following code:

    @model List<AzureStorageTodos.Models.TodoEntityModel>
    
    <table class="table table-bordered">
        <thead>
            <tr>
                <th>Title</th>
                <th>Description</th>
                <th>Completed</th>
                <th> </th>
            </tr>
        </thead>
        <tbody>
            @foreach(var todo in Model) 
            {
                if(todo is not null) 
                {
                    @Html.DisplayFor(m => todo, "ListOfTodosListItem")
                }            
            }
        </tbody>
    </table>
    
    Create a new file called “TodosHomePageViewModel.cshtml”.

    Give it the following code:

    @model AzureStorageTodos.Web.Models.TodosHomePageViewModel
    
    <ul class="nav nav-tabs" id="TodoListsTab" role="tablist">
    
        <li class="nav-item" role="presentation">
            <button class="nav-link active" id="all-tab" data-bs-toggle="tab" data-bs-target="#AllTodos" type="button" role="tab" aria-controls="AllTodos" aria-selected="true">All</button>
        </li>
    
        <li class="nav-item" role="presentation">
            <button class="nav-link" id="completed-tab" data-bs-toggle="tab" data-bs-target="#CompletedTodos" type="button" role="tab" aria-controls="CompletedTodos">Completed</button>
        </li>
    
        <li class="nav-item" role="presentation">
            <button class="nav-link" id="incomplete-tab" data-bs-toggle="tab" data-bs-target="#IncompleteTodos" type="button" role="tab" aria-controls="IncompleteTodos">Incomplete</button>
        </li>
    
    </ul>
    <div class="tab-content p-4" id="TodosListContent">
    
        <div class="tab-pane fade show active" id="AllTodos" role="tabpanel" aria-labelledby="all-tab">
            @if(Model.AllTodos is not null)
            {
                @Html.DisplayFor(m => m.AllTodos, "ListOfTodos")
            }        
        </div>
    
        <div class="tab-pane fade" id="CompletedTodos" role="tabpanel" aria-labelledby="completed-tab">
            @if(Model.CompletedTodos is not null)
            {
                @Html.DisplayFor(m => m.CompletedTodos, "ListOfTodos")
            }        
        </div>
    
        <div class="tab-pane fade" id="IncompleteTodos" role="tabpanel" aria-labelledby="incomplete-tab">
            @if(Model.IncompleteTodos is not null)
            {
               @Html.DisplayFor(m => m.IncompleteTodos, "ListOfTodos")
            }        
        </div>    
    
    </div>
    
    Create a new file called “TodoEntityModel.cshtml”.

    Give it the following code:

    @model AzureStorageTodos.Models.TodoEntityModel
    
    <dl>
        <dt>@Html.DisplayNameFor(m => m.Id)</dt>
        <dd>@Html.DisplayFor(m => m.Id)</dd>
    
        <dt>@Html.DisplayNameFor(m => m.OwnerUserId)</dt>
        <dd>@Html.DisplayFor(m => m.OwnerUserId)</dd>
    
        <dt>@Html.DisplayNameFor(m => m.Title)</dt>
        <dd>@Html.DisplayFor(m => m.Title)</dd>
    
        <dt>@Html.DisplayNameFor(m => m.Description)</dt>
        <dd>@Html.DisplayFor(m => m.Description)</dd>
    
        <dt>@Html.DisplayNameFor(m => m.Completed)</dt>
        <dd>@Html.DisplayFor(m => m.Completed)</dd>
    </dl>
    
    To the “Views” folder and the “Shared” folder create a new folder called “EditorTemplates”.

    To the new “EditorTemplates” folder create a new file called “TodoEntityModel.cshtml”.

    Give it the following code:

    @model AzureStorageTodos.Models.TodoEntityModel
    
    @Html.HiddenFor(m => m.OwnerUserId)
    <div class="form-group">
        <label asp-for="Id"></label>
        <input class="form-control" asp-for="Id" readonly />
    </div>
    <div class="form-group">
        <label asp-for="Title"></label>
        <input class="form-control" asp-for="Title" />
    </div>
    <div class="form-group">
        <label asp-for="Description"></label>
        <textarea class="form-control" asp-for="Description"></textarea>
    </div>
    <div class="form-group">
        <label asp-for="Completed"></label>
        @Html.EditorFor(m => m.Completed)
    </div>
    
    <input class="btn btn-primary" type="submit" value="Save" />
    
    To the “Views” folder create a new folder called “Todos”.

    In the new “Todos” folder create a new file called “Index.cshtml”.

    Give it the following code:

    @model AzureStorageTodos.Web.Models.TodosHomePageViewModel
    
    @{
        ViewData["Title"] = "Todos";
    }
    
    <h1>@ViewData["Title"]</h1>
    
    <a class="btn btn-primary mb-4" asp-action="Create" asp-controller="Todos">Create</a>
    <p class="text-info mb-4">* If you recently made changes to a Todo, the effects may not be immediate while the Azure Function Rebuilds the List. Refresh the page a few times after making changes.</p>
    
    @Html.DisplayForModel()
    
    Create a new file called “Details.cshtml”.

    Give it the following code:

    @model AzureStorageTodos.Models.TodoEntityModel
    @{
        ViewData["Title"] = "Todo Details";
    }
    
    <h1>@ViewData["Title"]</h1>
    
    <a class="mb-4" asp-action="Index" asp-controller="Todos">Return to List</a>
    
    @Html.DisplayForModel()
    
    Create a new file called “Delete.cshtml”.

    Give it the following code:

    @model AzureStorageTodos.Models.TodoEntityModel
    @{
        ViewData["Title"] = "Delete Todo";
    }
    
    <h1>@ViewData["Title"]</h1>
    
    <a class="mb-4" asp-action="Index" asp-controller="Todos">Return to List</a>
    
    @Html.DisplayForModel()
    
    <form asp-action="Delete" method="POST">
        @Html.HiddenFor(m => m.Id)
        @Html.HiddenFor(m => m.OwnerUserId)
        @Html.HiddenFor(m => m.Title)
        @Html.HiddenFor(m => m.Description)
        @Html.HiddenFor(m => m.Completed)
        <input class="btn btn-danger" type="submit" value="Confirm" />
    </form>
    
    Create a new file called “Create.cshtml”.

    Give it the following code:

    @model AzureStorageTodos.Models.TodoEntityModel
    
    @{
        ViewData["Title"] = "Create Todo";
    }
    
    <h1>@ViewData["Title"]</h1>
    
    <a class="mb-4" asp-action="Index" asp-controller="Todos">Return to List</a>
    
    <form asp-action="Create" asp-controller="Todos" method="post">
        @Html.EditorForModel()
    </form>
    
    Create a new file called “Edit.cshtml”.

    Give it the following code:

    @model AzureStorageTodos.Models.TodoEntityModel
    @{
        ViewData["Title"] = "Edit Todo";
    }
    
    <h1>@ViewData["Title"]</h1>
    
    <a class="mb-4" asp-action="Index" asp-controller="Todos">Return to List</a>
    
    <form asp-action="Edit" asp-controller="Todos" method="post">
        @Html.EditorForModel()
    </form>
    
    Build a link to the Todos controller by modifying the “Views\Shared\_Layout.cshtml” file.

    The new code should look like this:

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>@ViewData["Title"] - AzureStorageTodos.Web</title>
        <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
        <link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
        <link rel="stylesheet" href="~/AzureStorageTodos.Web.styles.css" asp-append-version="true" />
    </head>
    <body>
        <header>
            <nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
                <div class="container-fluid">
                    <a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">AzureStorageTodos.Web</a>
                    <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target=".navbar-collapse" aria-controls="navbarSupportedContent"
                            aria-expanded="false" aria-label="Toggle navigation">
                        <span class="navbar-toggler-icon"></span>
                    </button>
                    <div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
                        <ul class="navbar-nav flex-grow-1">
                            <li class="nav-item">
                                <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
                            </li>
                            <li class="nav-item">
                                <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
                            </li>
                            <li class="nav-item">
                                <a class="nav-link text-dark" asp-area="" asp-controller="Todos" asp-action="Index">Todos</a>
                            </li>
                        </ul>
                        <partial name="_LoginPartial" />
                    </div>
                </div>
            </nav>
        </header>
        <div class="container">
            <main role="main" class="pb-3">
                @RenderBody()
            </main>
        </div>
    
        <footer class="border-top footer text-muted">
            <div class="container">
                © 2022 - AzureStorageTodos.Web - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
            </div>
        </footer>
        <script src="~/lib/jquery/dist/jquery.min.js"></script>
        <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
        <script src="~/js/site.js" asp-append-version="true"></script>
        @await RenderSectionAsync("Scripts", required: false)
    </body>
    </html>
    

    Test the application locally

    With all of this in place the application should be ready to test. Go to Run and Debug. Ensure that the “Attach to functions and launch web app” is selected as the profile and press the play button or open any file and press F5.

    Click the Todos link. Practice creating, editing, deleting, and viewing Todos. As you do. Check your terminal output labeled “Host Start” and you will see that the function has been executing a few times as you were adding and deleting items.

    Use the Azure Storage Explorer. Review the “Todos” and “TodoUsers” tables. Review the “todo-lists” container and it’s file and folders. Double click on them to review them.

    Sample Screen Shots from my computer:

    With the application running locally it is now time to start preparing to move it to the cloud.

    Build Azure Resources

    All of these items will be created using the Azure Portal @ https://portal.azure.com/.

    Create an Azure Resource Group

    Search for Resource Group. Give it any name and select a region close to you. I will be using “rg-sw-storage-todos-demo-001”

    Create an Azure App Configuration

    Select the resource group that you created earlier. Click the Create button to add services to the resource group. Search for “App Configuration” and then click on “Create”

    Make sure it is associated with the appropriate Subscription and Resource Group. Select the appropriate Location. Give it a name and select the free pricing tier. I will use the name “app-config-storage-todos-demo”

    Create an Azure Key Vault

    Select the resource group that you created earlier. Click the Create button to add service to the resource group. Search for “Key Vault” and then click on “Create”.

    Make sure it is associated with the appropriate Subscription and Resource Group. Select the appropriate Location and standard pricing tier. I will use the name: “kv-storage-todos”. Also select the “Azure role-based access control” permission model.

    Create an Azure Application App Service Plan

    Select the resource group that you created earlier. Click the Create button to add service to the resource group. Search for “App Service Plan” and then click on “Create”

    Make sure it is associated with the appropriate Subscription and Resource Group. Select the appropriate region and give it a name. I would also recommend changing the SKU and Size to free if possible. I will also be using the Linux version. The name that I will use is: “asp-sw-storage-todos-demo-001”

    Create an Azure App Service

    Select the resource group that you created earlier. Click the Create button to add service to the resource group. Search for “Web App” and then click on “Create”

    Make sure that it is associated with the appropriate Subscription and Resource Group. Give it a name. Select Runtime stack of .NET 6, and Linux OS. Select the appropriate region, and the App Service Plan that you created earlier. I will use the name: “sw-storage-todos-demo”

    Create an Azure Storage Account

    Select the resource group that you created earlier. Click the Create button to add service to the resource group. Search for “Storage Account” and then click on “Create”.

    Be sure that the subscription and resource group are set to the ones that you created earlier. Give the storage account a name and select a region close to you or with at least the same resource group location. Standard Performance and Locally-redundant storage should be sufficient. I will be using the name of “swstoragetodosdemo”

    Create an Azure Functions App

    Select the resource group that you created earlier. Click the Create button to add service to the resource group. Search for “Function App” and then click on “Create”.

    Be sure that the subscription and resource group are set to the ones that you created earlier. Give the storage account a name and select a region close to your or with at least the same resource group location. I will be using the name ” sw-storage-todos-demo-functions”. I will be publishing code and the runtime stack is .NET 6. The operating system will be Linux. The plan type will be “App Service plan”. Select the App Service Plan that you created earlier.

    Your resource group should look something like this:

    When you click on the App Service plan and click on Apps, it should look something like this:

    Copy settings to Azure App Configuration and Azure Key Vault

    In Visual Studio Code open the appsettings.json file. In your browse go to the Azure App Configuration that you created earlier and go to the “Configuration Explorer” blade.

    We will be copying everything in the AzureAd key to here. The way App Configuration works is the keys are colon delimited such that…
    "AzureAd": {
        "Instance": "https://login.microsoftonline.com/",
    
    …Is actually “AzureAd:Instance”.
      1 – Click on “Create” and select Key-Value. The key will be “AzureAd:Instance”. The value will be https://login.microsoftonline.com/ and the label will Development. We will have different values for the different instances of our application.

      2 – Repeat those steps for every key in the “AzureAD” instance except for Client Secret. We will do that differently.

      3 – Before we add the AzureAd:ClientSecret your app config should look something like this:

      4 – Browse to the Key Vault Resource that you created earlier.

      5 – Go to access control (IAM). Click on “Add”. Add yourself to the “Key Vault Secrets Officer” role.

      6 – Click on the “Secrets” blade.

      7 – Click on the “Generate/Import” button

    The Upload options is manual. The name should be something like: “AzureAdClientSecret-Development”. And the value should be the value from your appsettings.json

    After that is saved return to the App Configuration resource that you created earlier.

    This time, when going to the Configuration Explorer, click on “Create” and select the “Key-Vault Reference” option.

    Be sure that the Key is”AzureAd:ClientSecret” and the Label is “Developmen.t”
      8 – Select the Subscription, Resource Group, and Key Vault that you created earlier.

      9 – Select the “AzureAdClientSecret-Development”

      10 – Click on Apply.

    With all of that in there we can now delete the AzureAd key from our appsettings.json file. I am also going to remove the other comments from this json file.

    It should now look like this

    {
      "DownstreamApi": {
        "BaseUrl": "https://graph.microsoft.com/v1.0",
        "Scopes": "user.read"
      },
      "ConnectionStrings": {
        "AzureStorage": "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;QueueEndpoint=http://127.0.0.1:10001/devstoreaccount1;TableEndpoint=http://127.0.0.1:10002/devstoreaccount1;"
      },
      "Logging": {
        "LogLevel": {
          "Default": "Information",
          "Microsoft.AspNetCore": "Warning"
        }
      },
      "AllowedHosts": "*"
    }
    
    We will also add a key vault secret for our AzureStorage Connection String and then add it to our app configuration.

    Similar to what was done earlier. Browse to the Azure Key Vault resource that you created earlier. Go to the Secrets blade. Click the Generate/Import button to create a new secret. It will be a manual upload. The name will be “AzureStorageConnectionString-Development”. The value will be the value from appsettings.json.

      11 – Click on Create.

    When done return to the App Configuration resource that you created earlier. Go to the Configuration explorer again and click the “Create” button and select Key Vault Reference and this time be sure that the name is “ConnectionStrings:AzureStorage”. The label is “Development.”

    Next, select the appropriate Subscription, Resource Group and Key Vault. Then Select the AzureStorageConnectionString-Development” Secret and click on Apply. You should be able to delete the connection strings key from your appsettings.json now.

    Your App Configuration should look something like this:
    Your appconfig.json should look something like this:
    {
      "DownstreamApi": {
        "BaseUrl": "https://graph.microsoft.com/v1.0",
        "Scopes": "user.read"
      },  
      "Logging": {
        "LogLevel": {
          "Default": "Information",
          "Microsoft.AspNetCore": "Warning"
        }
      },
      "AllowedHosts": "*"
    }
    

    Sign-in to Azure CLI, Setup Container Environment Variables and Setup Web App and Functions Project to read from Azure App Configuration and Key Vault

    Go to a terminal in Visual Studio Code. Return to /workspace. Execute the following command:

    az login
    This should open a browser.
      1 – Log in to the browser and return to the CLI when prompted. You should see a JSON result summarizing your account.
      2 – To the .devcontainer folder add two new files called: devcontainer.env and devcontainer.env.example

    The purpose of this is to make it easier for our peer developers to get up to speed when they are onboarded on to the project.

    The devcontainer.env file will be yours on your computer, and you will be giving instructions in your README file to your other developers to rename the devcontainer.env.example file to devcontainer.env with instructions on filling it out.

    In your azure portal browse to the App Configuration resource that you created earlier. Go to the access keys blade. Grab the URL that is in your address bar. Update your devcontainer.env.example file as follows:

    APPCONFIG_CONNECTION_STRING=Connection string from <Address in your address bar>
    ASPNETCORE_ENVIRONMENT=Development
    
    On this page is a link that says “Connection String.”

    Click the two pages icon to copy the connection string.

    Update the devcontainer.env file to have the following code:
    APPCONFIG_CONNECTION_STRING=<Connection string that you just copied from the portal (without the angle brackets)>
    ASPNETCORE_ENVIRONMENT=Development
    
    Add the following code to the docker-compose.yml file just before the build key, but indented from app.
    env_file: devcontainer.env
    The docker-compose.yml file should look like this:
    version: '3'
    
    services:
      app:
        env_file: devcontainer.env
        build: 
          context: .
          dockerfile: Dockerfile
          args:
            # Update 'VARIANT' to pick a version of .NET: 3.1-focal, 6.0-focal
            VARIANT: "6.0-focal"
            # Optional version of Node.js
            NODE_VERSION: "lts/*"
    
        volumes:
          - ..:/workspace:cached
    
        # Overrides default command so things don't shut down after the process ends.
        command: sleep infinity
    
        # Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function.
        network_mode: service:azurite
    
        # Uncomment the next line to use a non-root user for all processes.
        # user: vscode
    
        # Use "forwardPorts" in **devcontainer.json** to forward an app port locally. 
        # (Adding the "ports" property to this file will not forward from a Codespace.)
    
      azurite:
        image: mcr.microsoft.com/azure-storage/azurite
        restart: unless-stopped
        command: "azurite -d /opt/azurite/azurite_logs.txt"
    
        # Add "forwardPorts": ["1433"] to **devcontainer.json** to forward MSSQL locally.
        # (Adding the "ports" property to this file will not forward from a Codespace.)
    
    This will make it so that when your development container loads that it will load environment variables from the devcontainer.env file.

    Of course, the dev container is running right now, so it has not yet read those variables. It won’t read them just from restarting either. We need to rebuild them.

    Do a CTRL+SHIFT+P to bring up the Visual Studio Code Command Pallet and select the “Remote-Containers: Rebuild Remote Container” option.

    After it is done rebuilding, we can confirm that the environment was read by executing the following commands from the Visual Studio Code terminal:
    echo $APPCONFIG_CONNECTION_STRING
    echo $ASPNETCORE_ENVIRONMENT
    
    This should produce the values for those variables from the devcontainer.env file. Since we rebuilt the devcontainer we have to repeat a few steps. Issue the following commands:
    dotnet dev-certs https --trust
    dotnet restore
    az login
    We will also need the Microsoft.Azure.AppConfiguration.AspNetCore nuget package added to our Web Application and Functions application. Execute the following commands (assuming that you are still at /workspace).
    cd AzureStroageTodos.Web
    dotnet add package Microsoft.Azure.AppConfiguration.AspNetCore
    
    cd ..
    cd AzureStorageTods.AzureFunctions
    dotnet add package Microsoft.Azure.AppConfiguration.AspNetCore
    
    After that is complete do a CTRL+SHIFT+P to open the command pallet again and select the option to Restart OmniSharp.
    We are now ready to have our Web App and Functions Project read configuration and secrets from Azure App Configuration and Key Vault.

    To the AzureStorageTodos.Web project, modify the Program.cs file add the following using:

    using Microsoft.Extensions.Configuration.AzureAppConfiguration;
    Add the following code between var builder = WebApplication.CreateBuilder(args) and var initialScopes = builder.Configuration[“DownstreamApi:Scopes”]?.Split(‘ ‘);
    string? appConfigConnectionString = Environment.GetEnvironmentVariable("APPCONFIG_CONNECTION_STRING");
    string? aspNetCoreEnvironmentName = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
    
    builder.Host.ConfigureAppConfiguration(configurationBuilder => {
        if(appConfigConnectionString is not null && aspNetCoreEnvironmentName is not null) 
        {
            configurationBuilder.AddAzureAppConfiguration(azureAppConfigurationOptions => {
                azureAppConfigurationOptions.Connect(appConfigConnectionString)
                .Select(KeyFilter.Any, LabelFilter.Null)
                .Select(KeyFilter.Any, aspNetCoreEnvironmentName)
                .ConfigureKeyVault(kv =>
                {
                    kv.SetCredential(new DefaultAzureCredential());
                });
            });
        }
    });
    
    To recap what is going on here is we are reading the environment variables of APPCONFIG_CONNECTION_STRING and ASPNETCORE_ENVIRONMENT. We are then using that information to add an AppConfiguration that will connect using the connection string provided. Select only the Labels that match our ASPNETCORE_ENVIRONMENT name, or have no label at all. We also say when Key Vault references are needed add a key vault reference and use our DefaultAzureCredential which will be the credential we used when we did the az login.

    Your Program.cs file should look something like this:

    using Microsoft.AspNetCore.Authentication;
    using Microsoft.AspNetCore.Authorization;
    using Microsoft.AspNetCore.Authentication.OpenIdConnect;
    using Microsoft.AspNetCore.Mvc.Authorization;
    using Microsoft.Identity.Web;
    using Microsoft.Identity.Web.UI;
    using Graph = Microsoft.Graph;
    using Microsoft.Extensions.Azure;
    using Azure.Identity;
    using AzureStorageTodos.Repository.Interface;
    using AzureStorageTodos.Repository.BaseRepositories;
    using AzureStorageTodos.Repository.Repositories;
    using AzureStorageTodos.Service.Interface;
    using AzureStorageTodos.Service.RepositoryClients;
    using AzureStorageTodos.Service.Services;
    using Microsoft.Extensions.Configuration.AzureAppConfiguration;
    
    var builder = WebApplication.CreateBuilder(args);
    
    string? appConfigConnectionString = Environment.GetEnvironmentVariable("APPCONFIG_CONNECTION_STRING");
    string? aspNetCoreEnvironmentName = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
    
    builder.Host.ConfigureAppConfiguration(configurationBuilder => {
        if(appConfigConnectionString is not null && aspNetCoreEnvironmentName is not null) 
        {
            configurationBuilder.AddAzureAppConfiguration(azureAppConfigurationOptions => {
                azureAppConfigurationOptions.Connect(appConfigConnectionString)
                .Select(KeyFilter.Any, LabelFilter.Null)
                .Select(KeyFilter.Any, aspNetCoreEnvironmentName)
                .ConfigureKeyVault(kv =>
                {
                    kv.SetCredential(new DefaultAzureCredential());
                });
            });
        }
    });
    
    // Add services to the container.
    var initialScopes = builder.Configuration["DownstreamApi:Scopes"]?.Split(' ');
    
    builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
        .AddMicrosoftIdentityWebApp(options => {
            options.Instance = builder.Configuration["AzureAd:Instance"];
            options.Domain = builder.Configuration["AzureAd:Domain"];
            options.TenantId = builder.Configuration["AzureAd:TenantId"];
            options.ClientId = builder.Configuration["AzureAd:ClientId"];
            options.ClientSecret = builder.Configuration["AzureAd:ClientSecret"];
            options.CallbackPath = builder.Configuration["AzureAd:CallbackPath"];
    
            options.Events = new WebAppOpenIdConnectEvents();
        })
        .EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
            .AddMicrosoftGraph(builder.Configuration.GetSection("DownstreamApi"))
            .AddInMemoryTokenCaches();
    
    builder.Services.AddControllersWithViews(options =>
    {
        var policy = new AuthorizationPolicyBuilder()
            .RequireAuthenticatedUser()
            .Build();
        options.Filters.Add(new AuthorizeFilter(policy));
    });
    builder.Services.AddRazorPages()
        .AddMicrosoftIdentityUI();
    
    // Register Azure Clients
    builder.Services.AddAzureClients(azureClientsBuilder => {
        string azureStorageConnectionString = builder.Configuration.GetConnectionString("AzureStorage");    
    
        azureClientsBuilder.AddTableServiceClient(azureStorageConnectionString);
        azureClientsBuilder.AddBlobServiceClient(azureStorageConnectionString);
        azureClientsBuilder.AddQueueServiceClient(azureStorageConnectionString).ConfigureOptions(queueOptions => {
            queueOptions.MessageEncoding = Azure.Storage.Queues.QueueMessageEncoding.Base64;
        });    
        
        azureClientsBuilder.UseCredential(new DefaultAzureCredential());
    });
    
    // Register Repositories
    builder.Services.AddTransient<IUsersRepository, UsersRepository>();
    builder.Services.AddTransient<ITodosRepository, TodosRepository>();
    builder.Services.AddTransient<ITodoListRepository, TodoListRepository>();
    builder.Services.AddTransient<ITodoUpdatedQueue, TodoUpdatedQueue>();
    
    // Register Services
    builder.Services.AddTransient<IUsersRepositoryClient, UsersRepositoryClient>();
    builder.Services.AddTransient<ITodosRepositoryClient, TodosRepositoryClient>();
    builder.Services.AddTransient<ITodoListRepositoryClient, TodoListRepositoryClient>();
    builder.Services.AddTransient<ITodoListManager, TodoListManager>();
    
    var app = builder.Build();
    
    // Configure the HTTP request pipeline.
    if (!app.Environment.IsDevelopment())
    {
        app.UseExceptionHandler("/Home/Error");
        // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
        app.UseHsts();
    }
    
    app.UseHttpsRedirection();
    app.UseStaticFiles();
    
    app.UseRouting();
    
    app.UseAuthentication();
    app.UseAuthorization();
    
    app.MapControllerRoute(
        name: "default",
        pattern: "{controller=Home}/{action=Index}/{id?}");
    app.MapRazorPages();
    
    app.Run();
    
    We need similar functionality in our Functions Application as well. In the AzureStorageTods.AzureFunctions Startup.cs file add the following ConfigureAppConfiguration override function:
        public override void ConfigureAppConfiguration(IFunctionsConfigurationBuilder builder)
        {
            string appConfigConnectionString = Environment.GetEnvironmentVariable("APPCONFIG_CONNECTION_STRING");
            string aspNetCoreEnvironmentName = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
            
            builder.ConfigurationBuilder.AddAzureAppConfiguration(azureAppConfigurationBuilder => {
                azureAppConfigurationBuilder.Connect(appConfigConnectionString)
                .Select(KeyFilter.Any, LabelFilter.Null)
                .Select(KeyFilter.Any, aspNetCoreEnvironmentName)
                .ConfigureKeyVault(kv =>
                {
                    kv.SetCredential(new DefaultAzureCredential());
                });           
            });
                    
            base.ConfigureAppConfiguration(builder);
        }
    
    Your entire Startup.cs file should look something like this:
    using Azure.Identity;
    using Microsoft.Extensions.Azure;
    using Microsoft.Azure.Functions.Extensions.DependencyInjection;
    using Microsoft.Extensions.DependencyInjection;
    using Microsoft.Extensions.Configuration;
    using AzureStorageTodos.Repository.Interface;
    using AzureStorageTodos.Repository.Repositories;
    using AzureStorageTodos.Service.Interface;
    using AzureStorageTodos.Service.RepositoryClients;
    using AzureStorageTodos.Service.Services;
    using System;
    using Microsoft.Extensions.Configuration.AzureAppConfiguration;
    
    [assembly: FunctionsStartup(typeof(AzureStorageTodos.AzureFunctions.Startup))]
    
    namespace AzureStorageTodos.AzureFunctions;
    
    public class Startup : FunctionsStartup
    {
        public override void ConfigureAppConfiguration(IFunctionsConfigurationBuilder builder)
        {
            string appConfigConnectionString = Environment.GetEnvironmentVariable("APPCONFIG_CONNECTION_STRING");
            string aspNetCoreEnvironmentName = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
            
            builder.ConfigurationBuilder.AddAzureAppConfiguration(azureAppConfigurationBuilder => {
                azureAppConfigurationBuilder.Connect(appConfigConnectionString)
                .Select(KeyFilter.Any, LabelFilter.Null)
                .Select(KeyFilter.Any, aspNetCoreEnvironmentName)
                .ConfigureKeyVault(kv =>
                {
                    kv.SetCredential(new DefaultAzureCredential());
                });           
            });
                    
            base.ConfigureAppConfiguration(builder);
        }
    
        public override void Configure(IFunctionsHostBuilder builder)
        {
            var config = builder.GetContext().Configuration;
    
            string azureWebJobsStorage = config.GetValue<string>("AzureWebJobsStorage");
    
            // Register Azure Clients
            builder.Services.AddAzureClients(azureClientsBuilder => {
                azureClientsBuilder.AddTableServiceClient(azureWebJobsStorage);
                azureClientsBuilder.AddBlobServiceClient(azureWebJobsStorage);
                
                azureClientsBuilder.UseCredential(new DefaultAzureCredential());
            });
    
            // Register Repositories
            builder.Services.AddTransient<ITodosRepository, TodosRepository>();
            builder.Services.AddTransient<ITodoListRepository, TodoListRepository>();
    
            // Register Services
            builder.Services.AddTransient<ITodosRepositoryClient, TodosRepositoryClient>();
            builder.Services.AddTransient<ITodoListRepositoryClient, TodoListRepositoryClient>();
            builder.Services.AddTransient<ITodoListManager, TodoListManager>();
        }
    }
    

    Verify App Configuration is being downloaded locally and that your application still runs locally

    Go to the Run and Debug screen. Ensure that the “Attach to functions and launch web app” profile is still selected and press the green play button or press F5.

    Try adding and removing Todos and such again. Once you are sure that the application is still running locally continue on to the next steps.

    Add Environment Variables to the Web App and Functions App in Azure

    The Web App and Functions App will need an environment variable for your App Configuration Connection String and ASPNETCORE_ENVIRONMENT.

    First grab the Connection String to the App Configuration again by going to the App Configuration resource that you created earlier and going to the Keys blade and click the two pages button to copy the Connection String and note it down in a notepad.

    Then go to the Web App resource that you created in Azure earlier. Go to the Configuration Blade.

    In this case we are going to add the connection string as an application setting. The reason that we are doing that in this case is because they alter the environment variable name just slightly from the name we give it so it can’t be read with the current state of our code. Click on New application setting.

    Give it the name of “APPCONFIG_CONNECTION_STRING” with the value of what your noted down in the empty notepad.

    Click on “Ok”

    Add an additional application setting named: “ASPNETCORE_ENVIRONMENT” and give it the value of “Production”

    Your application settings should look something like this:

    Click on Save and then Continue. The application will restart.

    Go to the Functions App that you created in Azure Earlier. Add these same settings again in there.

    Save and Continue those changes as well.

    Your Functions App configuration should look something like this:

    Add Configuration and Secrets for production to the App Configuration and Key Vault

    With all of that in place it is now time for us to create a new App Registration in Azure Active Directory, and then configure out Azure App Configuration and Key Vault with the details from the registration.

    Take note of your current Web App URL by going to the Azure Web App that you created earlier. You will need this information to create a new Azure App Registration in the Active Directory.

    Browse to the Azure Active Directory resource in the Azure Portal.

    Go to the App Registrations Tab

    Give it any name that you like. I will be using “Azure Storage Todos – Production”. The supported account types are going to be “Accounts in this organizational directory only)”. The Redirect URI is going to be a Web platform and will be the azurewebsites.net URL that is being used by the Azure Web App service with adding a /signin-oidc at the end. For me that would mean https://sw-storage-todos-demo.azurewebsites.net/signin-oidc

    Next do these things:
      1 – Click on Register
      2 – Take note of your Application/ Client Id to a separate notepad. You will need this when we build the Production App Configuration Labels.
      3 – Go to Certificates and Secrets.
      4 – Create a new Client Secret. Give it a description of “Azure Storage Todos – Production” and click on Add. Take note of the value that you get there. (Again, make sure you get the Value, and not the Secret Id).
      5 – Go to the Key Vault resource that you created earlier.
      6 – Go to the Secrets blade again.
      7 – Generate / Import a new secret.

    Once again, this is a Manual upload. The name should be “AzureAdClientSecret-Production” the value will be the client secret that you just got from the App Registration Screen.

    We also need to add a new AzureStorageConnectionString for production to the Key Vault. To get the correct Value go to the Storage Account resource that you created earlier.

    Go to the “Access Keys” blade.

    Copy the connection string and save it to a notepad temporarily.
    This should open a browser. Once open:
      8 – Return to the Key Vault resource that you created earlier.
      9 – Go to the Secrets blade again
      10 – Generate / Import a new secret.

    Again, this is a Manual upload. The name should be “AzureStorageConnectionString-Production” and the value will be the connection string value you just got from the storage account window.

    Your Key Vault Secrets blade should look something like this now

    Return to the App Configuration resource that you created earlier.

    Copy all of the App Configurations but give them the production Label except for the AzureAd:ClientId, AzureAd:ClientSecret, and ConnectionStrings:AzureStorage. For example:

      • Click on the Create Button and Select Key Value
      • Give it the name of AzureAd:CallbackPath
      • Give it the same value as the other AzureAd:CallbackPath
      • Give it the label of Production
      • Click on Apply

    Repeat those steps for

      • AzureAd:Domain
      • AzureAd:Instance
      • AzureAd:TenantId

    Again, ensure that the label that you are using is “Production”.

    When it comes to AzureAd:ClientId. grab the ClientId from that new App Registration that you just did in Azure Ad.

    When it comes to AzureAd:ClientSecret. add it as a Key vault reference. Then select the Key Vault Reference for your AzureAdClientSecret-Production

    Do the same for ConnectionStrings:AzureStorage, except you will select the AzureStorageConnectionString-Production Secret.
    Your AppConfig should look something like this now:
    You now have an app configuration for each of your environments. This makes it so that the app configuration can be downloaded from both versions in the same way without any code changes.

    The trick though is that the application needs to be run by an identity that has permission to download our app configuration and secrets from the key vault.

    Furthermore, the Functions App is currently configured to run on it’s own storage account that it created. We need to update it to use our storage account.

    Browse to the Storage account that we created earlier.

    Click on the “Access Keys” blade.

    Show and copy the connection string here.
    Browse to the Functions application that we created earlier and….
      1 – Click on Configuration.
      2 – Click on “Edit” for the AzureWebJobsStorage” setting.
      3 – Paste in the value that you got from the other storage account connection string.

    Create and Configure Web Application and Function Application Identities to read App Configuration and Key Vault Secrets

    In this demonstration we will be using a System assigned Identity. There are options for user assigned identities too. Our key vault is currently using Role Based Access Control which makes using system assigned identities easier.

    In the azure portal go to the Web Application Resource that you created earlier.

    Go to the “Identity” blade

    On the System assigned tab move the status to On and click on Save and then click on Yes to confirm

    Now that we have an identity running our application let’s give that identity access to our Azure Resources (just like we need when we are running in development).

    Browse to the App Configuration resource that you created earlier.

    Click on the “Access control (IAM)” blade.

    Click on the “Add role assignment” button

    Select the “App Configuration Data Reader” role and then click on “Next”

    When you get to the Members tab change the assign access to radio button to “Managed identity” (which is what we just created earlier).
    Click on the “Select members” button

    Be sure to select the Subscription, and Managed identity type for App Service and select the app service identity and then click on the “Select” button.

    Click on Review and Assign to assign this user the appropriate role.

    Now that the user has access to our App Configuration, let’s give it access to our Key Vault as well.

    Go to the Key Vault resource that you created earlier.

    Once again. Go to the Access control (IAM) tab.

    Click to add a role assignment again. This time select the “Key Vault Secrets User” role.

    Once again select a managed identity and then select the managed identity for your Web Application. Click on Review and Assign until that role has been added to your Web Application user.

    The user will also need to be able to read and write data to our storage account. Go to the Storage Account Azure resource that you created earlier.

    Click on the Access Control (IAM) blade there. Click on Add Role Assignment. Add it to the “Storage Account Contributor” role.

    At this point our Web Application has access to the App Configuration and Key Vault. We now need to give our Functions Application the same treatment.

    Go to the Functions Application resource that you created earlier. From there is an Identity Blade as well. Click on that and create another managed identity. Repeat the steps in this section to add the Functions Application identity to the “App Configuration Data Reader”, “Key Vaults Secrets User” and “Storage Account Contributor” roles like was done for the Web Application.

    Publish to GitHub

    In order to prepare to publish to GitHub let’s create a few files and initialize a git repository.

    In Visual Studio Code open a terminal assuming that you are still at the /workspace folder execute the following commands:

    dotnet new gitignore
    This will create a .gitignore file in your project. Open it and add the following line to the end of it.
    # Dev Container Environment
    devcontainer.env
    
    To be sure that any windows user that downloads our project will have the correct line endings when interacting with our repository create a new file called .gitattributes to the root of the project. Give it the following code:
    * text=auto eol=lf
    Create a README.md file in the root of the folder. Give it the following code:
    # Azure Storage Todos
    After cloning this repository. Open it in Visual Studio Code and when prompted say "No" to re-open in container at first. 
    
    In order to configure your dev container to connect to our Azure App Configuration go to the .devcontainer folder. Make a copy of devcontainer.env.example and name it devcontainer.env. Copy the connection string from the link provided there to properly configure your APPCONFIG_CONNECTION_STRING environment variable.
    
    Once you have a valid connection string do a CTRL+SHIFT+P to bring up the command pallet and select the option to re-open in container from there.
    
    Once your container is built and Visual Studio Code is running from the container do a CTRL+SHIFT+` to bring up a command prompt from the container.
    
    Execute the following commands (these commands will likely need to be done any time you rebuild the dev container):
    
    ```bash
    dotnet restore
    dotnet dev-certs https --trust
    az login
    gh auth login
    ```
    
    After you have successfully logged in to the Azure CLI in your dev container you will be able to download the App Configuration and User Secrets for Development.
    
    What we have done here is made it so that the devcontainer.env file from our computer will not be uploaded to GitHub, but we will be telling people how to create the devcontainer.env file on their computer and how to start up the dev container and allow it to connect to azure app config.

    Execute the following commands

    git init
    git add .
    git commit -a -m "Initial Commit"
    
    You will get some warnings about line endings being changed. This is totally fine and what we want in fact. We have also initialized and created our first commit locally. Now it is time to get it up to github.

    Execute the following commands

    gh auth login
    Select the options to login to GitHub.com with HTTPS, and Authenticate Git with your GitHub credentials and Use the Web Browser option.

    Login to GitHub with your account in the browser.

    Provide the onetime code from the CLI

    Authorize GitHub and close the browser.

    Execute the following command

    gh repo create
    This will allow you to create a github repo interactively. I selected the following options as seen in teh image below:
      • Push an existing local repository to GitHub
      • .
      • azure-storage-todos-demo
      • Demo of how to use aAzure Storage
      • Public
      • Yes
      • Origin
      • Yes

    This was the output from my screen and image referred to above:.

    Go to github.com to see your repository there.

    Build GitHub Actions to Deploy to Azure App Services and Functions App from GitHub repository

    This is where things can get a little tricky. We have a unique situation where we are deploying multiple projects to separate places within one repository.

    From a high level what we need to do is build the Web Application, and Functions Applications separately. Upload them as artifacts and then use a release server to upload the artifacts to the Web Application and Functions Applications on Azure.

    We will also need the publish profiles from our Web Application and Functions Application as secrets in GitHub that can be used by the release agent computer.

    To make things more simple for the purpose of this demonstration we will create those secrets first, and then the GitHub actions second (since the GitHub Action will be reading these secrets).

    I would recommend using two browser windows / tabs in order to complete this.

    In your GitHub tab browse to Settings. Then click on Secrets and select actions.

    In your other tab that I will call your Azure tab browse to https://portal.azure.com/ and go to the Web Application resource that you created earlier.

    Click on the “Get publish profile button”

    Open the file in a text editor of some sort (Like Visual Studio Code).

    In your GitHub tab click on the “New repository secret” button.

    The secret name for this one will be AZURE_WEBAPP_PUBLISH_PROFILE and the value will be the value from the text editor that has the publish profile text in it.
    Click on Add Secret when ready

    On your Azure tab go to the Functions Application that you created earlier.

    There is another Get publish profile there. Click that to download that one.

    In your GitHub tab add another secret with the name of AZURE_FUNCTIONAPP_PUBLISH_PROFILE

    Your repository secrets should now look something like this:
    Click on the Actions tab in the main navigation menu for the Repository.

    Search for Azure Web App and select the “Deploy a .NET Core app to an Azure Web App and then select Configure

    Give it the following code:

    # This workflow will build and push a .NET Core app to an Azure Web App when a commit is pushed to your default branch.
    #
    # This workflow assumes you have already created the target Azure App Service web app.
    # For instructions see https://docs.microsoft.com/en-us/azure/app-service/quickstart-dotnetcore?tabs=net60&pivots=development-environment-vscode
    #
    # To configure this workflow:
    #
    # 1. Download the Publish Profile for your Azure Web App. You can download this file from the Overview page of your Web App in the Azure Portal.
    #    For more information: https://docs.microsoft.com/en-us/azure/app-service/deploy-github-actions?tabs=applevel#generate-deployment-credentials
    #
    # 2. Create a secret in your repository named AZURE_WEBAPP_PUBLISH_PROFILE, paste the publish profile contents as the value of the secret.
    #    For instructions on obtaining the publish profile see: https://docs.microsoft.com/azure/app-service/deploy-github-actions#configure-the-github-secret
    #
    # 3. Change the value for the AZURE_WEBAPP_NAME. Optionally, change the AZURE_WEBAPP_PACKAGE_PATH and DOTNET_VERSION environment variables below.
    #
    # For more information on GitHub Actions for Azure: https://github.com/Azure/Actions
    # For more information on the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy
    # For more samples to get started with GitHub Action workflows to deploy to Azure: https://github.com/Azure/actions-workflow-samples
    
    name: Build and deploy ASP.Net Core app to an Azure Web App
    
    env:
      AZURE_WEBAPP_NAME: sw-storage-todos-demo    # set this to the name of your Azure Web App
      AZURE_FUNCTIONAPP_NAME: sw-storage-todos-demo-functions  # set this to your application's name
      AZURE_WEBAPP_PACKAGE_PATH: './myapp'      # set this to the path to your web app project, defaults to the repository root
      AZURE_FUNCTIONAPP_PACKAGE_PATH: './myfunctions'    # set this to the path to your web app project, defaults to the repository root
      DOTNET_VERSION: '6.0.x'                 # set this to the .NET Core version to use
    
    on:
      push:
        branches:
          - "master"
      workflow_dispatch:
    
    permissions:
      contents: read
    
    jobs:
      build:
        runs-on: ubuntu-latest
    
        steps:
          - uses: actions/checkout@v3
    
          - name: Set up .NET Core
            uses: actions/setup-dotnet@v2
            with:
              dotnet-version: ${{ env.DOTNET_VERSION }}
          
          - name: Set up dependency caching for faster builds
            uses: actions/cache@v3
            with:
              path: ~/.nuget/packages
              key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }}
              restore-keys: |
                ${{ runner.os }}-nuget-
    
          - name: Build with dotnet
            run: dotnet build $GITHUB_WORKSPACE/AzureStorageTodos.Web/AzureStorageTodos.Web.csproj /property:GenerateFullPaths=true --configuration Release
    
          - name: dotnet publish
            run: dotnet publish $GITHUB_WORKSPACE/AzureStorageTodos.Web/AzureStorageTodos.Web.csproj /property:GenerateFullPaths=true -c Release -o ${{env.DOTNET_ROOT}}/myapp
    
          - name: Build function
            run: dotnet build $GITHUB_WORKSPACE/AzureStorageTodos.AzureFunctions/AzureStorageTodos_AzureFunctions.csproj /property:GenerateFullPaths=true --configuration Release --output ${{env.DOTNET_ROOT}}/myfunctions      
    
          - name: Upload web app artifact for deployment job
            uses: actions/upload-artifact@v3
            with:
              name: .net-app
              path: ${{env.DOTNET_ROOT}}/myapp
    
          - name: Upload functions app artificat for deployment job
            uses: actions/upload-artifact@v3
            with:
              name: .net-functions
              path: ${{env.DOTNET_ROOT}}/myfunctions
    
      deploy:
        permissions:
          contents: none
        runs-on: ubuntu-latest
        needs: build
        environment:
          name: 'Development'
          url: ${{ steps.deploy-to-webapp.outputs.webapp-url }}
    
        steps:
          - name: Download web app artifact from build job
            uses: actions/download-artifact@v3
            with:
              name: .net-app
              path: ${{ env.AZURE_WEBAPP_PACKAGE_PATH }}
    
          - name: Download functions app artificat from build job
            uses: actions/download-artifact@v3
            with:
              name: .net-functions
              path: ${{ env.AZURE_FUNCTIONAPP_PACKAGE_PATH }}
    
          - name: Deploy to Azure Web App
            id: deploy-to-webapp
            uses: azure/webapps-deploy@v2
            with:
              app-name: ${{ env.AZURE_WEBAPP_NAME }}
              publish-profile: ${{ secrets.AZURE_WEBAPP_PUBLISH_PROFILE }}
              package: ${{ env.AZURE_WEBAPP_PACKAGE_PATH }}
    
          - name: Deploy to Azure Functions
            uses: Azure/functions-action@v1
            with:
              app-name: ${{ env.AZURE_FUNCTIONAPP_NAME }}          
              publish-profile: ${{ secrets.AZURE_FUNCTIONAPP_PUBLISH_PROFILE }}
              package: ${{ env.AZURE_FUNCTIONAPP_PACKAGE_PATH }}
    
    Modify the values for your actual AZURE_WEBAPP_NAME and AZURE_FUNCTIONAPP_NAME. Everything else should be the same. You may need to tweek this file a few times to get the results that you want.

    Click on the “Start Commit” button and then click on Commit new file.

    Click on the Actions tab and you will see that a workflow is running.
    You can click on it to watch what it is doing. In the end you should end up with an application that has been deployed to Azure Web Application and Azure Functions.

    Test your application on Azure

    Go to the Web Application Resource that you created earlier. On the overview blade there is a link to your application. When you first sign in you will be asked to give consent on behalf of your organization. Click on Accept.

    Click on the “Todos” link.

    In another browser go to the Storage Account Resource that you created earlier. Click on the “Open in Explorer” button.

    In the storage accounts under your subscription you should be able to find this storage account. Expand Tables and you will see that a TodoUsers table has been created with your record.
    Add a Todo in your web application. If your fast enough you should be able to see the Queue Message in your explorer.
    After it has gone away. Refresh your browser in your Todos application and you will see your list of Todos growing.
    If you look in your storage explorer you will also see that the Blob Container for todo-lists has been created and the folders and files that we would expect to see are there too.

    Conclusion

    Azure offers a wider variety of Storage solutions and making them work together to achieve a set of goals can be very rewarding. We demonstrated how to use all three storage offerings that an Azure Storage account makes available. We are using Tables to store User Demographics and Individual Todos. We are using Blobs to preserve lists of Todos, and we are using Queues as a trigger to let a function know that the Todo List should be rebuilt.

    Keeping secrets out of application code is important. When you have people that collaborate on this project with you, they will need at least the same permissions that you gave the Web Application and Functions Application identities. You may want to elevate them to contributor so that they can add configurations and secrets as well.

    GitHub repository: https://github.com/woodman231/azure-storage-todos-demo

    About Intertech

    Intertech is a Software Development Consulting Firm that provides single and multiple turnkey software development teams, available on your schedule and configured to achieve success as defined by your requirements independently or in co-development with your team. Blackslate Software teams combine proven full-stack, DevOps, Agile-experienced lead consultants with Delivery Management, User Experience, Software Development, and QA experts in Business Process Automation (BPA), Microservices, Client- and Server-Side Web Frameworks of multiple technologies, Custom Portal and Dashboard development, Cloud Integration and Migration (Azure and AWS), and so much more. Each Blackslate Software employee leads with the soft skills necessary to explain complex concepts to stakeholders and team members alike and makes your business more efficient, your data more valuable, and your team better. In addition, Blackslate Software is a trusted partner of more than 4000 satisfied customers and has a 99.70% “would recommend” rating.