A high-performance embedded key-value database for .NET with support for multiple storage engines, ACID transactions, and encryption.
-
Two Storage Engines
- B-Tree - Optimized for read-heavy workloads with excellent random access
- LSM-Tree - For sustained high-volume ingest into tables with few or no secondary
indexes; see
Docs/WitSQL.md§ 14.9 for where it wins and where it does not
-
Encryption
- AES-256-GCM with hardware acceleration
- ChaCha20-Poly1305 via BouncyCastle (Blazor WASM compatible)
- Password-based key derivation (PBKDF2)
-
ACID Transactions
- Atomicity, Consistency, Isolation, Durability
- Write-Ahead Logging (WAL)
- Crash recovery
-
Concurrency
- Reader-writer locking
- File locking for multi-process safety
- Async/await support
-
SQL Support
- Full SQL parser (WitSQL dialect)
- ADO.NET provider
- Entity Framework Core provider
- Window functions, CTEs, subqueries
LATERAL/CROSS APPLY/OUTER APPLY,VALUESas a table source,TOP n- 90+ built-in functions, plus window functions -
Docs/WitSQL.md§ 5 and § 7
-
User-Defined Functions and Stored Procedures
CREATE FUNCTION— a scalar expression over its parameters, callable anywhere an expression may appear: aSELECTlist, aWHERE, aCHECK, a computed column, an index keyCREATE PROCEDURE/CALL— a body of statements, invoked as one unit of work, and reachable from ADO.NET throughCommandType.StoredProcedure- SQL bodies only: no external code and no assembly loading
- Reported by
INFORMATION_SCHEMA.ROUTINESand.PARAMETERS
-
Fluent API
- Easy configuration with builder pattern
- Extensible via extension methods
- Simple static factory methods
-
Provider System
- Pluggable storage, encryption, cache, and journal providers
- Auto-detection of settings when reopening databases
- Easy registration of custom providers
| Package | Description |
|---|---|
| OutWit.Database.Core | Core storage engine (B+Tree, LSM-Tree, MVCC) |
| OutWit.Database.Core.BouncyCastle | ChaCha20-Poly1305 encryption provider |
| OutWit.Database.Core.IndexedDb | IndexedDB storage for Blazor WebAssembly |
| OutWit.Database.Parser | SQL parser (ANTLR4-based) |
| OutWit.Database | SQL execution engine |
| OutWit.Database.AdoNet | ADO.NET provider |
| OutWit.Database.EntityFramework | Entity Framework Core provider |
# Core storage engine
dotnet add package OutWit.Database.Core
# SQL engine with ADO.NET
dotnet add package OutWit.Database.AdoNet
# Entity Framework Core
dotnet add package OutWit.Database.EntityFramework
# Optional: BouncyCastle encryption (for Blazor WASM)
dotnet add package OutWit.Database.Core.BouncyCastle
# Optional: IndexedDB storage (for Blazor WASM)
dotnet add package OutWit.Database.Core.IndexedDbusing OutWit.Database.Core.Builder;
// Create a new database
using var db = WitDatabase.Create("mydata.db");
// Or with encryption
using var db = WitDatabase.Create("secure.db", "my-password");
// Store and retrieve data
db.Put("user:1"u8, """{"name": "John", "age": 30}"""u8);
var value = db.Get("user:1"u8);
db.Delete("user:1"u8);using OutWit.Database.AdoNet;
using var connection = new WitDbConnection("Data Source=mydb.witdb");
connection.Open();
using var cmd = connection.CreateCommand();
cmd.CommandText = "CREATE TABLE Users (Id INT PRIMARY KEY, Name VARCHAR(100))";
cmd.ExecuteNonQuery();
cmd.CommandText = "INSERT INTO Users (Id, Name) VALUES (@id, @name)";
cmd.Parameters.AddWithValue("@id", 1);
cmd.Parameters.AddWithValue("@name", "John Doe");
cmd.ExecuteNonQuery();using var cmd = connection.CreateCommand();
// A function is a scalar expression over its parameters, callable anywhere an
// expression may appear - a SELECT list, a WHERE, a CHECK, an index key.
cmd.CommandText = "CREATE FUNCTION Doubled(N INT) RETURNS INT AS BEGIN RETURN N * 2; END";
cmd.ExecuteNonQuery();
cmd.CommandText = "SELECT Doubled(21)";
var answer = cmd.ExecuteScalar(); // 42
// A procedure is a body of statements. The last statement's result is the call's.
cmd.CommandText = @"
CREATE PROCEDURE RecentUsers AS BEGIN
SELECT * FROM Users ORDER BY Id DESC;
END";
cmd.ExecuteNonQuery();
// Invoked the ordinary ADO.NET way.
using var call = connection.CreateCommand();
call.CommandType = CommandType.StoredProcedure;
call.CommandText = "RecentUsers";
using var reader = call.ExecuteReader();Bodies are SQL — no external code and no assembly loading. See
Docs/WitSQL.md § 2.10–2.11 for the rules: what a body may contain, how determinism
decides whether a function may key an index, and why a trigger body may not CALL.
using Microsoft.EntityFrameworkCore;
using OutWit.Database.EntityFramework.Extensions;
public class AppDbContext : DbContext
{
public DbSet<User> Users => Set<User>();
protected override void OnConfiguring(DbContextOptionsBuilder options)
=> options.UseWitDb("Data Source=myapp.witdb");
}
// Usage
using var context = new AppDbContext();
context.Database.EnsureCreated();
context.Users.Add(new User { Name = "John" });
context.SaveChanges();using OutWit.Database.Core.Builder;
using OutWit.Database.Core.IndexedDb;
using OutWit.Database.Core.BouncyCastle;
// In Blazor component
var db = new WitDatabaseBuilder()
.WithIndexedDbStorage("MyDatabase", JSRuntime)
.WithBouncyCastleEncryption("password") // Works in browser
.WithBTree()
.Build();
await ((StorageIndexedDb)db.Store).InitializeAsync();| Method | Description |
|---|---|
WithFilePath(path) |
Use file-based storage |
WithMemoryStorage() |
Use in-memory storage |
WithBTree() |
Use B-Tree engine (default) |
WithLsmTree() |
Use LSM-Tree engine |
| Method | Description |
|---|---|
WithEncryption(password) |
AES-GCM with password |
WithBouncyCastleEncryption(password) |
ChaCha20-Poly1305 |
| Method | Description |
|---|---|
WithTransactions() |
Enable ACID transactions |
WithMvcc() |
Enable MVCC |
WithFileLocking() |
Enable file locking |
+---------------------------------------------------------------+
| WitDatabaseBuilder |
| (Fluent API for database configuration) |
+---------------------------------------------------------------+
| TransactionalStore |
| (ACID transactions, locking) |
+---------------------------------------------------------------+
| +----------------+ +----------------+ |
| | StoreBTree | | StoreLsm | |
| | (B+Tree engine)| | (LSM-Tree engine)| |
| +----------------+ +----------------+ |
+---------------------------------------------------------------+
| ProviderRegistry |
| (Pluggable providers for all components) |
+---------------------------------------------------------------+
Measured with Benchmarks/OutWit.Database.Benchmarks (BenchmarkDotNet, ShortRun, in-process) on a
Ryzen 9 5950X under .NET 10, against SQLite (Microsoft.Data.Sqlite) and LiteDB. Re-measured
2026-08-11. Every figure is the mean of two identical passes taken on an otherwise idle machine,
and anything whose two passes disagreed by more than 10% is left out rather than quoted - three
write rows fell out that way. Default configuration means a bare Data Source=... connection
string - MVCC on, durable commit, B+Tree - which is what an ADO.NET or EF Core consumer gets.
| Operation | WitDatabase | LiteDB | SQLite |
|---|---|---|---|
| Point query by primary key, x100 | 0.26 ms | 1.27 ms | 4.86 ms |
| Seek on a UNIQUE index, x100 (5,000 rows) | 0.52 ms | 1.95 ms | 5.09 ms |
| Sequential reads, x100 | 0.35 ms | 7.59 ms | 5.02 ms |
Index range scan (BETWEEN), 5,000 rows |
0.93 ms | 4.58 ms | 0.20 ms |
SELECT ... LIMIT 100 |
0.12 ms | 0.15 ms | 0.07 ms |
| Full scan, 1,000 rows | 0.80 ms | 1.40 ms | 0.18 ms |
GROUP BY, 1,000 rows |
1.01 ms | 1.50 ms | 0.22 ms |
ORDER BY, 1,000 rows |
1.66 ms | 1.78 ms | 0.23 ms |
INNER JOIN over 4 tables |
0.48 ms | 0.18 ms | 0.09 ms |
| Operation | WitDatabase | LiteDB | SQLite |
|---|---|---|---|
| Mixed transaction (insert/update/select) | 4.85 ms | 7.33 ms | 6.96 ms |
| 100 inserts in one transaction | 5.07 ms | 1.00 ms | 6.82 ms |
UPDATE by key, in a transaction |
5.16 ms | 1.74 ms | 6.88 ms |
| 100 inserts without a transaction | 301 ms | 10.7 ms | 925-1233 ms |
| microseconds per row | B+Tree | LSM |
|---|---|---|
| MVCC on - what a plain connection string gives | 36.8 | 772 |
MVCC=false, no secondary indexes |
13.6-17.2 | 11.0-18.3 |
MVCC=false, three secondary indexes |
22.9-23.2 | 33.6-38.6 |
Read all of it honestly:
- Lookups and indexed access are the strong side - 3-20x faster than LiteDB and, on small operations, faster than SQLite too. Part of the SQLite margin is the P/Invoke crossing its managed wrapper pays per call; that is not an engine result, but it is what a .NET consumer experiences, because there is no other way to reach SQLite from managed code.
- Scans, sorts and joins are the weak side - SQLite is 4-13x faster there, and the gap is allocation: a scan costs roughly 2.4 KB per row returned. LiteDB, also managed, pays about the same, so this does not move the comparison against it.
- Autocommit is expensive by choice. Every statement outside a transaction is durable, which is what makes 301 ms against LiteDB's 10.7 - and 3-4x faster than SQLite doing the same thing. Batch writes in a transaction when throughput matters.
Store=lsmis not simply "write-optimised", and with the default settings it is much worse. WithMVCC=falseit matches the B+Tree on indexless ingest and is ~1.6x behind with three secondary indexes. With MVCC on - the default - it costs 20x the B+Tree per row, and no batch size amortises it. SeeDocs/WitSQL.md§ 14.9 before choosing it.COUNT(*)is deliberately not in the tables above. WitDatabase answers it from a cached per-table counter in ~1 microsecond while the other two count rows; that is a real property worth knowing and it is not a speed comparison.
Reproduce with:
dotnet run -c Release --project Benchmarks/OutWit.Database.Benchmarks -- verify
dotnet run -c Release --project Benchmarks/OutWit.Database.Benchmarks -- --filter "*"The first command is the equivalence check: it runs every benchmark body once and compares what WitDatabase, SQLite and LiteDB actually return. A timing comparison between engines that do not compute the same thing is not a measurement, so that check is meant to be green before any figure above is believed.
Earlier figures withdrawn twice, and both times are worth recording. Releases before 11.1.0 advertised transaction ratios measured on the least discriminating workload in the suite, and before that "4-20x faster" numbers from a benchmark project no longer in the repository. What changed in between: the benchmark suite had no assertion of any kind in 113 methods, so nothing had ever checked that the three engines compute the same answer, and several published claims turned out to describe an engine that no longer existed.
The 2026-08-11 re-measurement found the write paths 1.5-1.7x slower than the 2026-08-02 baseline on the same benchmark and the same machine, while SQLite's and LiteDB's numbers on those same rows moved by less than 13%. The heavy read paths did not move - full scan,
ORDER BY, point query and unique-index seek are all within 6% of the baseline - but three lighter ones did:LIMIT 1000.08 -> 0.12 ms, sequential reads 0.31 -> 0.35, index range scan 0.83 -> 0.93. All of that is recorded rather than explained: it is a real difference between 11.1.0 and 12.8.0 and it has not yet been diagnosed.
- .NET 10.0
- Windows, Linux, or macOS
WitDatabase/
+-- Sources/
| +-- Core/
| | +-- OutWit.Database.Core/ # Storage engine
| | +-- OutWit.Database.Core.BouncyCastle/ # ChaCha20 encryption
| | +-- OutWit.Database.Core.IndexedDb/ # Blazor WASM storage
| +-- Engine/
| | +-- OutWit.Database.Parser/ # SQL parser
| | +-- OutWit.Database/ # SQL engine
| +-- Providers/
| +-- OutWit.Database.AdoNet/ # ADO.NET provider
| +-- OutWit.Database.EntityFramework/ # EF Core provider
+-- Tools/
| +-- OutWit.Database.Studio/ # Database management tool
+-- Samples/
+-- Benchmarks/
- Docs/WitSQL.md - The WitSQL language specification: types, statements, functions, routines, transactions and concurrency
- Docs/KnownIssues.md - Known issues
- CHANGELOG.md - What changed in each release, and why
- Sources/Core/OutWit.Database.Core/EXTENSIBILITY.md - Extension guide
The audit and the phase plans are working papers rather than documentation and are no longer shipped
in Docs/. Source comments that cite one by name - AUDIT-2026-07.md, NEXT-SESSION-PLAN.md,
PHASE*-*.md - refer to files that remain in the git history.
# All tests
dotnet test
# Specific project
dotnet test Sources/Core/OutWit.Database.Core.TestsLicensed under the Apache License, Version 2.0. See LICENSE.
If you use WitDatabase in a product, a mention is appreciated (but not required), for example: "Powered by WitDatabase https://witdatabase.io/".
"WitDatabase" and the WitDatabase logo are used to identify the official project by Dmitry Ratner.
You may:
- refer to the project name in a factual way (e.g., "built with WitDatabase");
- use the name to indicate compatibility (e.g., "WitDatabase-compatible").
You may not:
- use "WitDatabase" as the name of a fork or a derived product in a way that implies it is the official project;
- use the WitDatabase logo to promote forks or derived products without permission.
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
See CHANGELOG.md for version history. Each package carries its own ROADMAP.md for
what is planned in that layer.