Unified.Data.Tables.Abstractions 0.2.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package Unified.Data.Tables.Abstractions --version 0.2.0
                    
NuGet\Install-Package Unified.Data.Tables.Abstractions -Version 0.2.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="Unified.Data.Tables.Abstractions" Version="0.2.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Unified.Data.Tables.Abstractions" Version="0.2.0" />
                    
Directory.Packages.props
<PackageReference Include="Unified.Data.Tables.Abstractions" />
                    
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 Unified.Data.Tables.Abstractions --version 0.2.0
                    
#r "nuget: Unified.Data.Tables.Abstractions, 0.2.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 Unified.Data.Tables.Abstractions@0.2.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=Unified.Data.Tables.Abstractions&version=0.2.0
                    
Install as a Cake Addin
#tool nuget:?package=Unified.Data.Tables.Abstractions&version=0.2.0
                    
Install as a Cake Tool

Unified.Data.Tables

A small, reusable Azure Table Storage data layer for .NET: a generic IStorage<T> repository with in-memory caching, optimistic concurrency, builder-driven partial updates, role-gated properties, and a reflection-based object-graph serializer that transparently handles nested types and the 64 KB per-cell limit.

It exists so the same battle-tested storage primitives can be shared across projects instead of being copy-pasted into each one.


Features

  • Generic repositoryIStorage<T> over Azure Tables, one table per entity type (typeof(T).Name).
  • Reflection-based serializer — flattens nested objects into Parent_Child columns, stores enums as strings and money-style decimals as doubles, and falls back to JSON (then GZip) for collections and complex graphs. Oversized cells are compressed — and, as a last resort, truncated — so a single large property can never blow the 64 KB limit and lose the whole row.
  • In-memory caching — reads are served from IMemoryCache (1 hour sliding TTL); writes keep the entity cache coherent and invalidate the relevant query caches automatically.
  • Optimistic concurrency (ETag) — a caller-supplied ETag enforces strict concurrency (a conflict surfaces as RequestFailedException 412 → map to 409); without one, a stale-cache conflict is retried once.
  • Partial (Merge) updatesUpdateAsync(id, builder) writes only the columns you declare, leaving the rest of the row untouched with no read required.
  • Protected properties — mark a property [ProtectedProperty("admin,...")] and role-gate writes through a pluggable IProtectedPropertyAuthorizer (the package itself has no ASP.NET Core dependency).
  • Batch partition deleteDeletePartitionAsync removes a whole partition in 100-entity transactions.
  • Composite id conventionId is "{PartitionKey}|{RowKey}"; row keys may themselves contain |.

Installation

dotnet add package Unified.Data.Tables

This is shipped as two packages:

Package Contents Use it in
Unified.Data.Tables TableStorage<T>, the serializer, DI helpers (Azure dependencies) server / host projects
Unified.Data.Tables.Abstractions Entity, IStorage<T>, UpdateBuilder<T>, [ProtectedProperty], IProtectedPropertyAuthorizer — no Azure/hosting deps shared/domain & Blazor WebAssembly projects

Unified.Data.Tables references the abstractions transitively, so most apps just install it. In a browser-safe shared library that only defines entities and repository contracts, reference the abstractions alone:

dotnet add package Unified.Data.Tables.Abstractions

Requires .NET 10 and an Azure Storage account (or the local Azurite emulator).


Quick start

1. Define an entity

Derive from Entity. Id is the composite "{PartitionKey}|{RowKey}"; Created, Modified and ETag are managed for you.

using Unified.Data.Tables;

public sealed class Customer : Entity
{
    public string Name { get; set; } = "";
    public string Email { get; set; } = "";
    public decimal Balance { get; set; }
}

2. Register it

using Azure.Data.Tables;
using Unified.Data.Tables;

// Option A — build the TableServiceClient from a connection string:
builder.Services.AddUnifiedTableStorage(connectionString);

// Option B — you already register a TableServiceClient yourself:
builder.Services.AddSingleton(_ => new TableServiceClient(connectionString));
builder.Services.AddUnifiedTableStorage();

Both register IMemoryCache and the open-generic IStorage<T>TableStorage<T> mapping as singletons.

3. Use it

public sealed class CustomerService(IStorage<Customer> storage)
{
    public Task<Customer> Add(Customer c)          => storage.CreateAsync(c);
    public Task<Customer?> Get(string id)          => storage.OneAsync(id);
    public Task<bool> Exists(string id)            => storage.ExistsAsync(id);
    public Task<IEnumerable<Customer>> InRegion(string region) => storage.QueryAsync(region);
    public Task Remove(string id)                  => storage.DeleteAsync(id);
}
// PartitionKey = "eu", RowKey = "alice@example.com"
var customer = new Customer
{
    Id = "eu|alice@example.com",
    Name = "Alice",
    Email = "alice@example.com",
    Balance = 100m
};

