SnapshotStore 0.1.3

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

SnapshotStore

CI NuGet License: MIT

Incremental read-through snapshot cache. LoadAsync(name, source) starts from a stored snapshot, asks the source only for records changed since its high-water mark, merges by key, and writes the result back when enough changed. The snapshot is an optimisation, never a source of truth: cache failures are logged and absorbed, source failures propagate.

Built for the common two-level shape: a Table (or any paged store that only supports "changed since") is the system of record, and a Blob holds the materialised snapshot on top of it — one big cheap read instead of re-reading every row, every time. SnapshotStore.Table + SnapshotStore.Azure ship exactly that pair. Redis is a single-storage alternative when you want both sides in the same place.

Why this instead of rolling your own

  • The hard part is the CAS. Two writers loading concurrently must never let a slow one overwrite a fresher snapshot. Both backings get this via a version token (Blob ETag, or a Redis-side counter) with the exact same contract, so switching backing doesn't change SnapshotStore<T>'s behaviour at all.
  • The merge is idempotent by construction. The high-water read bound is inclusive on purpose — the record that set the mark is safely re-read — so there is no off-by-one to get wrong at the boundary.
  • A failed cache never breaks correctness. Read, decode or write failures on the snapshot are logged and absorbed; only a failure of the source (the real data) propagates. Get this backwards and a cache bug looks like data loss.
  • AOT-friendly and dependency-light. The core (SnapshotStore) has two dependencies total (Microsoft.Extensions.Logging.Abstractions, OpenTelemetry.Api) and no storage SDK — see Storage backings.

Quick start

The common case — Table Storage as your data, Blob Storage as the cached snapshot:

dotnet add package SnapshotStore
dotnet add package SnapshotStore.Table
dotnet add package SnapshotStore.Azure
var source = new TableRecordSource<Order>(tableClient, OrderJson.Default.Order);   // SnapshotStore.Table
await source.WriteAsync(order, ct);   // however your data actually gets into Table

var store = new SnapshotStore<Order>(
    new AzureBlobSnapshotStore(blobContainer),                                     // SnapshotStore.Azure
    new JsonGzipSnapshotSerializer<Order>(OrderJson.Default.Order));

Snapshot<Order> snap = await store.LoadAsync($"{tenant}/{accountId}/Order", source, ct);

LoadAsync reads the Blob snapshot, asks Table only for rows changed since its high-water mark, and writes the merged result back to Blob. A runnable version, against a real Azurite, is in samples/SnapshotStore.Sample.

Already have a different source of "changed since" — a paged API, your own Table client, anything else? Implement IRecordSource<T> (one method) instead of using SnapshotStore.Table; the Blob snapshot side is unchanged.

Or use Redis for both sides instead of Table+Blob (SnapshotStore.Redis):

var store = new SnapshotStore<Order>(
    new RedisSnapshotStore(database, keyPrefix: "myservice:"),   // ISnapshotBlobStore
    new JsonGzipSnapshotSerializer<Order>(OrderJson.Default.Order));

var source = new RedisRecordSource<Order>(database, name: "orders", OrderJson.Default.Order, keyPrefix: "myservice:");
await source.WriteAsync(order, ct);

Storage backings

Two things are pluggable, independently: where the snapshot is kept (ISnapshotBlobStore) and where the source data comes from (IRecordSource<T>). Each backing is its own package that pulls in only its own client — none of SnapshotStore.Table, .Azure or .Redis restores another's dependency, and the core restores none of them. Mix and match by referencing whichever packages you need.

Package Contents Depends on
SnapshotStore interfaces, SnapshotStore<T>, JsonGzipSnapshotSerializer<T>, telemetry Microsoft.Extensions.Logging.Abstractions, OpenTelemetry.Api
SnapshotStore.Table TableRecordSource<T>(TableClient) — source side only, no snapshot storage. One row per record (RowKey = your Key, any characters — encoded transparently), a queryable timestamp column for "changed since". A property caps at 64 KB, an entity at 1 MB: many small-to-medium records, not a few huge ones core, Azure.Data.Tables
SnapshotStore.Azure AzureBlobSnapshotStore(BlobContainerClient) — snapshot side only, ETag-conditional writes. No IRecordSource<T>; pair it with SnapshotStore.Table or your own source core, Azure.Storage.Blobs
SnapshotStore.Redis RedisSnapshotStore(IDatabase) (Lua CAS, byte-size cap) and RedisRecordSource<T> (sorted-set index + hash, keyset-paginated so a tie band larger than one page is never skipped) — both sides, one store core, StackExchange.Redis

