Azure Functions: Working with Azure Table Stage to Perform CRUD Operations

In this article, we will see how to use Azure Function with HTTP Trigger that will interact with Azure Table Storage to perform CRUD Operations. The reason behind creating this post is to let developers understand the use of Azure Functions to perform HTTP Operations and also understand Azure Table Storage for storing data.    

What is Azure Table Storage?

Azure Table Storage is a service that is used to store non-relational and schemaless data in the form of key/attribute in the Azure cloud. Since Azure Table Storage is schemaless, we can easily adapt it to store data as per our application needs. The advantage of Azure Table Storage is the data read/write operations are more cost-effective than Azure SQL. 

The advantage offered by Azure Table Storage is that we can store data tba TBs. Generally, large datasets without any complex join with and easy query requirements are recommended to store in Azure Table Storage.

To access Azure Table Storage, we use Azure.Data.Tables package. This is the Azure Table client library for .NET. This library provides a TableServiceClient class. This class provides methods to perform Read/Write operations on Azure Tables.

Creating Azure Table Storage

To create Azure Table Storage, an Azure subscription is needed. Click on this link to get the Azure Subscription. After creating a subscription, visit the portal using https://portal.azure.com and click on Resource groups as shown in Figure 1. A Resource Group is a logical repository (like a folder) where all the resources e.g. Storage Account, Azure SQL, Virtual Machine, etc. are kept 



Figure 1: Creating Resource Group

On clicking on the Resource groups icon, the page will be shown where the list of all resource groups will be displayed. This page contains the Create link to create a resource group. Once this link is clicked, the page as shown in Figure 2 will be displayed where we can enter details as shown in Figure 2 to create a resource group.





Figure 2: Resource Group Details

To create a resource group, the Subscription name must be selected, then provide the resource group name and select the region. Once this information is entered, click on the Review + create button to create the resource group.

Once the resource group is created, click on the resource group name. This will navigate to the resource group details page where the Create link is provided to create a new resource as shown in Figure 3



Figure 3: Resource Group details

Once the Create link is clicked, the Marketplace page will be displayed. On this page search for the Azure storage account as shown in Figure 4



Figure 4: Creating Azure Storage Account       

Azure Storage Account, is an Azure Service that provides economical cloud storage using Tables, BLOBs, Queues, Files, etc. Click on Create as shown in Figure 4, to navigate to the Create a storage account page. On this page, enter details like Subscription, Resource Group name, Storage account name, Region, Performance, etc. as shown in Figure 5



Figure 5: Creating Storage Account

Once this basic information is entered, you can click on the Advanced button, this will show a new tab where security information is provided. Keep the default selection on this page, likewise, tabs for Networking Data Protection, Encryption, and Tags, will be shown, keep defaults for each tab and then click on the Review button. Azure will verify all selections and once they are validated, the Create button will appear. Click on the Create button to create a Storage account.  Once the Storage Account is created, the details page can be seen as shown in Figure 6



Figure 6: The Storage Account Details

This page shows Data storage options like Containers (Blobs), FIle shares, Queues, and Tables. The Security + networking shows Access keys. We need Access keys to access storage from the client applications. We also have a Storage browser that will show all storage provided in the Storage Account as shown in Figure 7



Figure 7: The Storage Explorer

As shown in Figure 6, we need access keys to access the storage account and storage services from the client application, click on the Access Keys  and copy the connection string and keep it in Notepad as shown in Figure 8



Figure 8: The Storage Connection String      


Once the Storage Account is created its tie for use to create the client application using Azure Function.          

Azure Functions

Azure Functions is a serverless solution provided on Azure Cloud this allows writing less code, with less maintenance infrastructure, which saves costs. Instead of worrying about deploying and maintaining servers, the cloud infrastructure provides all the up-to-date resources needed to keep your applications running.

Step 1: Open Visual Studio 2022 and create a new Azure Functions project. Name this project AzFuncTableOperation. While creating the Project make sure that the HttpTrigger is selected so that the function can be executed with HTTP Requests. Figure 9 shows the HttpTrigger selection.  In this project add Azure.Data.Tables NuGet package reference.   






Figure 9: HTTP trigger

The project will be created with Function1.cs. Rename the Function1.cs to TableOperationsFn.cs. 