await storage.CreateAsync(customer);           // ids are normalized: trim → spaces to '-' → lower-case
var loaded  = await storage.OneAsync("eu|alice@example.com");
var euOnes  = await storage.QueryAsync("eu");  // scope to a partition (omit for the whole table)

IStorage<T>

Method Description
CreateAsync(entity) Insert a new row; returns it with its populated ETag.
OneAsync(id) Fetch one row by composite id, or null.
ExistsAsync(id) Whether a row exists (cache-aware).
QueryAsync(partition?) All rows, or just one partition when supplied.
UpdateAsync(entity) Full replace with ETag concurrency.
UpdateAsync(id, builder) Partial Merge — writes only the declared columns.
DeleteAsync(id) Delete one row.
DeletePartitionAsync(partition) Batch-delete a whole partition; returns the count.

Partial updates

Only the properties you set are written; everything else on the row is preserved, and no read is needed.

await storage.UpdateAsync("eu|alice@example.com", b => b
    .SetProperty(x => x.Balance, 250m)
    .SetProperty(x => x.Name, "Alice A."));

Optimistic concurrency

OneAsync/QueryAsync populate Entity.ETag. Pass it back on UpdateAsync(entity) for a strict check — a concurrent modification throws RequestFailedException with status 412.

var c = await storage.OneAsync(id);
c!.Balance += 100;
await storage.UpdateAsync(c);   // 412 if the row changed since it was read

Protected properties

Gate sensitive columns behind roles. Enforcement runs on the whole-entity update path and needs an IProtectedPropertyAuthorizer; if none is registered, changing a protected value is denied.

public sealed class Employee : Entity
{
    public string Name { get; set; } = "";

    [ProtectedProperty("admin,accountant")]
    public decimal Salary { get; set; }
}

Provide an authorizer from your host (this is where the ClaimsPrincipal lives — kept out of the package so it stays dependency-light):

public sealed class RoleAuthorizer(IHttpContextAccessor accessor) : IProtectedPropertyAuthorizer
{
    public bool IsAllowed(string roles) =>
        roles.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)
             .Any(role => accessor.HttpContext?.User.IsInRole(role) == true);
}

builder.Services.AddHttpContextAccessor();
builder.Services.AddSingleton<IProtectedPropertyAuthorizer, RoleAuthorizer>();

For the builder path, protected properties are rejected unless you opt in after verifying authorization:

await storage.UpdateAsync(id, b => b.AllowProtected().SetProperty(x => x.Salary, 5000m));

Serialization

TableStorage<T> uses TableEntitySerializer under the hood, but you can call it directly:

TableEntity row = customer.ToTableEntity(partitionKey, rowKey);
Customer     back = row.FromTableEntity<Customer>();
  • Scalars map to native cells; enums become strings; decimal is stored as double.
  • Nested objects fan out to Parent_Child columns.
  • Collections / complex graphs serialize to JSON (__Json column suffix), GZip-compressed (__GZip) when they exceed the cell limit.
  • Legacy tolerance — a stored date surfaced by the SDK as a DateTime or a string still deserializes into a DateTimeOffset (or DateTime) property without throwing.
  • Set persistType: true to embed the type name and use the late-bound FromTableEntity() overload.

Id convention

Entity.Id is a composite string split on the first |:

Id PartitionKey RowKey
"eu|alice" eu alice
"vision|exec|agent" vision exec|agent
"single" single single

Ids are normalized on write (trim → replace spaces with '-' → ToLowerInvariant). Id is also stored as a data column, so the full composite id is preserved on read.


Building & testing

dotnet build Unified.Data.Tables.slnx -c Release
dotnet test  Unified.Data.Tables.slnx -c Release

The test suite mixes fast unit tests (Azure SDK mocked with NSubstitute) with integration tests that run against a local Azurite emulator. The integration tests self-skip when Azurite is not reachable, so dotnet test is safe to run anywhere.


License

Copyright © Serhii Seletskyi. Add a LICENSE file (for example, MIT) before publishing to NuGet.

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

    • No dependencies.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on Unified.Data.Tables.Abstractions:

Package Downloads
Unified.Data.Tables

Azure Table Storage implementation for Unified.Data.Tables: TableStorage<T> (IStorage<T>) with in-memory caching, ETag optimistic concurrency, builder-driven partial (Merge) updates, role-gated protected properties, and a reflection-based object-graph serializer (nested types, JSON/GZip fallback, 64KB cell handling). Builds on Unified.Data.Tables.Abstractions.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.5.3 112 7/13/2026
0.5.2 113 7/13/2026
0.5.1 125 7/12/2026
0.5.0 133 7/12/2026
0.4.0 128 7/10/2026
0.3.1 120 7/10/2026
0.3.0 115 7/10/2026
0.2.1 114 7/6/2026
0.2.0 110 7/6/2026