SynesthesiaDev.Nocturne.Database
2026.707.1
dotnet add package SynesthesiaDev.Nocturne.Database --version 2026.707.1
NuGet\Install-Package SynesthesiaDev.Nocturne.Database -Version 2026.707.1
<PackageReference Include="SynesthesiaDev.Nocturne.Database" Version="2026.707.1" />
<PackageVersion Include="SynesthesiaDev.Nocturne.Database" Version="2026.707.1" />
<PackageReference Include="SynesthesiaDev.Nocturne.Database" />
paket add SynesthesiaDev.Nocturne.Database --version 2026.707.1
#r "nuget: SynesthesiaDev.Nocturne.Database, 2026.707.1"
#:package SynesthesiaDev.Nocturne.Database@2026.707.1
#addin nuget:?package=SynesthesiaDev.Nocturne.Database&version=2026.707.1
#tool nuget:?package=SynesthesiaDev.Nocturne.Database&version=2026.707.1
🌙 Nocturne
Lightweight easy-to-use local database for C#
No reflection, no hidden native wrapper, no magic
Usage
Create one global database object
public static readonly NocturneDatabase NOCTURNE_DATABASE = new NocturneDatabase
{
FilePath = "./data/database.nocturne",
CompactOnLaunch = true,
AutomaticallyCompact = false
};
In all of your "models" you then define a collection object:
public record Person(string Name, int Age, bool IsCool)
{
public static readonly NocturneCollection<string, Person> DB_COLLECTION = Program.NOCTURNE_DATABASE.For(
collectionKey: "people",
schemaVersion: 1,
keySerializer: KeySerializers.STRING,
valueSerializer: DATABASE_SERIALIZER
);
}
Every model needs their own serializer. Nocturne does no do any source generation or reflection by design so you will need to provide your own serializer.
You can either write one manually:
public static readonly INocturneSerializer<Person> DATABASE_SERIALIZER = NocturneSerializer.For<Person>
(
(buffer) =>
{
var name = buffer.ReadString();
var age = buffer.ReadInt();
var isCool = buffer.ReadBoolean();
return new Person(name, age, isCool);
},
(buffer, person) =>
{
buffer.WriteString(person.Name);
buffer.WriteInt(person.Age);
buffer.WriteBoolean(person.IsCool);
}
);
Ore uses an IBinaryCodec which is from Codon library that comes packaged in:
public static readonly IBinaryCodec<Person> CODEC = BinaryCodecs.For<Person>()
.Field(BinaryCodecs.STRING, p => p.Name)
.Field(BinaryCodecs.INT, p => p.Age)
.Field(BinaryCodecs.BOOLEAN, p => p.IsCool)
.Build((name, age, cool) => new Person(name, age, cool));
public static readonly INocturneSerializer<Person> DATABASE_SERIALIZER = NocturneSerializer.FromCodec(CODEC);
Collections
You can then do all the fun stuff like Insert, Find, FindOrNull, FindOrAdd, Delete, Nuke, etc. by calling Person.DB_COLLECTION:
var newPerson = new Person("John Person", 21, false);
Person.DB_COLLECTION.Insert("john", newPerson);
var john = Person.DB_COLLECTION.Find("john");
Person.DB_COLLECTION.Delete("john");
//fuck it, who needs prod anyway
Person.DB_COLLECTION.Nuke();
Schema Migrations
If you change your model structure, Nocturne handles step-by-step migrations using byte buffers. For example, migrating from Version 0 (which had a FunFact string) to Version 1 (current):
IMigrationStrategy.Migrations()
.Add(0, buffer =>
{
// 1. Read the old schema format out of the buffer
var name = BinaryCodecs.STRING.Read(buffer);
var age = BinaryCodecs.INT.Read(buffer);
var isCool = BinaryCodecs.BOOLEAN.Read(buffer);
var _ = BinaryCodecs.STRING.Read(buffer); // Discard old "FunFact" string
// 2. Write the clean data into the new schema format
var newBuffer = Unpooled.Buffer();
BinaryCodecs.STRING.Write(newBuffer, name);
BinaryCodecs.INT.Write(newBuffer, age);
BinaryCodecs.BOOLEAN.Write(newBuffer, isCool);
return newBuffer; // buffer is automatically released if you choose to use pooled one
})
.Build()
You may also use IMigrationStrategy.DeleteIfMigrationRequired() which deletes the collection and creates new one with new schema if a migration is required
Compaction
This is a log-style database, meaning it's append-only. Records do not get truly deleted from the database file, only marked as dead.
Calling Database.Compact() will remove all records marked as dead or outdated and compact the file down. Remember to call it once in a while, otherwise your database will grow exponentially!
There are properties CompactOnLaunch which compacts the database file when the database launches and AutomaticallyCompact which compacts the database automatically when over 50% of its content is dead/outdated. Do keep in mind compacting does block the main thread and may take a good 500ms (or more depending on your data size)
If you are working with performance-critical stuff, I recommend managing compactions manually instead of relying on AutomaticallyCompact
Backups
You may take a backup of the database by selecting the file and pressing CTRL + C followed by CTRL + V and adding "dkjglkg.bak" at the end of the file name
(pro tip: if the file name is already taken, smash your keyboard again. Repeat until you land on not taken file name)
(ultra pro tip: if you are a real pro, you may even set up a CRON job for this!)
Lastly, this is a learning project I made practically in one sitting. You should probably not use it in production code...
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Faster.Map (>= 8.1.1)
- Serilog (>= 4.4.0-dev-02437)
- Serilog.Sinks.TextWriter (>= 3.0.0)
- SynesthesiaDev.Codon.BinaryCodec (>= 2026.623.0)
- SynesthesiaUtils (>= 2026.614.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 2026.707.1 | 47 | 7/7/2026 |
| 2026.707.0 | 41 | 7/7/2026 |
| 2026.706.0 | 59 | 7/6/2026 |