DataAccess.AzureStorage 4.1.0

dotnet add package DataAccess.AzureStorage --version 4.1.0
                    
NuGet\Install-Package DataAccess.AzureStorage -Version 4.1.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.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="DataAccess.AzureStorage" Version="4.1.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.1.0
                    
#r "nuget: DataAccess.AzureStorage, 4.1.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.1.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.1.0
                    
Install as a Cake Addin
#tool nuget:?package=DataAccess.AzureStorage&version=4.1.0
                    
Install as a Cake Tool

CI Pipeline

GitHub repo size GitHub stars GitHub license Contributions welcome

DataAccess.AzureStorage

A dependency-light data access layer over Azure Table Storage and Azure Blob Storage, built on the official Azure.Data.Tables and Azure.Storage.Blobs SDKs. 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 — 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.

Also it covers the DataAccess.AzureStorage.Blob namespace — blob file and connection string are supplied by the caller, and everything else (container creation, file upload-download-delete, optimistic concurrency) is handled for you.

Features

Azure Table Storage

  • Simple connection-string based initialization
  • Automatic table creation
  • Table name validation
  • Strongly typed entity support
  • Dynamic entity support
  • CRUD operations
  • OData filtering
  • Async APIs
  • CancellationToken support
  • ETag-based optimistic concurrency
  • Automatic handling of supported Azure Table EDM/CLR types
  • Clear exception wrapping
  • Reusable access objects

Azure Blob Storage

  • Simple connection-string based initialization
  • Automatic container creation
  • Container name validation
  • Upload from Stream
  • Download to byte[]
  • Download content type
  • Blob existence check
  • Blob deletion
  • Delete by absolute URL
  • Download by absolute URL
  • Virtual directory support
  • Overwrite/non-overwrite upload modes
  • Blob ETag information
  • Blob details including size, content type and last modified time
  • Async APIs
  • CancellationToken support
  • Path sanitization and basic path-traversal protection

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)
  • NuGet package: Azure.Data.Blobs (v12.26.0 or compatible)
  • An Azure Storage account connection string (or the Azurite emulator connection string for local dev)
  • The library accepts: UseDevelopmentStorage=true

Installation

Install the DataAccess.AzureStorage package from NuGet.

Install-Package DataAccess.AzureStorage

Or:

dotnet add package DataAccess.AzureStorage

The package currently targets .NET Standard 2.0 and .NET Standard 2.1.

The package depends on:

Azure.Data.Tables >= 12.12.0
Azure.Storage.Blobs >= 12.26.0

These dependencies are defined by the package specification and project configuration.

Part 1 — Table DataAccess.AzureStorage.Table

Architecture

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

IAzureTableAccess / AzureTableAccess       — public contract; program against this for testability/DI
IAzureTableEntity / AzureTableEntity       — 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
EdmTypeMap                                 — internal static class to handle Supported CLR Type for entity
Type Role
AzureStorageManager 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).
IAzureTableEntity Your custom entities inherit from this instead of Azure.Data.Tables.AzureTableEntity directly. Exposes PartitionKey, RowKey, Timestamp, ETag.
AzureTableEntity Your custom entities inherit from IAzureTableEntity.
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.
var tableAccess = new AzureTableAccess("MyTable", "<connection string>");
tableAccess.InsertEntity(myEntity);
tableAccess.ReplaceEntity(myEntity);

Getting started

