In this article, we are going to learn Azure Table Storage in depth.

We’ll start by discussing the basic Azure Table Storage concepts and then see how to create an Azure Storage Account from the portal. After that, we’ll learn how to create an ASP.NET Core Web API for performing CRUD operations on NoSQL data with the Table Storage in the back-end. 

To download the source code for this article, you can visit our Azure Table Storage with ASP.NET Core repository.

We have organized the article into the following sections:

Azure Table Storage – Intro

Azure Table storage is a cloud-based NoSQL datastore. We can use it to store large amounts of structured, non-relational data. Azure Table offers a schemaless design, which provides us a convenient way to store a collection of entities. An entity can contain a set of properties, and each property can contain a name-value pair. Azure Table is a lightweight, easy to manage service which makes it a good choice for people who are just getting started with NoSQL data and want to leverage cloud data services.

While integrating Azure Table Storage into our applications, we can either use the Azure Tables Client Library or the Azure Cosmos DB Table API Client Library. Both of these can seamlessly work with either Azure Table Storage or Azure Cosmos DB Table Service endpoints without any code changes.    

Azure Table Storage vs Azure Cosmos DB Table API

Both the Azure Table Storage and Azure Cosmos DB Table API share the same table data model and even provide the same CRUD operations through their SDKs. That means we can use either service for achieving the same functionality, but the major difference is performance and scalability. 

Table Storage is a lightweight and much cheaper option which makes it a good choice when we’re just starting with small storage requirements. On the other hand, the Azure Cosmos DB Table API is a premium service that provides two-level indexes, global distribution, throughput-optimized tables, etc, and of course much more expensive. 

Since these services use the same data model and client libraries, it is possible to migrate from Table Storage to Cosmos DB Table Service with minimal changes, which offers greater flexibility for developers and architects.

Structure of an Azure Table Storage

An Azure Table Storage is organized into Storage Accounts, Tables, Entities, and Properties:

Accounts: All access to Azure Storage is done through a storage account. The storage account’s name acts as a namespace for accessing all storage types like Blobs, Queues, Tables, etc.

Table: A Storage account can have multiple tables. A Table consists of a collection of entities, but it doesn’t enforce a schema on the entities, which means a single table can contain entities that have different sets of properties. This gives us a flexible way of storing schemaless data.

Entity: An entity is a set of properties and can be compared to a row in a database. There is a size limit of 1 MB for an entity in Table Storage.

Properties: A property is a name-value pair and each entity can have up to 252 properties. There is a size limit of 64 KB for a property. 

System Properties

Apart from the user-defined properties, an entity will always have three system properties – PartitionKey, RowKey, and Timestamp. While we are responsible for managing the values of PartitionKey and RowKey, the storage manages the value of Timestamp and there is no option to modify it.

Within a table, entities are organized by partitions. A partition is a consecutive range of entities having the same PartitionKey value. The PartitionKey denotes the first part of an entity’s primary key and every operation on the entity needs to provide this value.

The RowKey property denotes the second part of Table’s primary key. This helps to uniquely identify an entity within a partition. So we need to provide the RowKey property as well with the PartitionKey for performing any operation on an entity.

A combination of PartitionKey and RowKey uniquely identifies an entity within a table. 

We can represent the structure of a Tables Storage this way:

azure table storage structure

Knowing the Storage Account name and table name, we can access the Azure Table Storage by the URL format: http://<storage account>.table.core.windows.net/<table>

Creating an Azure Table Storage

We have already explained the process of creating an Azure Storage Account in the Creating Azure Storage section of the Upload Files to Azure with .NET Core Web API and Blazor WebAssembly article. Following similar steps, we can create and configure a storage account:

azure table storage menu

For accessing the Tables section, we can use the Tables menu:

azure table storage tables seection

Initially, the storage account will not have any tables in it, but we are going to create an API that will create a table and insert entities into it. Make sure to note the connection string as mentioned in the linked article as we are going to need that later for connecting to the storage account from the app.

Building an API that Connects with Azure Table Storage

Now let’s build an ASP.NET Core Web API that connects with Azure Table Storage and performs read and write operations. We are going to build an API to work with grocery store items. For that, we can create an ASP.NET Core Web API project. 

As discussed in the previous section, there are two client libraries for working with Azure Tables. In this example, we are going to use the Cosmos DB Table Client Library which is richer and more flexible.

So let’s add the Microsoft.Azure.Cosmos.Table NuGet package to our project: 

microsoft.azure.cosmos.table nuget package

