LiteDbX is a modern rewrite/evolution of LiteDB focused on an async-first embedded document database for .NET.
Today the project includes:
- async-first database and collection APIs built around
ValueTaskandIAsyncEnumerable<T> - explicit
ILiteTransactionscopes instead of thread-boundBeginTrans/Commit/Rollback - a native
Query()builder plus a provider-backedAsQueryable()LINQ surface - async-only file storage handles via
ILiteStorage<TFileId>andILiteFileHandle<TFileId> - optional AES-GCM encryption through the separate
LiteDbX.Encryption.Gcmpackage - updated shared access modes for async-aware direct, shared, and lock-file usage
Current status
LiteDbX now exposes an async-only public lifecycle and data-access surface: open databases with
await LiteDatabase.Open(...),await LiteRepository.Open(...), orawait LiteEngine.Open(...), use async CRUD/query APIs, and dispose withawait using. The material underdocs/async-redesign/remains useful as design history and implementation background, but the primary async-only API shape is now in place.
Core package:
Install-Package LiteDbXOptional AES-GCM provider:
Install-Package LiteDbX.Encryption.GcmCurrent targets in this repository are:
netstandard2.0netstandard2.1net10.0
LiteDbX operations are async-only. Use the explicit open lifecycle and await / await using for database work and disposal.
The canonical entry points are:
await LiteDatabase.Open(...)await LiteRepository.Open(...)await LiteEngine.Open(...)
public class Customer
{
public int Id { get; set; }
public string Name { get; set; }
public int Age { get; set; }
public string[] Phones { get; set; }
public bool IsActive { get; set; }
}
await using var db = await LiteDatabase.Open(@"MyData.db");
var customers = db.GetCollection<Customer>("customers");
await customers.EnsureIndex(x => x.Name, unique: true);
var customer = new Customer
{
Name = "John Doe",
Phones = new[] { "8000-0000", "9000-0000" },
Age = 39,
IsActive = true
};
var id = await customers.Insert(customer);
var activeAdults = await customers.Query()
.Where(x => x.IsActive && x.Age >= 18)
.OrderBy(x => x.Name)
.ToList();
await foreach (var row in customers.Find(x => x.IsActive))
{
Console.WriteLine($"{row.Name} ({row.Age})");
}BsonMapper exposes serialization switches that let you tune document shape.
Two useful examples are:
EmptyStringToNull- converts empty strings to BSONnullDontSerializeEmptyCollections- omits empty collection members from serialized documents
var mapper = new BsonMapper
{
EmptyStringToNull = true,
DontSerializeEmptyCollections = true
};
var doc = mapper.ToDocument(new
{
Name = "",
Tags = new string[0],
Scores = new Dictionary<string, int>()
});With DontSerializeEmptyCollections = true:
- empty array/list members are omitted from the document
- empty dictionary members are omitted from the document
- top-level collection serialization is unchanged, so
mapper.Serialize(new List<int>())still produces[] - strings are not treated as collections
SerializeNullValues still controls null member handling:
- if
SerializeNullValues = false,nullmembers are omitted - if
SerializeNullValues = true,nullmembers are written as BSONnull - empty collections are still omitted when
DontSerializeEmptyCollections = true
LiteDbX no longer centers transactions around thread affinity.
Use BeginTransaction() and the returned ILiteTransaction scope.
await using var db = await LiteDatabase.Open(@"MyData.db");
var customers = db.GetCollection<Customer>("customers");
await using var tx = await db.BeginTransaction();
await customers.Insert(new Customer
{
Name = "Ana",
Age = 28,
IsActive = true
}, tx);
var names = await customers.Query(tx)
.Where(x => x.IsActive)
.Select(x => x.Name)
.ToList();
await tx.Commit();If the transaction scope is disposed before Commit(), LiteDbX rolls it back.
Prefer this pattern everywhere:
await using var db = await LiteDatabase.Open("filename=my-data.db");For repository-style code, prefer:
await using var repo = await LiteRepository.Open("filename=my-data.db");Async-only notes:
- open databases and repositories through
LiteDatabase.Open(...),LiteRepository.Open(...), orLiteEngine.Open(...) - configure runtime pragmas through the async
Pragma(...)APIs - dispose database, repository, engine, transaction, and file-storage handles with
await using/DisposeAsync()
LiteDbX exposes two complementary query surfaces:
collection.Query()— the native LiteDbX query buildercollection.AsQueryable()— a provider-backedIQueryable<T>adapter for supported LINQ shapes
AsQueryable() does not replace Query().
Provider-backed LINQ lowers back into the same native query model used by Query().
- the full native LiteDbX query surface
- direct
BsonExpressioncontrol - grouped/manual query composition
- the clearest escape hatch for unsupported LINQ shapes
Native query composition remains synchronous, but execution is async-only. Typical terminals include:
ToEnumerable()ToDocuments()ToList()ToArray()First()/FirstOrDefault()Single()/SingleOrDefault()Count()/LongCount()/Exists()GetPlan()
- familiar LINQ composition over a collection root
- supported single-source query shapes
- translation into LiteDbX native execution without leaving LINQ syntax early
var rows = await customers
.AsQueryable()
.Where(x => x.IsActive)
.OrderBy(x => x.Name)
.Select(x => new { x.Id, x.Name })
.ToListAsync();Transaction-aware roots are available too:
await using var tx = await db.BeginTransaction();
var names = await customers
.AsQueryable(tx)
.Where(x => x.IsActive)
.Select(x => x.Name)
.ToArrayAsync();Provider-backed LINQ composes synchronously but executes asynchronously via LiteDbX extension methods such as:
ToAsyncEnumerable()ToListAsync()ToArrayAsync()FirstAsync()FirstOrDefaultAsync()SingleAsync()SingleOrDefaultAsync()AnyAsync()CountAsync()LongCountAsync()GetPlanAsync()
Do not rely on synchronous enumeration or synchronous LINQ materialization for provider-backed LiteDbX queries. Those paths are expected to fail clearly rather than silently doing sync-over-async work.
The current provider is intentionally narrower than full LINQ-to-Objects or EF-style providers.
Supported core operators include:
WhereSelectOrderByOrderByDescendingThenByThenByDescendingSkipTake
Supported grouped LINQ is intentionally narrow and engine-aligned:
GroupBy(key)- optional grouped
Where(...)lowering to nativeHAVING - grouped aggregate projections such as:
Select(g => new { g.Key, Count = g.Count() })Select(g => new { g.Key, Sum = g.Sum(x => x.SomeField) })
For unsupported shapes, fall back to collection.Query().
LiteDbX file storage now exposes async-only handles instead of a public Stream subclass.
db.FileStoragegives you the default storage using_files/_chunksdb.GetStorage<TFileId>(...)gives you a custom file-id type or collection namesOpenRead(...)/OpenWrite(...)returnILiteFileHandle<TFileId>Upload(...)/Download(...)remain async
await using var db = await LiteDatabase.Open(@"MyData.db");
await using var writer = await db.FileStorage.OpenWrite("readme-demo", "demo.txt");
await writer.Write(System.Text.Encoding.UTF8.GetBytes("hello LiteDbX"));
await writer.Flush();LiteDbX currently supports two AES-based encrypted file modes:
ECB— built into the coreLiteDbXpackageGCM— provided by the optionalLiteDbX.Encryption.Gcmpackage
To use GCM:
- reference
LiteDbX.Encryption.Gcm - call
GcmEncryptionRegistration.Register()once at startup - configure
AESEncryption = AESEncryptionType.GCM - provide a password
using LiteDbX.Encryption.Gcm;
GcmEncryptionRegistration.Register();
var cs = new ConnectionString("filename=secure.db;password=secret;encryption=GCM");
await using var db = await LiteDatabase.Open(cs);Important notes:
- if no password is supplied, encryption is not used
- ECB remains built in and needs no extra registration
- GCM is explicit by design; LiteDbX does not auto-load the provider
- existing encrypted files reopen according to their stored format, so older ECB files remain readable and existing GCM files reopen as GCM
For more detail, see:
docs/gcm-setup.mddocs/aes-gcm-mode.md
LiteDbX currently supports three connection types:
| Mode | Intended use | Cross-process guarantee | Explicit ILiteTransaction support |
|---|---|---|---|
ConnectionType.Direct |
normal dedicated engine access | none beyond normal file semantics | ✅ Supported |
ConnectionType.Shared |
async-safe serialized access inside one process | ❌ No — in-process only | ❌ Not supported |
ConnectionType.LockFile |
physical-file cross-process write coordination | ✅ Yes, via lock file | ❌ Not supported |
Additional notes:
Sharedis a supported in-process mode. It no longer implies the old named-mutex cross-process behaviour.LockFileis supported only for physical filename-based databases; it does not support custom streams,:memory:, or:temp:.- Both
SharedandLockFilesupport nested single-call operations, but not long-lived explicit transaction scope across arbitrary user code.
If you need explicit transaction scopes, prefer Direct.
If you need cross-process file coordination, prefer LockFile rather than assuming Shared provides the old named-mutex behavior.
- desktop and local applications
- embedded per-user or per-tenant data stores
- application file formats
- tools and services that want a lightweight single-file document database with async-friendly APIs