ASP.NET Core – How To Create A Development Environment For Using Azure Queue Storage Using The ASP.NET Core Framework.

The purpose of this document is to describe the steps necessary to create a development environment for using Azure Queue Storage. We will be creating an MVC Application and some Azure functions using the ASP.NET Core framework.

Azure Queue Storage is one of Microsoft’s solutions for messaging and can be a very powerful tool for inter-process communications, and event triggers. In this demonstration we will be creating an ASP.NET Core MVC Web Application. That web application is going to display the contents of a message log blob. It will also allow the user to send a message to the storage queue. We will also be creating an Azure Functions project from a Queue Trigger. The trigger will take the content of the message and append it to the Azure Storage blob.

In this tutorial we will:

      • Create a development container with C# and Azurite (to emulate Azure Storage). The container will also have the tools needed to create Azure Function Projects.
      • Confirm connectivity to Azurite
      • Create a new C# solution
      • Create an MVC Web Application
      • Create an Azure Functions Project
      • Configure Debugging for both the Web Application and Functions Project
      • Build out the Functions Project
      • Build out the Web Application
      • Test the Web Application
Before beginning this tutorial be sure to download the Azure Storage Explorer to your computer. https://azure.microsoft.com/en-us/features/storage-explorer/.

Create a Development Container with C# and Azurite

 

    1 – Create a new folder on your computer. I will be calling mine “azure-queue-storage-demo”

    2 – Open the folder with Visual Studio Code.

    3 – Create a .devcontainer folder

    4 – Create a Dockerfile within that .devcontainer folder.

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="none"
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.

    5 – 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.

    6 – 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 csarp 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 instead 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
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;
    1 – Right-click on Storage Accounts and click on Connect to Azure Storage.

    2 – Select that you want to connect to a Storage account or service.

    3 – Select connection string

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

    5 – 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-queue-storage-demo.sln

Create an MVC Web Application

The web application will display the Blob Content and Send Messages to the Queue.

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

dotnet new mvc -o AzureQueueStorageDemo.Web
dotnet sln add ./AzureQueueStorageDemo.Web/
dotnet dev-certs https --trust
When that is done do a CTRL+SHIFT+P to bring up the command pallet.

Select the option to Restart OmniSharp.

When prompted say “Yes” to add the Debug files.

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 AzureQueueStorageDemo.AzureFunctions
cd AzureQueueStorageDemo.AzureFunctions/
func init (select options 1, and 1)
This will create the initial CSPROJ file for the Azure Functions Project. Let us now add our first Queue Trigger.

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

func new (select options 1, give it the name of AddMessageLogRequest)
Modify the “local.settings.json” file. Modify the “AzureWebJobsStorage” to be the connection string to the Azurite container. Your local.settings.json file should look like this:
{
    "IsEncrypted": false,
    "Values": {
        "AzureWebJobsStorage": "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;",
        "FUNCTIONS_WORKER_RUNTIME": "dotnet"
    }
}
Execute the following command in the terminal.
func start
This should at least have your functioning running now.

Open the AddMessageLogRequest.cs file.

You can see that all it is doing at this time is looking for messages to be sent to the “myqueue-items” message queue and then log that the function was processed and give the message content.

Open your storage explorer.

Go to the storage account associated with the Dev Container. Create a queue named “myqueue-items”. And then send a message and you will see that your terminal notices that a message has been received.

While this function is running you really won’t see the message stay in the queue for long. You can keep hitting refresh in the storage explorer but as long as this trigger worked properly the message will be removed and that is by design.

Press CTRL + C to exit out of the function.

You can even create a bunch of messages while the trigger is shut down, and then turn the trigger back on with the func start command, and then all of the unprocessed messages will be processed once you start the functions up again.

Now that we have confirmed that we have a valid queue function. Let’s go ahead and setup our debugger and start logging the messages that we receive to a Blob in Blob storage.

Configure Debugging for both the Web Application and Functions Project

Earlier when you restarted OmniSharp and said Yes to add the Debug files it should have created a .vscode folder with a launch.json and tasks.json files.

