PeterJuhasz.Repositories.Abstractions 2.1.0-preview.8

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

Repositories

Lightweight storage abstractions over blobs: objects, collections, sets, counters, queues, indexes and locks, with optimistic concurrency, serialization, compression, caching and locking.

Examples, store objects easily in blob storage:

/dashboards/{customerId}/{dashboardId}.json
/favorites/{customerId}/favorites.json
/audit-logs/{customerId}/{year}/{month}/{day}.jsonl
/exports/{customerId}/{exportId}.tsv
/count-of-unread-alerts/{customerId}.txt
/profile-pictures/{userId}/{pictureId}.webp

Example to map to a repository:

var repository = blobServiceClient.GetBlobContainerClient("dashboards")
	.GetPartition(customerId)
	.GetBlob($"{dashboardId}.json")
		.WithCompression(BrotliCompressionOptions.Maximum)
		.WithCaching(CacheOptions.AlwaysRevalidate)
	.AsJsonObjectRepository<Dashboard>(JsonSerializerOptions.Web);

See more examples below.

Repositories

Blob

IBlob is the lowest level abstraction: a named binary payload with an ETag, media type and metadata.

IBlob blob = partition.GetBlob("item.json");

Reads and writes carry a concurrency token for optimistic concurrency:

IBlob.BlobReadResult? result = await blob.ReadAsync(ct);
string etag = await blob.WriteAsync(data, result?.Info.ConcurrencyToken, new(MediaType: "application/json"), ct);
await blob.DeleteIfExistsAsync(ct);

Use TransformAsync to read, change and write back with automatic retries on conflict:

await blob.TransformAsync(data => Append(data), ct);

Object repository

The IObjectRepository<T> stores single objects.

var repository = container.GetPartition().GetBlob("item.json")
	.AsObjectRepository<Item>(serializer);

Usage:

Versioned<Item> versioned = await repository.CreateAsync(newItem, ct);
bool exists = await repository.ExistsAsync(ct);
versioned = await repository.UpdateAsync(updatedItem, versioned.ETag, ct);
await repository.DeleteAsync(versioned.ETag, ct);

Use ApplyAsync to update an existing object:

await repository.ApplyAsync(item => item with { Count = item.Count + 1 }, ct);

Collection repository

The ICollectionRepository<T> stores a list of items in a single blob.

var repository = partition.GetBlob("items.json").AsJsonCollectionRepository<Item>(JsonSerializerOptions.Web);

Usage:

await repository.AddAsync(item, ct);
IReadOnlyCollection<Item> items = await repository.ListAsync(ct);
await repository.UpdateAsync(i => i.Id == id, i => i with { Done = true }, ct);
await repository.DeleteAsync(i => i.Id == id, ct);

All mutations go through ApplyAsync, which retries on concurrency conflicts:

await repository.ApplyAsync(items => items.Safe().Add(item), ct);

Use AsStreamingCollectionRepository instead to serialize directly to and from the network stream, without buffering the whole collection:

var repository = partition.GetBlob("items.json").AsStreamingJsonCollectionRepository<Item>(JsonSerializerOptions.Web);

await foreach (var item in repository.AsAsyncEnumerableAsync(ct))
{
}

Binary repository

The IBinaryRepository stores raw payloads, as BinaryData or as a stream.

var repository = partition.GetBlob("photo.jpg").AsBinaryRepository();

Usage:

await repository.CreateAsync(BinaryData.FromBytes(bytes), ct);
BinaryData? data = await repository.GetAsync(ct);
await using Stream? stream = await repository.GetStreamAsync(ct);
await repository.DeleteIfExistsAsync(ct);

Append blob

An IAppendBlob can only be appended to. Exposed as a collection repository of newline delimited items:

var repository = partition.GetAppendBlob("log.jsonl").AsJsonLineCollectionRepository<LogEntry>(JsonSerializerOptions.Web);

Usage:

await repository.AddAsync(entry, ct);
await repository.AddRangeAsync([entry1, entry2], ct);

await foreach (var item in repository.AsAsyncEnumerableAsync(ct))
{
}

Items can only be added and the whole blob cleared, so ApplyAsync throws NotSupportedException.

Counter

The IDistributedCounter stores a single number, updated with optimistic concurrency.

IDistributedCounter counter = partition.GetBlob("visits").AsCounter();

Usage:

int value = await counter.IncrementAsync(ct);
value = await counter.DecrementAsync(delta: 5, ct);
value = await counter.GetAsync(ct);
await counter.ResetAsync(ct);

Queue

The IStorageQueue<T> sends and receives messages.

Usage:

await queue.SendMessageAsync(job, new(), ct);
long count = await queue.GetCountAsync(ct);

await foreach (var message in queue.ReceiveAsync(batchCount: 32, visibilityTimeout: TimeSpan.FromMinutes(5), ct))
{
}

Use WithDeduplication to drop messages already sent, tracked in a set:

var deduplicated = queue.WithDeduplication(set, job => job.Id);

Set

The ISetRepository<T> stores a set of unique items in a single blob.

ISetRepository<string> set = partition.GetBlob("tags.json").AsSetRepository<string>(serializer);

Usage:

bool added = await set.AddAsync("red", ct);
bool contains = await set.ContainsAsync("red", ct);
await set.DeleteAsync("red", ct);

Alternatively, store each item as an empty blob named after the item, which scales to large sets and makes membership checks a single request:

ISetRepository<string> set = partition.AsStringSetRepositoryAsNames();

One to one foreign key index

The IOneToOneForeignKeyIndex maps a foreign key to a single principal key, stored as one small blob per foreign key.

IOneToOneForeignKeyIndex index = partition.AsOneToOneForeignKeyIndex();

Usage:

await index.AddAsync(email, userId, ct);
string? userId = await index.GetOrDefaultAsync(email, ct);
await index.DeleteIfExistsAsync(email, ct);

AddAsync throws ConflictException if the foreign key is already taken, which makes it usable for reserving unique values.

One to many foreign key index

The IOneToManyForeignKeyIndex maps a principal key to many foreign keys, stored as one collection blob per principal key.

IOneToManyForeignKeyIndex index = partition.AsOneToManyForeignKeyIndexInBlob(JsonSerializerOptionsJsonCollectionSerializer<string>.Web);

Usage:

await index.AddAsync(userId, orderId, ct);
bool contains = await index.ContainsAsync(userId, orderId, ct);

await foreach (var key in index.ListAsync(userId, ct))
{
}

Alternatively, store each relation as an empty blob in a sub partition per principal key, which avoids rewriting the whole list on every change:

IOneToManyForeignKeyIndex index = partition.AsOneToManyForeignKeyIndexInBlobNames();

Lock

The IDistributedLock coordinates across processes by taking a lease on a blob.

IDistributedLock distributedLock = partition.GetBlob("job.lock").AsLock(new(WaitPeriod: TimeSpan.FromSeconds(1)));

Usage:

if (await distributedLock.TryEnterAsync(TimeSpan.FromMinutes(1), ct) is string token)
{
	try
	{
		// do work
	}
	finally
	{
		await distributedLock.ReleaseAsync(token);
	}
}

Use WaitForEnterAsync to wait for the lock instead of failing fast.

Serialization

ISerializer<T> serializes a single object, ICollectionSerializer<T> a collection.

JSON

Pass JsonSerializerOptions:

var repository = blob.AsJsonObjectRepository<Item>(JsonSerializerOptions.Web);

Pass JsonTypeInfo or a JsonSerializerContext for source generated, AOT friendly serialization:

var repository = blob.AsJsonObjectRepository<Item>(AppJsonSerializerContext.Default.Item);
var collection = blob.AsJsonCollectionRepository<Item>(AppJsonSerializerContext.Default);

CSV, TSV

Collections can be stored as separated values:

var csv = blob.AsCsvCollectionRepository<Item>();
var tsv = blob.AsTsvCollectionRepository<Item>();

Compression

WithCompression wraps a blob so its content is compressed on write and decompressed on read, setting Content-Encoding accordingly.

Brotli

var blob = partition.GetBlob("items.json").WithBrotliCompression();

GZip

var blob = partition.GetBlob("items.json").WithCompression(GZipCompressionOptions.Maximum);

Caching

WithCaching wraps a blob with an in-memory cache. CacheOptions.Immutable never revalidates, CacheOptions.AlwaysRevalidate checks the ETag on every read.