Step 2: Since we will be using the Azure Table Storage, we need to create a class implementing ITableEntity interface. This interface PartitionKey, RowKey, etc properties for storing entities in table storage. In the project add a new folder named Models in this folder add a new class file and name it ProductEntity.cs. In this file, we will add ProductEntity class as shown in Listing 1


using Azure;
using Azure.Data.Tables;

namespace AzFuncTableOperation.Models
{
    public class ProductEntity : ITableEntity
    {
        public string? PartitionKey { get ; set; }
        public string? RowKey { get; set; }
        public DateTimeOffset? Timestamp { get; set; }
        public ETag ETag { get; set; }
        public int ProductId { get; set; }
        public string? ProductName { get; set; }
        public string? Manufacturer { get; set; }
        public int Price { get; set; }
    }
}

Listing 1: The ProductEntity class

We will be creating Azure Function with HTTP Trigger for handling Http Requests and sending HTTP responses. To send a standard response to the request we will add a new class in the Models folder named ResponseObject by adding a new class file in the Models folder as shown in Listing 2. This will be a generic class.

namespace AzFuncTableOperation.Models
{
    public class ResponseObject<T>
    {
        public IEnumerable<T>? Records { get; set; }
        public T? Record { get; set; }
        public int StatusCode { get; set; }
        public string StatusMessage { get; set; } = string.Empty;
    }
}


Listing 2: The ResponseObject class

In this class, the T will be TableEntity that will be responded to the HTTP request received by the Azure Function.    

Step 3: In the project, add a new folder named Services. In this folder add a new interface file named ITableOperations.cs. In this file, we will add an interface named ITableOperations. In the interface, we will declare generic methods for performing CRUD operations on the Table Entity. Listing  3 shows the code for the interface.


using AzFuncTableOperation.Models;

namespace AzFuncTableOperation.Services
{
    public interface ITableOperations
    {
        Task<ResponseObject<ProductEntity>> GetAsync();
        Task<ResponseObject<ProductEntity>> GetAsync(string partitionKey, string rowKey);
        Task<ResponseObject<ProductEntity>> CreateAsync(ProductEntity entity);
        Task<ResponseObject<ProductEntity>> UpdateAsync(ProductEntity entity);
        Task<ResponseObject<ProductEntity>> DeleteAsync(string partitionKey, string rowKey);
    }
}

Listing 3: ITableOperations interface

In the Services folder, add a new class file named TableOperations.cs. In this class file, we will add a class named TableOperations. This class will implement the ITableOperations interface. The class will use TableServiceClient class to perform operations on Azure Table. This class provides synchronous and asynchronous methods to perform table-level operations with Azure Tables hosted in either Azure storage accounts. This class has a method named GetTableClient() to retrieve the Table reference on which we want to perform CRUD Operations. This method returns an instance of TableClient class. The TableClient class provides various methods to perform Read/Write operations on Table. These methods are as follows

  • Query(): To read all entities from Table
  • GetEntity(): To read an entity from a table based on PartitionKey and RowKey
  • AddEntityAsync(): To create new entity in table
  • UpdateEntityAsync(): To update an existing entity based on PartitionKey and Etag
  • DeleteEntityAsync(): To delete an entity based on PartitionKey and RowKey
Listing 4 shows code for performing operations on the Azure Table


using AzFuncTableOperation.Models;
using Azure.Data.Tables;
using Azure.Data.Tables.Models;

namespace AzFuncTableOperation.Services
{
    public class TableOperations : ITableOperations
    {
        TableServiceClient serviceClient;
        string tableName = "Products";
        TableItem table;
        TableClient tableClient;

        ResponseObject<ProductEntity> response = new ResponseObject<ProductEntity>();
        
        public TableOperations()
        {
            serviceClient = new TableServiceClient("[CONNECTION-STRING-COPIED-FROM-PORTAL]");

            #region Create Table If Not Exist and get the table client
              table = serviceClient.CreateTableIfNotExists(tableName);
            tableClient = serviceClient.GetTableClient(tableName);
            #endregion
        }

        async Task<ResponseObject<ProductEntity>> ITableOperations.CreateAsync(ProductEntity entity)
        {
            try
            {
                var result = await tableClient.AddEntityAsync<ProductEntity>(entity);
                response.Record = entity;
               
                response.StatusMessage = "Entiry is created successfully";
            }
            catch (Exception ex)
            {
                response.StatusMessage = ex.Message;
            }
            return response;
        }

