ASP.NET Core – How To Create A Development Environment For Using Azure Blob Storage With Repository Pattern

The purpose of this document is to describe the steps necessary to create a development environment for using Azure Blob Storage using the repository pattern.

Azure Blob Storage is one of Microsoft’s solutions for storing files in the cloud. It is one of the features available when creating any storage account on Azure. Depending on your needs, it can replace SQL Server for several use cases and may even cost less.

In this tutorial we will:

      • Create a development container with C# and Azurite (to emulate Azure Storage)
      • Confirm connectivity to Azurite
      • Create a new C# solution
      • Create an MVC Web Application
      • Create class libraries to interact with the Azure Storage
      • Build the Entity Models
      • Build the Repository Layer
      • Build the Service Layer
      • Register repositories and services
      • Implement controllers and views
      • Test the 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 (to emulate Azure Storage)

 

    1 – Create a new folder on your computer. I will be calling mine “azure-blob-storage-repository-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 <your-package-here>" 2>&1
    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"

    # Add "forwardPorts": ["1433"] to **devcontainer.json** to forward MSSQL locally.
    # (Adding the "ports" property to this file will not forward from a Codespace.)
    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"
    }

}
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.

Now, 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-blob-storage-repository-demo.sln

Create an MVC Web Application

We need a web application to interact with our objects.

Execute the following commands:

dotnet new mvc -o BlobStorageRepositoryDemo.Web
dotnet sln add ./BlobStorageRepositoryDemo.Web/

Create Class Libraries to Interact with Azure Storage

We need to create some model classes. We have a presentation layer already. Let us use Class Libraries for the other Model, Repository and Service Layers.

Execute the following commands at the /workspace command prompt:

dotnet new classlib -o BlobStorageRepositoryDemo.Models
dotnet new classlib -o BlobStorageRepositoryDemo.Repository
dotnet new classlib -o BlobStorageRepositoryDemo.Service

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

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

cd BlobStorageRepositoryDemo.Repository
dotnet add reference ../BlobStorageRepositoryDemo.Models/
cd ..
cd BlobStorageRepositoryDemo.Service
dotnet add reference ../BlobStorageRepositoryDemo.Models/
dotnet add reference ../BlobStorageRepositoryDemo.Repository/
cd ..
cd BlobStorageRepositoryDemo.Web
dotnet add reference ../BlobStorageRepositoryDemo.Models/
dotnet add reference ../BlobStorageRepositoryDemo.Repository/
dotnet add reference ../BlobStorageRepositoryDemo.Service/
cd ..
Furthermore, the repository layer, and web layer will need access to the Azure.Storage.Blobs NuGet package.

Execute the following commands, assuming that the prompt is still at /workspace/

cd BlobStorageRepositoryDemo.Repository
dotnet add package Azure.Storage.Blobs
cd ..
cd BlobStorageRepositoryDemo.Web
dotnet add package Azure.Storage.Blobs
cd ..

Build the Entity Models

We will be creating a TODO application in this example. So, we will be creating Base Entity classes and a TODO Entity. A Repository Layer for the TODOs, a Service Layer for the TODOs and have the controllers integrate with the service layer.

Blobs are just files. In this demonstration we will be using a Blob Container as an analogy for a Database, a Folder within the container as a Table, and a File as a record. The file will be a JSON Serialized version of our TODO class. The file will be named the .json.

To the “BlobStorageRepositoryDemo.Models” class library, delete the Class1.cs file and create a new file called: IBaseAzureStorageEntityModel.cs

Give the file the following code:

namespace BlobStorageRepositoryDemo.Models;

public interface IBaseAzureStorageEntityModel
{
    public string Id {get; set;}
}
Create a new file called BaseAzureStorageEntityModel.cs

Give the file the following code:

namespace BlobStorageRepositoryDemo.Models;

public class BaseAzureStorageEntityModel : IBaseAzureStorageEntityModel
{
    public BaseAzureStorageEntityModel()
    {
        this.Id = Guid.NewGuid().ToString();        
    }

    public BaseAzureStorageEntityModel(string id)
    {
        this.Id = id;        
    }

    public string Id {get; set;}
}
Create a new file called Todo.cs

