DataAccess.AzureStorage 4.0.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package DataAccess.AzureStorage --version 4.0.0
                    
NuGet\Install-Package DataAccess.AzureStorage -Version 4.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="DataAccess.AzureStorage" Version="4.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="DataAccess.AzureStorage" Version="4.0.0" />
                    
Directory.Packages.props
<PackageReference Include="DataAccess.AzureStorage" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add DataAccess.AzureStorage --version 4.0.0
                    
#r "nuget: DataAccess.AzureStorage, 4.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package DataAccess.AzureStorage@4.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=DataAccess.AzureStorage&version=4.0.0
                    
Install as a Cake Addin
#tool nuget:?package=DataAccess.AzureStorage&version=4.0.0
                    
Install as a Cake Tool

CI Pipeline

GitHub repo size GitHub stars GitHub license Contributions welcome

DataAccess.AzureStorage

A small, dependency-light data access layer over Azure Table Storage, built on top of the official Azure.Data.Tables SDK. It targets .NET Standard 2.0 / 2.1, so it can be consumed from .NET Framework, .NET Core, and modern .NET projects alike.

This document covers the DataAccess.AzureStorage.Table namespace specifically — table name and connection string are supplied by the caller, and everything else (table creation, entity CRUD, optimistic concurrency, schema-less rows) is handled for you.

var tableAccess = new AzureTableAccess("MyTable", "<connection string>");
tableAccess.InsertEntity(myEntity);
tableAccess.ReplaceEntity(myEntity);

Requirements

  • .NET Standard 2.0 or 2.1 compatible runtime (.NET Framework 4.6.1+, .NET Core 2.0+, .NET 5+, etc.)
  • NuGet package: Azure.Data.Tables (v12.10.0 or compatible)
  • An Azure Storage account connection string (or the Azurite emulator connection string for local dev)

Architecture

AzureManager                    (abstract) — holds & validates the connection string
    └── AzureTableManager       (abstract) — owns TableServiceClient / TableClient, table name validation & creation
            └── AzureTableAccess (sealed)  — implements IAzureTableAccess; all CRUD + query operations

IAzureTableAccess                — public contract; program against this for testability/DI
TableEntity                      — abstract base for your own entity classes (implements ITableEntity)
IDynamicTableEntity / DynamicTableEntity
                                  — schema-less entity for when the row shape isn't known at compile time
EdmType                          — enum of the Azure Table Storage EDM types DynamicTableEntity accepts
Type Role
AzureManager Validates and stores the connection string. Base for any future Azure service manager (Table, Blob, etc.).
AzureTableManager Creates the TableServiceClient, validates table names against Azure's naming rules, creates the table on first use, and guards every operation with EnsureTableReady().
AzureTableAccess The class you actually instantiate. Implements IAzureTableAccess. Sealed — not meant to be subclassed further.
IAzureTableAccess The interface to depend on in your own code (constructors, DI registrations, mocks in unit tests).
TableEntity Your custom entities inherit from this instead of Azure.Data.Tables.TableEntity directly. Exposes PartitionKey, RowKey, Timestamp, ETag.
DynamicTableEntity A ready-to-use entity for tables whose column shape you don't want to model as a C# class — properties are stored in a dictionary instead.

Getting started

Constructors

// Table name known up front — table is created (if missing) immediately.
var tableAccess = new AzureTableAccess("Customers", connectionString);

// Table name decided later.
var tableAccess = new AzureTableAccess(connectionString);
tableAccess.SetTableName("Customers");

Calling any CRUD method before a table name has been set throws a clear InvalidOperationException ("No table has been selected...") rather than a NullReferenceException.

Table naming rules

