Extensions for Azure.Data.Tables with typed CRUD clients. Supports complex nested entities, arrays, and IEnumerable via flattened table properties.
The main focus is usage in Azure Functions. Table access uses a Storage Account connection string. SAS and other authentication methods are not supported yet, but can be added when required.
Package: WebGate.Azure.TableUtils
License: Apache-2.0
dotnet add package WebGate.Azure.TableUtils| Target Framework | net10.0 |
| Package version | 10.x.x |
The NuGet package major version matches the .NET target framework major version.
net10.0→ package version10.x.x- A future uplift to
net11.0would start at package version11.0.0
Within a major line, use minor/patch for library changes that stay on the same TFM.
POCOs are mapped to Azure Table properties by reflection:
- Readable/writable properties are included.
- Nested objects are flattened with
_as separator (Parent.Child→ columnParent_Child). nullproperty values are skipped on serialize.- Value types,
string, andbyte[]are stored directly. - Dedicated converters handle enums, TimeSpan, decimal (InvariantCulture string), arrays, and IEnumerable (JSON via Newtonsoft.Json).
ObjectSerializer (POCO → properties) and ObjectBuilder (TableEntity → POCO) implement this mapping. Clients use them automatically.
Registers and resolves TypedAzureTableClient<T> and MultiEntityAzureTableClient instances.
var connectionString = "MY_STRING";
var extendedTableService = new ExtendedAzureTableClientService(connectionString);Initialize the service in Startup / Program.cs of an Azure Function (or host).
One table per POCO type:
var simplePocoAzureTableClient = extendedTableService.CreateAndRegisterTableClient<SimplePoco>("simplePocoTable");
var parentPocoAzureTableClient = extendedTableService.CreateAndRegisterTableClient<ParentPoco>("parentPocoTable");extendedTableService.AddInitializedTableClient<SimplePoco>(existingTableClient);var simplePocoAzureTableClient = extendedTableService.GetTypedTableClient<SimplePoco>();Throws ArgumentOutOfRangeException if the type was not registered.
Store different entity types in one table. Row keys are prefixed with the registered type name (or a custom prefix):
var multiEntityTableClient = extendedTableService.CreateAndRegisterMultiEntityTableClient("allpocos");
multiEntityTableClient.RegisterType<SimplePoco>();
multiEntityTableClient.RegisterType<MainWithParent>("mwp");
multiEntityTableClient.RegisterType<PocoWithListChildren>();SimplePoco and PocoWithListChildren use their type name as prefix; MainWithParent uses mwp.
var multiEntityTableClient = extendedTableService.GetMultiEntityAzureTableClientByTableName("allpocos");The table name used at registration is the lookup key.
Results from both clients are wrapped in TableEntityResult<T>. For TypedAzureTableClient<T>, T is the POCO type. For MultiEntityAzureTableClient list queries, T is object.
public class TableEntityResult<T>(ITableEntity tableEntity, T entity)
{
public string RowKey { get; set; } = tableEntity.RowKey;
public string PartitionKey { set; get; } = tableEntity.PartitionKey;
public ETag ETag { get; set; } = tableEntity.ETag;
public DateTimeOffset? Timestamp { get; set; } = tableEntity.Timestamp;
public T Entity { get; set; } = entity;
}Decorator around Azure.Data.Tables.TableClient with POCO serialize/deserialize (including nested entities, arrays, and IEnumerable).
var typedTableClient = extendedTableService.GetTypedTableClient<MyPoco>();var connectionString = "MY_STRING";
var tableClient = new TableClient(connectionString, "MyPoco"); // Azure.Data.Tables
await tableClient.CreateIfNotExistsAsync();
var typedTableClient = new TypedAzureTableClient<MyPoco>(tableClient);Underlying SDK client: typedTableClient.TableClient (GetTableClient() is obsolete).
Preferred 10.x surface: upserts + gets (serialize/deserialize). Use TableClient for deletes and other raw SDK calls.
Examples below use a client bound to MyPoco.
List<TableEntityResult<MyPoco>> pocos = await typedTableClient.GetAllAsync();All rows; no partition filter.
List<TableEntityResult<MyPoco>> pocos = await typedTableClient.GetAllAsync("mypoco");All rows for the given partition key.
TableEntityResult<MyPoco>? poco = await typedTableClient.GetByIdAsync("1018301");Looks up by row key id. Partition key is typeof(T).ToString() (typically the full type name, e.g. MyNamespace.MyPoco). Returns null if not found.
TableEntityResult<MyPoco>? poco = await typedTableClient.GetByIdAsync("9201u819", "mypoco");Returns null if not found.
Prefer GetAllAsync / GetAllAsync(partitionKey). For custom OData filters, query with TableClient.QueryAsync and map via TableEntityResult<T>.BuildTableEntityResult<T>(…).
MyPoco poco = new MyPoco();
// populate poco
Azure.Response result = await typedTableClient.InsertOrMergeAsync("001", "SimplePoco", poco);Upsert with TableUpdateMode.Merge. The parameter is object so partial DTOs (not necessarily T) can be merged.
MyPoco poco = new MyPoco();
// populate poco
Azure.Response result = await typedTableClient.InsertOrReplaceAsync("001", "SimplePoco", poco);Upsert with TableUpdateMode.Replace.
Parameter order is reversed vs the Azure SDK:
| 1st arg | 2nd arg | |
|---|---|---|
| This library (obsolete) | rowKey |
partitionKey |
TableClient.DeleteEntityAsync |
partitionKey |
rowKey |
// old:
await typedTableClient.DeleteEntityAsync("001", "SimplePoco");
// new:
await typedTableClient.TableClient.DeleteEntityAsync("SimplePoco", "001");Decorator around TableClient with the same mapping capabilities, plus multiple entity types in one table. Each registered type gets a row-key prefix ({prefix}_{rowKey}). Types must be registered before insert/get-by-type. Unregistered row prefixes on read throw ArgumentOutOfRangeException.
var multiEntityTableClient = extendedTableService.GetMultiEntityAzureTableClientByTableName("allpocos");var connectionString = "MY_STRING";
var tableClient = new TableClient(connectionString, "allpocos"); // Azure.Data.Tables
await tableClient.CreateIfNotExistsAsync();
var multiEntityTableClient = new MultiEntityAzureTableClient(tableClient);
multiEntityTableClient.RegisterType<SimplePoco>();
multiEntityTableClient.RegisterType<MainWithParent>("mwp");
multiEntityTableClient.RegisterType<PocoWithListChildren>();Underlying SDK client: multiEntityTableClient.TableClient (GetTableClient() is obsolete).
Preferred 10.x surface: registry + upserts + gets (+ DeleteEntityByTypeAsync for prefix-aware delete). Raw delete by full row key: TableClient.DeleteEntityAsync.
Examples below assume SimplePoco, MainWithParent, and PocoWithListChildren are registered.
List<TableEntityResult<object>> allPocos = await multiEntityTableClient.GetAllAsync();
List<SimplePoco> simplePocos = allPocos.Select(res => res.Entity).OfType<SimplePoco>().ToList();List<TableEntityResult<object>> allPocos = await multiEntityTableClient.GetAllAsync("mypoco");
List<SimplePoco> simplePocos = allPocos.Select(res => res.Entity).OfType<SimplePoco>().ToList();TableEntityResult<MyPoco>? poco = await multiEntityTableClient.GetByIdAsync<MyPoco>("9201u819", "mypoco");Resolves the stored row key as {registeredPrefix}_{rowKey}. Returns null if not found. Throws if T is not registered.
var query = $"PartitionKey eq '{partitionKey}'";
List<TableEntityResult<object>> allPocos = await multiEntityTableClient.GetAllByQueryAsync(query);
List<SimplePoco> simplePocos = allPocos.Select(res => res.Entity).OfType<SimplePoco>().ToList();OData filter as supported by TableClient.QueryAsync. Pass null for an unfiltered query. Needed here so row keys are still resolved via the type registry.
MyPoco poco = new MyPoco();
// populate poco
Azure.Response result = await multiEntityTableClient.InsertOrMergeAsync("001", "SimplePoco", poco);Stores row key as {prefix}_001. Type of obj must be registered.
MyPoco poco = new MyPoco();
// populate poco
Azure.Response result = await multiEntityTableClient.InsertOrReplaceAsync("001", "SimplePoco", poco);Azure.Response result = await multiEntityTableClient.DeleteEntityByTypeAsync<SimplePoco>("001", "SimplePoco");Builds the row key from the registered prefix for T. Prefer this when you know the entity type.
Parameter order is reversed vs the Azure SDK:
| 1st arg | 2nd arg | |
|---|---|---|
| This library (obsolete) | completeRowKey |
partitionKey |
TableClient.DeleteEntityAsync |
partitionKey |
rowKey |
// old:
await multiEntityTableClient.DeleteEntityAsync(result.RowKey, result.PartitionKey);
// new:
await multiEntityTableClient.TableClient.DeleteEntityAsync(result.PartitionKey, result.RowKey);Apache-2.0
2026, WebGate Consulting AG