        async Task<ResponseObject<ProductEntity>> ITableOperations.DeleteAsync(string partitionKey, string rowKey)
        {
            try
            {
                var result = await tableClient.DeleteEntityAsync(partitionKey, rowKey);
                response.StatusMessage = "Entiry is deleted successfully";
            }
            catch (Exception ex)
            {
                response.StatusMessage = ex.Message;
            }
            return response;
        }

        Task<ResponseObject<ProductEntity>> ITableOperations.GetAsync()
        {
            try
            {
                
                var queryResult = tableClient.Query<ProductEntity>();
                response.Records = queryResult;
                response.StatusMessage = "Products Data is Read Successfully";
                
            }
            catch (Exception ex)
            {
                response.StatusMessage = ex.Message;
            }
            return Task.FromResult(response);
        }

        Task<ResponseObject<ProductEntity>> ITableOperations.GetAsync(string partitionKey, string rowKey)
        {
            try
            {
                var queryResults = tableClient.GetEntity<ProductEntity>(partitionKey,rowKey);
                response.Record = queryResults.Value;
                response.StatusMessage = "Product Data is Read Successfully";
            }
            catch (Exception ex)
            {
                response.StatusMessage = ex.Message;
            }
            return Task.FromResult(response);
        }

       async Task<ResponseObject<ProductEntity>> ITableOperations.UpdateAsync(ProductEntity entity)
        {
            try
            {
                entity.PartitionKey = entity.Manufacturer;
                entity.RowKey = entity.ProductId.ToString();
                // get record for update based on PartitionKey and RowKey
                var queryResults = tableClient.GetEntity<ProductEntity>(entity.PartitionKey, entity.RowKey);
                var result = await tableClient.UpdateEntityAsync(entity, queryResults.Value.ETag);
               
                response.StatusMessage = "Product Entity is updated successfully";
            }
            catch (Exception ex)
            {
                response.StatusMessage = ex.Message;
            }
            return response;
        }
    }
}


Listing 4: Operations on Azure Table Storage

So we have added logic for performing operations on the table. Make sure that the connection string copied from the portal for Azure Storage Account is passed to the TableServiceClient constructor so that the Azure Function will be able to connect to Storage Account to perform operations on the table. The table will be created if it does not already exist.  

Step 4: As shown in Figure 9, we have selected .NET 6.0 Isolated target framework for Azure Function. This provides the built-in Dependency Injection Container (DI Container). We can use this container to register our service classes in it. Open the Program.cs and modify it by registering the TableOperations class in DI container as shown in Listing 5


using AzFuncTableOperation.Services;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

var host = new HostBuilder()
    .ConfigureFunctionsWorkerDefaults()
    .ConfigureServices(s=>s.AddScoped<ITableOperations, TableOperations>())
    .Build();

host.Run();

Listing 5: Registering TableOperations class in DI Container    

Step 5: In the TableOperationsFn.cs file, add the code as shown in Listing 6.


using AzFuncTableOperation.Models;
using AzFuncTableOperation.Services;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using System.Net;
using System.Text.Json;

namespace AzFuncTableOperation
{
    public class TableOperationsFn
    {
        ITableOperations tableOperations;
        HttpResponseData response;
        public TableOperationsFn(ITableOperations operations )
        {
            tableOperations = operations;
        }

        

        [Function("Get")]
        public async Task<HttpResponseData> Get([HttpTrigger(AuthorizationLevel.Function, 
                "get", Route = "Products")] HttpRequestData req)
        {

            try
            {

                  response = req.CreateResponse(HttpStatusCode.OK);
                //  response.Headers.Add("Content-Type", "text/plain; charset=utf-8");
                var responseData = await tableOperations.GetAsync();
                await response.WriteAsJsonAsync<ResponseObject<ProductEntity>>(responseData);

                return response;
            }
            catch (Exception ex)
            {
                await response.WriteAsJsonAsync(ex.Message);
            }
            return response;
        }

