Skip to content

Repository files navigation

WebGate.Azure.TableUtils

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 & Versioning

Target Framework net10.0
Package version 10.x.x

The NuGet package major version matches the .NET target framework major version.

  • net10.0 → package version 10.x.x
  • A future uplift to net11.0 would start at package version 11.0.0

Within a major line, use minor/patch for library changes that stay on the same TFM.


Entity mapping

POCOs are mapped to Azure Table properties by reflection:

  • Readable/writable properties are included.
  • Nested objects are flattened with _ as separator (Parent.Child → column Parent_Child).
  • null property values are skipped on serialize.
  • Value types, string, and byte[] 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.


ExtendedAzureTableClientService

Registers and resolves TypedAzureTableClient<T> and MultiEntityAzureTableClient instances.

Create a service

var connectionString = "MY_STRING";
var extendedTableService = new ExtendedAzureTableClientService(connectionString);

Initialize the service in Startup / Program.cs of an Azure Function (or host).

Register TypedAzureTableClients

One table per POCO type:

var simplePocoAzureTableClient = extendedTableService.CreateAndRegisterTableClient<SimplePoco>("simplePocoTable");
var parentPocoAzureTableClient = extendedTableService.CreateAndRegisterTableClient<ParentPoco>("parentPocoTable");

Register an already initialized TableClient

extendedTableService.AddInitializedTableClient<SimplePoco>(existingTableClient);

Resolve a TypedAzureTableClient

var simplePocoAzureTableClient = extendedTableService.GetTypedTableClient<SimplePoco>();

Throws ArgumentOutOfRangeException if the type was not registered.

Register a MultiEntityAzureTableClient

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.

Resolve a MultiEntityAzureTableClient

var multiEntityTableClient = extendedTableService.GetMultiEntityAzureTableClientByTableName("allpocos");

The table name used at registration is the lookup key.


TableEntityResult<T>

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;
}

TypedAzureTableClient<T>

Decorator around Azure.Data.Tables.TableClient with POCO serialize/deserialize (including nested entities, arrays, and IEnumerable).

Get from service

var typedTableClient = extendedTableService.GetTypedTableClient<MyPoco>();

Initialize inline

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.

GetAllAsync()

List<TableEntityResult<MyPoco>> pocos = await typedTableClient.GetAllAsync();

All rows; no partition filter.

GetAllAsync(string partitionKey)

List<TableEntityResult<MyPoco>> pocos = await typedTableClient.GetAllAsync("mypoco");

All rows for the given partition key.

GetByIdAsync(string id)

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.

GetByIdAsync(string rowKey, string partitionKey)

TableEntityResult<MyPoco>? poco = await typedTableClient.GetByIdAsync("9201u819", "mypoco");

Returns null if not found.

GetAllByQueryAsync(string? query) — obsolete

Prefer GetAllAsync / GetAllAsync(partitionKey). For custom OData filters, query with TableClient.QueryAsync and map via TableEntityResult<T>.BuildTableEntityResult<T>(…).

InsertOrMergeAsync(string rowKey, string partitionKey, object obj)

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.

InsertOrReplaceAsync(string rowKey, string partitionKey, object obj)

MyPoco poco = new MyPoco();
// populate poco
Azure.Response result = await typedTableClient.InsertOrReplaceAsync("001", "SimplePoco", poco);

Upsert with TableUpdateMode.Replace.

DeleteEntityAsync — obsolete (compile error)

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");

MultiEntityAzureTableClient

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.

Get from service

var multiEntityTableClient = extendedTableService.GetMultiEntityAzureTableClientByTableName("allpocos");

Initialize inline

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.

GetAllAsync()

List<TableEntityResult<object>> allPocos = await multiEntityTableClient.GetAllAsync();
List<SimplePoco> simplePocos = allPocos.Select(res => res.Entity).OfType<SimplePoco>().ToList();

GetAllAsync(string partitionKey)

List<TableEntityResult<object>> allPocos = await multiEntityTableClient.GetAllAsync("mypoco");
List<SimplePoco> simplePocos = allPocos.Select(res => res.Entity).OfType<SimplePoco>().ToList();

GetByIdAsync<T>(string rowKey, string partitionKey)

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.

GetAllByQueryAsync(string? query)

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.

InsertOrMergeAsync<T>(string rowKey, string partitionKey, T obj)

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.

InsertOrReplaceAsync<T>(string rowKey, string partitionKey, T obj)

MyPoco poco = new MyPoco();
// populate poco
Azure.Response result = await multiEntityTableClient.InsertOrReplaceAsync("001", "SimplePoco", poco);

DeleteEntityByTypeAsync<T>(string rowKey, string partitionKey)

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.

DeleteEntityAsync(completeRowKey, partitionKey) — obsolete (compile error)

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);

License

Apache-2.0


Copyright

2026, WebGate Consulting AG

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages