Soenneker.Librarian.FileSystem 4.0.2

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

alternate text is missing from this package README image alternate text is missing from this package README image

Soenneker.Librarian

Document storage for .NET 10 with interchangeable memory, JSON file, and Redis providers.

  • Async document operations — store, retrieve, update, and delete JSON by ID.
  • Atomic batches — update related documents together, with conditions to prevent conflicting writes.
  • Typed repositories — work with document classes through ILibrarianRepository<TDocument>.
  • Indexed queries — filter, sort, count, and page with LINQ or explicit async index methods.

Quick start · Providers · Queries · Typed repositories · Performance

Installation

Install the provider you need. Shared dependencies are included automatically.

dotnet add package Soenneker.Librarian.Memory
# Or: dotnet add package Soenneker.Librarian.FileSystem
# Or: dotnet add package Soenneker.Librarian.Redis
Package Purpose
Soenneker.Librarian.Memory In-process document storage
Soenneker.Librarian.FileSystem In-memory documents backed by a JSON file
Soenneker.Librarian.Redis Shared document storage and indexes in Redis
Soenneker.Librarian.Core Typed repository and local container implementation
Soenneker.Librarian.Abstractions Database, container, and repository contracts

Quick start

This standalone example registers the memory provider, stores a document, and reads it back:

using Microsoft.Extensions.DependencyInjection;
using Soenneker.Librarian.Abstractions;
using Soenneker.Librarian.Memory.Registrars;

var services = new ServiceCollection();
services.AddLogging();
services.AddMemoryLibrarianDatabaseAsSingleton();

await using var provider = services.BuildServiceProvider();
var database = provider.GetRequiredService<ILibrarianDatabase>();
var users = await database.GetContainer("users");

await users.AddItem("user-1", """{"name":"Alex","age":30,"active":true}""");
string? json = await users.GetItem("user-1");

In a hosted application, register the provider on builder.Services and inject ILibrarianDatabase into your service.

Choose a provider

Memory FileSystem Redis
Use when Data can be temporary One process needs local persistence Multiple instances share documents
Documents live in Process memory Process memory, backed by JSON Redis
Writes persist Never Periodic save, about every 5 seconds; atomic batches save immediately Before the mutation returns
Explicit flush No-op await database.Save() No-op
Index lifetime Until unload or disposal Rebuilt after unload or restart Persisted in Redis

Register one provider for ILibrarianDatabase. Each registrar also offers an AsScoped() variant; a scoped memory database has its own data.

FileSystem

using Soenneker.Librarian.FileSystem.Registrars;

builder.Services.AddFileSystemLibrarianDatabaseAsSingleton();

Add to appsettings.json:

{
  "Librarian": {
    "FileSystem": { "FilePath": "data/librarian.json" }
  }
}
  • Use one database owner per file.
  • Await database.Save() when changes must be flushed explicitly.
  • Dispose the database's owner asynchronously to complete shutdown persistence.

Redis

using Soenneker.Librarian.Redis.Registrars;

builder.Services.AddRedisLibrarianDatabaseAsSingleton();

Add to application configuration:

{
  "Azure": { "Redis": { "ConnectionString": "localhost:6379" } },
  "Librarian": { "Redis": { "Key": "my-app:librarian", "KeyPrefix": "librarian", "Database": -1 } }
}
  • Key is the required namespace; Database is optional (-1 uses the connection default).
  • Reads fetch current Redis data; writes update documents and indexes together.
  • No RedisJSON, Redis Search, or Lua scripts required.
  • Redis persistence and eviction settings determine durability. Use a non-evicting database for document storage.
  • Cluster deployments require Redis 8 or later.

See the Redis guide for supported queries, index costs, concurrency, and deployment details.

Applications that already resolve a shared Redis database can use new RedisLibrarianDatabase(key, storeFactory) with a Func<CancellationToken, ValueTask<IDatabase>>. The caller retains connection ownership. GetServerTime() reads UTC time from the primary owning the namespace's hash slot using native Redis commands, without Lua.

Query documents

The following examples use the users container from the quick start and this model:

public sealed record User(string Name, int Age, bool Active);

LINQ

using System.Linq;

var adults = users.BuildQueryable<User>()
    .Where(user => user.Active && user.Age >= 18 && user.Age <= 65)
    .OrderBy(user => user.Age)
    .Take(25)
    .ToList();

Indexes are created automatically for supported filters and ordering, then maintained on writes. The first indexed query pays the index creation cost.

Behavior Memory / FileSystem Redis
Query execution Synchronous Synchronous server calls
Unsupported expressions Can fall back to local evaluation Throw NotSupportedException
Filtering and ordering Indexed where supported Must precede Skip / Take
Projection Supported through LINQ Materialize the page, then project locally