To the Tasks.json add the following code to clean, and build the Azure Functions after the “watch” label key:

        {
            "label": "clean (functions)",
            "options": {
                "cwd": "${workspaceFolder}/AzureQueueStorageDemo.AzureFunctions"
            },
            "command": "dotnet",
            "args": [
              "clean",
              "/property:GenerateFullPaths=true",
              "/consoleloggerparameters:NoSummary"
            ],
            "type": "process",
            "problemMatcher": "$msCompile"
          },
          {
            "label": "build (functions)",
            "options": {
                "cwd": "${workspaceFolder}/AzureQueueStorageDemo.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}/AzureQueueStorageDemo.AzureFunctions/bin/Debug/net6.0"
            },
            "command": "host start",
            "isBackground": true,
            "problemMatcher": "$func-dotnet-watch"
          }
In total the tasks.json file should look something like this:
{
    "version": "2.0.0",
    "tasks": [
        {
            "label": "build",
            "command": "dotnet",
            "type": "process",
            "args": [
                "build",
                "${workspaceFolder}/AzureQueueStorageDemo.Web/AzureQueueStorageDemo.Web.csproj",
                "/property:GenerateFullPaths=true",
                "/consoleloggerparameters:NoSummary"
            ],
            "problemMatcher": "$msCompile"
        },
        {
            "label": "publish",
            "command": "dotnet",
            "type": "process",
            "args": [
                "publish",
                "${workspaceFolder}/AzureQueueStorageDemo.Web/AzureQueueStorageDemo.Web.csproj",
                "/property:GenerateFullPaths=true",
                "/consoleloggerparameters:NoSummary"
            ],
            "problemMatcher": "$msCompile"
        },
        {
            "label": "watch",
            "command": "dotnet",
            "type": "process",
            "args": [
                "watch",
                "run",
                "--project",
                "${workspaceFolder}/AzureQueueStorageDemo.Web/AzureQueueStorageDemo.Web.csproj"
            ],
            "problemMatcher": "$msCompile"
        },
        {
            "label": "clean (functions)",
            "options": {
                "cwd": "${workspaceFolder}/AzureQueueStorageDemo.AzureFunctions"
            },
            "command": "dotnet",
            "args": [
              "clean",
              "/property:GenerateFullPaths=true",
              "/consoleloggerparameters:NoSummary"
            ],
            "type": "process",
            "problemMatcher": "$msCompile"
          },
          {
            "label": "build (functions)",
            "options": {
                "cwd": "${workspaceFolder}/AzureQueueStorageDemo.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}/AzureQueueStorageDemo.AzureFunctions/bin/Debug/net6.0"
            },
            "command": "host start",
            "isBackground": true,
            "problemMatcher": "$func-dotnet-watch"
          }
    ]
}
To the launch.json file add the following after the “.NET Core Attach” field:
        {
            "name": "Attach to .NET Functions",
            "type": "coreclr",
            "request": "attach",
            "processId": "${command:azureFunctions.pickProcess}"
        },
Also add a compounds key:
    "compounds": [
        {
            "name": "Attach to functions and launch web app",
            "configurations": [".NET Core Launch (web)", "Attach to .NET Functions"],
            "stopAll": true
        }
    ]
In total your launch.json should look something like this:
{
    "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}/AzureQueueStorageDemo.Web/bin/Debug/net6.0/AzureQueueStorageDemo.Web.dll",
            "args": [],
            "cwd": "${workspaceFolder}/AzureQueueStorageDemo.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
        }
    ]
}
What is neat about these files is that they are configured to clean and build both the MVC Project and Functions project. Then they are individually able to connect to the debuggers after those builds. Then when we add a compound to the mix it is now building and debugging both at the same time.

You also still have the option of only debugging one at a time as well. Look at the debug area, and when you click the drop down you will see four different options to select from when debugging. Which ever one is in the drop down is what F5 will use.

When you have both launched you will see that the debugger area has a 2 symbol on it since it is using two debuggers now.
You can practice setting a break point in the function that says that it received a message in the queue or in the home controller before the index view opens to see the results if you like.

