Generated Code¶
The Azure Table Storage generator produces standard .NET interfaces and implementations that work with plain async/await — no effects required. For the optional Eff<RT, T> composition layer, see Effects Composition.
Entity Mapper Methods¶
For each [TableEntity], the generator adds two methods to the partial type:
ToTableEntity()¶
Converts the entity to an Azure.Data.Tables.TableEntity for storage:
public Azure.Data.Tables.TableEntity ToTableEntity()
{
var entity = new Azure.Data.Tables.TableEntity(ThreadTs, PhoneNumber);
entity["DisplayName"] = DisplayName;
entity["MessageCount"] = MessageCount;
entity["CreatedAt"] = CreatedAt;
entity["ThreadTs"] = ThreadTs;
entity["PhoneNumber"] = PhoneNumber;
return entity;
}
The partition key and row key are passed to the TableEntity constructor. All data properties (including the key properties) are stored as named values.
When a constant partition key is used, the literal value replaces the property reference:
var entity = new Azure.Data.Tables.TableEntity("PhoneMappings", ThreadTs);
// PhoneNumber property is NOT added to the entity
FromTableEntity()¶
Reconstructs the entity from a TableEntity:
public static Conversation FromTableEntity(Azure.Data.Tables.TableEntity entity)
{
return new Conversation
{
ThreadTs = entity.GetString("ThreadTs") ?? "",
PhoneNumber = entity.GetString("PhoneNumber") ?? "",
DisplayName = entity.GetString("DisplayName") ?? "",
MessageCount = entity.GetInt32("MessageCount") ?? 0,
CreatedAt = entity.GetDateTimeOffset("CreatedAt") ?? default,
};
}
Non-nullable properties use null-coalescing with type-appropriate defaults ("" for strings, 0 for integers, default for value types). Nullable properties omit the fallback:
// Nullable properties — no default fallback
return new Item
{
UserId = entity.GetString("UserId") ?? "",
ItemId = entity.GetString("ItemId") ?? "",
Description = entity.GetString("Description"), // string? — no ??
SortOrder = entity.GetInt32("SortOrder"), // int? — no ??
};
Property Conversion Strategies¶
The generator automatically selects a conversion strategy based on the property type:
| Strategy | Property Types | ToTableEntity() |
FromTableEntity() |
|---|---|---|---|
| Native | string, bool, int, long, double, byte[], DateTime, DateTimeOffset, Guid |
Direct assignment | entity.GetString(), entity.GetInt32(), etc. |
| Enum | Any enum type |
.ToString() |
Enum.Parse<T>() |
| TypedId | Types with [TypedId] attribute |
.Value.ToString() |
TypedId.Parse() |
| Json | All other types (List<T>, custom objects, etc.) |
JsonSerializer.Serialize() |
JsonSerializer.Deserialize<T>() |
| Option | Option<T> (LanguageExt) |
Nullable handling | Nullable handling |
DSAT04 — JSON serialization diagnostic
Properties that use the Json strategy trigger an informational diagnostic (DSAT04) at compile time: "Property '{name}' on '{type}' will be stored as JSON because its type is not natively supported by Azure Table Storage." This is informational only — no action required.
JSON serialization example¶
[TableEntity("Users")]
public partial record User
{
[PartitionKey] public required string UserId { get; init; }
[RowKey] public required string Email { get; init; }
public required List<string> Tags { get; init; } // ← DSAT04 info
}
Generated conversion:
// ToTableEntity
entity["Tags"] = JsonSerializer.Serialize(Tags);
// FromTableEntity
Tags = JsonSerializer.Deserialize<List<string>>(entity.GetString("Tags") ?? "[]") ?? new(),
Generated Store Interface¶
For each [TableEntity], the generator produces a store interface with four async CRUD methods:
public interface IConversationStore
{
Task UpsertAsync(Conversation entity, CancellationToken cancellationToken = default);
Task<Conversation?> GetAsync(string partitionKey, string rowKey, CancellationToken cancellationToken = default);
Task DeleteAsync(string partitionKey, string rowKey, CancellationToken cancellationToken = default);
Task<List<Conversation>> QueryByPartitionAsync(string partitionKey, CancellationToken cancellationToken = default);
}
Store Implementation¶
The generated ConversationStore is a sealed class backed by a TableServiceClient:
public sealed class ConversationStore(TableServiceClient tableServiceClient) : IConversationStore
{
private TableClient? _tableClient;
private TableClient GetTableClient()
{
_tableClient ??= tableServiceClient.GetTableClient("Conversations");
return _tableClient;
}
public async Task UpsertAsync(Conversation entity, CancellationToken cancellationToken = default)
{
var client = GetTableClient();
await client.CreateIfNotExistsAsync(cancellationToken: cancellationToken);
await client.UpsertEntityAsync(entity.ToTableEntity(), TableUpdateMode.Replace, cancellationToken);
}
public async Task<Conversation?> GetAsync(
string partitionKey, string rowKey, CancellationToken cancellationToken = default)
{
var client = GetTableClient();
try
{
var response = await client.GetEntityAsync<TableEntity>(
partitionKey, rowKey, cancellationToken: cancellationToken);
return Conversation.FromTableEntity(response.Value);
}
catch (RequestFailedException ex) when (ex.Status == 404)
{
return null;
}
}
public async Task DeleteAsync(
string partitionKey, string rowKey, CancellationToken cancellationToken = default)
{
var client = GetTableClient();
try
{
await client.DeleteEntityAsync(partitionKey, rowKey, cancellationToken: cancellationToken);
}
catch (RequestFailedException ex) when (ex.Status == 404)
{
// Idempotent delete — ignore 404
}
}
public async Task<List<Conversation>> QueryByPartitionAsync(
string partitionKey, CancellationToken cancellationToken = default)
{
var client = GetTableClient();
var results = new List<Conversation>();
await foreach (var entity in client.QueryAsync<TableEntity>(
e => e.PartitionKey == partitionKey, cancellationToken: cancellationToken))
{
results.Add(Conversation.FromTableEntity(entity));
}
return results;
}
}
Key implementation details:
- Lazy
TableClient— initialized on first use via null-coalescing assignment (??=) - Table auto-creation —
CreateIfNotExistsAsyncis called before every upsert - Idempotent deletes — 404 responses are caught and silently ignored
- Null on miss —
GetAsyncreturnsnullwhen the entity is not found (404)
DI Registration¶
The generator creates a registration extension method per entity:
public static class ConversationStoreServiceCollectionExtensions
{
public static IServiceCollection AddConversationStore(this IServiceCollection services)
{
services.TryAddSingleton<IConversationStore, ConversationStore>();
return services;
}
}
TryAddSingleton means concrete implementations registered first take priority — register a custom implementation before calling AddConversationStore to override the default.
Injecting and Using Directly¶
Use the generated interface with standard constructor injection:
public class ConversationService(IConversationStore store)
{
public async Task<Conversation?> GetConversation(
string threadTs, string phone, CancellationToken ct)
=> await store.GetAsync(threadTs, phone, ct);
public async Task<List<Conversation>> ListByThread(
string threadTs, CancellationToken ct)
=> await store.QueryByPartitionAsync(threadTs, ct);
}