Overview
The Azure Table Storage module provides patterns for working with Azure Table Storage, a NoSQL cloud-based data storage service designed for storing structured, semi-structured, or unstructured data at scale.
This module consumes your Domain Model from the Domain Designer and generates a complete Azure Table Storage implementation including repositories, table entities, and Unit of Work patterns.
Installation
Intent.Azure.TableStorage
What is Azure Table Storage?
Azure Table Storage is a cloud-based NoSQL data store that:
Stores large amounts of structured data
Provides highly available and scalable storage
Uses a key-based schema for flexible data access
Offers automatic load balancing
Integrates seamlessly with other Azure services
Use Cases
IoT Data Store time-series telemetry data
User Data Profile information and preferences
Logs & Events Application logs and audit trails
Metadata Large amounts of flexible metadata
What’s Generated
This module generates:
Unit of Work - Transaction management
Table Entities - Azure Table Storage entities
Repositories - Data access layer
Configuration - Dependency injection setup
App Settings - Connection string configuration
Domain Modeling
Composite Primary Keys
Unlike traditional databases, Azure Table Storage uses composite keys consisting of:
PartitionKey - Logical grouping of entities
RowKey - Unique identifier within a partition
Together, they form the primary key: [PartitionKey, RowKey]
Example Domain Model
// Domain Model in Intent Architect
public class Customer
{
// Maps to PartitionKey
public string Region { get ; set ; }
// Maps to RowKey
public string CustomerId { get ; set ; }
// Additional properties
public string Name { get ; set ; }
public string Email { get ; set ; }
public DateTime CreatedDate { get ; set ; }
}
Design your PartitionKey to distribute data evenly across partitions for optimal performance. Common strategies include region, date ranges, or customer segments.
Compositional Relationships
Table Storage data is flat. If you model compositional relationships in your domain:
Child objects are JSON serialized into the parent entity
Stored as a single column in the flat table
Example:
public class Order
{
public string PartitionKey { get ; set ; } // CustomerId
public string RowKey { get ; set ; } // OrderId
// Complex object - will be JSON serialized
public List < OrderItem > Items { get ; set ; }
}
// Stored in Azure Table as:
// PartitionKey: "CUST001"
// RowKey: "ORD-12345"
// Items: "[{\"ProductId\":\"PRD001\",\"Quantity\":2},...]"
Generated Code
Repository Interface
public interface ICustomerRepository
{
Task < Customer > FindByIdAsync (
string partitionKey ,
string rowKey ,
CancellationToken cancellationToken = default );
Task < List < Customer >> FindAllAsync ( CancellationToken cancellationToken = default );
Task < Customer > AddAsync (
Customer entity ,
CancellationToken cancellationToken = default );
Task UpdateAsync (
Customer entity ,
CancellationToken cancellationToken = default );
Task DeleteAsync (
Customer entity ,
CancellationToken cancellationToken = default );
Task < CursorPagedList < Customer >> FindAllAsync (
int pageSize ,
string continuationToken = null ,
CancellationToken cancellationToken = default );
}
Table Entity
public class CustomerTableEntity : ITableEntity
{
public string PartitionKey { get ; set ; } // Region
public string RowKey { get ; set ; } // CustomerId
public DateTimeOffset ? Timestamp { get ; set ; }
public ETag ETag { get ; set ; }
// Business properties
public string Name { get ; set ; }
public string Email { get ; set ; }
public DateTime CreatedDate { get ; set ; }
}
Unit of Work
public interface ITableStorageUnitOfWork : IUnitOfWork
{
Task SaveChangesAsync ( CancellationToken cancellationToken = default );
}
public class TableStorageUnitOfWork : ITableStorageUnitOfWork
{
private readonly TableServiceClient _client ;
public async Task SaveChangesAsync ( CancellationToken cancellationToken = default )
{
// Batch operations for efficiency
await ExecuteBatchOperationsAsync ( cancellationToken );
}
}
Configuration
Local Development (Azurite)
appsettings.Development.json:
{
"ConnectionStrings" : {
"TableStorage" : "UseDevelopmentStorage=true"
}
}
Production
appsettings.json:
{
"ConnectionStrings" : {
"TableStorage" : "DefaultEndpointsProtocol=https;AccountName=myaccount;AccountKey=mykey;EndpointSuffix=core.windows.net"
}
}
Using Managed Identity
{
"TableStorage" : {
"ServiceUri" : "https://myaccount.table.core.windows.net"
}
}
services . AddSingleton ( sp =>
{
var config = sp . GetRequiredService < IConfiguration >();
var serviceUri = new Uri ( config [ "TableStorage:ServiceUri" ]);
return new TableServiceClient ( serviceUri , new DefaultAzureCredential ());
});
Usage Examples
Customer Repository
public class CustomerService
{
private readonly ICustomerRepository _repository ;
private readonly ITableStorageUnitOfWork _unitOfWork ;
public CustomerService (
ICustomerRepository repository ,
ITableStorageUnitOfWork unitOfWork )
{
_repository = repository ;
_unitOfWork = unitOfWork ;
}
public async Task CreateCustomerAsync (
CreateCustomerCommand command ,
CancellationToken cancellationToken )
{
var customer = new Customer
{
Region = command . Region ,
CustomerId = Guid . NewGuid (). ToString (),
Name = command . Name ,
Email = command . Email ,
CreatedDate = DateTime . UtcNow
};
await _repository . AddAsync ( customer , cancellationToken );
await _unitOfWork . SaveChangesAsync ( cancellationToken );
}
public async Task < Customer > GetCustomerAsync (
string region ,
string customerId ,
CancellationToken cancellationToken )
{
return await _repository . FindByIdAsync (
region ,
customerId ,
cancellationToken );
}
}
Querying by Partition
public async Task < List < Customer >> GetCustomersByRegionAsync (
string region ,
CancellationToken cancellationToken )
{
var filter = TableClient . CreateQueryFilter ( $"PartitionKey eq { region } " );
var customers = new List < Customer >();
await foreach ( var entity in _tableClient . QueryAsync < CustomerTableEntity >(
filter ,
cancellationToken : cancellationToken ))
{
customers . Add ( MapToCustomer ( entity ));
}
return customers ;
}
public async Task < CursorPagedList < Customer >> GetCustomersPagedAsync (
int pageSize ,
string continuationToken ,
CancellationToken cancellationToken )
{
return await _repository . FindAllAsync (
pageSize ,
continuationToken ,
cancellationToken );
}
// Usage
var page1 = await GetCustomersPagedAsync ( 100 , null , cancellationToken );
var page2 = await GetCustomersPagedAsync ( 100 , page1 . ContinuationToken , cancellationToken );
Batch Operations
public async Task BulkInsertCustomersAsync (
List < Customer > customers ,
CancellationToken cancellationToken )
{
// Group by partition key for batch operations
var batches = customers
. GroupBy ( c => c . Region )
. Select ( g => g . ToList ());
foreach ( var batch in batches )
{
var tableBatch = new List < TableTransactionAction >();
foreach ( var customer in batch . Take ( 100 )) // Max 100 per batch
{
var entity = MapToTableEntity ( customer );
tableBatch . Add ( new TableTransactionAction (
TableTransactionActionType . Add ,
entity ));
}
await _tableClient . SubmitTransactionAsync (
tableBatch ,
cancellationToken );
}
}
Query Patterns
Filter Expressions
// Exact match
var filter = TableClient . CreateQueryFilter ( $"Email eq { email } " );
// Range query
var filter = TableClient . CreateQueryFilter (
$"CreatedDate ge { startDate } and CreatedDate le { endDate } " );
// Contains (limited support)
var filter = TableClient . CreateQueryFilter ( $"Name ge { 'A' } and Name lt { 'B' } " );
// Multiple conditions
var filter = TableClient . CreateQueryFilter (
$"PartitionKey eq { region } and Status eq { 'Active' } " );
Supported Operators
Operator Description Example eqEqual Status eq 'Active'neNot equal Status ne 'Deleted'gtGreater than Price gt 100geGreater than or equal CreatedDate ge datetime'2024-01-01'ltLess than Stock lt 10leLess than or equal Age le 65andLogical AND Region eq 'US' and Status eq 'Active'orLogical OR Status eq 'New' or Status eq 'Pending'notLogical NOT not (Status eq 'Deleted')
Table Storage does not support LIKE or contains operations. Use partition key and row key prefixes for efficient filtering.
Local Development
Azurite Setup
Install:
Run:
azurite --location ./azurite
Default Endpoints:
Blob: http://127.0.0.1:10000
Queue: http://127.0.0.1:10001
Table: http://127.0.0.1:10002
Azure Storage Explorer
Browse and manage tables using Azure Storage Explorer :
Download and install
Connect to Azurite or your Azure account
Navigate to Tables
View, query, and modify entities
Partition Key Design
Good Partition Key Examples:
// Time-based (if queries are time-ranged)
PartitionKey = date . ToString ( "yyyy-MM" );
// Geographic
PartitionKey = user . Country ;
// Customer segment
PartitionKey = customer . Tier ; // "Premium", "Standard", "Basic"
// Hash-based (for even distribution)
PartitionKey = customerId . GetHashCode (). ToString ( "X" ). Substring ( 0 , 2 );
Avoid Hot Partitions:
// Bad - All entities in one partition
PartitionKey = "Customer" ;
// Bad - Timestamp creates sequential hot partition
PartitionKey = DateTime . UtcNow . ToString ( "yyyy-MM-dd-HH-mm-ss" );
Query Optimization
Fastest queries specify both PartitionKey and RowKey: // Fastest - O(1)
var customer = await _tableClient . GetEntityAsync < CustomerTableEntity >(
partitionKey : "US-West" ,
rowKey : "CUST-12345" );
Next best - queries within a single partition: // Fast - scans single partition
var filter = TableClient . CreateQueryFilter (
$"PartitionKey eq { 'US-West' } and Status eq { 'Active' } " );
Avoid queries without PartitionKey filter: // Slow - full table scan
var filter = TableClient . CreateQueryFilter ( $"Email eq { email } " );
Project Only Needed Properties
Select only required properties: var query = _tableClient . QueryAsync < CustomerTableEntity >(
filter : filter ,
select : new [] { "Name" , "Email" });
Batch Operations
Batch operations improve performance:
public async Task BatchUpdateAsync (
List < Customer > customers ,
CancellationToken cancellationToken )
{
// Must be same partition
var partitionKey = customers . First (). Region ;
var batch = new List < TableTransactionAction >();
foreach ( var customer in customers . Take ( 100 ))
{
var entity = MapToTableEntity ( customer );
batch . Add ( new TableTransactionAction (
TableTransactionActionType . UpdateMerge ,
entity ));
}
await _tableClient . SubmitTransactionAsync ( batch , cancellationToken );
}
Batch Limitations:
Maximum 100 operations per batch
All entities must be in the same partition
Total batch size cannot exceed 4 MB
Best Practices
Design partition keys for:
Even distribution of data
Query patterns alignment
Avoiding hot partitions
Logical data grouping
Consider using composite strategies: // Combine region and time
PartitionKey = $" { region } _ { date : yyyyMM } " ;
Use meaningful row keys:
Natural identifiers when possible
Sortable for range queries
Include timestamps for ordering
// Time-sortable
RowKey = $" { DateTime . MaxValue . Ticks - DateTime . UtcNow . Ticks : D19 } " ;
// Composite
RowKey = $" { customerId } _ { orderId } " ;
Keep entities small:
Maximum entity size: 1 MB
Maximum property size: 64 KB (string)
Prefer denormalization over large entities
Consider splitting large objects across multiple entities
Use ETags for optimistic concurrency: try
{
await _tableClient . UpdateEntityAsync (
entity ,
entity . ETag ,
TableUpdateMode . Replace );
}
catch ( RequestFailedException ex ) when ( ex . Status == 412 )
{
// Concurrency conflict - entity was modified
// Reload and retry
}
Handle common errors: try
{
await _repository . AddAsync ( customer , cancellationToken );
}
catch ( RequestFailedException ex )
{
switch ( ex . Status )
{
case 404 :
// Table doesn't exist
await CreateTableAsync ();
break ;
case 409 :
// Entity already exists
await _repository . UpdateAsync ( customer , cancellationToken );
break ;
case 412 :
// Precondition failed (ETag mismatch)
throw new ConcurrencyException ();
default :
throw ;
}
}
Monitoring
Application Insights
public class MonitoredCustomerRepository : ICustomerRepository
{
private readonly ICustomerRepository _inner ;
private readonly TelemetryClient _telemetry ;
public async Task < Customer > FindByIdAsync (
string partitionKey ,
string rowKey ,
CancellationToken cancellationToken )
{
var stopwatch = Stopwatch . StartNew ();
try
{
var result = await _inner . FindByIdAsync (
partitionKey , rowKey , cancellationToken );
_telemetry . TrackDependency (
"Azure Table Storage" ,
"FindById" ,
stopwatch . Elapsed ,
success : true );
return result ;
}
catch ( Exception ex )
{
_telemetry . TrackException ( ex );
throw ;
}
}
}
Key Metrics
Request latency
Throttling events (HTTP 503)
Success/failure rates
Entity size distribution
Partition distribution
Migration from Other Data Stores
From SQL Database
Changes needed:
Redesign schema around partition/row keys
Denormalize data (no joins)
Implement application-level transactions
Adjust query patterns
From Cosmos DB
Similarities:
Both are NoSQL
Schema-flexible
Partition-based
Key differences:
Table Storage: Simpler, lower cost, key-value only
Cosmos DB: Richer queries, global distribution, multiple APIs
Resources
Table Storage Docs Official documentation
Design Patterns Table design guide
Azurite Local development emulator
Performance Guide Optimization best practices