Build out the Functions Project

Dependency injection is sort of a foreign concept to the functions projects out of the box. Luckily it is still available to be used. Let’s add some packages to get started.

At the /workspace/AzureQueueStorageDemo.AzureFunctions/ directory, execute the following commands:

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
Create a Startup.cs file in the AzureQueueStorageDemo.AzureFunctions folder.

Give it the following code:

using Azure.Identity;
using Microsoft.Extensions.Azure;
using Microsoft.Azure.Functions.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;

[assembly: FunctionsStartup(typeof(AzureQueueStorageDemo.Startup))]

namespace AzureQueueStorageDemo;

public class Startup : FunctionsStartup
{
    public override void Configure(IFunctionsHostBuilder builder)
    {
        builder.Services.AddAzureClients(azureClientsBuilder => {
            azureClientsBuilder.AddBlobServiceClient(builder.GetContext().Configuration["AzureWebJobsStorage"]);

            azureClientsBuilder.UseCredential(new DefaultAzureCredential());
        });                
    }

    public override void ConfigureAppConfiguration(IFunctionsConfigurationBuilder builder)
    {        
        base.ConfigureAppConfiguration(builder);
    }
}
Modify the AddMessageLogRequest.cs file to have the following contents:
using Azure.Storage.Blobs;
using System;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Host;
using Microsoft.Extensions.Logging;
using Azure.Storage.Blobs.Specialized;
using System.IO;
using Microsoft.Extensions.Configuration;

namespace AzureQueueStorageDemo.AzureFunctions
{
    public class AddMessageLogRequest
    {
        private readonly BlobServiceClient _blobServiceClient;

        public AddMessageLogRequest(BlobServiceClient blobServiceClient)
        {
            _blobServiceClient = blobServiceClient;
        }

        [FunctionName("AddMessageLogRequest")]
        public void Run([QueueTrigger("myqueue-items")] string myQueueItem, ILogger log)
        {
            BlobContainerClient containerClient = _blobServiceClient.GetBlobContainerClient("my-message-log-container");
            containerClient.CreateIfNotExists();

            AppendBlobClient appendBlobClient = containerClient.GetAppendBlobClient("message-log.txt");
            appendBlobClient.CreateIfNotExists();

            // Convert myQueueItem to a stream
            Stream contentStream = new MemoryStream();
            StreamWriter streamWriter = new StreamWriter(contentStream);
            streamWriter.Write(myQueueItem + Environment.NewLine);
            streamWriter.Flush();
            contentStream.Position = 0;

            appendBlobClient.AppendBlock(contentStream);

            log.LogInformation($"C# Queue trigger function processed: {myQueueItem}");
        }
    }
}
This is the code that takes the message queue text from the “my-queue-items” Azure Storage Queue and writes it to a file called “message-log.txt” in a container called “my-message-log-container”.

At this point you can run just the functions app. Use the storage explorer and send some more messages. You should start noticing that it is creating this file and more information is being put in to it as you send more and more messages.

Once you are satisfied that the queue will do what you expect it to do. Let’s build out the web application to make it easier to send messages to the queue.

Build out the Web Application

Our web application will need some libraries to be able to connect to the Blob Storage and Queue Storage. Execute the following commands from the /workspace/AzureQueueStorageDemo.Web/ folder

dotnet add package Azure.Storage.Blobs
dotnet add package Azure.Storage.Queues
dotnet add package Microsoft.Extensions.Azure
In Program.cs, and between the var builder = WebApplication.CreateBuilder(args); and builder.Services.AddControllersWithViews(); lines, add the following code:
// Register Azure Clients
builder.Services.AddAzureClients(azureClientsBuilder => {
    azureClientsBuilder.AddQueueServiceClient(builder.Configuration.GetConnectionString("AzureStorage")).ConfigureOptions(queueOptions => {
        queueOptions.MessageEncoding = QueueMessageEncoding.Base64;        
    });

    azureClientsBuilder.AddBlobServiceClient(builder.Configuration.GetConnectionString("AzureStorage"));    
});
Create a “Services” folder inside of the /workspace/AzureQueueStorageDemo.Web/ folder

