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

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

Azure Table Storage is Microsoft’s Cloud NoSQL / Document storage. 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-table-storage-repository-demo”.

    2 – Open the folder with Visual Studio Code.

We will start with the C# and SQL Server Development Container Files, but we will replace the SQL Server parts with Azurite parts.

    3 – Open the command pallet (CTRL+SHIFT+P) and select the “Remote-Containers: Add Development Container Configuration Files…”
    4 – Select the “C# (.NET) and MS SQL” option
    5 – Select “6.0-focal”
    6 – Select “lts/*” for the Nodejs version
    7 – Check the boxes for Azure CLI and GitHub CLI
When prompted to open the container, ignore it as there are some adjustments that we will need to make to the .devcontainer files.

By default, the devcontainer.json file should appear. Let’s first change the name property to: “C# (.NET) and Azurite.”

    1 – Change the “customizations:vscode:settings” key to be an empty object.

    2 – In the “customizations:vscode:extensions” key, replace the “ms-mssql.mssql” value with “ms-azuretools.vscode-azurefunctions”

    3 – Uncomment the forwardPorts key and replace it with the following values in the array: 10000, 10001, 10002. Azure storage technically has three services: Blob, Table, and Queue. This will open the ports for all 3.

    4 – Remove the “postCreateCommand” field and value.

Your devcontainer.json should look like this

// 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"
    }

}
    5 – Next open your docker-compose.yml file.

    6 – In “services:app”, change “network_mode: service:db” to be “network_mode: service:azurite”.

    7 – Change “db:” to “azurite:”. Change that image under the new azure key to be: “mcr.microsoft.com/azure-storage/azurite”.

    8 – Remove the “environment” key and replace with:
    command: “azurite”

Your docker-compose.yml should look like this:

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.)
    9 – Open the Dockerfile.

    10 – Remove this block of code

# Install SQL Tools: SQLPackage and sqlcmd
COPY mssql/installSQLtools.sh installSQLtools.sh
RUN bash ./installSQLtools.sh \
     && apt-get clean -y && rm -rf /var/lib/apt/lists/* /tmp/library-scripts
Your Dockerfile should look like this
# [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
    11 – Delete the .devcontainer/mssql folder.

    12 – 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-table-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 TableStorageRepositoryDemo.Web
    dotnet sln add ./TableStorageRepositoryDemo.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 TableStorageRepositoryDemo.Models
    dotnet new classlib -o TableStorageRepositoryDemo.Repository
    dotnet new classlib -o TableStorageRepositoryDemo.Service
    
    dotnet sln add ./TableStorageRepositoryDemo.Models
    dotnet sln add ./TableStorageRepositoryDemo.Repository
    dotnet sln add ./TableStorageRepositoryDemo.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 TableStorageRepositoryDemo.Repository
    dotnet add reference ../TableStorageRepositoryDemo.Models/
    cd ..
    cd TableStorageRepositoryDemo.Service
    dotnet add reference ../TableStorageRepositoryDemo.Models/
    dotnet add reference ../TableStorageRepositoryDemo.Repository/
    cd ..
    cd TableStorageRepositoryDemo.Web
    dotnet add reference ../TableStorageRepositoryDemo.Models/
    dotnet add reference ../TableStorageRepositoryDemo.Repository/
    dotnet add reference ../TableStorageRepositoryDemo.Service/
    cd ..
    
    Furthermore, the repository layer, and web layer will need access to the Azure.Data.Tables NuGet package.

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

    cd TableStorageRepositoryDemo.Repository
    dotnet add package Azure.Data.Tables
    cd ..
    cd TableStorageRepositoryDemo.Web
    dotnet add package Azure.Data.Tables
    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.

    Azure Table Storage is very different from SQL Server. That is why patterns like this are referred to as NoSQL databases. There are many different ways to implement this. In this particular demonstration we will use a “Azure Storage Table” more analogous to a “Database” a “Partition Key” to a “Table”, and a “Row Key” as the unique identifier for a row in the Table.

    To the “TableStorageRepositoryDemo.Models” class library, delete the Class1.cs file.

    Next, create a new file called: IBaseAzureStorageEntityModel.cs

    Give the file the following code:

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

    Give the file the following code:

    namespace TableStorageRepositoryDemo.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 TableStorageRepositoryDemo.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 our Entity models, let’s work on a way to store them.

    To the TableStorageRepositoryDemo.Repository project delete Class1.cs and add a new file called IAzureStorageRepository.cs

    Give the file the following code:

    using TableStorageRepositoryDemo.Models;
    
    namespace TableStorageRepositoryDemo.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);
    }
    
    Next, Create a new file called AzureTableStorageRepository.cs

    Our goal with this file will be to create an abstract class that we can re-use with other entity models.

    Give the file the following code:

    using Azure;
    using Azure.Data.Tables;
    using System.Reflection;
    using TableStorageRepositoryDemo.Models;
    
    namespace TableStorageRepositoryDemo.Repository;
    
    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 DeletAsync(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 really 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.

    Now that we have a reusable abstract class for a repository layer. Let’s go ahead and create some repositories to use this abstract class.

    Create a new file called ITodoAzureTableStorageRepository.cs

    Give it the following code:

    using TableStorageRepositoryDemo.Models;
    
    namespace TableStorageRepositoryDemo.Repository;
    
    public interface ITodoAzureTableStorageRepository : IAzureStorageRepository<Todo>
    {
    }
    
    Create a new file called TodoAzureTableStorageRepository.cs

    Give it the following code:

    using Azure.Data.Tables;
    using TableStorageRepositoryDemo.Models;
    
    namespace TableStorageRepositoryDemo.Repository;
    
    public class TodoAzureTableStorageRepository : AzureTableStorageRepository<Todo>, ITodoAzureTableStorageRepository
    {
        public TodoAzureTableStorageRepository(TableServiceClient tableServiceClient) : base(tableServiceClient, "TodosTable", "TodoItems")
        {        
        }
    }
    
    When we created the ITodoAzureTableStorageRepository interface we said that we want to have it look like the IAzureStorageRepsoitory, and specified that we will be using the TableStorageRepositoryDemo.Models.Todo class as the type parameter.

    When we created the TodoAzureTableStorageRepository class we said that we want to use the functionality from the AzureTableStorageRepository with the TableStorageRespositoryDemo.Models.Todo class as the type parameter, and that functionality should match the signature of ITodoAzureSTableStorageRepository. We created a constructor that will also need the TableServiceClient, which again will be registered during the web app’s startup. We then also give values to the base abstract class for the tableName and default partitionKey. This basically makes it so all of the code that we used in AzureTableStorageRepository was coded by hand again in a new class with new types, tables names, and row keys. 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 “TableStorageRepositoryDemo.Service” project delete Class1.cs

    Create a new file called IAzureStorageRepositoryClient.cs

    Give it the following code:

    using TableStorageRepositoryDemo.Models;
    
    namespace TableStorageRepositoryDemo.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 AzureTableStorageRepositoryClient.cs

    Give it the following code:

    using TableStorageRepositoryDemo.Models;
    using TableStorageRepositoryDemo.Repository;
    
    namespace TableStorageRepositoryDemo.Service;
    
    public abstract class AzureTableStorageRepositoryClient<T> : IAzureStorageRepositoryClient<T> where T : IBaseAzureStorageEntityModel
    {
        private readonly IAzureStorageRepository<T> _tableStorageRepository;
    
        public AzureTableStorageRepositoryClient(IAzureStorageRepository<T> tableStorageRepository)
        {
            _tableStorageRepository = tableStorageRepository;
        }
    
        public virtual Task DeletAsync(string id)
        {
            try {
                return _tableStorageRepository.DeletAsync(id);
            }
            catch {
                // Do nothing
            }
            
            return Task.CompletedTask;
        }
    
        public virtual async Task<List<T>?> GetAllAsync()
        {
            try {
                return await _tableStorageRepository.GetAllAsync();
            }
            catch {
                // Do nothing, return null
            }
    
            return null;
        }
    
        public virtual async Task<T?> GetOneAsync(string id)
        {
            try {
                return await _tableStorageRepository.GetOneAsync(id);
            }
            catch {
                // Do nothing, return null
            }
    
            return default(T);
        }
    
        public virtual async Task<T?> UpsertAsync(T entityDetails)
        {
            try {
                var results = await _tableStorageRepository.UpsertAsync(entityDetails);
    
                return results;
            }
            catch {
                // Do nothing, return null
            }
    
            return default(T);
        }
    }
    
    Now that we have an abstract class that interfaces with the repository, let’s create a concrete class for the Todos to interact with this service.

    Create a new file called ITodoService.cs

    Give it the following code:

    using TableStorageRepositoryDemo.Models;
    
    namespace TableStorageRepositoryDemo.Service;
    
    public interface ITodoService : IAzureStorageRepositoryClient<Todo>
    {
    }
    
    Create a new file called TodoService.cs

    Give it the following code:

    using TableStorageRepositoryDemo.Models;
    using TableStorageRepositoryDemo.Repository;
    
    namespace TableStorageRepositoryDemo.Service;
    
    public class TodoService : AzureTableStorageRepositoryClient<Todo>, ITodoService
    {
        public TodoService(ITodoAzureTableStorageRepository 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 TableClientService 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 TableClientService 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/TableStroageRepositoryDemo.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 TableStorageRepositoryDemo.Repository;
    using TableStorageRepositoryDemo.Service;
    
    Add the following code after var builder = WebApplication.CreateBuilder(args); and before builder.Services.AddControllersWithViews();
    // Register Azure Clients
    builder.Services.AddAzureClients(azureClientsBuilder => {
        azureClientsBuilder.AddTableServiceClient(builder.Configuration.GetConnectionString("AzureStorage"));
    
        azureClientsBuilder.UseCredential(new DefaultAzureCredential());
    });
    
    // Register Repositories
    builder.Services.AddTransient<ITodoAzureTableStorageRepository, TodoAzureTableStorageRepository>();
    
    // Register Services
    builder.Services.AddTransient<ITodoService, TodoService>();
    
    The entire Program.cs should look like this:
    using Azure.Identity;
    using Microsoft.Extensions.Azure;
    using TableStorageRepositoryDemo.Repository;
    using TableStorageRepositoryDemo.Service;
    
    var builder = WebApplication.CreateBuilder(args);
    
    // Register Azure Clients
    builder.Services.AddAzureClients(azureClientsBuilder => {
        azureClientsBuilder.AddTableServiceClient(builder.Configuration.GetConnectionString("AzureStorage"));
    
        azureClientsBuilder.UseCredential(new DefaultAzureCredential());
    });
    
    // Register Repositories
    builder.Services.AddTransient<ITodoAzureTableStorageRepository, TodoAzureTableStorageRepository>();
    
    // 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 TableStorageRepositoryDemo.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 TableStorageRepositoryDemo.Models;
    using TableStorageRepositoryDemo.Service;
    
    namespace TableStorageRepositoryDemo.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 TableStorageRepositoryDemo.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 TableStorageRepositoryDemo.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<TableStorageRepositoryDemo.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 TableStorageRepositoryDemo.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 TableStorageRepositoryDemo.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 TableStorageRepositoryDemo.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 TableStorageRepositoryDemo.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"] - TableStorageRepositoryDemo.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="~/TableStorageRepositoryDemo.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">TableStorageRepositoryDemo.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 - TableStorageRepositoryDemo.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/TableStorageRepositoryDemo.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.

    Conclusion

    There are many different ways to use Azure Storage, and to even use Azure Table 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-table-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.