Enforced automatically (matches Azure's own requirements):

  • 3–63 characters
  • Must start with a letter
  • Letters and digits only
  • Cannot be the reserved name tables

An invalid name throws ArgumentException before any network call is made.

Defining your own entity

using DataAccess.AzureStorage.Table;

public class CustomerEntity : TableEntity
{
    public CustomerEntity() { }

    public CustomerEntity(string partitionKey, string rowKey)
    {
        PartitionKey = partitionKey;
        RowKey = rowKey;
    }

    public string Name { get; set; }
    public string Email { get; set; }
    public int Age { get; set; }
}

CRUD operations

Every operation below has a synchronous and an ...Async version. Async versions accept an optional CancellationToken.

Insert

Fails if the entity already exists (true insert — uses the SDK's AddEntity, which returns a 409 Conflict on a duplicate PartitionKey/RowKey).

var entity = new CustomerEntity("UK", Guid.NewGuid().ToString())
{
    Name = "Jane Doe",
    Email = "jane.doe@example.com",
    Age = 32
};

tableAccess.InsertEntity(entity);
await tableAccess.InsertEntityAsync(entity);

Upsert (insert-or-merge)

Use this when you want "create it if it's not there, merge changed fields if it is" — no error on duplicates.

tableAccess.UpsertEntity(entity);
await tableAccess.UpsertEntityAsync(entity);

Replace

Overwrites all properties of an existing entity. Requires the entity to already exist.

tableAccess.ReplaceEntity(entity);
await tableAccess.ReplaceEntityAsync(entity);

Merge

Updates only the properties present on the entity you pass in; any other stored properties on that row are left untouched.

tableAccess.MergeEntity(entity);
await tableAccess.MergeEntityAsync(entity);

Delete

tableAccess.DeleteEntity(entity);
await tableAccess.DeleteEntityAsync(entity);

// Or by key, without loading the entity first:
tableAccess.DeleteEntity(partitionKey: "UK", rowKey: "abc123");
await tableAccess.DeleteEntityAsync(partitionKey: "UK", rowKey: "abc123");

Retrieve

// Everything in the table
List<CustomerEntity> all = tableAccess.RetrieveEntities<CustomerEntity>();

// Filtered with an OData filter string
List<CustomerEntity> uk = tableAccess.RetrieveEntities<CustomerEntity>("PartitionKey eq 'UK'");

// Single entity
CustomerEntity one = tableAccess.RetrieveEntity<CustomerEntity>(
    "PartitionKey eq 'UK' and RowKey eq 'abc123'");

// Async
List<CustomerEntity> allAsync = await tableAccess.RetrieveEntitiesAsync<CustomerEntity>();

Filter strings follow standard Azure Table Storage OData filter syntax. Common operators: eq, ne, gt, ge, lt, le, and, or.


Optimistic concurrency (ETag)

Replace, Merge, and Delete all accept an optional ETag? ifMatch parameter:

CustomerEntity loaded = tableAccess.RetrieveEntity<CustomerEntity>(
    "PartitionKey eq 'UK' and RowKey eq 'abc123'");

loaded.Age = 33;

// Uses loaded.ETag automatically — fails with a conflict if someone else
// changed the row since you read it.
tableAccess.ReplaceEntity(loaded);

// Or pass an ETag explicitly:
tableAccess.ReplaceEntity(loaded, someTrackedETag);

// Force an unconditional write regardless of concurrent changes:
tableAccess.ReplaceEntity(loaded, ETag.All);

If you don't pass ifMatch and the entity's own ETag was never populated (e.g. a brand-new object you built yourself rather than one returned by Retrieve), the call falls back to ETag.All (unconditional) — so simple insert-then-write flows keep working without you having to think about ETags at all.


Schema-less rows with DynamicTableEntity

For tables where the columns aren't known at compile time (or vary row to row), use DynamicTableEntity instead of a custom TableEntity subclass. It supports the same full set of operations:

var entity = new DynamicTableEntity
{
    PartitionKey = "UK",
    RowKey = Guid.NewGuid().ToString()
};

entity.Set(new Dictionary<string, object>
{
    ["Name"] = "Jane Doe",
    ["Age"] = 32,
    ["SignupDate"] = DateTimeOffset.UtcNow
});

tableAccess.InsertEntity(entity);
tableAccess.UpsertEntity(entity);
tableAccess.ReplaceEntity(entity);
tableAccess.MergeEntity(entity);
tableAccess.DeleteEntity(entity);

List<DynamicTableEntity> rows = tableAccess.RetrieveEntities("PartitionKey eq 'UK'");

foreach (var row in rows)
{
    Console.WriteLine($"{row.PartitionKey}/{row.RowKey}: {row.Properties["Name"]}");
}

Notes:

  • Properties holds only your custom columnsPartitionKey, RowKey, Timestamp, and ETag are read directly off the entity itself (not duplicated inside the dictionary), so they can never go stale. Call entity.ToDictionary() if you want one flat dictionary containing everything.
  • Supported property types: string, byte[], bool, DateTime, DateTimeOffset, double, int, long, Guid — matching Azure Table Storage's own EDM type set (see EdmType). Unsupported types and null values are silently skipped, since Table Storage has no first-class "null property" concept — omit the property instead of storing null.

Error handling

All SDK-level failures are caught and re-thrown as InvalidOperationException, with the original exception preserved as InnerException (so ex.InnerException still gives you the real Azure SDK RequestFailedException, status code, etc. — nothing is swallowed).

try
{
    tableAccess.InsertEntity(entity);
}
catch (InvalidOperationException ex)
{
    Console.WriteLine(ex.Message);            // "Insert failed for PartitionKey='UK', RowKey='abc123'."
    Console.WriteLine(ex.InnerException);      // The original Azure.RequestFailedException
}

Argument problems (null entity, missing PartitionKey/RowKey, invalid table name) throw ArgumentNullException / ArgumentException immediately, before any network call is made.


Thread safety & reuse

AzureTableAccess is safe to share as a long-lived singleton per table — create one instance and reuse it across your application rather than constructing a new one per call. SetTableName is safe to call from multiple threads and is a no-op (no network call) if the table name hasn't actually changed.


Full example

using DataAccess.AzureStorage.Table;

var tableAccess = new AzureTableAccess("Customers", "<connection string>");

var entity = new CustomerEntity("UK", Guid.NewGuid().ToString())
{
    Name = "Jane Doe",
    Email = "jane.doe@example.com",
    Age = 32
};

tableAccess.InsertEntity(entity);

var loaded = tableAccess.RetrieveEntity<CustomerEntity>(
    $"PartitionKey eq '{entity.PartitionKey}' and RowKey eq '{entity.RowKey}'");

loaded.Age = 33;
tableAccess.ReplaceEntity(loaded); // ETag-checked automatically

foreach (var c in tableAccess.RetrieveEntities<CustomerEntity>("PartitionKey eq 'UK'"))
{
    Console.WriteLine($"{c.RowKey}: {c.Name}, age {c.Age}");
}

tableAccess.DeleteEntity(loaded);

Local development with Azurite

To test without a real Azure account, run the Azurite storage emulator and use:

UseDevelopmentStorage=true

as the connection string.

Product 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 is compatible. 
.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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
4.1.0 94 8/29/2026
4.0.0 130 8/24/2026
3.0.0 348 4/17/2025
2.0.0 942 7/19/2022 2.0.0 is deprecated because it is no longer maintained and has critical bugs.
1.1.0 1,210 12/16/2020 1.1.0 is deprecated because it is no longer maintained and has critical bugs.

This is 4.0.0 version for accessing Azure Storage