Create a new file called “IAzureQueueStorageService.cs”.

Give it the following code:

namespace AzureQueueStorageDemo.Web.Services;

public interface IAzureQueueStorageService
{
    public Task SendMessageAsync(string message);
}
Create a file called “AzureQueueStorageService.cs”.

Give it the following code:

using Azure;
using Azure.Storage.Queues;

namespace AzureQueueStorageDemo.Web.Services;

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;                
    }
}
Create a file called IMyQueueItemsQueStorageService.cs.

Give it the following code:

namespace AzureQueueStorageDemo.Web.Services;

public interface IMyQueueItemsQueStorageService : IAzureQueueStorageService
{    
}
Create a file called MyQueueItemsAzureQueueStorageService.cs.

Give it the following code:

using Azure.Storage.Queues;

namespace AzureQueueStorageDemo.Web.Services;

public class MyQueueItemsAzureQueueStorageService : AzureQueueStorageService, IMyQueueItemsQueStorageService
{
    public MyQueueItemsAzureQueueStorageService(QueueServiceClient queueServiceClient) : base (queueServiceClient, "myqueue-items")
    {        
    }
}
Here we just built a reusable service layer for the QueueServiceClient that is registered via the Dependency injection and Azure Extensions. When we create a solid version of the abstract class we pass it a queue name. When we use the SendMessageAsync method we pass the message that we want to send.

Now modify the Program.cs file again and add this just after the previous code that you added earlier:

// Register Services
builder.Services.AddTransient<IMyQueueItemsQueStorageService, MyQueueItemsAzureQueueStorageService>();
In total your Program.cs should look something like this:
using Azure.Identity;
using Azure.Storage.Queues;
using AzureQueueStorageDemo.Web.Services;
using Microsoft.Extensions.Azure;

var builder = WebApplication.CreateBuilder(args);

// Register Azure Clients
builder.Services.AddAzureClients(azureClientsBuilder => {
    azureClientsBuilder.AddQueueServiceClient(builder.Configuration.GetConnectionString("AzureStorage")).ConfigureOptions(queueOptions => {
        queueOptions.MessageEncoding = QueueMessageEncoding.Base64;        
    });

    azureClientsBuilder.AddBlobServiceClient(builder.Configuration.GetConnectionString("AzureStorage"));    
});

// Register Services
builder.Services.AddTransient<IMyQueueItemsQueStorageService, MyQueueItemsAzureQueueStorageService>();

// Add services to the container.
builder.Services.AddControllersWithViews();

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.UseAuthorization();

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

app.Run();
Now that we have that in place, we are ready to start reading the blob and writing to the queue.

To the Models folder in the Web Application add a file called MessageLogViewModel.cs.

Give it the following code:

namespace AzureQueueStorageDemo.Web.Models;

public class MessageLogViewModel
{
    public string? MessageLogContents {get; set;}
}
To the Controllers folder in the Web Application add a file called MessagesControler.cs.

Give it the following code:

using Azure.Storage.Blobs;
using System.Diagnostics;
using Microsoft.AspNetCore.Mvc;
using AzureQueueStorageDemo.Web.Services;
using AzureQueueStorageDemo.Web.Models;

namespace AzureQueueStorageDemo.Web.Controllers;

public class MessagesController : Controller
{
    private readonly IMyQueueItemsQueStorageService _myQueueItemsQueStorageService;
    private readonly BlobServiceClient _blobServiceClient;

    public MessagesController(IMyQueueItemsQueStorageService myQueueItemsQueStorageService, BlobServiceClient blobServiceClient)
    {
        _myQueueItemsQueStorageService = myQueueItemsQueStorageService;
        _blobServiceClient = blobServiceClient;
    }

    [HttpGet]
    public async Task<IActionResult> Index()
    {
        var results = new MessageLogViewModel();

        results.MessageLogContents = "";

        var containerClient = _blobServiceClient.GetBlobContainerClient("my-message-log-container");
        await containerClient.CreateIfNotExistsAsync();

        var blobClient = containerClient.GetBlobClient("message-log.txt");

        if(blobClient.Exists()) {
            var blobContent = await blobClient.OpenReadAsync();

            var streamReader = new StreamReader(blobContent);

            results.MessageLogContents = await streamReader.ReadToEndAsync();            
        }        

        return View(results);
    }