This library supports working with both Azure Table Storage and Azure Cosmos DB Table API. Cosmos DB provides several advanced features over Azure Table Storage but shares the same data model and operations. 

Next, let’s make sure to configure the connection string in the appsettings file:    

{
...
  },
  "StorageConnectionString": "DefaultEndpointsProtocol=https;AccountName=codemazestorage;AccountKey=UjBmo....;EndpointSuffix=core.windows.net",
  "AllowedHosts": "*"
}

Cool!

Defining the Entity

Now we need to create an Entity for storing data in the Azure Table Storage. Let’s define an entity for representing a grocery item. Remember that If we need to store an entity in Azure Table Storage, we need to inherit that from the TableEntity base class:

using Microsoft.Azure.Cosmos.Table;

namespace GroceryStoreAPI.Models
{
    public class GroceryItemEntity : TableEntity
    {
        public string Id { get; set; }
        public string Name { get; set; }
        public string Category { get; set; }
        public int Price { get; set; }
    }
}

As discussed earlier, while working with Azure Tables, we need to specify a partition key and a row key for our entity. For this example, let’s choose Category as the partition key and Id as the row key. Always choose these values in such a way that the partition key can partition our data into groups and the row key can identify an entity uniquely within a partition.

Building the Service

Now it’s time to build the storage service.

For that, first let’s create an ITableStorageService interface: 

using GroceryStoreAPI.Models;
using System.Threading.Tasks;

namespace GroceryStoreAPI.Services
{
    public interface ITableStorageService
    {
        Task<GroceryItemEntity> RetrieveAsync(string category, string id);
        Task<GroceryItemEntity> InsertOrMergeAsync(GroceryItemEntity entity);
        Task<GroceryItemEntity> DeleteAsync(GroceryItemEntity entity);
    }
}

After that let’s create a TableStorageService class implementing the interface: 

using GroceryStoreAPI.Models;
using Microsoft.Azure.Cosmos.Table;
using Microsoft.Extensions.Configuration;
using System.Threading.Tasks;

namespace GroceryStoreAPI.Services
{
    public class TableStorageService : ITableStorageService
    {
        private const string TableName = "Item";
        private readonly IConfiguration _configuration;

        public TableStorageService(IConfiguration configuration)
        {
            _configuration = configuration;
        }
                
        public async Task<GroceryItemEntity> RetrieveAsync(string category, string id)
        {
            var retrieveOperation = TableOperation.Retrieve<GroceryItemEntity>(category, id);
            return await ExecuteTableOperation(retrieveOperation) as GroceryItemEntity;
        }

        public async Task<GroceryItemEntity> InsertOrMergeAsync(GroceryItemEntity entity)
        {
            var insertOrMergeOperation = TableOperation.InsertOrMerge(entity);
            return await ExecuteTableOperation(insertOrMergeOperation) as GroceryItemEntity;
        }

        public async Task<GroceryItemEntity> DeleteAsync(GroceryItemEntity entity)
        {
            var deleteOperation = TableOperation.Delete(entity);
            return await ExecuteTableOperation(deleteOperation) as GroceryItemEntity;
        } 

        private async Task<object> ExecuteTableOperation(TableOperation tableOperation)
        {
            var table = await GetCloudTable();
            var tableResult = await table.ExecuteAsync(tableOperation);
            return tableResult.Result;
        }

        private async Task<CloudTable> GetCloudTable()
        {
            var storageAccount = CloudStorageAccount.Parse(_configuration["StorageConnectionString"]);
            var tableClient = storageAccount.CreateCloudTableClient(new TableClientConfiguration());
            var table = tableClient.GetTableReference(TableName);
            await table.CreateIfNotExistsAsync();
            return table;
        }
    }
}

The GetCloudTable() method contains the code to connect to Azure Storage and access the Tables inside it. It will create a Table if it does not exist and return the reference to it.  

For performing any operation on Azure Tables, first, we need to define a TableOperation and then execute that on the CloudTable object. Every table operation will return a TableResult object which will have the execution details like the status, the result object, etc. The ExecuteTableOperation() method handles all these.

In the RetrieveAsync() method, we retrieve an entity by passing the category and Id which are the partition key and row key respectively. 

Similarly, the InsertOrMergeAsync() method will perform the InsertOrMerge operation. This is a special operation that inserts the given entity into a table if it does not exist and merges the contents in case the entity already exists.

Finally, we have the DeleteAsync() method that will perform a delete operation on the entity. We have to specify the partition key and row key for deleting an entity as well.

