Posted in:

I often make use of Azure Table Storage bindings when I'm demoing Azure Functions. Although Table Storage isn't particularly powerful as a database (it's essentially a key-value store with very limited querying capabilities), it's very cheap to run, and when you create an Azure Functions app, you also create a storage account, which means you've already got everything you need to get started.

However, a number of recent changes to Azure Functions and the extensions have broken my Table storage bindings sample code. In this post, I'll go through the changes that take you from an old version 2 Azure Functions app using Table storage bindings to the latest V4.

As a demo, I'll use my Azure Functions Todo Sample app which shows how to implement a simple CRUD API for a TODO app using a variety of backing stores including SQL, Cosmos DB, Blob Storage, In-Memory, and Table Storage. Most of the changes I'll be discussing in this post can be found in this commit.

Upgrading your function app

First, my Function App was targetting V2 of Azure Functions, but we can move to v4 by updating the csproj file by changing the TargetFramework to net6.0 and the AzureFunctionsVersion to v4.

<PropertyGroup>
  <TargetFramework>net6.0</TargetFramework>
  <AzureFunctionsVersion>v4</AzureFunctionsVersion>
</PropertyGroup>

You should also take the opportunity to update your package references to the latest version. In particular, be sure to update to the latest functions SDK:

<PackageReference Include="Microsoft.NET.Sdk.Functions" Version="4.1.0" />

Table storage binding extensions

Where things get a little bit painful is that the Table storage bindings have recently moved to a new location. Previously they are in the Microsoft.Azure.WebJobs.Extensions.Storage NuGet package, but now they have moved to the Microsoft.Azure.WebJobs.Extensions.Tables NuGet package.

There was an unfortunate period of time when the new Microsoft.Azure.WebJobs.Extensions.Storage had been released as v5.0.0 with no table binding support, but the Microsoft.Azure.WebJobs.Extensions.Tables was still in preview. This caused quite a bit of confusion for people, but the good news is that Microsoft.Azure.WebJobs.Extensions.Tables is now available as v1.0.0. You can learn more about the changes in the official documentation for the table storage binding

My application uses both blob storage and table storage bindings, so I referenced both packages:

<PackageReference Include="Microsoft.Azure.WebJobs.Extensions.Storage" Version="5.0.0" />
<PackageReference Include="Microsoft.Azure.WebJobs.Extensions.Tables" Version="1.0.0" />

Use the latest bindings

My v2 demo app was still using types from Microsoft.WindowsAzure.Storage and Microsoft.WindowsAzure.Storage.Table to bind to. To use the newer types, replace those namespaces with the namespaces from the latest SDKs:

using Azure;
using Azure.Data.Tables;

The binding is still called Table, so some bindings will continue to work with no changes. For example my function to fetch a TODO item by id could still use the same Table binding attribute and bind to an object representing my TODO entity.

[FunctionName("Table_GetTodoById")]
public static IActionResult GetTodoById(
    [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "tabletodo/{id}")] HttpRequest req,
    [Table(TableName, PartitionKey, "{id}", Connection = "AzureWebJobsStorage")] TodoTableEntity todo,
    ILogger log, string id)

But if you were binding to a CloudTable as I was, you need to change that to a TableClient. In the example below, I'm using the DeleteEntityAsync method on TableClient to delete a row. The methods on TableClient are easier to use than the older SDK, but they throw different exception types so we need to switch to RequestFailedException to detect not found errors.

[FunctionName("Table_DeleteTodo")]
public static async Task<IActionResult> DeleteTodo(
    [HttpTrigger(AuthorizationLevel.Anonymous, "delete", Route = "tabletodo/{id}")] HttpRequest req,
    [Table(TableName, Connection = "AzureWebJobsStorage")] TableClient todoTable,
    ILogger log, string id)
{
    try
    {
        await todoTable.DeleteEntityAsync(PartitionKey, id, ETag.All);
    }
    catch (RequestFailedException e) when (e.Status == 404)
    {
        return new NotFoundResult();
    }
    return new OkResult();
}

TableEntity

Another issue you might run into is that the entities you bind to should inherit from ITableEntity which has properties including PartitionKey, RowKey and ETag. This is the same as before but previously you could use a helpful base class called TableEntity from the old SDK. I just made my own BaseTableEntity to use instead:

public class BaseTableEntity : ITableEntity
{
    public string PartitionKey { get; set; }
    public string RowKey { get; set; }
    public DateTimeOffset? Timestamp { get; set; }
    public ETag ETag { get; set; }
}

So my TodoTableEntity now looks like this:

