WitDatabase is built on a modular, provider-based architecture. Every major component — storage, encryption, caching, journaling, and indexing — is defined by an interface and can be replaced with a custom implementation.
1. Overview
Architecture Diagram
[[Svg Src="./witdatabase-builder-extensibility.svg" Alt="witdatabase-builder-extensibility"]]
Pluggable Interfaces
| Interface | Purpose | Built-in Implementations |
|---|---|---|
IKeyValueStore |
Storage engine | StoreBTree, StoreLsm, StoreInMemory |
IStorage |
Page-level storage backend | StorageFile, StorageMemory, StorageIndexedDb |
ICryptoProvider |
Encryption algorithm | EncryptorProviderAesGcm, BouncyCastleCryptoProvider |
IPageCache |
Page caching strategy | PageCacheLru, PageCacheShardedClock |
ITransactionJournal |
Transaction durability | RollbackJournal, WalTransactionJournal |
ISecondaryIndexFactory |
Secondary index creation | SecondaryIndexFactoryKeyValueStore |
Provider Keys
Each provider has a unique key used for registration and lookup:
| Interface | Provider Keys |
|---|---|
IKeyValueStore |
btree, lsm, inmemory |
IStorage |
file, memory, encrypted, indexeddb |
ICryptoProvider |
aes-gcm, chacha20-poly1305 |
IPageCache |
clock, lru |
ITransactionJournal |
rollback, wal |
Why Extensibility?
Common use cases for custom providers:
| Scenario | Custom Provider |
|---|---|
| HSM integration | ICryptoProvider → Hardware Security Module |
| Cloud storage | IStorage → S3, Azure Blob, GCS |
| Enterprise key management | ICryptoProvider → Azure Key Vault, HashiCorp Vault |
| Custom caching | IPageCache → Redis-backed cache |
| Specialized indexes | ISecondaryIndexFactory → Full-text, spatial |
| Custom durability | ITransactionJournal → Network-replicated journal |
2. Provider System
IProvider Interface
All pluggable components implement IProvider:
public interface IProvider
{
/// <summary>
/// Gets the unique key identifying this provider type.
/// Examples: "aes-gcm", "chacha20-poly1305", "file", "memory", "btree"
/// </summary>
string ProviderKey { get; }
}
ProviderRegistry
The global ProviderRegistry manages provider factories:
using OutWit.Database.Core.Providers;
// Register a provider factory
ProviderRegistry.Instance.Register<ICryptoProvider>("my-crypto",
parameters => new MyCryptoProvider(parameters.GetRequired<byte[]>("key")));
// Create a provider instance
var crypto = ProviderRegistry.Instance.Create<ICryptoProvider>("my-crypto",
new ProviderParameters().Set("key", myKey));
// Check if registered
bool exists = ProviderRegistry.Instance.IsRegistered<ICryptoProvider>("my-crypto");
// Get all registered keys
var keys = ProviderRegistry.Instance.GetRegisteredKeys<ICryptoProvider>();
// Returns: ["aes-gcm", "chacha20-poly1305", "my-crypto"]
ProviderParameters
Parameters are passed to provider factories during creation:
// Create parameters
var parameters = new ProviderParameters()
.Set("key", encryptionKey)
.Set("iterations", 100000)
.Set("user", "tenant1");
// Get required parameter (throws if missing)
var key = parameters.GetRequired<byte[]>("key");
// Get optional parameter with default
var iterations = parameters.Get("iterations", 10000);
// Check if parameter exists
if (parameters.Has("user"))
{
var user = parameters.Get<string>("user");
}
Auto-Registration with ModuleInitializer
Use [ModuleInitializer] for automatic registration when your assembly loads:
using System.Runtime.CompilerServices;
using OutWit.Database.Core.Interfaces;
using OutWit.Database.Core.Providers;
namespace MyCompany.WitDatabase.CustomProviders;
public static class MyProviderRegistration
{
private static bool _initialized;
[ModuleInitializer]
public static void Initialize()
{
if (_initialized) return;
// Register your custom providers
ProviderRegistry.Instance.Register<ICryptoProvider>("my-hsm",
p => new HsmCryptoProvider(p.GetRequired<string>("keyId")));
ProviderRegistry.Instance.Register<IStorage>("s3-storage",
p => new S3Storage(
p.GetRequired<string>("bucket"),
p.Get("region", "us-east-1")));
_initialized = true;
}
}
When your assembly is loaded, providers are automatically registered.
Using Custom Providers
// Via builder with provider key
var db = new WitDatabaseBuilder()
.WithStorage("s3-storage", new ProviderParameters()
.Set("bucket", "my-database-bucket")
.Set("region", "eu-west-1"))
.WithEncryption("my-hsm", new ProviderParameters()
.Set("keyId", "production-key-001"))
.Build();
// Or pass instance directly
var db = new WitDatabaseBuilder()
.WithStorage(new S3Storage("my-bucket", "us-east-1"))
.WithEncryption(new HsmCryptoProvider("key-001"))
.Build();
Provider Persistence
Some provider settings are stored in the database header:
| Setting | Persisted | Notes |
|---|---|---|
Store type (btree/lsm) |
✅ Yes | Auto-detected on reopen |
| Encryption enabled | ✅ Yes | Requires password on reopen |
| Encryption provider key | ✅ Yes | e.g., aes-gcm |
| Transactions enabled | ✅ Yes | Auto-detected |
| Page size | ✅ Yes | Cannot change after creation |
| Cache type/size | ❌ No | Can change on reopen |
| Journal mode | ❌ No | Can change on reopen |
// Inspect database without opening
var info = WitDatabase.GetDatabaseInfo("app.witdb");
Console.WriteLine(
Loading...
quot;Store: {info.StoreProvider}"); // "btree"
Console.WriteLine(
Loading...
quot;Encrypted: {info.RequiresEncryption}"); // true
Console.WriteLine(
Loading...
quot;Encryption: {info.EncryptionProvider}"); // "aes-gcm"
3. Custom Crypto Provider
ICryptoProvider Interface
public interface ICryptoProvider : IProvider, IDisposable
{
/// <summary>
/// Encrypts plaintext using AEAD (Authenticated Encryption with Associated Data).
/// </summary>
/// <param name="nonce">Unique nonce (typically 12 bytes).</param>
/// <param name="plaintext">Data to encrypt.</param>
/// <param name="ciphertext">Output buffer for encrypted data.</param>
/// <param name="tag">Output buffer for authentication tag (typically 16 bytes).</param>
void Encrypt(ReadOnlySpan<byte> nonce, ReadOnlySpan<byte> plaintext,
Span<byte> ciphertext, Span<byte> tag);
/// <summary>
/// Decrypts ciphertext and verifies authentication tag.
/// </summary>
/// <returns>True if authentication succeeded; false if tampered.</returns>
bool Decrypt(ReadOnlySpan<byte> nonce, ReadOnlySpan<byte> ciphertext,
ReadOnlySpan<byte> tag, Span<byte> plaintext);
/// <summary>
/// Creates a clone of this provider (for concurrent operations).
/// </summary>
ICryptoProvider Clone();
/// <summary>
/// Size of nonce in bytes (typically 12).
/// </summary>
int NonceSize { get; }
/// <summary>
/// Size of authentication tag in bytes (typically 16).
/// </summary>
int TagSize { get; }
}
Example: HSM Crypto Provider
using OutWit.Database.Core.Interfaces;
using System.Security.Cryptography;
namespace MyCompany.WitDatabase.Hsm;
/// <summary>
/// Crypto provider that delegates encryption to a Hardware Security Module.
/// </summary>
public sealed class HsmCryptoProvider : ICryptoProvider
{
private readonly IHsmClient _hsmClient;
private readonly string _keyId;
private bool _disposed;
public HsmCryptoProvider(string keyId, IHsmClient? hsmClient = null)
{
_keyId = keyId ?? throw new ArgumentNullException(nameof(keyId));
_hsmClient = hsmClient ?? HsmClientFactory.Create();
}
public string ProviderKey => "my-hsm";
public int NonceSize => 12;
public int TagSize => 16;
public void Encrypt(ReadOnlySpan<byte> nonce, ReadOnlySpan<byte> plaintext,
Span<byte> ciphertext, Span<byte> tag)
{
ThrowIfDisposed();
// Call HSM for encryption
var result = _hsmClient.Encrypt(_keyId, nonce.ToArray(), plaintext.ToArray());
result.Ciphertext.AsSpan().CopyTo(ciphertext);
result.Tag.AsSpan().CopyTo(tag);
}
public bool Decrypt(ReadOnlySpan<byte> nonce, ReadOnlySpan<byte> ciphertext,
ReadOnlySpan<byte> tag, Span<byte> plaintext)
{
ThrowIfDisposed();
try
{
var result = _hsmClient.Decrypt(_keyId, nonce.ToArray(),
ciphertext.ToArray(), tag.ToArray());
result.AsSpan().CopyTo(plaintext);
return true;
}
catch (CryptographicException)
{
return false; // Authentication failed
}
}
public ICryptoProvider Clone()
{
return new HsmCryptoProvider(_keyId, _hsmClient);
}
public void Dispose()
{
if (!_disposed)
{
// HSM client may be shared, don't dispose
_disposed = true;
}
}
private void ThrowIfDisposed()
{
ObjectDisposedException.ThrowIf(_disposed, this);
}
}
Example: Azure Key Vault Provider
using Azure.Identity;
using Azure.Security.KeyVault.Keys;
using Azure.Security.KeyVault.Keys.Cryptography;
public sealed class AzureKeyVaultCryptoProvider : ICryptoProvider
{
private readonly CryptographyClient _cryptoClient;
private readonly string _keyId;
public AzureKeyVaultCryptoProvider(string vaultUrl, string keyName)
{
var keyClient = new KeyClient(new Uri(vaultUrl), new DefaultAzureCredential());
var key = keyClient.GetKey(keyName);
_keyId = key.Value.Id.ToString();
_cryptoClient = new CryptographyClient(key.Value.Id, new DefaultAzureCredential());
}
public string ProviderKey => "azure-keyvault";
public int NonceSize => 12;
public int TagSize => 16;
public void Encrypt(ReadOnlySpan<byte> nonce, ReadOnlySpan<byte> plaintext,
Span<byte> ciphertext, Span<byte> tag)
{
// Azure Key Vault uses AES-GCM with nonce prepended
var input = new byte[nonce.Length + plaintext.Length];
nonce.CopyTo(input);
plaintext.CopyTo(input.AsSpan(nonce.Length));
var result = _cryptoClient.Encrypt(EncryptionAlgorithm.A256Gcm, input);
// Extract ciphertext and tag from result
result.Ciphertext.AsSpan(0, plaintext.Length).CopyTo(ciphertext);
result.Ciphertext.AsSpan(plaintext.Length, TagSize).CopyTo(tag);
}
public bool Decrypt(ReadOnlySpan<byte> nonce, ReadOnlySpan<byte> ciphertext,
ReadOnlySpan<byte> tag, Span<byte> plaintext)
{
try
{
var input = new byte[ciphertext.Length + tag.Length];
ciphertext.CopyTo(input);
tag.CopyTo(input.AsSpan(ciphertext.Length));
var result = _cryptoClient.Decrypt(EncryptionAlgorithm.A256Gcm, input);
result.Plaintext.AsSpan().CopyTo(plaintext);
return true;
}
catch
{
return false;
}
}
public ICryptoProvider Clone() => new AzureKeyVaultCryptoProvider(_keyId);
public void Dispose() { }
}
Registration and Usage
// Register the provider
ProviderRegistry.Instance.Register<ICryptoProvider>("azure-keyvault", p =>
new AzureKeyVaultCryptoProvider(
p.GetRequired<string>("vaultUrl"),
p.GetRequired<string>("keyName")));
// Use in database
var db = new WitDatabaseBuilder()
.WithFilePath("secure.witdb")
.WithBTree()
.WithEncryption("azure-keyvault", new ProviderParameters()
.Set("vaultUrl", "https://my-vault.vault.azure.net/")
.Set("keyName", "database-key"))
.Build();
4. Custom Storage Provider
IStorage Interface
public interface IStorage : IProvider, IDisposable
{
/// <summary>
/// Reads a page from storage.
/// </summary>
void ReadPage(long pageNumber, Span<byte> buffer);
/// <summary>
/// Writes a page to storage.
/// </summary>
void WritePage(long pageNumber, ReadOnlySpan<byte> buffer);
/// <summary>
/// Reads a page asynchronously.
/// </summary>
ValueTask ReadPageAsync(long pageNumber, Memory<byte> buffer,
CancellationToken cancellationToken = default);
/// <summary>
/// Writes a page asynchronously.
/// </summary>
ValueTask WritePageAsync(long pageNumber, ReadOnlyMemory<byte> buffer,
CancellationToken cancellationToken = default);
/// <summary>
/// Ensures all writes are persisted to storage.
/// </summary>
void Flush();
/// <summary>
/// Flushes asynchronously.
/// </summary>
ValueTask FlushAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Gets the page size in bytes.
/// </summary>
int PageSize { get; }
/// <summary>
/// Gets the current number of pages.
/// </summary>
long PageCount { get; }
}
Example: S3 Storage Provider
using Amazon.S3;
using Amazon.S3.Model;
using OutWit.Database.Core.Interfaces;
namespace MyCompany.WitDatabase.CloudStorage;
/// <summary>
/// Storage provider backed by Amazon S3.
/// Stores each page as a separate S3 object.
/// </summary>
public sealed class S3Storage : IStorage
{
private readonly IAmazonS3 _s3Client;
private readonly string _bucket;
private readonly string _prefix;
private readonly int _pageSize;
private long _pageCount;
private bool _disposed;
public S3Storage(string bucket, string region, string prefix = "witdb/",
int pageSize = 4096)
{
_bucket = bucket;
_prefix = prefix;
_pageSize = pageSize;
_s3Client = new AmazonS3Client(Amazon.RegionEndpoint.GetBySystemName(region));
// Load page count from metadata
_pageCount = LoadPageCount();
}
public string ProviderKey => "s3-storage";
public int PageSize => _pageSize;
public long PageCount => _pageCount;
public void ReadPage(long pageNumber, Span<byte> buffer)
{
ReadPageAsync(pageNumber, buffer.ToArray(), default).AsTask().Wait();
}
public async ValueTask ReadPageAsync(long pageNumber, Memory<byte> buffer,
CancellationToken cancellationToken = default)
{
ThrowIfDisposed();
var key =
Loading...
quot;{_prefix}page_{pageNumber:D10}";
try
{
var response = await _s3Client.GetObjectAsync(new GetObjectRequest
{
BucketName = _bucket,
Key = key
}, cancellationToken);
using var stream = response.ResponseStream;
await stream.ReadExactlyAsync(buffer, cancellationToken);
}
catch (AmazonS3Exception ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
{
// Page doesn't exist, return zeros
buffer.Span.Clear();
}
}
public void WritePage(long pageNumber, ReadOnlySpan<byte> buffer)
{
WritePageAsync(pageNumber, buffer.ToArray(), default).AsTask().Wait();
}
public async ValueTask WritePageAsync(long pageNumber, ReadOnlyMemory<byte> buffer,
CancellationToken cancellationToken = default)
{
ThrowIfDisposed();
var key =
Loading...
quot;{_prefix}page_{pageNumber:D10}";
using var stream = new MemoryStream(buffer.ToArray());
await _s3Client.PutObjectAsync(new PutObjectRequest
{
BucketName = _bucket,
Key = key,
InputStream = stream
}, cancellationToken);
if (pageNumber >= _pageCount)
{
_pageCount = pageNumber + 1;
await SavePageCount(cancellationToken);
}
}
public void Flush()
{
// S3 writes are immediately durable
}
public ValueTask FlushAsync(CancellationToken cancellationToken = default)
{
return ValueTask.CompletedTask;
}
private long LoadPageCount()
{
try
{
var response = _s3Client.GetObjectAsync(new GetObjectRequest
{
BucketName = _bucket,
Key =
Loading...
quot;{_prefix}metadata"
}).Result;
using var reader = new StreamReader(response.ResponseStream);
return long.Parse(reader.ReadToEnd());
}
catch
{
return 0;
}
}
private async Task SavePageCount(CancellationToken cancellationToken)
{
using var stream = new MemoryStream();
using var writer = new StreamWriter(stream);
await writer.WriteAsync(_pageCount.ToString());
await writer.FlushAsync(cancellationToken);
stream.Position = 0;
await _s3Client.PutObjectAsync(new PutObjectRequest
{
BucketName = _bucket,
Key =
Loading...
quot;{_prefix}metadata",
InputStream = stream
}, cancellationToken);
}
public void Dispose()
{
if (!_disposed)
{
_s3Client.Dispose();
_disposed = true;
}
}
private void ThrowIfDisposed()
{
ObjectDisposedException.ThrowIf(_disposed, this);
}
}
Registration and Usage
// Register the provider
ProviderRegistry.Instance.Register<IStorage>("s3-storage", p =>
new S3Storage(
p.GetRequired<string>("bucket"),
p.Get("region", "us-east-1"),
p.Get("prefix", "witdb/"),
p.Get("pageSize", 4096)));
// Use in database
var db = new WitDatabaseBuilder()
.WithStorage("s3-storage", new ProviderParameters()
.Set("bucket", "my-database-bucket")
.Set("region", "eu-west-1")
.Set("prefix", "production/db/"))
.WithBTree()
.WithEncryption("password")
.Build();
Performance Considerations
Cloud storage has different characteristics than local files:
Aspect
Local File
Cloud Storage (S3)
Latency
~0.1 ms
~50-100 ms
Throughput
Limited by SSD
Limited by network
Cost
Fixed
Per-operation
Durability
Single disk
Multi-AZ replication
Recommendations for cloud storage:
- Use large page sizes (16-64 KB) to reduce operation count
- Implement local caching layer
- Use eventually-consistent reads where possible
- Consider batch operations for bulk writes
5. Custom Cache Provider
IPageCache Interface
public interface IPageCache : IProvider, IDisposable
{
/// <summary>
/// Gets a page from cache, loading from storage if necessary.
/// </summary>
CachedPage GetPage(long pageNumber);
/// <summary>
/// Creates a new page in cache (for newly allocated pages).
/// </summary>
CachedPage CreatePage(long pageNumber);
/// <summary>
/// Marks a page as dirty (modified).
/// </summary>
void MarkDirty(long pageNumber);
/// <summary>
/// Releases a reference to a page.
/// </summary>
void ReleasePage(long pageNumber);
/// <summary>
/// Evicts a specific page from cache.
/// </summary>
void Evict(long pageNumber);
/// <summary>
/// Flushes all dirty pages to storage.
/// </summary>
void FlushAll();
/// <summary>
/// Clears all pages from cache.
/// </summary>
void Clear();
/// <summary>
/// Gets the number of cached pages.
/// </summary>
int Count { get; }
/// <summary>
/// Gets the number of dirty pages.
/// </summary>
int DirtyCount { get; }
}
Example: Redis-Backed Cache
using StackExchange.Redis;
using OutWit.Database.Core.Cache;
using OutWit.Database.Core.Interfaces;
namespace MyCompany.WitDatabase.DistributedCache;
/// <summary>
/// Page cache backed by Redis for distributed scenarios.
/// </summary>
public sealed class RedisCacheProvider : IPageCache
{
private readonly IStorage _storage;
private readonly IDatabase _redis;
private readonly string _keyPrefix;
private readonly int _pageSize;
private readonly TimeSpan _expiry;
public RedisCacheProvider(IStorage storage, string redisConnection,
string keyPrefix = "witdb:", TimeSpan? expiry = null)
{
_storage = storage;
_keyPrefix = keyPrefix;
_pageSize = storage.PageSize;
_expiry = expiry ?? TimeSpan.FromMinutes(30);
var connection = ConnectionMultiplexer.Connect(redisConnection);
_redis = connection.GetDatabase();
}
public string ProviderKey => "redis-cache";
public CachedPage GetPage(long pageNumber)
{
var key =
Loading...
quot;{_keyPrefix}{pageNumber}";
var cached = _redis.StringGet(key);
if (cached.HasValue)
{
var page = new CachedPage(pageNumber, _pageSize);
((byte[])cached!).AsSpan().CopyTo(page.Data);
return page;
}
// Load from storage
var newPage = new CachedPage(pageNumber, _pageSize);
_storage.ReadPage(pageNumber, newPage.Data);
// Cache in Redis
_redis.StringSet(key, newPage.Data.ToArray(), _expiry);
return newPage;
}
public CachedPage CreatePage(long pageNumber)
{
var page = new CachedPage(pageNumber, _pageSize);
page.Data.Clear();
page.MarkDirty();
return page;
}
public void MarkDirty(long pageNumber)
{
// Track dirty pages locally or in Redis set
_redis.SetAdd(
Loading...
quot;{_keyPrefix}dirty", pageNumber);
}
public void ReleasePage(long pageNumber)
{
// No-op for Redis cache
}
public void Evict(long pageNumber)
{
_redis.KeyDelete(
Loading...
quot;{_keyPrefix}{pageNumber}");
}
public void FlushAll()
{
var dirtyPages = _redis.SetMembers(
Loading...
quot;{_keyPrefix}dirty");
foreach (var pageNum in dirtyPages)
{
var pageNumber = (long)pageNum;
var key =
Loading...
quot;{_keyPrefix}{pageNumber}";
var data = _redis.StringGet(key);
if (data.HasValue)
{
_storage.WritePage(pageNumber, (byte[])data!);
}
}
_redis.KeyDelete(
Loading...
quot;{_keyPrefix}dirty");
_storage.Flush();
}
public void Clear()
{
// Clear all keys with prefix
// (In production, use SCAN instead of KEYS)
}
public int Count => 0; // Would need to track
public int DirtyCount => (int)_redis.SetLength(
Loading...
quot;{_keyPrefix}dirty");
public void Dispose() { }
}
6. Custom Transaction Journal
ITransactionJournal Interface
public interface ITransactionJournal : IProvider, IDisposable
{
/// <summary>
/// Logs the beginning of a transaction.
/// </summary>
void BeginTransaction(long transactionId);
/// <summary>
/// Logs a Put operation within a transaction.
/// </summary>
void LogPut(long transactionId, ReadOnlySpan<byte> key,
ReadOnlySpan<byte> value, ReadOnlySpan<byte> oldValue);
/// <summary>
/// Logs a Delete operation within a transaction.
/// </summary>
void LogDelete(long transactionId, ReadOnlySpan<byte> key,
ReadOnlySpan<byte> oldValue);
/// <summary>
/// Logs the commit of a transaction.
/// </summary>
void CommitTransaction(long transactionId);
/// <summary>
/// Logs the rollback of a transaction.
/// </summary>
void RollbackTransaction(long transactionId);
/// <summary>
/// Ensures all pending writes are flushed to disk.
/// </summary>
void Sync();
/// <summary>
/// Recovers transactions after a crash.
/// </summary>
/// <returns>Number of operations recovered.</returns>
int Recover(IKeyValueStore store);
/// <summary>
/// Creates a checkpoint, truncating the journal.
/// </summary>
void Checkpoint();
}
Example: Network-Replicated Journal
namespace MyCompany.WitDatabase.ReplicatedJournal;
/// <summary>
/// Transaction journal that replicates to remote servers for high availability.
/// </summary>
public sealed class ReplicatedJournal : ITransactionJournal
{
private readonly ITransactionJournal _localJournal;
private readonly IReplicationClient[] _replicas;
private readonly int _requiredAcks;
public ReplicatedJournal(string localPath, string[] replicaUrls, int requiredAcks = 1)
{
_localJournal = new WalTransactionJournal(localPath);
_replicas = replicaUrls.Select(url => new ReplicationClient(url)).ToArray();
_requiredAcks = requiredAcks;
}
public string ProviderKey => "replicated-wal";
public void BeginTransaction(long transactionId)
{
_localJournal.BeginTransaction(transactionId);
ReplicateAsync("BEGIN", transactionId).Wait();
}
public void LogPut(long transactionId, ReadOnlySpan<byte> key,
ReadOnlySpan<byte> value, ReadOnlySpan<byte> oldValue)
{
_localJournal.LogPut(transactionId, key, value, oldValue);
// Replicate asynchronously for performance
_ = ReplicateAsync("PUT", transactionId, key.ToArray(), value.ToArray());
}
public void LogDelete(long transactionId, ReadOnlySpan<byte> key,
ReadOnlySpan<byte> oldValue)
{
_localJournal.LogDelete(transactionId, key, oldValue);
_ = ReplicateAsync("DELETE", transactionId, key.ToArray());
}
public void CommitTransaction(long transactionId)
{
_localJournal.CommitTransaction(transactionId);
// Wait for required acknowledgments before returning
var acks = ReplicateAsync("COMMIT", transactionId).Result;
if (acks < _requiredAcks)
{
throw new InvalidOperationException(
Loading...
quot;Failed to replicate commit: got {acks} acks, required {_requiredAcks}");
}
}
public void RollbackTransaction(long transactionId)
{
_localJournal.RollbackTransaction(transactionId);
_ = ReplicateAsync("ROLLBACK", transactionId);
}
public void Sync() => _localJournal.Sync();
public int Recover(IKeyValueStore store) => _localJournal.Recover(store);
public void Checkpoint() => _localJournal.Checkpoint();
private async Task<int> ReplicateAsync(string operation, long transactionId,
byte[]? key = null, byte[]? value = null)
{
var tasks = _replicas.Select(r => r.ReplicateAsync(operation, transactionId, key, value));
var results = await Task.WhenAll(tasks);
return results.Count(r => r);
}
public void Dispose()
{
_localJournal.Dispose();
foreach (var replica in _replicas)
{
replica.Dispose();
}
}
}
7. Quick Reference
Interface Summary
Interface
Methods
Built-in Providers
IProvider
ProviderKey
Base for all providers
ICryptoProvider
Encrypt, Decrypt, Clone, NonceSize, TagSize
aes-gcm, chacha20-poly1305
IStorage
ReadPage, WritePage, Flush, PageSize, PageCount
file, memory, indexeddb
IPageCache
GetPage, CreatePage, MarkDirty, FlushAll, Count
clock, lru
ITransactionJournal
BeginTransaction, LogPut, CommitTransaction, Recover
rollback, wal
IKeyValueStore
Get, Put, Delete, Scan, BeginTransaction
btree, lsm, inmemory
ISecondaryIndexFactory
CreateIndex
Built-in B+Tree
ProviderRegistry API
// Registration
ProviderRegistry.Instance.Register<T>(key, factory);
ProviderRegistry.Instance.RegisterOrReplace<T>(key, factory);
// Creation
T provider = ProviderRegistry.Instance.Create<T>(key, parameters);
// Query
bool exists = ProviderRegistry.Instance.IsRegistered<T>(key);
var keys = ProviderRegistry.Instance.GetRegisteredKeys<T>();
ProviderParameters API
var p = new ProviderParameters()
.Set("key", value)
.Set<int>("count", 100);
var value = p.Get<string>("key");
var count = p.Get("count", defaultValue: 50);
var required = p.GetRequired<byte[]>("key");
bool has = p.Has("key");
Custom Provider Checklist
When implementing a custom provider:
ICryptoProvider:
- Implement AEAD encryption (Encrypt + authentication tag)
- Return
false from Decrypt on authentication failure (don't throw)
- Make
Clone() return independent instance
- Use standard nonce (12 bytes) and tag (16 bytes) sizes
- Clear sensitive data in
Dispose()
IStorage:
- Handle page reads for non-existent pages (return zeros)
- Implement both sync and async methods
- Ensure
Flush() persists all writes
- Track
PageCount accurately
IPageCache:
- Load pages from storage on cache miss
- Track dirty pages for flushing
- Handle eviction when cache is full
- Implement reference counting if needed
ITransactionJournal:
- Persist commit marker durably before returning from
CommitTransaction
- Implement
Recover() to replay/undo after crash
- Support
Checkpoint() to truncate journal
Builder Methods for Custom Providers
var db = new WitDatabaseBuilder()
// Storage
.WithStorage(IStorage instance)
.WithStorage(string providerKey, ProviderParameters parameters)
// Encryption
.WithEncryption(ICryptoProvider instance)
.WithEncryption(string providerKey, ProviderParameters parameters)
// Cache
.WithCache(IPageCache instance)
.WithCache(string providerKey, ProviderParameters parameters)
// Journal
.WithJournal(ITransactionJournal instance)
.WithJournal(string providerKey, ProviderParameters parameters)
// Store
.WithStore(IKeyValueStore instance)
.Build();
Example: Full Custom Configuration
// Register all custom providers
public static class CustomProviders
{
[ModuleInitializer]
public static void Register()
{
ProviderRegistry.Instance.Register<ICryptoProvider>("azure-keyvault",
p => new AzureKeyVaultCryptoProvider(
p.GetRequired<string>("vaultUrl"),
p.GetRequired<string>("keyName")));
ProviderRegistry.Instance.Register<IStorage>("s3",
p => new S3Storage(
p.GetRequired<string>("bucket"),
p.Get("region", "us-east-1")));
ProviderRegistry.Instance.Register<IPageCache>("redis",
p => new RedisCacheProvider(
p.GetRequired<IStorage>("storage"),
p.GetRequired<string>("connection")));
}
}
// Use custom providers
var db = new WitDatabaseBuilder()
.WithStorage("s3", new ProviderParameters()
.Set("bucket", "prod-database")
.Set("region", "eu-west-1"))
.WithEncryption("azure-keyvault", new ProviderParameters()
.Set("vaultUrl", "https://myvault.vault.azure.net/")
.Set("keyName", "db-key"))
.WithBTree()
.WithTransactions()
.Build();