With that, our service is ready. Next, let’s create an API Controller and consume this service.

Creating the Controller

Let’s define an ItemsController with GET, POST, PUT and DELETE operations:

using Microsoft.AspNetCore.Mvc;
using System;
using System.Threading.Tasks;
using GroceryStoreAPI.Models;
using GroceryStoreAPI.Services;

namespace GroceryStoreAPI.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class ItemsController : ControllerBase
    {
        private readonly ITableStorageService _storageService;

        public ItemsController(ITableStorageService storageService)
        {
            _storageService = storageService ?? throw new ArgumentNullException(nameof(storageService));
        }

        [HttpGet]
        [ActionName(nameof(GetAsync))]
        public async Task<IActionResult> GetAsync([FromQuery] string category, string id)
        {
            return Ok(await _storageService.RetrieveAsync(category, id));
        }

        [HttpPost]
        public async Task<IActionResult> PostAsync([FromBody] GroceryItemEntity entity)
        {
            entity.PartitionKey = entity.Category;

            string Id = Guid.NewGuid().ToString();
            entity.Id = Id;
            entity.RowKey = Id;

            var createdEntity = await _storageService.InsertOrMergeAsync(entity);

            return CreatedAtAction(nameof(GetAsync), createdEntity);
        }

        [HttpPut]
        public async Task<IActionResult> PutAsync([FromBody] GroceryItemEntity entity)
        {
            entity.PartitionKey = entity.Category;
            entity.RowKey = entity.Id;

            await _storageService.InsertOrMergeAsync(entity);
            return NoContent();
        }

        [HttpDelete]
        public async Task<IActionResult> DeleteAsync([FromQuery] string category, string id)
        {
            var entity = await _storageService.RetrieveAsync(category, id);
            await _storageService.DeleteAsync(entity);
            return NoContent();
        }
    }
}

First, we inject the TableStorageService service into the controller.

The GetAsync() endpoint is pretty straightforward and just calls the RetrieveAsync() method of the service by passing the category and Id.

Both the PostAsync() and PutAsync() endpoints first set the PartitionKey and RowKey property on the Entity and then call the InsertOrMergeAsync() method.

The DeleteAsync() endpoint first retrieves the entity using the partition key and row key and then deletes it from the Table.

Finally, let’s configure the dependency injection for the service in the Startup class:

public void ConfigureServices(IServiceCollection services)
{
...
    services.AddScoped<ITableStorageService, TableStorageService>();
}

With this, the Web API project is ready. The next step is deploying it to an Azure API App. 

Deploying the API to Azure

For the ease of testing the API endpoints, let’s modify the Configure() method of the Startup class to enable swagger for the hosted version:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseSwagger();
    app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "GroceryStoreAPI v1"));
    app.UseHttpsRedirection();

...
}

We have explained the process of publishing an ASP.NET Core Web API into Azure in the Deploying ASP.NET Core Web API to Azure API Apps article. By following the steps mentioned in that article, we can create an Azure API app and deploy our API into it:

api deployed

Excellent!

Our API app is now ready in Azure:

swagger ui

Now let’s proceed to test the endpoints.

Testing the API

We’ll start by inserting a new grocery item using the POST endpoint: 

POST request

Cool!

The operation is successful and we have inserted a new grocery item into the Azure Table Storage.

Next, let’s try to fetch the entity using the GET endpoint. Remember that we need to provide the Partition Key and Row Key for that, which is the category and Id properties respectively for this example:

GET request

This will return the entity from Azure Table Storage.

Excellent!

Now, let’s try to update the price property of the entity using the PUT endpoint:

PUT request

Once the update operation is completed, let’s fetch the entity once again using the GET endpoint to verify the changes:

GET after Update

Great!

We can see that the price is updated.

Finally, let’s delete the entity using the DELETE endpoint:

DELETE request

We can verify that the entity is deleted by performing the GET operation once again:

GET after Delete

This time we receive a 204 No Content response with an empty body which indicates that the entity is not found in the Azure Table Storage.

Conclusion

We can represent the architecture of this example with a simple diagram:

architecture

Users can access the Azure API App and work with NoSQL data. The API App connects with the Azure Table Storage in the back-end for storing the data.

We have discussed the following topics in this article:

  • An introduction to Azure Table Storage, its structure, and how it compares with the Azure Cosmos DB Table API
  • Creating an Azure Table Storage from Azure Portal.
  • Building an ASP.NET Core Web API to connect with Azure Table Storage and work with NoSQL data

Until the next article,

All the best.