-
Notifications
You must be signed in to change notification settings - Fork 1
MiniDB
MiniDB is a fast and lightweight database system built for UltimateServer. It stores serialized objects inside a single binary database file and maintains an index for instant lookups. Each entry is accessible by a unique string key, making MiniDB ideal for storing user accounts, server settings, tokens, and other small data objects.
MiniDB uses two files:
-
server.mdb– binary file containing all stored objects -
mdb.index– text file containing lookup information
Each entry in the index contains:
-
Offset – position of the object in
server.mdb - Length – total byte length of the object chunk
- TypeName – the type the object was serialized as
- LastModified – timestamp of when the record was last updated
Objects are serialized using DataContractSerializer and stored as:
[4-byte length prefix][object binary data]MiniDB can be configured through the MiniDBOptions class:
var options = new MiniDBOptions
{
DatabaseFile = "server.mdb",
IndexFile = "mdb.index",
AutoSaveInterval = TimeSpan.FromMinutes(5), // Auto-save every 5 minutes
AutoSaveThreshold = 100 // Auto-save after 100 operations
};
MiniDB must be started before use:
await miniDB.Start();
Before shutdown, save the index and dispose resources:
await miniDB.Stop();
// Or if using MiniDB as a resource:
miniDB.Dispose();
To store any object, call InsertDataAsync<T> with a unique key:
var adminUser = new User {
Username = "Admin",
Role = "Administrator",
CreatedAt = DateTime.UtcNow
};
await MiniDB.InsertDataAsync("admin-username", adminUser);
This stores the object inside server.mdb and writes an index entry to mdb.index.
To update an existing entry or insert if it doesn't exist, use UpsertDataAsync:
adminUser.LastLogin = DateTime.UtcNow;
await MiniDB.UpsertDataAsync("admin-username", adminUser);
Retrieve data by specifying both the key and the expected type:
User? admin = await MiniDB.GetDataAsync<User>("admin-username");
if (admin != null)
Console.WriteLine($"Loaded admin: {admin.Username}");
MiniDB automatically:
- Reads the index entry
- Jumps to the object location in the database
- Deserializes the bytes back into the requested C# type
Important: Type must match what was originally stored, or MiniDB throws a type mismatch error.
To delete an entry:
await MiniDB.DeleteAsync("admin-username");
This removes the index entry (but does not shrink the database file).
For better performance when working with multiple records, use batch operations:
var operations = new List<BatchOperation>
{
new BatchOperation { Key = "user1", Data = user1, Type = BatchOperationType.Insert },
new BatchOperation { Key = "user2", Data = user2, Type = BatchOperationType.Upsert },
new BatchOperation { Key = "old-user", Type = BatchOperationType.Delete }
};
await MiniDB.BatchOperationAsync(operations);
Rebuilds the database file to remove space from deleted records:
await MiniDB.CompactAsync();
This is a long-running operation and should be called during maintenance periods.
Get database statistics and metrics:
var stats = await MiniDB.GetStatsAsync();
Console.WriteLine($"Total entries: {stats.TotalEntries}");
Console.WriteLine($"Database size: {stats.DatabaseFileSize} bytes");
Console.WriteLine($"Read operations: {stats.ReadOperations}");
Console.WriteLine($"Write operations: {stats.WriteOperations}");
All async operations support cancellation tokens:
var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromSeconds(30));
try {
await MiniDB.CompactAsync(cts.Token);
}
catch (OperationCanceledException) {
Console.WriteLine("Operation was cancelled");
}
// Lifecycle
Task Start();
Task Stop();
void Dispose();
// Basic operations
Task InsertDataAsync<T>(string key, T obj, CancellationToken cancellationToken = default);
Task UpsertDataAsync<T>(string key, T obj, CancellationToken cancellationToken = default);
Task<T?> GetDataAsync<T>(string key, CancellationToken cancellationToken = default);
Task DeleteAsync(string key, CancellationToken cancellationToken = default);
// Batch operations
Task BatchOperationAsync(IEnumerable<BatchOperation> operations, CancellationToken cancellationToken = default);
// Utility
Task<bool> ContainsKeyAsync(string key, CancellationToken cancellationToken = default);
Task<IEnumerable<string>> GetAllKeysAsync(CancellationToken cancellationToken = default);
Task<DatabaseStats> GetStatsAsync(CancellationToken cancellationToken = default);
Task CompactAsync(CancellationToken cancellationToken = default);
MiniDB provides specific exception types for different error scenarios:
-
MiniDBException– Base exception for all MiniDB errors -
MiniDBKeyNotFoundException– Thrown when a requested key doesn't exist -
MiniDBTypeMismatchException– Thrown when the requested type doesn't match the stored type
Example error handling:
try {
var user = await MiniDB.GetDataAsync<User>("nonexistent-key");
} catch (MiniDBKeyNotFoundException ex) {
Console.WriteLine($"Key not found: {ex.Message}");
} catch (MiniDBTypeMismatchException ex) {
Console.WriteLine($"Type mismatch: {ex.Message}");
} catch (MiniDBException ex) {
Console.WriteLine($"Database error: {ex.Message}");
}
- Objects must be serializable by
DataContractSerializer - Support for complex nested objects
- Store-as-single-chunk structure makes access O(1)
- Deleting does not reclaim space (use compaction to reclaim space)
- Each record tracks its last modification time
- ReaderWriterLockSlim allows multiple concurrent reads while ensuring exclusive writes
- Batch operations reduce lock contention for bulk operations
- Auto-save functionality reduces risk of data loss
- Metrics tracking helps identify performance bottlenecks
- Not designed for millions of entries
- Manual compaction required to reclaim space
- Stored types cannot change without breaking deserialization
- File I/O operations may limit performance under high concurrency
- Use readable, unique keys (e.g.
user-admin,config-server) - Always call
Start()at server boot andStop()orDispose()at shutdown - Avoid storing extremely large objects
- Keep your data models consistent to avoid type mismatch errors
- Use batch operations when working with multiple records
- Monitor database statistics to identify when compaction is needed
- Configure auto-save based on your application's durability requirements
- Use cancellation tokens for long-running operations like compaction