Give the file the following code:

namespace BlobStorageRepositoryDemo.Models;

public class Todo : BaseAzureStorageEntityModel
{
    public string? Title {get; set;}
    public string? Description {get; set;}
    public bool Completed {get; set;} = false;    
}

Build the Repository Layer

Now that we have some entities it is time for us to work on a way to store these entities. Since our repository will be working with Blob files. Let’s create a Service that more directly interacts with Azure’s Blob Files as a foundation for our Repository.

To the BlobStorageRepositoryDemo.Repository class library delte Class1.cs.

Create a new file called IBlobStorageService.cs.

Give it the following code:

namespace BlobStorageRepositoryDemo.Repository;

public interface IBlobStorageService
{
    Task DeleteDocumentAsync(string blobName);
    Task<Stream> GetBlobContentAsync(string blobName);
    Task UpdateBlobContentAsync(string blobName, Stream content);
    Task<List<string>?> GetListOfBlobsInFolderAsync(string folderName);
}
Next, create a new file called BlobStorageService.cs.

Give the file the following code:

using Azure.Storage.Blobs;

namespace BlobStorageRepositoryDemo.Repository;

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;
    }
}
Allot is going on in this file, and 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 start up of the web application. The name of the container will be given when the class is initiated as well.

Now that we have an easy way to interact with blob files. Let’s use that to start managing our application entities by creating a re-usable abstract class that will take a Type parameter matching BaseAzureStorageEntityModel.

Create a new file called IAzureStorageRepository.cs.

Give it the following code:

using BlobStorageRepositoryDemo.Models;

namespace BlobStorageRepositoryDemo.Repository;

public interface IAzureStorageRepository<T> where T : IBaseAzureStorageEntityModel
{
    Task<T?> UpsertAsync(T entityDetails);
    Task<T?> GetOneAsync(string id);
    Task<List<T>?> GetAllAsync();
    Task DeletAsync(string id);
}
Create a new file called AzureBlobStorageRepository.cs.

Give it the following code:

using System.Text.Json;
using Azure.Storage.Blobs;
using BlobStorageRepositoryDemo.Models;

namespace BlobStorageRepositoryDemo.Repository;

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 DeletAsync(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;
    }
}
Allot is going on in here. 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 ready to create a repository for our TODO class.

Create a new file called ITodoAzureBlobStorageRepository.cs.

Give it the following code:

using BlobStorageRepositoryDemo.Models;

namespace BlobStorageRepositoryDemo.Repository;

public interface ITodoAzureBlobStorageRepository : IAzureStorageRepository<Todo>
{
}
Create a new file called TodoAzureBlobStorageRepository.cs.

Give it the following code:

using Azure.Storage.Blobs;
using BlobStorageRepositoryDemo.Models;

namespace BlobStorageRepositoryDemo.Repository;

public class TodoAzureBlobStorageRepository : AzureBlobStorageRepository<Todo>, ITodoAzureBlobStorageRepository
{
    public TodoAzureBlobStorageRepository(BlobServiceClient blobServiceClient) : base(blobServiceClient, "todos", "TodoItems")
    {        
    }
}
When we created the ITodoAzureBlobStorageRepository interface we said that we want to have it look like the IAzureStorageRepsoitory, and specified that we will be using the BlobStorageRepositoryDemo.Models.Todo class as the type parameter.

When we created the TodoAzureBlobStorageRepository class we said that we want to use the functionality from the AzureBlobStorageRepository with the BlobStorageRespositoryDemo.Models.Todo class as the type parameter, and that functionality should match the signature of ITodoAzureBlobStorageRepository. We created a constructor that will also need the BlobServiceClient, which again will be registered during the web app’s startup. We then also give values to the base abstract class for the container name and folder name. This basically makes it so all of the code that we used in AzureBlobStorageRepository was coded by hand again in a new class with new types, container and folder names. But since we have this abstract class, we avoided having to copy and paste that code.

If we ever wanted to do this again at this point now all we would need to do is create a new model class, then create a new Repository Interface and Model in this fashion and we will be all set.

Furthermore, if we have specific new functions that we want the repository to do we can add those as methods to these specific instances of the Interface and Class.