The caller owns credentials/connections in every package — a TableClient, BlobContainerClient or IConnectionMultiplexer passed in; none of them read config or build a client for you.

Behaviour spec

  • Load. Read the stored snapshot (if any); on any read/decode failure or an incompatible format version, treat it as absent — log, don't fail. Ask the source for records Timestamp >= highWater (or everything if there is no snapshot); the bound is inclusive, so the merge must be (and is) idempotent by key. Merge by Key, a later record replacing an earlier one; advance the high-water mark to the largest timestamp seen. Write back only when MinChangesToSave (default 100) or more records changed.

  • Concurrency. Writes are compare-and-swap on the blob's version: the first write only if none exists, later ones only if the version read at the start of the load is still current. A slow writer can never replace a newer snapshot with an older one.

  • Failure semantics.

    Failure Behaviour
    Snapshot read fails, is corrupt, or an old format version Log, rebuild from source
    Snapshot write fails Log, return the data; next load re-reads the delta
    Another writer saved first (version mismatch) Keep theirs, Saved = false
    Source fails Exception propagates — never masked by stale cache

Telemetry

Same shape as RedisEvents: one static ActivitySource and Meter, both named SnapshotStore. Without wiring, spans and metrics are produced and dropped at zero cost (StartActivity returns null; every attribute is guarded by IsAllDataRequested).

tracing.AddSnapshotStore();   // TracerProviderBuilder
metrics.AddSnapshotStore();   // MeterProviderBuilder

Spans — snapshots.load with snapshots.cache.read, snapshots.source.read, snapshots.cache.write children. A source failure marks the load span failed; an absorbed cache failure marks only its child.

Attribute On Meaning
snapshots.name all Snapshot name — unbounded, so span-only, never a metric tag
snapshots.record_type load Record type name
snapshots.cache load, cache.read hit | miss | incompatible | error
snapshots.records / snapshots.changes / snapshots.saved load Result size, records merged, whether written
snapshots.since_unix_ms source.read High-water mark the read started from; absent for a full read
snapshots.pages / snapshots.changes source.read Pages returned, records merged
snapshots.save_result / snapshots.bytes cache.write saved | conflict | error; compressed size

Metrics — tags are record_type plus a closed set of outcomes; never the snapshot name.

Instrument Type Tags
snapshots.loads counter record_type, cache
snapshots.changes counter record_type
snapshots.saves counter record_type, result
snapshots.cache.errors counter record_type, operation (read / write)
snapshots.source.errors counter record_type
snapshots.load.duration histogram, ms record_type, cache
snapshots.source.duration histogram, ms record_type
snapshots.records histogram record_type
snapshots.snapshot.bytes histogram, By record_type

No gauges — the store holds no state between loads.

Development

dotnet build SnapshotStore.slnx
dotnet test SnapshotStore.slnx --filter "TestType!=PerfTest"   # Redis/Table tests need Docker (real Redis/Azurite)

Versions come from Nerdbank.GitVersioning (version.json); every push to main that passes tests publishes a new patch version to NuGet. See CHANGELOG.md and CONTRIBUTING.md.

Status: 0.x — the API may still move. Spans/metrics attribute names are more likely to be stable than anything else.

Licence

MIT.

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.

NuGet packages (3)

Showing the top 3 NuGet packages that depend on SnapshotStore:

Package Downloads
SnapshotStore.Azure

Azure Blob storage backing for SnapshotStore.

SnapshotStore.Redis

Redis backing for SnapshotStore: a snapshot store, and a record source, over StackExchange.Redis.

SnapshotStore.Table

Azure Table Storage record source for SnapshotStore: the common two-level shape, Table as the system of record, Blob (SnapshotStore.Azure) as the cached snapshot.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.1.3 136 9/22/2026
0.1.2 117 9/22/2026