        [Function("Post")]
        public async Task<HttpResponseData> Post([HttpTrigger(AuthorizationLevel.Function, 
                "post", Route ="Products")] HttpRequestData req)
        {

            try
            {
                response = req.CreateResponse(HttpStatusCode.OK);
                ProductEntity? product = JsonSerializer.Deserialize<ProductEntity>
                        ( new StreamReader(req.Body).ReadToEnd());
                product.PartitionKey = product.Manufacturer;
                product.RowKey = product.ProductId.ToString();
                

                var responseData = await tableOperations.CreateAsync(product);
                await response.WriteAsJsonAsync<ResponseObject<ProductEntity>>(responseData);

                return response;
            }
            catch (Exception ex)
            {
                await response.WriteAsJsonAsync(ex.Message);
            }
            return response;
        }


        [Function("GetSingle")]
        public async Task<HttpResponseData> GetSingle([HttpTrigger(AuthorizationLevel.Function,
                 "get", Route = "Products/{partitionkey}/{rowkey}")]
                     HttpRequestData req, string? partitionkey, string? rowkey)
        {

            try
            {
                response = req.CreateResponse(HttpStatusCode.OK);
                
                var responseData = await tableOperations.GetAsync(partitionkey,rowkey);
                await response.WriteAsJsonAsync<ResponseObject<ProductEntity>>(responseData);

                return response;
            }
            catch (Exception ex)
            {
                await response.WriteAsJsonAsync(ex.Message);
            }
            return response;
        }

        [Function("Put")]
        public async Task<HttpResponseData> Put([HttpTrigger(AuthorizationLevel.Function, 
                "put", Route = "Products")] HttpRequestData req)
        {

            try
            {
                response = req.CreateResponse(HttpStatusCode.OK);
                ProductEntity? product = JsonSerializer.Deserialize<ProductEntity>
                    (new StreamReader(req.Body).ReadToEnd());

                var responseData = await tableOperations.UpdateAsync(product);
                await response.WriteAsJsonAsync<ResponseObject<ProductEntity>>(responseData);

                return response;
            }
            catch (Exception ex)
            {
                await response.WriteAsJsonAsync(ex.Message);
            }
            return response;
        }

        [Function("Delete")]
        public async Task<HttpResponseData> Delete([HttpTrigger(AuthorizationLevel.Function, "delete", Route = "Products/{partitionkey}/{rowkey}")] HttpRequestData req, string? partitionkey, string? rowkey)
        {

            try
            {
                response = req.CreateResponse(HttpStatusCode.OK);

                var responseData = await tableOperations.DeleteAsync(partitionkey, rowkey);
                await response.WriteAsJsonAsync<ResponseObject<ProductEntity>>(responseData);

                return response;
            }
            catch (Exception ex)
            {
                await response.WriteAsJsonAsync(ex.Message);
            }
            return response;
        }
    }
}


Listing 6: The Function Code

Carefully reading the code in Listing 6, we can see that the Function class is constructor injected using the ITableOperations interface. Hence the TableOperations instance is available to the Function class so that operations can be performed on the Table Entity. In the function class, we have Function methods for Get, Post, Put, and Delete operations that are accessing respective methods from the TableOperations class. 

To Test the function run the project using  F5 key. The function will be locally hosted as shown in Figure 10



Figure 10: The Function locally hosted  

Visit back to the folder and from the Azure Storage Account, click on Storage browser, you can now see the Products table is created as shown in Figure 11



Figure 11: The Table is created 

Open Postman or Advanced REST Client (ARC) tool to test the HTTP calls to the function. Test the Post request as shown in Figure 12



Figure 12: The  HTTP Post request      

Make the request the new Product Entity will be created in the Azure table. We can see this newly created entry as shown in Figure 13



Figure 13: The Entry created in Table

That's it, we can test other requests e.g  the GET request will show the result as shown in Figure 14



Figure 14:  The GET Request

We have set the RowKey to the ProductId which is unique for each Entity in the Tale and the PartitionKey we set to the Manufacturer. The PartitionKey is used for the logically defined arrangement of the entities in the Azure Table so that it can be queried, updated, and deleted easily. The code for this article can be downloaded from this link.


 

Popular posts from this blog

Uploading Excel File to ASP.NET Core 6 application to save data from Excel to SQL Server Database

ASP.NET Core 6: Downloading Files from the Server

ASP.NET Core 6: Using Entity Framework Core with Oracle Database with Code-First Approach