    [HttpGet]
    public IActionResult Create()
    {
        var newMessage = new MessageLogViewModel();

        return View(newMessage);
    }

    [HttpPost]
    public async Task<IActionResult> Create(MessageLogViewModel newMessage)
    {
        if(newMessage.MessageLogContents is not null) {
            await _myQueueItemsQueStorageService.SendMessageAsync(newMessage.MessageLogContents);
        }

        return RedirectToAction("Index");
    }
}
Here we are injecting our MyQueueItemsStorageService as well as a BlobServiceClient. When the Index page is requested we put the contents of the blob file in to the MessageLogViewModel and pass it to the Index page. When we get a request to create a message we pass it an empty MessageLogViewModel, and when a Create is posted with a valid MessageLogViewModel we take the content that was request and send it to the Message Queue. After the message has been sent to the queue, it will return to the Index page. The results of seeings new stuff might not be 100% immediate because the function has not triggered between the user hit submit and saw the Index page again. The advantage to this is that the user does not have to sit and wait and watch a form submit when there is an expensive operation going on. The down side is that it is not immediate. So we will be putting some warning messages on the page to let the user know what could be happening.

To the Views folder create a new folder called “Messages”.

To the new Messages folder create a new file called Index.cshtml.

Give it the following code

@model MessageLogViewModel
@{
    ViewData["Title"] = "Messages in the Messages Log";
}

<h1>@ViewData["Title"]</h1>

<a class="btn btn-primary" asp-action="Create">Add a Message</a>

<p class="text-info">*If you recently added some messages, then you may need to refresh a few times to see the results of the Azure Function Processing the message.</p>

<pre class="form-control" readonly>
@Model.MessageLogContents
</pre>
To the new Messages folder create a new file called Create.cshtml.

Give it the following code

@model MessageLogViewModel
@{
    ViewData["Title"] = "Add a Message to the Messages Log";
}

<h1>@ViewData["Title"]</h1>

<form asp-action="Create" method="post">
    <div class="form-group">
        <label asp-for="MessageLogContents"></label>
        <textarea class="form-control" asp-for="MessageLogContents"></textarea>
    </div>
    <input class="btn btn-primary" type="submit" value="Send" />
</form>
To the Views -> Shared -> _Layout.cshtml add the following line of code after the privacy link:
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-controller="Messages" asp-action="Index">Messages</a>
                        </li>
In total your _Layout.cshtml should look something 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"] - AzureQueueStorageDemo.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="~/AzureQueueStorageDemo.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">AzureQueueStorageDemo.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="Messages" asp-action="Index">Messages</a>
                        </li>
                    </ul>
                </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 - AzureQueueStorageDemo.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>
With all of this in place we should now be able to run the web application and functions together.

Test the Web Application

Click on the Messages link. If you still have some test data from before when you were testing the queue out directly through the Azure explorer you should already see some messages. If not. Click on the Create button, and start adding messages.

You will notice that the results won’t be immediate and that you may need to refresh the Index page a few times before the newer messages come up.

Take note of your message-log.txt file in the Azure Storage Explorer. The size should increase each time the function executes.

You can also download the blob file using the Storage Explorer to view it’s contents to see how it looks.

You may also want to practice adding and removing break points to see the code in action.

You can even delete the message-log.txt file manually from the storage explorer to start over.

Conclusion

Azure Queue Storage is just one of many Microsoft Technologies that have event driven messaging. This can allow your web application some freedom and cycle time from doing expensive operations that are required after a particular action has been completed, thus allowing the user to return to the next page, while the processing of the request continues in another area.

GitHub Repo: https://github.com/woodman231/azure-queue-storage-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. Intertech 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 Intertech 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, Intertech is a trusted partner of more than 4000 satisfied customers and has a 99.70% “would recommend” rating.