Build the Service Layer

The service layer in this demonstration will start by creating a repository client. Basically, a class that will use a repository class. It will be an abstract class again with virtual public methods that can be overridden for each class instance. The abstract class will simply try to pass the functionality along to the repository, and if any error occurs return null. When we override a method, we can add logic and directly use the repository, or base class methods.

To the “BlobStorageRepositoryDemo.Service” project delete Class1.cs

Create a new file called IAzureStorageRepositoryClient.cs.

Give it the following code:

using BlobStorageRepositoryDemo.Models;

namespace BlobStorageRepositoryDemo.Service;

public interface IAzureStorageRepositoryClient<T> where T : IBaseAzureStorageEntityModel
{
    Task<T?> UpsertAsync(T entityDetails);
    Task<T?> GetOneAsync(string id);
    Task<List<T>?> GetAllAsync();
    Task DeletAsync(string id);
}
Create a new file called AzureBlobStorageRepositoryClient.cs

Give it the following code:

using BlobStorageRepositoryDemo.Models;
using BlobStorageRepositoryDemo.Repository;

namespace BlobStorageRepositoryDemo.Service;

public abstract class AzureBlobStorageRepositoryClient<T> : IAzureStorageRepository<T> where T : IBaseAzureStorageEntityModel
{
    private readonly IAzureStorageRepository<T> _blobStorageRepository;

    public AzureBlobStorageRepositoryClient(IAzureStorageRepository<T> blobStorageRepository)
    {
        _blobStorageRepository = blobStorageRepository;
    }

    public Task DeletAsync(string id)
    {
        try {
            return _blobStorageRepository.DeletAsync(id);
        }
        catch {
            // Do nothing
        }

        return Task.CompletedTask;
    }

    public async Task<List<T>?> GetAllAsync()
    {
        try {
            return await _blobStorageRepository.GetAllAsync();
        }
        catch {
            // Do nothing, return null
        }

        return null;
    }

    public async Task<T?> GetOneAsync(string id)
    {
        try {
            return await _blobStorageRepository.GetOneAsync(id);
        }
        catch {
            // Do nothing, return null
        }

        return default(T);
    }

    public async Task<T?> UpsertAsync(T entityDetails)
    {
        try {
            var results = await _blobStorageRepository.UpsertAsync(entityDetails);

            return results;
        }
        catch {
            // Do nothing, return null
        }

        return default(T);
    }
}
Now that we have a reusable abstract class to work with let’s have it work with our Todo class.

Create a new file called ITodoService.cs

Give it the following code:

using BlobStorageRepositoryDemo.Models;

namespace BlobStorageRepositoryDemo.Service;

public interface ITodoService : IAzureStorageRepositoryClient<Todo>
{
}
Create a new file called TodoService.cs.

Give it the following code:

using BlobStorageRepositoryDemo.Models;
using BlobStorageRepositoryDemo.Repository;

namespace BlobStorageRepositoryDemo.Service;

public class TodoService : AzureBlobStorageRepositoryClient<Todo>, ITodoService
{
    public TodoService(ITodoAzureBlobStorageRepository repository) : base (repository)
    {        
    }
}
As pointed out during the repository layer. Any time we build a new entity class we will want to build a new Interface and Class like this for that new entity class.

Register Repositories and Services

Now that we have our Entities, Repositories and Services set up, it is time to let our Web Application know that they exist.

To begin though we must first register the BlobClientService that the repository is going to depend on (and thus the Service Layer will depend on). After that we can register the Repository and Service Interfaces.

In order to register the BlobClientService we need to include a few more NuGet packages for the Web Application. To add them execute the following commands from a terminal. Ensure that you are in the /workspace/BlobStroageRepositoryDemo.Web/ folder.

dotnet add package Microsoft.Extensions.Azure
dotnet add package Azure.Identity
Use the command pallet (CTRL+SHIFT+P) to restart OmniSharp
Open Program.cs from the TableStorageRepositoryDemo.Web project

Add the following “usings”

