Update Your Azure Functions Table Storage Bindings
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.