Your documents.
Your stack.
A consistent .NET API for documents, indexed queries, and atomic writes. Run in memory, on disk, or with Redis and PostgreSQL.
{
"id": "customer-1",
"name": "Alex",
"active": true
}Storage that fits
your application.
Use the storage your workload needs.
Keep a consistent API as your application grows.
Memory
Fast, in-process storage for temporary data, tests, and getting started.
FileSystem
In-memory documents backed by a JSON file. Local persistence for a single process.
Redis
Share documents and indexes across instances. No Redis modules or Lua required.
PostgreSQL
Persistent documents, SQL-backed queries, and atomic transactions.
From setup to
your first document.
Get a container, add a document, read it back. Async operations and dependency injection fit naturally into your .NET application.
Your first document, step by stepInstall a provider.
Shared dependencies come with it.
dotnet add package Soenneker.Librarian.Memory
// Register your storage provider.
builder.Services.AddMemoryLibrarianDatabaseAsSingleton();
// Inject ILibrarianDatabase into your service.
var users = await database.GetContainer("users");
await users.AddItem("user-1", """{"name":"Alex"}""");
var json = await users.GetItem("user-1");
The pieces your
data layer needs.
Work with your types.
Typed repositories handle serialization and document operations. Register source-generated JSON contracts for trim-safe, Native AOT compatible models.
Native AOT guide ↗Find more. Scan less.
Filter, order, count, and page with LINQ or explicit async index methods. Supported LINQ queries create indexes automatically and maintain them on writes.
Query capabilities ↗Keep changes together.
Apply related writes across containers in one atomic batch. Add conditions to detect conflicting changes before any writes are applied.
Atomic batch guide ↗See how Librarian
compares.
Librarian, LiteDB, and sqlite-net running the same queries against 100,000 documents. All use in-memory storage with warm indexes.
Methodology & benchmark source ↗| Query | Librarian | LiteDB 5.0.21 | sqlite-net 1.11.285 |
|---|---|---|---|
| Equality · 10 documents | 8.983 | 29.341 | 11.866 |
| Range page · 10 documents | 10.337 | 41.159 | 13.113 |
| Count · 10 matches | 2.013 | 29.925 | 3.574 |
| Exists · 50% hits | 2.228 | 22.159 | 2.537 |
| First match | 2.989 | 18.134 | 6.204 |
| Skip 90,000 · take 10 | 8.644 | 53,694.283 | 1,240.940 |
Compare managed allocations Bytes per operation
| Query | Librarian | LiteDB | sqlite-net |
|---|---|---|---|
| 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 |
Managed allocations on the executing thread only. SQLite native allocations are excluded. sqlite-net allocates fewer managed bytes for the existence check.
Medians over seven rounds on an AMD Ryzen Threadripper PRO 9995WX, Windows 10.0.26200, .NET 10.0.12 x64. Single-threaded execution, tiered compilation disabled. Includes query construction, execution, and materialization.
Warm reads only; excludes index construction, writes, persistence, and concurrency. LiteDB uses native expression queries; sqlite-net uses its synchronous expression API and SQL SELECT EXISTS for existence checks. Deep paging includes each engine’s offset traversal strategy. Results vary by workload and hardware.