MasterMemory
repository·master·Indexed 23 days ago
https://github.com/cysharp/mastermemoryA high-performance, embedded, read-only in-memory document database for .NET and Unity. It utilizes C# source generators to provide type-safe, zero-allocation querying based on user-defined schemas, offering a smaller memory footprint and faster performance than SQLite.
What's inside MasterMemory
- MasterMemory is a source generator-based, embedded, typed, readonly, in-memory document database designed for .NET and Unity. It is optimized for high performance, claiming to be significantly faster than SQLite with zero allocation per query and a much smaller memory footprint. It uses C# source generators to automatically create a typed database structure from your schema classes, providing type-safe queries and full IDE autocompletion.
Define a MasterMemory Table
masterTables are defined using classes or records marked with the
[MemoryTable]attribute. The class must also be serializable by MessagePack (use[MessagePackObject(true)]).[MemoryTable("name")]: Defines the table name used in the binary data.[PrimaryKey]: Marks the unique identifier for the table. Required for every table.[SecondaryKey(indexNo, keyOrder)]: Defines secondary indexes. Use[NonUnique]if the key does not guarantee a single result.[StringComparisonOption]: Configures how string keys are compared (default isOrdinal).[IgnoreMember]: Use this on computed properties to prevent MessagePack from attempting to serialize them.
[MemoryTable("person"), MessagePackObject(true)] public record Person { [PrimaryKey] public required int PersonId { get; init; } [SecondaryKey(0), NonUnique] [SecondaryKey(1, keyOrder: 1), NonUnique] public required int Age { get; init; } [SecondaryKey(2), NonUnique] [SecondaryKey(1, keyOrder: 0), NonUnique] public required Gender Gender { get; init; } public required string Name { get; init; } }Install MasterMemory via NuGet (.NET)
masterTo use MasterMemory in a standard .NET project, install the library (which includes both the Runtime and the Source Generator/Analyzer) via NuGet.
dotnet add package MasterMemoryBuild and Query a MemoryDatabase
masterMasterMemory follows a two-phase workflow: Build (creating the binary data) and Query (loading the binary into a
MemoryDatabase).1. Build Phase
Use
DatabaseBuilderto collect data and generate a byte array.2. Query Phase
Load the byte array into a
MemoryDatabase. The generated table classes provide type-safe methods for searching.Query Methods:
FindBy***(key): Returns a single item. ThrowsKeyNotFoundExceptionif not found (unlessIsReturnNullIfKeyNotFoundis enabled in options).TryFindBy***(key, out result): Returnsboolindicating if the key exists.FindClosestBy***(key, selectLower): Returns the nearest value (default is lower).FindRangeBy***(min, max): Returns aRangeView<T>containing items within the inclusive range.
Install MasterMemory in Unity
masterMasterMemory requires Unity
2022.3.12f1or later to support C# Incremental Source Generators.- Install NuGetForUnity.
- Open NuGet -> Manage NuGet Packages.
- Search for "MasterMemory" and click Install.
Note on C# features: Since Unity may not support the
requiredkeyword (C# 11), use theinitkeyword to ensure immutability. You may need to defineIsExternalInitto enableinitsupport:namespace System.Runtime.CompilerServices { internal sealed class IsExternalInit { } }Extend Generated Table Classes
masterGenerated table classes are
partial. You can extend them by creating anotherpartialclass in the same namespace. This allows you to add custom methods or perform post-construction logic using theOnAfterConstructmethod.Use
OnAfterConstructto initialize cached fields or derived data after the table is fully loaded.public sealed partial class MonsterTable { int maxHp; readonly int minHp; partial void OnAfterConstruct() { maxHp = All.Select(x => x.MaxHp).Max(); // Use Unsafe.AsRef to set readonly fields Unsafe.AsRef(minHp) = All.Select(x => x.MaxHp).Min(); } public IEnumerable<Monster> GetRangedMonster(int arg1) { return All.Where(x => x.Hp > arg1); } }Configure Source Generator with MasterMemoryGeneratorOptions
masterConfigure the behavior of the Source Generator using the
[assembly: MasterMemoryGeneratorOptions]attribute. This can be placed in any.csfile in your project.Namespace: The namespace for generated files. Defaults to the project'sRootNamespaceorMasterMemory.IsReturnNullIfKeyNotFound: Iftrue,FindBy***methods returnT?(null) instead of throwingKeyNotFoundExceptionwhen a key is missing.PrefixClassName: Adds a prefix to generated classes (e.g.,FooDatabaseBuilderinstead ofDatabaseBuilder). Useful for avoiding conflicts in multi-project solutions.
[assembly: MasterMemoryGeneratorOptions( Namespace = "MyConsoleApp", IsReturnNullIfKeyNotFound = true, PrefixClassName = "Foo" )]Validate Data with IValidatable
masterYou can implement custom validation logic by having your table classes implement
IValidatable<T>. TheMemoryDatabase.Validate()method will then execute these rules.Key Validation Capabilities:
- Reference Checks: Ensure a foreign key exists in another table using
validator.GetReferenceSet<TRef>(). - Predicate Checks: Validate individual properties using
Validate(predicate). - Global Checks: Use
validator.CallOnce()to perform checks that require looking at the entire table (e.g., uniqueness of a non-indexed field).
[MemoryTable("quest_master"), MessagePackObject(true)] public class Quest : IValidatable<Quest> { [PrimaryKey] public int Id { get; } public int RewardId { get; } public int Cost { get; } void IValidatable<Quest>.Validate(IValidator<Quest> validator) { // Check if RewardId exists in Item table var items = validator.GetReferenceSet<Item>(); if (this.RewardId > 0) { items.Exists(x => x.RewardId, x => x.ItemId); } // Range check validator.Validate(x => x.Cost >= 10 && x.Cost <= 20); // Global uniqueness check if (validator.CallOnce()) { var quests = validator.GetTableSet(); quests.Where(x => x.RewardId != 0).Unique(x => x.RewardId); } } }- Reference Checks: Ensure a foreign key exists in another table using
Modify Data with ImmutableBuilder
masterSince
MemoryDatabaseis read-only, useToImmutableBuilder()to create a builder that allows adding, removing, or replacing data. Once modifications are complete, call.Build()to create a newMemoryDatabaseinstance.// Create builder from existing database var builder = db.ToImmutableBuilder(); // Modify data builder.Diff(addOrReplaceData); builder.RemovePerson(new[] { 1, 10, 100 }); builder.ReplaceAll(newData); // Create new database MemoryDatabase newDatabase = builder.Build(); // Convert back to DatabaseBuilder to save to file var newBuilder = newDatabase.ToDatabaseBuilder(); var newBinary = newBuilder.Build();Optimize MemoryDatabase Construction
masterFor large databases, the construction time of
MemoryDatabasecan be significant. You can speed up the process by enabling parallel construction using themaxDegreeOfParallelismparameter in the constructor.It is recommended to use
Environment.ProcessorCountto utilize all available CPU cores.var database = new MemoryDatabase(bin, maxDegreeOfParallelism: Environment.ProcessorCount);