using Azure.Identity;
using Microsoft.Extensions.Azure;
using BlobStorageRepositoryDemo.Repository;
using BlobStorageRepositoryDemo.Service;
Add the following code after var builder = WebApplication.CreateBuilder(args); and before builder.Services.AddControllersWithViews();
// Register Azure Clients
builder.Services.AddAzureClients(azureClientsBuilder => {
    azureClientsBuilder.AddBlobServiceClient(builder.Configuration.GetConnectionString("AzureStorage"));

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

// Register Repositories
builder.Services.AddTransient<ITodoAzureBlobStorageRepository, TodoAzureBlobStorageRepository>();

// Register Services
builder.Services.AddTransient<ITodoService, TodoService>();
The entire Program.cs should look like this:
using Azure.Identity;
using Microsoft.Extensions.Azure;
using BlobStorageRepositoryDemo.Repository;
using BlobStorageRepositoryDemo.Service;

var builder = WebApplication.CreateBuilder(args);

// Register Azure Clients
builder.Services.AddAzureClients(azureClientsBuilder => {
    azureClientsBuilder.AddBlobServiceClient(builder.Configuration.GetConnectionString("AzureStorage"));

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

// Register Repositories
builder.Services.AddTransient<ITodoAzureBlobStorageRepository, TodoAzureBlobStorageRepository>();

// Register Services
builder.Services.AddTransient<ITodoService, TodoService>();

// 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();
To your appsettings.json add a ConnectionStrings key, with a key inside of it called AzureStorage, with the value of the default connection string for azure storage.

Your appsettings.json file should look like this.

{
  "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": "*"
}

Implement Controllers and Views

Now that we have all of this in place it is time for the web application to start using these services that we have created.

In the BlobStorageRepositoryDemo.Web project, and the Controllers folder add a new file called “TodosController.cs”

Give it the following code:

using System.Diagnostics;
using Microsoft.AspNetCore.Mvc;
using BlobStorageRepositoryDemo.Models;
using BlobStorageRepositoryDemo.Service;

namespace BlobStorageRepositoryDemo.Web.Controllers;

public class TodosController : Controller
{
    private readonly ITodoService _todoService;

    public TodosController(ITodoService todoService)
    {
        _todoService = todoService;
    }

    [HttpGet]
    public async Task<IActionResult> Index()
    {
        var todos = await _todoService.GetAllAsync();

        return View(todos);
    }

    [HttpGet]
    public async Task<IActionResult> Details([FromRoute] string id)
    {
        var todo = await _todoService.GetOneAsync(id);

        return View(todo);
    }

    [HttpGet]
    public async Task<IActionResult> Edit([FromRoute] string id)
    {
        var todo = await _todoService.GetOneAsync(id);

        return View(todo);
    }

    [HttpPost]
    public async Task<IActionResult> Edit([FromForm] Todo todoItem)
    {
        var results = await _todoService.UpsertAsync(todoItem);

        return RedirectToAction("Index");
    }

    [HttpGet]
    public IActionResult Create()
    {
        var newTodo = new Todo();

        return View(newTodo);
    }

    [HttpPost]
    public async Task<IActionResult> Create([FromForm] Todo todoItem)
    {
        var results = await _todoService.UpsertAsync(todoItem);

        return RedirectToAction("Index");
    }

    [HttpGet]
    public async Task<IActionResult> Delete([FromRoute] string id)
    {
        var todoItem = await _todoService.GetOneAsync(id);

        return View(todoItem);
    }

    [HttpPost]
    public async Task<IActionResult> ConfirmDelete(Todo todoItem)
    {
        await _todoService.DeletAsync(todoItem.Id);

        return RedirectToAction("Index");
    }

}
Take note of the ITodoService todoService that we injected in to the controller. Take note of how it was used for the different methods through out the controller.

To the Views -> Shared folder add a new folder called DisplayTemplates. To the new DisplayTempaltes folder add a new file called Todo.cshtml.

Give it the following code:

@model BlobStorageRepositoryDemo.Models.Todo

<dl>
    <dt>@Html.DisplayNameFor(m => m.Id)</dt>
    <dd>@Html.DisplayFor(m => m.Id)</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>
    <dt>@Html.DisplayFor(m => m.Completed)</dt>    
</dl>
To the Views -> Shared folder add a new folder called EditorTemplates. To the new EditorTempaltes folder add a new file called Todo.cshtml

Give it the following code:

@model BlobStorageRepositoryDemo.Models.Todo

<input type="hidden" asp-for="Id" />

<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 add a new folder called Todos. In the new Todos folder add a new file called Index.cshtml

Give it the following code:

@model IEnumerable<BlobStorageRepositoryDemo.Models.Todo>
@{
    ViewData["Title"] = "Todos";
}

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

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

@if(Model is not null) {
    <table class="table table-bordered">
        <thead>
            <tr>
                <th>Title</th>                
                <th>Completed</th>
                <th> </th>
            </tr>
        </thead>
        <tbody>
            @foreach(var todoItem in Model) {
                <tr>
                    <td>@todoItem.Title</td>
                    @if(todoItem.Completed) {
                        <td>True</td>
                    }
                    else {
                        <td>False</td>
                    }
                    <td>
                        <a asp-action="Details" asp-route-id="@todoItem.Id">Details</a> | 
                        <a asp-action="Edit" asp-route-id="@todoItem.Id">Edit</a> | 
                        <a asp-action="Delete" asp-route-id="@todoItem.Id">Delete</a>
                    </td>
                </tr>
            }
        </tbody>
    </table>
}
To the same folder create a Details.cshtml file and give it the following code:
@model BlobStorageRepositoryDemo.Models.Todo
@{
    ViewData["Title"] = $"Todo - {Model.Title}";
}

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

@Html.DisplayForModel()

<div>
    <a asp-action="Index">Back to List</a> | 
    <a asp-action="Edit" asp-route-id="@Model.Id">Edit</a> | 
    <a asp-action="Delete" asp-route-id="@Model.Id">Delete</a>
</div>
To the same folder create a Create.cshtml file.

Give it the following code:

@model BlobStorageRepositoryDemo.Models.Todo
@{
    ViewData["Title"] = "Create Todo";
}

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

<form asp-action="Create" method="post">
    @Html.EditorForModel()
</form>

<div>
    <a asp-action="Index">Back to List</a>
</div>
To the same folder create an Edit.cshtml file.

Give it the following code:

@model BlobStorageRepositoryDemo.Models.Todo
@{
    ViewData["Title"] = $"Edit Todo - {Model.Title}";
}

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

<form asp-action="Edit" method="post">
    @Html.EditorForModel()
</form>

<div>
    <a asp-action="Index">Back to List</a>
</div>
To the same folder create a Delete.cshtml file.

Give it the following code:

@model BlobStorageRepositoryDemo.Models.Todo
@{
    ViewData["Title"] = $"Delete Todo - {Model.Title}?";
}

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

@Html.DisplayForModel()

<form asp-action="ConfirmDelete" method="post">
    @Html.HiddenFor(m => m.Id)
    @Html.HiddenFor(m => m.Title)
    @Html.HiddenFor(m => m.Description)
    @Html.HiddenFor(m => m.Completed)
    <a asp-action="Index">Back to List</a>
    <input class="btn btn-danger" type="submit" value="Confirm" />
</form>
To the Views -> Shared -> _Layout.cshtml add a link to the Todos Controller and Index action by adding the following code after the Privacy link:
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-controller="Todos" asp-action="Index">Todos</a>
                        </li>
Your entire _Layout.cshtml file 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"] - BlobStorageRepositoryDemo.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="~/BlobStorageRepositoryDemo.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">BlobStorageRepositoryDemo.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>
                </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 - BlobStorageRepositoryDemo.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

In a terminal, ensure that you are in the /workspace/BlobStorageRepositoryDemo.Web directory and run.

dotnet dev-certs https --trust 
dotnet run
When your web browser launches, Click on the Todos link in the top. Practice creating, viewing, editing, and deleting Todos.
As you are adding, editing, and deleting Todos use your Azure Storage Explorer to view the files be created in the todos container and the TodoItems folder. Double click them to download them and you will see their JSON data.

Conclusion

There are many different ways to use Azure Storage, and to even use Azure Blob storage. This article covers the use case of using it as a repository for your MVC Application Data.

GitHub Repository: https://github.com/woodman231/azure-blob-storage-repository-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.