For async query execution, use the explicit index methods below.

Async index queries

await users.EnsureIndex("age");

var page = await users.FindRangeByIndex<User>(
    "age", minimum: 18, maximum: 65, skip: 0, take: 25);

var thirtyYearOlds = await users.FindByIndex<User>("age", 30, take: 10);
int count = await users.CountByIndex("age", 30);
bool exists = await users.ExistsByIndex("age", 30);

foreach (User user in page.Items)
    System.Console.WriteLine(user.Name);
  • Use case-sensitive JSON field paths, such as age or address.city.
  • Call EnsureIndex before explicit queries; missing indexes throw instead of scanning.
  • Range bounds are inclusive; null means unbounded. skip must be nonnegative and take positive.
  • Index values support strings, decimal-compatible numbers, booleans, and null. Missing fields are excluded.
  • Results expose Items, Index, IndexEntriesExamined, and DocumentsDeserialized.

Document operations

Container method Behavior
AddItem(id, json) Adds a document; duplicate IDs throw
GetItem(id) Returns JSON, or null when missing
GetItemStrict(id) Returns JSON; missing IDs throw
UpdateItem(id, json) Updates an existing document; returns null when missing
UpdateItemStrict(id, json) Updates an existing document; missing IDs throw
DeleteItem(id) Deletes one document
GetAllItems() / GetAllIds() Returns all documents or IDs
DeleteAllItems() Removes every document in the container

All methods above are awaitable and accept a cancellation token. Document IDs are case-insensitive; container names are case-sensitive. The database owns container lifetime; use UnloadContainer() to release a container after stopping its concurrent operations.

Atomic batches

Update related documents across containers in one all-or-nothing operation with ILibrarianDatabase.Execute. Add conditions to prevent overwriting concurrent changes; if a condition fails, it returns false and applies no writes.

Available with all three providers, including coordination across application instances with Redis. See the atomic batch guide for an example, provider guarantees, and retry handling.

Typed repositories

Use LibrarianRepository<TDocument> for serialization and typed CRUD. Models must inherit from Document and have a nonempty ID when added.

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Soenneker.Documents.Document;
using Soenneker.Librarian.Abstractions;
using Soenneker.Librarian.Core;

public sealed class Customer : Document
{
    public string Email { get; set; } = "";
}

public sealed class CustomerRepository(
    IConfiguration configuration,
    ILogger<LibrarianRepository<Customer>> logger,
    ILibrarianDatabase database)
    : LibrarianRepository<Customer>(configuration, logger, database, "customers")
{
}

Register with builder.Services.AddScoped<ILibrarianRepository<Customer>, CustomerRepository>(), then inject ILibrarianRepository<Customer>.

await repository.AddItem(new Customer { Id = "customer-1", Email = "alex@example.com" });
Customer? customer = await repository.GetItem("customer-1");

Typed repositories also expose explicit index methods. Batch additions and updates execute sequentially; they are not atomic transactions.

Performance

100,000 documents, warm indexes, .NET 10.0.12 Release. Librarian LINQ, LiteDB 5.0.21 native expression queries, and sqlite-net-pcl 1.11.285 all use in-memory storage. Query construction and execution are included. Times are medians over seven rounds.

Query Librarian LiteDB sqlite-net
Equality, 10 documents 8.983 µs 29.341 µs 11.866 µs
Range page, 10 documents 10.337 µs 41.159 µs 13.113 µs
Count 10 matches 2.013 µs 29.925 µs 3.574 µs
Exists, 50% hits 2.228 µs 22.159 µs 2.537 µs
First match 2.989 µs 18.134 µs 6.204 µs
Skip 90,000, take 10 8.644 µs 53.694 ms 1.241 ms

Managed bytes allocated per operation:

Query Librarian B/op LiteDB B/op sqlite-net B/op
Equality, 10 documents 6,840 63,440 9,064
Range page, 10 documents 8,665 89,563 11,801
Count 10 matches 1,600 65,006 2,176
Exists, 50% hits 1,600 44,621 344
First match 1,896 43,887 4,864
Skip 90,000, take 10 6,944 189,235,872 8,912

SQLite native allocations are excluded. sqlite-net uses its synchronous expression API except for existence checks, which use parameterized SQL SELECT EXISTS. These measurements cover warm reads; index construction, writes, persistence, and concurrency are outside the comparison.

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 (1)

Showing the top 1 NuGet packages that depend on Soenneker.Librarian.FileSystem:

Package Downloads
Soenneker.Flywheel.Filesystem

Filesystem storage using Librarian for Soenneker Flywheel.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
4.0.3 0 9/15/2026
4.0.2 57 9/14/2026
4.0.1 43 9/14/2026