Skip to content
Alireza Janaki edited this page Dec 9, 2025 · 3 revisions

MiniDB – Lightweight Indexed Binary Database

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.

How MiniDB Works

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]

Configuration

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
};

Starting & Stopping MiniDB

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();

Basic Operations

Inserting Data

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.

Updating Data

To update an existing entry or insert if it doesn't exist, use UpsertDataAsync:


adminUser.LastLogin = DateTime.UtcNow;
await MiniDB.UpsertDataAsync("admin-username", adminUser);

Reading Data

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.

Deleting Data

To delete an entry:


await MiniDB.DeleteAsync("admin-username");

This removes the index entry (but does not shrink the database file).

Advanced Operations

Batch Operations

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);

Database Compaction

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.

Database Statistics

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}");

Cancellation Support

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");
}

MiniDB API Summary


// 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);

Error Handling

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}");
}

Serialization Notes

  • 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

Performance Considerations

  • 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

Limitations

  • 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

Best Practices

  • Use readable, unique keys (e.g. user-admin, config-server)
  • Always call Start() at server boot and Stop() or Dispose() 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

Clone this wiki locally