public class TodoTableEntity : BaseTableEntity
{
    public DateTime CreatedTime { get; set; }
    public string TaskDescription { get; set; }
    public bool IsCompleted { get; set; }
}

AsyncEnumerables

The latest Azure SDKs make use of IAsyncEnumerable<T> when they return multiple items. These are then batched into pages by returning AsyncPageable<T>, allowing you to retrieve a page of items at a time. IAsyncEnumerable is a really nice addition to the C# language (it arrived in C# 8), with the await foreach statement making it really easy to iterate through.

However, sometimes you want to use LINQ-like extension methods (e.g. to be able to do things like First or ToList) on an IAsyncEnumerable<T> and the System.Linq.Async NuGet package provides extension methods that enable you to do this.

For example, I wanted my TODO API to just return the first page of entities when you called the Get all TODOs endpoint, and that was as simple as using the FirstAsync entension method (note that I chose not to simply use ToListAsync as that could result in loading the entire contents of a large table into memory):

var page1 = await todoTable.QueryAsync<TodoTableEntity>().AsPages().FirstAsync();

return new OkObjectResult(page1.Values.Select(Mappings.ToTodo));

Gotcha: Azurite and auto-created tables

One nice feature of the table storage binding is that tables get auto-created if they don't exist. This is super convenient, but annoyingly it doesn't seem to work properly with Azurite (which is the new storage emulator). Visual Studio 2022 comes with a built-in version of Azurite, which automatically starts when you debug your function app.

This means that the out of the box dev experience for my demo app locally results in this error: "Azure.Data.Tables: The table specified does not exist.". The problem occurs when you bind to IAsyncCollector<T> which I do in my create TODO example. Other bindings do seem to create the table correctly. To work round this, either pre-create the table using the Azure Storage Explorer, or bind to a TableClient and explicitly call CreateIfNotExistsAsync.

Can I use this with isolated functions?

Over the next few years there is a plan to move all C# Azure Functions to the "Isolated process" model. This will completely change how you bind to Table storage.

You can already build isolated process C# functions today with .NET 6. However, the binding capabilities are not nearly as powerful. You have to use the TableInput and TableOutput bindings which are far more limited, and won't let you bind to a TableClient.

You can find a simple example of binding to table storage with isolated process C# functions here.

I am hopeful that the situation will be greatly improved by the time .NET 7 is released (although that is not too far away now). Isolated process functions also can't currently support Durable Functions, which is another reason why I have not moved over to using them yet.

If you really do want to use Table storage in isolated functions at the moment, probably the most straightforward approach is to completely ignore the binding support and just use the Azure Tables SDK directly. I tried this out with my TODO app, recreating the table storage bindings in an isolated function app and this approach worked just fine - I fetched the connection string from an environment variable. Here's a short snippet showing an example of how I used this approach in the create TODO endpoint:

public class TodoApiTableStorage
{
    private const string Route = "tabletodo";
    private const string TableName = "todos";
    private const string PartitionKey = "TODO";

    private readonly ILogger logger;
    private static TableClient? tableClient; 
    public TodoApiTableStorage(ILoggerFactory loggerFactory)
    {
        logger = loggerFactory.CreateLogger<TodoApiTableStorage>();
    }

    private TableClient GetTableClient()
    {
        if (tableClient == null)
        {
            var connectionString = Environment.GetEnvironmentVariable("AzureWebJobsStorage");
            logger.LogInformation("creating table client");
            tableClient = new TableClient(connectionString, TableName);
        }
        return tableClient;
    }

    [Function("Table_CreateTodo")]
    public async Task<HttpResponseData> CreateTodo(
        [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = Route)] HttpRequestData req)
    {
        var client = GetTableClient();
        logger.LogInformation("Creating a new todo list item");
        string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
        var input = JsonConvert.DeserializeObject<TodoCreateModel>(requestBody);

        var todo = new Todo() { TaskDescription = input.TaskDescription };
        await client.AddEntityAsync(todo.ToTableEntity());
        var resp = req.CreateResponse(HttpStatusCode.OK);
        await resp.WriteAsJsonAsync(todo);
        return resp;
    }

    // ...

Summary

The table storage bindings in Azure Functions have changed quite a bit recently and that can result in previously working code no longer doing what's expected. But hopefully once you know what the new NuGet packages are and how to use the new TableClient you should be able to get everything working as before. Feel free to try out the sample code associated with this article here on GitHub.

Want to learn more about how easy it is to get up and running with Azure Functions? Be sure to check out my Pluralsight courses Azure Functions Fundamentals and Microsoft Azure Developer: Create Serverless Functions