var blob = partition.GetBlob("item.json").WithCaching(CacheOptions.AlwaysRevalidate);

Use a SlidingExpiration to revalidate only after a period of inactivity:

var blob = partition.GetBlob("item.json").WithCaching(new(SlidingExpiration: TimeSpan.FromMinutes(5)));

Caching can be used on the repositories layer too.

Locking

WithLocking serializes access to a blob within the process, so concurrent callers do not race and retry against each other. For coordination across processes, use the distributed lock.

var blob = partition.GetBlob("item.json").WithLocking();

Pass a shared SemaphoreSlim to serialize access across multiple blobs:

var blob = partition.GetBlob("item.json").WithLocking(semaphore);

Providers

Azure

var container = new BlobServiceClient(connectionString).GetBlobContainerClient("data");
IBlobPartition partition = container.GetPartition("users");

Queues are backed by QueueClient:

var queue = new QueueServiceClient(connectionString).GetQueueClient("jobs");
IStorageQueue<Job> queue = queue.AsJson<Job>();

File system

Data can be stored in the file system, for example for local development or testing.

var fileBlob = new DirectoryInfo(@"C:\data").GetPartition("users")
	.GetBlob("item.json");

In-memory

In-memory implementations are useful for testing and prototyping.

IBlob blob = new InMemoryBlob("item.json", TimeProvider.System);
IAppendBlob appendBlob = new InMemoryAppendBlob("log.jsonl", TimeProvider.System);

Examples

Store objects in partitions

Structure:

/dashboards/{customerId}/{dashboardId}.json

Code example:

var repository = blobServiceClient.GetBlobContainerClient("dashboards")
	.GetPartition(customerId)
	.GetBlob($"{dashboardId}.json")
		.WithCompression(BrotliCompressionOptions.Maximum)
	.AsJsonObjectRepository<Dashboard>(JsonSerializerOptions.Web)
		.WithCaching(CacheOptions.AlwaysRevalidate);

Store a collection of objects in a single blob

Structure:

/favorites/{customerId}/favorites.json
/exports/{customerId}/{exportId}.tsv

Code example:

var repository = blobServiceClient.GetBlobContainerClient("favorites")
	.GetPartition(customerId)
	.GetBlob("favorites.json")
		.WithCompression(BrotliCompressionOptions.Maximum)
		.WithCaching(CacheOptions.AlwaysRevalidate)
	.AsJsonCollectionRepository<Favorite>(JsonSerializerOptions.Web);

Store log entries in an append blob

Structure:

/audit-logs/{customerId}/{year}/{month}/{day}.jsonl

Code example:

var repository = blobServiceClient.GetBlobContainerClient("audit-logs")
	.GetPartition(customerId)
	.GetAppendBlob($"{year}/{month}/{day}.jsonl")
	.AsJsonLineCollectionRepository<LogEntry>(JsonSerializerOptions.Web);

await repository.AddAsync(entry, ct);

Store a counter in a blob

Structure:

/count-of-unread-alerts/{customerId}.txt

Code example:

var repository = blobServiceClient.GetBlobContainerClient("count-of-unread-alerts")
	.GetPartition()
	.GetBlob($"{customerId}.txt")
	.AsCounter();

await repository.IncrementAsync(ct);

Store profile pictures in a blob

Structure:

/profile-pictures/{userId}/{pictureId}.webp

Code example:

var repository = blobServiceClient.GetBlobContainerClient("profile-pictures")
	.GetPartition(userId)
	.GetBlob($"{pictureId}.webp")
	.AsBinaryRepository()
	.WithCaching(CacheOptions.Immutable);

await repository.CreateAsync(image, ct);
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 PeterJuhasz.Repositories.Abstractions:

Package Downloads
PeterJuhasz.Repositories.AzureStorage

Package Description

PeterJuhasz.Repositories.InMemory

Package Description

PeterJuhasz.Repositories.FileSystem

Package Description

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
2.1.0-preview.8 998 9/27/2026
2.1.0-preview.7 152 9/26/2026
2.1.0-preview.6 83 9/25/2026
2.1.0-preview.5 114 9/24/2026
2.1.0-preview.4 313 9/22/2026
2.1.0-preview.3 166 9/20/2026
2.1.0-preview.2 69 9/19/2026
2.0.0-preview.1 67 9/19/2026