TableStorage 6.0.0-preview-07
dotnet add package TableStorage --version 6.0.0-preview-07
NuGet\Install-Package TableStorage -Version 6.0.0-preview-07
<PackageReference Include="TableStorage" Version="6.0.0-preview-07" />
<PackageVersion Include="TableStorage" Version="6.0.0-preview-07" />
<PackageReference Include="TableStorage" />
paket add TableStorage --version 6.0.0-preview-07
#r "nuget: TableStorage, 6.0.0-preview-07"
#:package TableStorage@6.0.0-preview-07
#addin nuget:?package=TableStorage&version=6.0.0-preview-07&prerelease
#tool nuget:?package=TableStorage&version=6.0.0-preview-07&prerelease
TableStorage
A streamlined, source-generated way of working with Azure Data Tables and Blob Storage. Built for performance, Native AOT compatibility, and developer ergonomics.
Packages
| Package | Description |
|---|---|
| TableStorage.Core | Core abstractions and LINQ helpers |
| TableStorage | Table Storage context, sets, and source generators |
| TableStorage.Blobs | Blob Storage support (Block and Append blobs) |
| TableStorage.Fluent | Polymorphic fluent entities (2–4 types per table) |
| TableStorage.Fluent.Extended | Extended fluent entities (5–16 types per table) |
| TableStorage.RuntimeCompilations | Runtime LINQ compilation for table queries |
| TableStorage.Blobs.RuntimeCompilations | Runtime LINQ compilation for blob queries |
| TableStorage.Fluent.RuntimeCompilations | Runtime LINQ compilation for fluent queries |
Installation
dotnet add package TableStorage.Core
dotnet add package TableStorage
For Blob Storage support:
dotnet add package TableStorage.Blobs
For polymorphic fluent entity support:
dotnet add package TableStorage.Fluent
Getting Started
Define a Context
Create your own TableContext and mark it with the [TableContext] attribute. This class must be partial.
[TableContext]
public partial class MyTableContext;
Define Models
Create your models — these must be classes with a parameterless constructor. Mark them with the [TableSet] attribute. This class must be partial.
[TableSet]
public partial class Model
{
public string Data { get; set; }
public bool Enabled { get; set; }
}
Properties can also be defined using the [TableSetProperty] attribute.
This is particularly useful if you are planning on using .NET 8+'s Native AOT, as the source generation will make sure any breaking reflection calls are avoided by the Azure.Core libraries.
Starting C# 13, you can also mark them as partial.
[TableSet]
[TableSetProperty(typeof(string), "Data")]
[TableSetProperty(typeof(bool), "Enabled")]
public partial class Model;
Key Aliases
Sometimes it's nice to have a pretty name for your PartitionKey and RowKey properties, as the original names might not always make much sense when reading your code.
You can use the PartitionKey and RowKey properties of TableSet to create a proxy for these two properties.
[TableSet(PartitionKey = "MyPrettyPartitionKey", RowKey = "MyPrettyRowKey")]
public partial class Model;
Base Class Inheritance
Models can inherit from base classes. The source generator correctly picks up properties defined in parent classes, including partition and row keys:
public abstract class BaseEntity
{
public string Category { get; set; }
public string CommonProperty { get; set; }
}
[TableSet(PartitionKey = nameof(Category), RowKey = nameof(ProductId))]
public partial class Product : BaseEntity
{
public partial string ProductId { get; set; }
public partial decimal Price { get; set; }
}
You can also override base class properties using virtual/override or hide them with new partial:
public abstract class BaseEntity
{
public virtual string Category { get; set; }
public string Name { get; set; }
}
// Override — the source generator uses the overridden property
[TableSet(PartitionKey = nameof(Category), RowKey = nameof(Id))]
public partial class Product : BaseEntity
{
public override string Category { get; set; }
public partial string Id { get; set; }
}
// Hide with 'new partial' — enables change tracking on the key
[TableSet(PartitionKey = nameof(Category), RowKey = nameof(Id))]
public partial class TrackedProduct : BaseEntity
{
public new partial string Category { get; set; }
public partial string Id { get; set; }
}
Change Tracking
TableSet has a TrackChanges property (default false) that optimizes what is sent back to the server when making changes to an entity.
When tracking changes, it's important to either use the TableSetProperty attribute to define your properties, or mark them as partial starting C# 13, otherwise they will not be tracked.
[TableSet(TrackChanges = true)]
[TableSetProperty(typeof(string), "Data")]
public partial class Model
{
public partial bool Enabled { get; set; }
}
Blob Support
Mark the model for Blob Storage support by setting SupportBlobs on the TableSet attribute to true.
When working with blobs, you can mark certain properties to be used as blob tags, either by decorating the property with [Tag] or by setting Tag to true on the TableSetProperty attribute.
[TableSet(SupportBlobs = true)]
[TableSetProperty(typeof(string), "Data", Tag = true)]
public partial class Model
{
[Tag]
public partial bool Enabled { get; set; }
}
Important: If you plan on using the default STJ serialization, or plan on using the source generated
JsonSerializerContext, you need to make sure that the properties you want to serialize are defined on your partial class definition. This includes your partition and row key. If you do not do this, STJ will not serialize them.
Register Your Context
Place your tables on your TableContext. The sample below will create 2 tables in table storage, named Models1 and Models2. It will also create a blob container named BlobModels1 which is a set for Block blobs. BlobModels2 is a set for Append blobs.
[TableContext]
public partial class MyTableContext
{
public TableSet<Model> Models1 { get; set; }
public BlobSet<Model> BlobModels1 { get; set; }
public AppendBlobSet<Model> BlobModels2 { get; set; }
public TableSet<Model> Models2 { get; set; }
}
Register your TableContext in your services. An extension method will be available specifically for your context.
builder.Services.AddMyTableContext(builder.Configuration.GetConnectionString("MyConnectionString"));
Inject and Use
public class MyService(MyTableContext context)
{
private readonly MyTableContext _context = context;
public async Task DoSomething(CancellationToken token)
{
var entity = await _context.Models1.GetEntityOrDefaultAsync("partitionKey", "rowKey", token);
if (entity is not null)
{
//Do more
}
}
}
For some special cases, your table name might not be known at compile time. To handle those, an extension method has been added:
var tableSet = context.GetTableSet<Model>("randomname");
Configuration
Table Options
Pass a configure delegate when registering your context:
builder.Services.AddMyTableContext(builder.Configuration.GetConnectionString("MyConnectionString"), Configure);
static void Configure(TableOptions options)
{
options.TableMode = TableUpdateMode.Merge;
}
| Property | Type | Default | Description |
|---|---|---|---|
TableMode |
TableUpdateMode |
Merge |
Update mode: Merge or Replace |
PageSize |
int? |
null |
Number of entities per page when querying |
CreateTableIfNotExists |
CreateIfNotExistsMode |
Always |
Always, Once (cached), or Disabled |
BulkOperation |
BulkOperation |
Replace |
Default bulk operation mode: Replace or Merge |
TransactionSafety |
TransactionSafety |
Enabled |
When Enabled, transactions are split by partition key and chunked |
TransactionChunkSize |
int |
100 |
Max operations per transaction batch (must be > 0) |
ChangesOnly |
bool |
false |
When true, only changed properties are sent during updates |
OptimizeQueries |
bool |
true |
When true, queries with multiple partition key comparisons are automatically split into per-partition sub-queries to avoid full table scans |
Blob Options
If you have defined any BlobSets, a third parameter becomes available to configure the blob service.
builder.Services.AddMyTableContext(builder.Configuration.GetConnectionString("MyConnectionString"), ConfigureTables, ConfigureBlobs);
static void ConfigureTables(TableOptions options)
{
options.TableMode = TableUpdateMode.Merge;
}
static void ConfigureBlobs(BlobOptions options)
{
options.UseTags = true;
}
| Property | Type | Default | Description |
|---|---|---|---|
CreateContainerIfNotExists |
CreateIfNotExistsMode |
Always |
Always, Once (cached), or Disabled |
Serializer |
IBlobSerializer |
STJ-based | Custom blob serializer implementation |
UseTags |
bool |
true |
Store partition/row key and tagged properties as blob tags |
LINQ
LINQ extension methods are provided in the TableStorage.Linq namespace that optimize queries specifically for Table Storage. These methods translate directly to server-side OData filters.
Since these return an instance that implements IAsyncEnumerable, System.Linq.Async is an excellent companion to these methods. Do keep in mind that as soon as you start using IAsyncEnumerable, any further operations will run client-side.
Available Methods
| Method | Description |
|---|---|
Where(predicate) |
Filter entities server-side using an expression |
SelectFields(selector) |
Retrieve only selected fields (returns original model type) |
Take(n) |
Limit the number of results |
FirstAsync() / FirstOrDefaultAsync() |
Get the first matching entity |
SingleAsync() / SingleOrDefaultAsync() |
Get the single matching entity |
ExistsIn(selector, elements) |
Filter to entities whose property value is in a collection |
NotExistsIn(selector, elements) |
Filter to entities whose property value is not in a collection |
FindAsync(partitionKey, rowKey) |
Find a single entity by partition/row key via LINQ |
FindAsync(keys) |
Find multiple entities by partition/row key pairs |
BatchDeleteAsync() |
Delete all matching entities |
BatchDeleteTransactionAsync() |
Delete all matching entities using transactions |
Examples
// Filter, select fields, take
var results = context.Models1
.Where(x => x.Enabled)
.SelectFields(x => new { x.Data })
.Take(10);
await foreach (var entity in results)
{
Console.WriteLine(entity.Data);
}
// Get single entity
var first = await context.Models1
.Where(x => x.Data == "test")
.FirstOrDefaultAsync();
// ExistsIn
var ids = new[] { "id1", "id2", "id3" };
var matches = context.Models1.ExistsIn(x => x.RowKey, ids);
// Batch delete
var deletedCount = await context.Models1
.Where(x => !x.Enabled)
.BatchDeleteAsync();
// Find by key
var entity = await context.Models1.FindAsync("partitionKey", "rowKey");
Note:
Selectwill include the actual transformation. If you want the original model, with only the selected fields retrieved, useSelectFieldsinstead. If you are using Native AOT, you will need to useSelectFieldsasSelectwill not work.
Bulk Operations
Bulk operations allow you to insert, update, upsert, or delete multiple entities efficiently using batched transactions:
var entities = new List<Model> { /* ... */ };
await context.Models1.BulkInsertAsync(entities);
await context.Models1.BulkUpdateAsync(entities);
await context.Models1.BulkUpsertAsync(entities);
await context.Models1.BulkDeleteAsync(entities);
BulkUpdateAsync and BulkUpsertAsync accept an optional BulkOperation parameter to choose between Replace and Merge mode. The default is configured via TableOptions.BulkOperation.
Transactions
Submit manual transactions with automatic partition key grouping and chunking:
var actions = entities.Select(e => new TableTransactionAction(TableTransactionActionType.UpsertReplace, e));
await context.Models1.SubmitTransactionAsync(actions);
When TransactionSafety is Enabled (the default), transactions are automatically grouped by partition key and split into chunks of TransactionChunkSize (default 100). Set TransactionSafety to Disabled to submit a raw transaction without any safety processing.
Blob Storage
Blob sets support BlobSet<T> (block blobs) and AppendBlobSet<T> (append blobs).
Block Blob Operations
await context.BlobModels1.AddEntityAsync(entity);
await context.BlobModels1.GetEntityAsync("partitionKey", "rowKey");
await context.BlobModels1.GetEntityOrDefaultAsync("partitionKey", "rowKey");
await context.BlobModels1.UpdateEntityAsync(entity);
await context.BlobModels1.UpsertEntityAsync(entity);
await context.BlobModels1.DeleteEntityAsync(entity);
await context.BlobModels1.DeleteAllEntitiesAsync("partitionKey");
bool exists = await context.BlobModels1.ExistsAsync("partitionKey", "rowKey");
Append Blob Operations
Append blobs support the same CRUD operations plus streaming append:
await context.BlobModels2.AppendAsync("partitionKey", "rowKey", stream);
Custom Serialization
Blob storage allows for custom serialization and deserialization. By default, System.Text.Json will be used for serialization.
You can define your own by implementing IBlobSerializer and passing it to the BlobOptions object.
Here's an example for a model that uses ProtoBuf:
builder.Services.AddMyTableContext(builder.Configuration.GetConnectionString("MyConnectionString"), ConfigureTables, ConfigureBlobs);
static void ConfigureTables(TableOptions options)
{
options.TableMode = TableUpdateMode.Merge;
}
static void ConfigureBlobs(BlobOptions options)
{
options.UseTags = true;
options.Serializer = new ProtoBufSerializer();
}
[TableSet(PartitionKey = nameof(PrettyPartition), RowKey = nameof(PrettyRow), SupportBlobs = true)]
[ProtoContract(IgnoreListHandling = true)] // Important to ignore list handling because we are generating an IDictionary implementation that is not supported by protobuf
public partial class Model
{
[ProtoMember(1)] public partial string PrettyPartition { get; set; } // We can partial the PK and RowKey to enable custom serialization attributes
[ProtoMember(2)] public partial string PrettyRow { get; set; }
[ProtoMember(3)] public partial int MyProperty1 { get; set; }
[ProtoMember(4)] public partial string MyProperty2 { get; set; }
[ProtoMember(5)] public partial string? MyNullableProperty2 { get; set; }
}
public sealed class ProtoBufSerializer : IBlobSerializer
{
public ValueTask<T?> DeserializeAsync<T>(string table, Stream entity, CancellationToken cancellationToken) where T : IBlobEntity
{
return ValueTask.FromResult<T?>(Serializer.Deserialize<T>(entity));
}
public BinaryData Serialize<T>(string table, T entity) where T : IBlobEntity
{
using MemoryStream stream = new();
Serializer.Serialize(stream, entity);
return new(stream.ToArray());
}
}
Native AOT Serialization
For some specific cases, the source generator will have to generate a .Deserialize call using System.Text.Json.
Since this is not supported when publishing with Native AOT, you can use the TableStorageSerializerContext property in your csproj file to set the fullname of a class that implements JsonSerializerContext to support native deserialization.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<PublishAot>true</PublishAot>
<TableStorageSerializerContext>TableStorage.Tests.Contexts.ModelSerializationContext</TableStorageSerializerContext>
</PropertyGroup>
</Project>
When configuring your context, you can also pass a JsonSerializerContext to the BlobOptions object to support native deserialization. Otherwise the default serialization will be used that relies on reflection.
static void ConfigureBlobs(BlobOptions options)
{
options.Serializer = new AotJsonBlobSerializer(MyJsonSerializerContext.Default);
}
Fluent Entities
Fluent entities allow you to store multiple entity types in a single Azure Table using a discriminator pattern. This enables polymorphic table storage with type-safe pattern matching.
dotnet add package TableStorage.Fluent # 2–4 types per table
dotnet add package TableStorage.Fluent.Extended # 5–16 types per table
Quick Example
[TableContext]
public partial class MyTableContext
{
public TableSet<FluentTableEntity<Customer, Order>> MixedEntities { get; set; }
}
// Store using implicit conversion
FluentTableEntity<Customer, Order> fluent = new Customer { Name = "John" };
await context.MixedEntities.AddEntityAsync(fluent);
// Retrieve and pattern match
var entity = await context.MixedEntities.GetEntityOrDefaultAsync(pk, rk);
var result = entity.SwitchCase(
case1: customer => $"Customer: {customer.Name}",
case2: order => $"Order: {order.OrderNumber}"
);
Three discriminator strategies are available:
FluentTableEntity<T1, T2>— uses a$typediscriminator columnFluentPartitionTableEntity<T1, T2>— uses thePartitionKeyas discriminatorFluentRowTypeTableEntity<T1, T2>— uses theRowKeyas discriminator
See the TableStorage.Fluent README for full documentation.
RuntimeCompilations
For scenarios where you need runtime-compiled LINQ expressions (e.g. dynamic query building), RuntimeCompilation packages are available:
dotnet add package TableStorage.RuntimeCompilations
dotnet add package TableStorage.Blobs.RuntimeCompilations
dotnet add package TableStorage.Fluent.RuntimeCompilations
Enable runtime compilation when registering your context:
services.AddMyTableContext(connectionString,
configureBlobs: x =>
{
x.EnableCompilationAtRuntime();
});
License
This project is licensed under the MIT License.
Contributing
Contributions are welcome! Please open an issue or submit a pull request.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- Azure.Data.Tables (>= 12.11.0)
- Microsoft.Bcl.AsyncInterfaces (>= 8.0.0)
- Microsoft.Extensions.Configuration.Abstractions (>= 3.1.0)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.0)
- TableStorage.Core (>= 6.0.0-preview.1 && < 7.0.0)
NuGet packages (4)
Showing the top 4 NuGet packages that depend on TableStorage:
| Package | Downloads |
|---|---|
|
TableStorage.RuntimeCompilations
Package Description |
|
|
TableStorage.Fluent
Package Description |
|
|
TableStorage.Fluent.Extended
Package Description |
|
|
TableStorage.Fluent.RuntimeCompilations
Package Description |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 6.0.0-preview-07 | 40 | 5/6/2026 |
| 6.0.0-preview-06 | 79 | 2/4/2026 |
| 6.0.0-preview-05 | 64 | 1/30/2026 |
| 6.0.0-preview-04 | 68 | 1/13/2026 |
| 6.0.0-preview-03 | 73 | 1/13/2026 |
| 6.0.0-preview-01 | 136 | 1/8/2026 |
| 5.5.0 | 207 | 11/28/2025 |
| 5.4.0 | 223 | 10/26/2025 |
| 5.3.0 | 122 | 9/26/2025 |
| 5.1.0 | 224 | 8/11/2025 |
| 5.1.0-preview.3 | 179 | 7/28/2025 |
| 5.1.0-preview.2 | 463 | 7/25/2025 |
| 5.1.0-preview.1 | 177 | 6/17/2025 |
| 5.0.2 | 228 | 6/5/2025 |
| 5.0.1 | 219 | 5/26/2025 |
| 5.0.0 | 211 | 5/26/2025 |
| 5.0.0-preview.7 | 219 | 4/16/2025 |
| 5.0.0-preview.6 | 154 | 3/28/2025 |
| 5.0.0-preview.5 | 144 | 3/28/2025 |
| 5.0.0-preview.4 | 182 | 3/19/2025 |