// 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 : AzureTableEntity
{
    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 AzureTableEntity 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.Get() 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.


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

Part 2 — Blob Storage DataAccess.AzureStorage.Blob

Architecture

AzureStorageManager             (abstract) — holds & validates the connection string
    └── AzureBlobManager        (abstract) — owns BlobServiceClient / BlobContainerClient, container name validation & creation
            └── AzureBlobAccess   (sealed) — implements IAzureBlobAccess; all upload/list/delete/download operations

IAzureBlobAccess / AzureBlobAccess         — public contract; program against this for testability/DI
IBlobRequestClient / BlobRequestClient     — describes a file to upload (stream, file name, content type, virtual directory path)
IBlobResponseClient / BlobResponseClient   — details of a blob returned after a successful upload (URI, container, ETag, etc.)
IBlobDetails / BlobDetails                 — metadata for a blob returned by a listing operation
Type Role
AzureStorageManager Validates and stores the connection string. Shared base for any Azure service manager (Table, Blob, etc.).
AzureBlobManager OCreates the BlobServiceClient, validates container names against Azure's naming rules, creates the container on first use, and guards every operation with EnsureContainerReady().
AzureBlobAccess The class you actually instantiate. Implements IAzureBlobAccess. Sealed — not meant to be subclassed further.
IAzureBlobAccess The interface to depend on in your own code (constructors, DI registrations, mocks in unit tests).
BlobRequestClient Describes a file to upload — file stream, file name, content type, and an optional virtual directory path (e.g. new[] { "invoices", "2026" }).
BlobResponseClient What you get back after a successful upload — the blob's Uri, ContainerName, AccountName, AbsolutePath, and ETag (for a later conditional operation).
BlobDetails Metadata for a single blob returned by ListBlobDetails — Path, ContentLength, ContentType, LastModified, ETag.
var blobAccess = new AzureBlobAccess("container-name", "<connection string>");
var blobRequestClient = new BlobRequestClient("<fileStream>", "<FileNameWithExtension>");
    blobRequestClient.Directories = new string[] { "NewFolder" };
    blobRequestClient.ContentType = <MIME Content Type>";
var blobResponseClient = blobAccess.Upload(blobRequestClient)
string filePath = blobResponseClient.Path;

Getting started

// Container name known up front — container is created (if missing) immediately.
var blobAccess = new AzureBlobAccess("customer-photos", connectionString);

// Container name decided later.
var blobAccess = new AzureBlobAccess(connectionString);
blobAccess.SetContainer("customer-photos");

Calling any operation before a container has been selected throws a clear InvalidOperationException ("No container has been selected...") rather than a NullReferenceException.

Container naming rules

Enforced automatically, matching Azure's own requirements:

  • 3–63 characters
  • lowercase letters/numbers/single hyphens only
  • must start and end with a letter or number
  • no consecutive hyphens
  • Casing is normalized automatically — "Customer-Photos" is accepted and stored as "customer-photos".

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

Uploading files

var request = new BlobRequestClient(fileStream, "invoice-1042.pdf")
{
    ContentType = "application/pdf",
    Directories = new[] { "invoices", "2026" } // uploads to "invoices/2026/invoice-1042.pdf"
};

IBlobResponseClient result = blobAccess.Upload(request);
await blobAccess.UploadAsync(request);

Console.WriteLine(result.Uri);   // https://mystorageacct.blob.core.windows.net/customer-photos/invoices/2026/invoice-1042.pdf
Console.WriteLine(result.ETag);  // for a later conditional operation against this exact blob

By default, Upload overwrites an existing blob at that path with no error. Pass overwrite: false for true "insert" semantics — fails instead if a blob already exists there:

blobAccess.Upload(request, overwrite: false);

Listing blobs

// Everything in the container
List<IBlobDetails> all = blobAccess.ListBlobDetails();

// Only blobs under a virtual folder path
List<IBlobDetails> invoices2026 = blobAccess.ListBlobDetails(new[] { "invoices", "2026" });

// Async
List<IBlobDetails> allAsync = await blobAccess.ListBlobDetailsAsync();

foreach (var blob in all)
{
    Console.WriteLine($"{blob.Path} — {blob.ContentLength} bytes, {blob.ContentType}, modified {blob.LastModified}");
}

Checking existence

bool exists = blobAccess.Exists("invoices/2026/invoice-1042.pdf");
bool existsAsync = await blobAccess.ExistsAsync("invoices/2026/invoice-1042.pdf");

Deleting blobs

Two forms, depending on whether you're working within the currently selected container or across containers:

// Name-relative — operates within the currently selected container.
bool deleted = blobAccess.Delete("invoices/2026/invoice-1042.pdf");
await blobAccess.DeleteAsync("invoices/2026/invoice-1042.pdf");

// By full URL — may belong to a different container than the one currently
// selected. Does NOT change the selected container, and does NOT create the
// target container if it's missing.
bool deletedByUrl = blobAccess.DeleteByUrl(
    "https://mystorageacct.blob.core.windows.net/archived-invoices/2024/old-invoice.pdf");
await blobAccess.DeleteByUrlAsync(url);

Both return false (not an exception) if no blob existed at that path.

Downloading files

// Name-relative
(byte[] fileContent, string contentType) = blobAccess.DownloadFile("invoices/2026/invoice-1042.pdf");
var asyncResult = await blobAccess.DownloadFileAsync("invoices/2026/invoice-1042.pdf");

// By full URL, across containers
var byUrl = blobAccess.DownloadFileByUrl("https://mystorageacct.blob.core.windows.net/archived-invoices/2024/old-invoice.pdf");

Downloads load the entire blob into memory as byte[]. For very large files, consider working directly against ContainerClient/BlobClient (exposed via the base class) for a streaming download instead.

Error handling (Blob)

Same pattern as the Table side — SDK failures are wrapped in InvalidOperationException with the original exception preserved as InnerException:

try
{
    blobAccess.Upload(request, overwrite: false);
}
catch (InvalidOperationException ex)
{
    Console.WriteLine(ex.Message);        // "Upload failed for blob 'invoices/2026/invoice-1042.pdf' in container 'customer-photos'."
    Console.WriteLine(ex.InnerException); // The original Azure.RequestFailedException
}

ArgumentException/ArgumentNullException are thrown immediately for bad input (empty blob name, invalid container name, a blobUrl missing a container or blob segment, a path-traversal segment like ..) — before any network call is made.


Thread safety & reuse

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


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 129 8/24/2026
3.0.0 348 4/17/2025
2.0.0 941 7/19/2022 2.0.0 is deprecated because it is no longer maintained and has critical bugs.
1.1.0 1,209 12/16/2020 1.1.0 is deprecated because it is no longer maintained and has critical bugs.

This is 4.1.0 version for accessing Azure Storage.
Rename 'TableEntity' to 'AzureTableEntity'.
Introduce interface 'IAzureTableEntity'
Bug fix for DateTime property and unsupported property