Coder Social home page Coder Social logo

mastermemory's Introduction

GitHub Actions Releases

MasterMemory

Embedded Typed Readonly In-Memory Document Database for .NET Core and Unity.

image

4700 times faster than SQLite and achieves zero allocation per query. Also the DB size is small. When SQLite is 3560kb then MasterMemory is only 222kb.

Table of Contents

Concept

  • Memory Efficient, Only use underlying data memory and do aggressively string interning.
  • Performance, Similar as dictionary lookup.
  • TypeSafe, 100% Type safe by pre code-generation.
  • Fast load speed, MasterMemory save data by MessagePack for C#, a fastest C# serializer so load speed is blazing fast.
  • Flexible Search, Supports multiple key, multiple result, range/closest query.
  • Validator, You can define custom data validation by C#.
  • Metadata, To make custom importer/exporter, get the all database metadata.

These features are suitable for master data management(write-once, read-heavy) on embedded application such as role-playing game. MasterMemory has better performance than any other database solutions. PalDB developed by LinkedIn has a similar concept(embeddable write-once key-value store), but the implementation and performance characteristics are completely different.

Getting Started(.NET Core)

MasterMemory uses C# to C# code-generator. Runtime library API is the same but how to code-generate has different way between .NET Core and Unity. This sample is for .NET Core(for Unity is in below sections).

Install the core library(Runtime and Annotations).

PM> Install-Package MasterMemory

Prepare the example table definition like following.

public enum Gender
{
    Male, Female, Unknown
}

// table definition marked by MemoryTableAttribute.
// database-table must be serializable by MessagePack-CSsharp
[MemoryTable("person"), MessagePackObject(true)]
public class Person
{
    // index definition by attributes.
    [PrimaryKey]
    public int PersonId { get; set; }

    // secondary index can add multiple(discriminated by index-number).
    [SecondaryKey(0), NonUnique]
    [SecondaryKey(1, keyOrder: 1), NonUnique]
    public int Age { get; set; }

    [SecondaryKey(2), NonUnique]
    [SecondaryKey(1, keyOrder: 0), NonUnique]
    public Gender Gender { get; set; }

    public string Name { get; set; }
}

Edit the .csproj, add MasterMemory.MSBuild.Tasks and add configuration like following.

<ItemGroup>
    <PackageReference Include="MasterMemory" Version="2.1.2" />
    <!-- Install MSBuild Task(with PrivateAssets="All", it means to use dependency only in build time). -->
    <PackageReference Include="MasterMemory.MSBuild.Tasks" Version="2.1.2" PrivateAssets="All" />
</ItemGroup>

<!-- Call code generator before-build. -->
<Target Name="MasterMemoryGen" BeforeTargets="BeforeBuild">
    <!-- Configuration of Code-Generator, `UsingNamespace`, `InputDirectory`, `OutputDirectory` and `AddImmutableConstructor`. -->
    <MasterMemoryGenerator UsingNamespace="$(ProjectName)" InputDirectory="$(ProjectDir)" OutputDirectory="$(ProjectDir)MasterMemory" />
</Target>

After the build, generated files(DatabaseBuilder.cs, ImmutableBuilder.cs, MasterMemoryResolver.cs, MemoryDatabase.cs and Tables/***Table.cs) in OutputDirectory.

image

Finally, you can regsiter and query by these files.

// to create database, use DatabaseBuilder and Append method.
var builder = new DatabaseBuilder();
builder.Append(new Person[]
{
    new Person { PersonId = 0, Age = 13, Gender = Gender.Male,   Name = "Dana Terry" },
    new Person { PersonId = 1, Age = 17, Gender = Gender.Male,   Name = "Kirk Obrien" },
    new Person { PersonId = 2, Age = 31, Gender = Gender.Male,   Name = "Wm Banks" },
    new Person { PersonId = 3, Age = 44, Gender = Gender.Male,   Name = "Karl Benson" },
    new Person { PersonId = 4, Age = 23, Gender = Gender.Male,   Name = "Jared Holland" },
    new Person { PersonId = 5, Age = 27, Gender = Gender.Female, Name = "Jeanne Phelps" },
    new Person { PersonId = 6, Age = 25, Gender = Gender.Female, Name = "Willie Rose" },
    new Person { PersonId = 7, Age = 11, Gender = Gender.Female, Name = "Shari Gutierrez" },
    new Person { PersonId = 8, Age = 63, Gender = Gender.Female, Name = "Lori Wilson" },
    new Person { PersonId = 9, Age = 34, Gender = Gender.Female, Name = "Lena Ramsey" },
});

// build database binary(you can also use `WriteToStream` for save to file).
byte[] data = builder.Build();

// -----------------------

// for query phase, create MemoryDatabase.
// (MemoryDatabase is recommended to store in singleton container(static field/DI)).
var db = new MemoryDatabase(data);

// .PersonTable.FindByPersonId is fully typed by code-generation.
Person person = db.PersonTable.FindByPersonId(10);

// Multiple key is also typed(***And * **), Return value is multiple if key is marked with `NonUnique`.
RangeView<Person> result = db.PersonTable.FindByGenderAndAge((Gender.Female, 23));

// Get nearest value(choose lower(default) or higher).
RangeView<Person> age1 = db.PersonTable.FindClosestByAge(31);

// Get range(min-max inclusive).
RangeView<Person> age2 = db.PersonTable.FindRangeByAge(20, 29);

All table(marked by MemoryTableAttribute) and methods(created by PrimaryKeyAttribute or SecondaryKeyAttribute) are typed.

image

You can invoke all indexed query by IntelliSense.

Getting Started(Unity)

Check the releases page, download MasterMemory.Unity.unitypackage(runtime) and MasterMemory.Generator.zip(cli code-generator). MasterMemory also depends on MessagePack-CSharp so you have to download MessagePack.Unity.2.*.*.unitypackage and mpc.zip from MessagePack-CSharp/releases page.

Prepare the example table definition like following.

public enum Gender
{
    Male, Female, Unknown
}

// table definition marked by MemoryTableAttribute.
// database-table must be serializable by MessagePack-CSsharp
[MemoryTable("person"), MessagePackObject(true)]
public class Person
{
    // index definition by attributes.
    [PrimaryKey]
    public int PersonId { get; set; }

    // secondary index can add multiple(discriminated by index-number).
    [SecondaryKey(0), NonUnique]
    [SecondaryKey(1, keyOrder: 1), NonUnique]
    public int Age { get; set; }

    [SecondaryKey(2), NonUnique]
    [SecondaryKey(1, keyOrder: 0), NonUnique]
    public Gender Gender { get; set; }

    public string Name { get; set; }
}

use the MasterMemory code generator by commandline. Commandline tool support platforms are win-x64, osx-x64 and linux-x64.

Usage: MasterMemory.Generator [options...]

Options:
  -i, -inputDirectory <String>              Input file directory(search recursive). (Required)
  -o, -outputDirectory <String>             Output file directory. (Required)
  -n, -usingNamespace <String>              Namespace of generated files. (Required)
  -p, -prefixClassName <String>             Prefix of class names. (Default: )
  -c, -addImmutableConstructor <Boolean>    Add immutable constructor to MemoryTable class. (Default: False)
  -t, -returnNullIfKeyNotFound <Boolean>    Return null if key not found on unique find method. (Default: False)
MasterMemory.Generator.exe -i "C:\UnitySample" -o "C:\UnitySample\Generated" -n "UnitySample"

Also you need to generated MessagePack-CSharp code generation.

Additional steps, you have to set up to use generated resolver.

public static class Initializer
{
    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
    public static void SetupMessagePackResolver()
    {
        StaticCompositeResolver.Instance.Register(new[]{
            MasterMemoryResolver.Instance, // set MasterMemory generated resolver
            GeneratedResolver.Instance,    // set MessagePack generated resolver
            StandardResolver.Instance      // set default MessagePack resolver
        });

        var options = MessagePackSerializerOptions.Standard.WithResolver(StaticCompositeResolver.Instance);
        MessagePackSerializer.DefaultOptions = options;
    }
}

The rest is the same as .NET Core version.

// to create database, use DatabaseBuilder and Append method.
var builder = new DatabaseBuilder();
builder.Append(new Person[]
{
    new Person { PersonId = 0, Age = 13, Gender = Gender.Male,   Name = "Dana Terry" },
    new Person { PersonId = 1, Age = 17, Gender = Gender.Male,   Name = "Kirk Obrien" },
    new Person { PersonId = 2, Age = 31, Gender = Gender.Male,   Name = "Wm Banks" },
    new Person { PersonId = 3, Age = 44, Gender = Gender.Male,   Name = "Karl Benson" },
    new Person { PersonId = 4, Age = 23, Gender = Gender.Male,   Name = "Jared Holland" },
    new Person { PersonId = 5, Age = 27, Gender = Gender.Female, Name = "Jeanne Phelps" },
    new Person { PersonId = 6, Age = 25, Gender = Gender.Female, Name = "Willie Rose" },
    new Person { PersonId = 7, Age = 11, Gender = Gender.Female, Name = "Shari Gutierrez" },
    new Person { PersonId = 8, Age = 63, Gender = Gender.Female, Name = "Lori Wilson" },
    new Person { PersonId = 9, Age = 34, Gender = Gender.Female, Name = "Lena Ramsey" },
});

// build database binary(you can also use `WriteToStream` for save to file).
byte[] data = builder.Build();

// -----------------------

// for query phase, create MemoryDatabase.
// (MemoryDatabase is recommended to store in singleton container(static field/DI)).
var db = new MemoryDatabase(data);

// .PersonTable.FindByPersonId is fully typed by code-generation.
Person person = db.PersonTable.FindByPersonId(10);

// Multiple key is also typed(***And * **), Return value is multiple if key is marked with `NonUnique`.
RangeView<Person> result = db.PersonTable.FindByGenderAndAge((Gender.Female, 23));

// Get nearest value(choose lower(default) or higher).
RangeView<Person> age1 = db.PersonTable.FindClosestByAge(31);

// Get range(min-max inclusive).
RangeView<Person> age2 = db.PersonTable.FindRangeByAge(20, 29);

All table(marked by MemoryTableAttribute) and methods(created by PrimaryKeyAttribute or SecondaryKeyAttribute) are typed.

image

You can invoke all indexed query by IntelliSense.

DataTable configuration

Element type of datatable must be marked by [MemoryTable(tableName)], datatable is generated from marked type. string tableName is saved in database binary, you can rename class name if tableName is same.

[PrimaryKey(keyOrder = 0)], [SecondaryKey(indexNo, keyOrder)], [NonUnique] can add to public property, [PrimaryKey] must use in MemoryTable, [SecondaryKey] is option.

Both PrimaryKey and SecondaryKey can add to multiple properties, it will be generated ***And***And***.... keyOrder is order of column names, default is zero(sequential in which they appear).

[MemoryTable("sample"), MessagePackObject(true)]
public class Sample
{
    [PrimaryKey]
    public int Foo { get; set; }
    [PrimaryKey]
    public int Bar { get; set; }
}

db.Sample.FindByFooAndBar((int Foo, int Bar))

// ----

[MemoryTable("sample"), MessagePackObject(true)]
public class Sample
{
    [PrimaryKey(keyOrder: 1)]
    public int Foo { get; set; }
    [PrimaryKey(keyOrder: 0)]
    public int Bar { get; set; }
}

db.Sample.FindByBarAndFoo((int Bar, int Foo))

Default of FindBy*** return type is single(if not found, returns null). It means key is unique by default. If mark [NonUnique] in same AttributeList, return type is RangeView<T>(if not found, return empty).

[MemoryTable("sample"), MessagePackObject(true)]
public class Sample
{
    [PrimaryKey, NonUnique]
    public int Foo { get; set; }
    [PrimaryKey, NonUnique]
    public int Bar { get; set; }
}

RangeView<Sample> q = db.Sample.FindByFooAndBar((int Foo, int Bar))
[MemoryTable("sample"), MessagePackObject(true)]
public class Sample
{
    [PrimaryKey]
    [SecondaryKey(0)]
    public int Foo { get; set; }
    [SecondaryKey(0)]
    [SecondaryKey(1)]
    public int Bar { get; set; }
}

db.Sample.FindByFoo(int Foo)
db.Sample.FindByFooAndBar((int Foo, int Bar))
db.Sample.FindByBar(int Bar)

[StringComparisonOption] allow to configure how compare if key is string. Default is Ordinal.

[MemoryTable("sample"), MessagePackObject(true)]
public class Sample
{
    [PrimaryKey]
    [StringComparisonOption(StringComparison.InvariantCultureIgnoreCase)]
    public string Foo { get; set; }
}

If computation property exists, add [IgnoreMember] of MessagePack should mark.

[MemoryTable("person"), MessagePackObject(true)]
public class Person
{
    [PrimaryKey]
    public int Id { get;}

    public string FirstName { get; }
    public string LastName { get; }

    [IgnoreMember]
    public string FullName => FirstName + LastName;
}

MemoryDatabase/RangeView

In default, MemoryDatabase do all string data automatically interning(see: Wikipedia/String interning). If multiple same string value exists in database(ex: "goblin","goblin", "goblin", "goblin", "goblin"....), standard database creates string value per query or store multiple same values. But MasterMemory stores single string value reference, it can save much memory if data is denormalized.

Use intern or not is selected in constructor. If you want to disable automatically interning, use internString:false.

MemoryDatabase(byte[] databaseBinary, bool internString = true, MessagePack.IFormatterResolver formatterResolver = null, int maxDegreeOfParallelism = 1).

MemoryDatabase has three(or four) query methods.

  • T|RangeView<T> FindBy***(TKey key)
  • bool TryFindBy***(TKey key, out T result)
  • T|RangeView<T> FindClosestBy***(TKey key, bool selectLower = true)
  • RangeView<T> FindRangeBy***(TKey min, TKey max, bool ascendant = true)

If index key is unique, generates FindBy*** and TryFindBy*** methods and then FindBy*** throws KeyNotFoundException when key is not found.

By*** is generated by PrimaryKey and SecondaryKey defines.

And has some utility properties.

  • int Count
  • RangeView<T> All
  • RangeView<T> AllReverse
  • RangeView<T> SortBy***
  • T[] GetRawDataUnsafe()

struct RangeView<T> : IEnumerable<T> is the view of database elements. It has following property/method.

  • T [int index]
  • int Count
  • T First
  • T Last
  • RangeView<T> Reverse
  • IEnumerator<T> GetEnumerator()

Extend Table

Generated table class is defined partial class so create same namespace and class name's partial class on another file, you can add your custom method to generated table.

Table class also defined partial OnAfterConstruct method, it called after table has been constructed. You can use it to store custom data to field after all data has been constructed.

// create MonsterTable.Partial.cs

public sealed partial class MonsterTable
{
    int maxHp;
#pragma warning disable CS0649
    readonly int minHp;
#pragma warning restore CS0649    

    // called after constructed
    partial void OnAfterConstruct()
    {
        maxHp = All.Select(x => x.MaxHp).Max();
        // you can use Unsafe.AsRef to set readonly field
        Unsafe.AsRef(minHp) = All.Select(x => x.MaxHp).Min();
    }
    
    // add custom method other than standard Find method
    public IEnumerable<Monster> GetRangedMonster(int arg1)
    {
        return All.Where....();
    }
}

ImmutableBuilder

If you want to add/modify data to loaded database, you can use ToImmutableBuilder method.

// Create ImmutableBuilder from original database.
var builder = db.ToImmutableBuilder();

// Add Or Replace compare with PrimaryKey
builder.Diff(addOrReplaceData);

// Remove by PrimaryKey
builder.RemovePerson(new[] { 1, 10, 100 });

// Replace all data
builder.ReplaceAll(newData);

// Finally create new database
MemoryDatabase newDatabase = builder.Build();

// If you want to save new database, you can convert to MemoryDatabase->DatabaseBuilder
var newBuilder = newDatabase.ToDatabaseBuilder();
var newBinary = newBuilder.Build(); // or use WriteToStream

MemoryDatabase's reference can use as snapshot.

// 1 game per 1 instance
public class GameRoom
{
    MemoryDatabase database;

    // The reference is a snapshot of the timing of game begins.
    public GameRoom(MemoryDatabase database)
    {
        this.database = database;
    }
}

Immutable Data

Element data is shared in the application so ideally should be immutable. But C# only has a constructor to create immutable data, it is too difficult to create many data tables.

Code generator has AddImmutableConstructor(-c, -addImmutableConstructor) option. If enabled it, code generator modify orignal file and add immutable constructor in target type. If you define property as {get; private set;} or {get;}, it will be immutable type.

// For the versioning, MessagePackObject is recommended to use string key.
[MemoryTable("person"), MessagePackObject(true)]
public class Person
{
    [PrimaryKey]
    public int PersonId { get; }
    public int Age { get; }
    public Gender Gender { get; }
    public string Name { get; }
}

// use AddImmutableConstructor="true" or -c option
<MasterMemoryGenerator UsingNamespace="$(ProjectName)" InputDirectory="$(ProjectDir)" OutputDirectory="$(ProjectDir)MasterMemory" AddImmutableConstructor="true" />
MasterMemory.Generator.exe -i "C:\UnitySample" -o "C:\UnitySample\Generated" -n "UnitySample" -c

// after generated...
[MemoryTable("person"), MessagePackObject(true)]
public class Person
{
    [PrimaryKey]
    public int PersonId { get; }
    public int Age { get; }
    public Gender Gender { get; }
    public string Name { get; }

    public Person(int PersonId, int Age, Gender Gender, string Name)
    {
        this.PersonId = PersonId;
        this.Age = Age;
        this.Gender = Gender;
        this.Name = Name;
    }
}

Validator

You can validate data by MemoryDatabase.Validate method. In default, it check unique key(data duplicated) and you can define custom validate logics.

// Implements IValidatable<T> to targeted validation
[MemoryTable("quest_master"), MessagePackObject(true)]
public class Quest : IValidatable<Quest>
{
    // If index is Unique, validate duplicate in default.
    [PrimaryKey]
    public int Id { get; }
    public string Name { get; }
    public int RewardId { get; }
    public int Cost { get; }

    void IValidatable<Quest>.Validate(IValidator<Quest> validator)
    {
        // get the external reference table
        var items = validator.GetReferenceSet<Item>();

        // Custom if logics.
        if (this.RewardId > 0)
        {
            // RewardId must exists in Item.ItemId
            items.Exists(x => x.RewardId, x => x.ItemId);
        }

        // Range check, Cost must be 10..20
        validator.Validate(x => x.Cost >= 10);
        validator.Validate(x => x.Cost <= 20);

        // In this region, only called once so enable to validate overall of tables.
        if (validator.CallOnce())
        {
            var quests = validator.GetTableSet();
            // Check unique othe than index property.
            quests.Where(x => x.RewardId != 0).Unique(x => x.RewardId);
        }
    }
}

[MemoryTable("item_master"), MessagePackObject(true)]
public class Item
{
    [PrimaryKey]
    public int ItemId { get; }
}

void Main()
{
    var db = new MemoryDatabase(bin);

    // Get the validate result.
    var validateResult = db.Validate();
    if (validateResult.IsValidationFailed)
    {
        // Output string format.
        Console.WriteLine(validateResult.FormatFailedResults());

        // Get the raw FaildItem[]. (.Type, .Message, .Data)
        // validateResult.FailedResults
    }
}

Following is list of validation methods.

// all void methods are assert function, it stores message to ValidateResult if failed.
interface IValidator<T>
{
    ValidatableSet<T> GetTableSet();
    ReferenceSet<T, TRef> GetReferenceSet<TRef>();
    void Validate(Expression<Func<T, bool>> predicate);
    void Validate(Func<T, bool> predicate, string message);
    void ValidateAction(Expression<Func<bool>> predicate);
    void ValidateAction(Func<bool> predicate, string message);
    void Fail(string message);
    bool CallOnce();
}

class ReferenceSet<TElement, TReference>
{
    IReadOnlyList<TReference> TableData { get; }
    void Exists<TProperty>(Expression<Func<TElement, TProperty>> elementSelector, Expression<Func<TReference, TProperty>> referenceElementSelector);
    void Exists<TProperty>(Expression<Func<TElement, TProperty>> elementSelector, Expression<Func<TReference, TProperty>> referenceElementSelector, EqualityComparer<TProperty> equalityComparer);
}

class ValidatableSet<TElement>
{
    IReadOnlyList<TElement> TableData { get; }
    void Unique<TProperty>(Expression<Func<TElement, TProperty>> selector);
    void Unique<TProperty>(Expression<Func<TElement, TProperty>> selector, IEqualityComparer<TProperty> equalityComparer);
    void Unique<TProperty>(Func<TElement, TProperty> selector, string message);
    void Unique<TProperty>(Func<TElement, TProperty> selector, IEqualityComparer<TProperty> equalityComparer, string message);
    void Sequential(Expression<Func<TElement, SByte|Int16|Int32|...>> selector, bool distinct = false);
    ValidatableSet<TElement> Where(Func<TElement, bool> predicate);
}

Metadata

You can get the table-info, properties, indexes by metadata api. It helps to make custom importer/exporter application.

var metaDb = MemoryDatabase.GetMetaDatabase();
foreach (var table in metaDb.GetTableInfos())
{
    // for example, generate CSV header
    var sb = new StringBuilder();
    foreach (var prop in table.Properties)
    {
        if (sb.Length != 0) sb.Append(",");

        // Name can convert to LowerCamelCase or SnakeCase.
        sb.Append(prop.NameSnakeCase);
    }
    File.WriteAllText(table.TableName + ".csv", sb.ToString(), new UTF8Encoding(false));
}

If creates console-app, our ConsoleAppFramework can easy to make helper applications.

Here is sample of reading and creating dynamic from csv. builder.AppendDynamic and System.Runtime.Serialization.FormatterServices.GetUninitializedObject will help it.

class Program
{
    static void Main(string[] args)
    {
        var csv = @"monster_id,name,max_hp
1,foo,100
2,bar,200";
        var fileName = "monster";

        var builder = new DatabaseBuilder();

        var meta = MemoryDatabase.GetMetaDatabase();
        var table = meta.GetTableInfo(fileName);

        var tableData = new List<object>();

        using (var ms = new MemoryStream(Encoding.UTF8.GetBytes(csv)))
        using (var sr = new StreamReader(ms, Encoding.UTF8))
        using (var reader = new TinyCsvReader(sr))
        {
            while ((reader.ReadValuesWithHeader() is Dictionary<string, string> values))
            {
                // create data without call constructor
                var data = System.Runtime.Serialization.FormatterServices.GetUninitializedObject(table.DataType);

                foreach (var prop in table.Properties)
                {
                    if (values.TryGetValue(prop.NameSnakeCase, out var rawValue))
                    {
                        var value = ParseValue(prop.PropertyInfo.PropertyType, rawValue);
                        if (prop.PropertyInfo.SetMethod == null)
                        {
                            throw new Exception("Target property does not exists set method. If you use {get;}, please change to { get; private set; }, Type:" + prop.PropertyInfo.DeclaringType + " Prop:" + prop.PropertyInfo.Name);
                        }
                        prop.PropertyInfo.SetValue(data, value);
                    }
                    else
                    {
                        throw new KeyNotFoundException($"Not found \"{prop.NameSnakeCase}\" in \"{fileName}.csv\" header.");
                    }
                }

                tableData.Add(data);
            }
        }

        // add dynamic collection.
        builder.AppendDynamic(table.DataType, tableData);

        var bin = builder.Build();
        var database = new MemoryDatabase(bin);
    }

    static object ParseValue(Type type, string rawValue)
    {
        if (type == typeof(string)) return rawValue;

        if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
        {
            if (string.IsNullOrWhiteSpace(rawValue)) return null;
            return ParseValue(type.GenericTypeArguments[0], rawValue);
        }

        if (type.IsEnum)
        {
            var value = Enum.Parse(type, rawValue);
            return value;
        }

        switch (Type.GetTypeCode(type))
        {
            case TypeCode.Boolean:
                // True/False or 0,1
                if (int.TryParse(rawValue, out var intBool))
                {
                    return Convert.ToBoolean(intBool);
                }
                return Boolean.Parse(rawValue);
            case TypeCode.Char:
                return Char.Parse(rawValue);
            case TypeCode.SByte:
                return SByte.Parse(rawValue, CultureInfo.InvariantCulture);
            case TypeCode.Byte:
                return Byte.Parse(rawValue, CultureInfo.InvariantCulture);
            case TypeCode.Int16:
                return Int16.Parse(rawValue, CultureInfo.InvariantCulture);
            case TypeCode.UInt16:
                return UInt16.Parse(rawValue, CultureInfo.InvariantCulture);
            case TypeCode.Int32:
                return Int32.Parse(rawValue, CultureInfo.InvariantCulture);
            case TypeCode.UInt32:
                return UInt32.Parse(rawValue, CultureInfo.InvariantCulture);
            case TypeCode.Int64:
                return Int64.Parse(rawValue, CultureInfo.InvariantCulture);
            case TypeCode.UInt64:
                return UInt64.Parse(rawValue, CultureInfo.InvariantCulture);
            case TypeCode.Single:
                return Single.Parse(rawValue, CultureInfo.InvariantCulture);
            case TypeCode.Double:
                return Double.Parse(rawValue, CultureInfo.InvariantCulture);
            case TypeCode.Decimal:
                return Decimal.Parse(rawValue, CultureInfo.InvariantCulture);
            case TypeCode.DateTime:
                return DateTime.Parse(rawValue, CultureInfo.InvariantCulture);
            default:
                if (type == typeof(DateTimeOffset))
                {
                    return DateTimeOffset.Parse(rawValue, CultureInfo.InvariantCulture);
                }
                else if (type == typeof(TimeSpan))
                {
                    return TimeSpan.Parse(rawValue, CultureInfo.InvariantCulture);
                }
                else if (type == typeof(Guid))
                {
                    return Guid.Parse(rawValue);
                }

                // or other your custom parsing.
                throw new NotSupportedException();
        }
    }

    // Non string escape, tiny reader with header.
    public class TinyCsvReader : IDisposable
    {
        static char[] trim = new[] { ' ', '\t' };

        readonly StreamReader reader;
        public IReadOnlyList<string> Header { get; private set; }

        public TinyCsvReader(StreamReader reader)
        {
            this.reader = reader;
            {
                var line = reader.ReadLine();
                if (line == null) throw new InvalidOperationException("Header is null.");

                var index = 0;
                var header = new List<string>();
                while (index < line.Length)
                {
                    var s = GetValue(line, ref index);
                    if (s.Length == 0) break;
                    header.Add(s);
                }
                this.Header = header;
            }
        }

        string GetValue(string line, ref int i)
        {
            var temp = new char[line.Length - i];
            var j = 0;
            for (; i < line.Length; i++)
            {
                if (line[i] == ',')
                {
                    i += 1;
                    break;
                }
                temp[j++] = line[i];
            }

            return new string(temp, 0, j).Trim(trim);
        }

        public string[] ReadValues()
        {
            var line = reader.ReadLine();
            if (line == null) return null;
            if (string.IsNullOrWhiteSpace(line)) return null;

            var values = new string[Header.Count];
            var lineIndex = 0;
            for (int i = 0; i < values.Length; i++)
            {
                var s = GetValue(line, ref lineIndex);
                values[i] = s;
            }
            return values;
        }

        public Dictionary<string, string> ReadValuesWithHeader()
        {
            var values = ReadValues();
            if (values == null) return null;

            var dict = new Dictionary<string, string>();
            for (int i = 0; i < values.Length; i++)
            {
                dict.Add(Header[i], values[i]);
            }

            return dict;
        }

        public void Dispose()
        {
            reader.Dispose();
        }
    }
}

Inheritance

Currently MasterMemory does not support inheritance. Recommend way to create common method, use interface and extension method. But if you want to create common method with common cached field(made by OnAfterConstruct), for workaround, create abstract class and all data properties to abstract.

public abstract class FooAndBarBase
{
    // all data properties to virtual
    public virtual int Prop1 { get; protected set; }
    public virtual int Prop2 { get; protected set; }

    [IgnoreMember]
    public int Prop3 => Prop1 + Prop2;

    public IEnumerable<FooAndBarBase> CommonMethod()
    {
        throw new NotImplementedException();
    }
}

[MemoryTable("foo_table"), MessagePackObject(true)]
public class FooTable : FooAndBarBase
{
    [PrimaryKey]
    public override int Prop1 { get; protected set; }
    public override int Prop2 { get; protected set; }
}

[MemoryTable("bar_table"), MessagePackObject(true)]
public class BarTable : FooAndBarBase
{
    [PrimaryKey]
    public override int Prop1 { get; protected set; }
    public override int Prop2 { get; protected set; }
}

Optimization

When invoking new MemoryDatabase(byte[] databaseBinary...), read and construct database from binary. If binary size is large then construct performance will slow down. MemoryDatabase has ctor(..., int maxDegreeOfParallelism = 1) option in constructor to construct in parallel.

var database = new MemoryDatabase(bin, maxDegreeOfParallelism: Environment.ProcessorCount);

The use of Parallel can greatly improve the construct performance. Recommend to use Environment.ProcessorCount.

If you want to reduce code size of generated code, Validator and MetaDatabase info can omit in runtime. Generated code has two symbols DISABLE_MASTERMEMORY_VALIDATOR and DISABLE_MASTERMEMORY_METADATABASE. By defining them, can be erased from the build code.

Code Generator

MasterMemory has two kinds of code-generator. MSBuild Task, .NET Core Global/Local Tools.

MSBuild Task(MasterMemory.MSBuild.Tasks) is recommended way to use in .NET Core csproj.

<MasterMemoryGenerator
    UsingNamespace="string:required"
    InputDirectory="string:required"
    OutputDirectory="string:required"
    PrefixClassName="string:optional, default= "
    AddImmutableConstructor="bool:optional, default=false"
    ReturnNullIfKeyNotFound="bool:optional, default=false"
/>

.NET Core Global/Local Tools can install from NuGet(MasterMemory.Generator), you need to install .NET runtime. Here is the sample command of install global tool.

dotnet tool install --global MasterMemory.Generator

Usage: MasterMemory.Generator [options...]

Options:
  -i, -inputDirectory <String>              Input file directory(search recursive). (Required)
  -o, -outputDirectory <String>             Output file directory. (Required)
  -n, -usingNamespace <String>              Namespace of generated files. (Required)
  -p, -prefixClassName <String>             Prefix of class names. (Default: )
  -c, -addImmutableConstructor <Boolean>    Add immutable constructor to MemoryTable class. (Default: False)
  -t, -returnNullIfKeyNotFound <Boolean>    Return null if key not found on unique find method. (Default: False)

After install, you can call by dotnet mmgen command. This is useful to use in CI. Here is the sample of CircleCI config.

version: 2.1
executors:
  dotnet:
    docker:
      - image: mcr.microsoft.com/dotnet/core/sdk:2.2
    environment:
      DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
      NUGET_XMLDOC_MODE: skip
jobs:
  gen-mastermemory:
    executor: dotnet
    steps:
      - checkout
      - run: dotnet tool install --global MasterMemory.Generator
      - run: dotnet mmgen -i ./ -o ./MasterMemory -n Test
      /* git push or store artifacts or etc...... */

License

This library is under the MIT License.

mastermemory's People

Contributors

cssho avatar e-k avatar github-actions[bot] avatar github129 avatar guitarrapc avatar kawai-yoshifumi-ab avatar kyubuns avatar mayuki avatar neuecc avatar tomiit avatar tompazourek avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

mastermemory's Issues

Doesn't work with Unity 2018.3 or MessagePack 1.7.3.5 (?)

Using Unity 2018.3.7. Importing MasterMemory as-is results in 58 different "type or namespace cannot be found" errors for the following: TypeBuilder, ModuleBuilder, AssemblyBuilder, LocalBuilder, FieldBuilder, MethodBuilder and ILGenerator.

When I delete the MessagePack folder and import the MessagePack 1.7.3.5, all those errors go away and a new one appears:

Assets\Plugins\MasterMemory\Scripts\Loader.cs(57,13): error CS0103: The name 'DefaultResolver' does not exist in the current context

How can I have two separate databases per application?

I'm using MasterMemory, but I intend to have multiple databases. One database will represent gameplay data and should be synchronized to cloud storage, but another will represent configuration data and should stay on one device. These databases will each be saved to separate files; how can I do this with MasterMemory?

CodeGen issue

Hi! I found minor issue. That issue not problem for me, but I think that report can be helpfull for someone.

Fail in console app running on MessagepackCompiler.RunAsync
MessagePackCompiler.CodeAnalysis.MessagePackGeneratorResolveFailedException: can't find matched constructor. type:global::_Game.DataModel.Stats
   at MessagePackCompiler.CodeAnalysis.TypeCollector.CollectObject(INamedTypeSymbol type) in d:\a\1\s\src\MessagePack.GeneratorCore\CodeAnalysis\TypeCollector.cs:line 541
   at MessagePackCompiler.CodeAnalysis.TypeCollector.CollectCore(ITypeSymbol typeSymbol) in d:\a\1\s\src\MessagePack.GeneratorCore\CodeAnalysis\TypeCollector.cs:line 384
   at MessagePackCompiler.CodeAnalysis.TypeCollector.CollectObject(INamedTypeSymbol type) in d:\a\1\s\src\MessagePack.GeneratorCore\CodeAnalysis\TypeCollector.cs:line 589
   at MessagePackCompiler.CodeAnalysis.TypeCollector.CollectCore(ITypeSymbol typeSymbol) in d:\a\1\s\src\MessagePack.GeneratorCore\CodeAnalysis\TypeCollector.cs:line 384
   at MessagePackCompiler.CodeAnalysis.TypeCollector.Collect() in d:\a\1\s\src\MessagePack.GeneratorCore\CodeAnalysis\TypeCollector.cs:line 319
   at MessagePackCompiler.CodeGenerator.GenerateFileAsync(String input, String output, String conditionalSymbol, String resolverName, String namespace, Boolean useMapMode, String multipleIfDirectiveOutputSymbols) in d:\a\1\s\src\MessagePack.GeneratorCore\CodeGenerator.cs:line 58
   at MessagePack.Generator.MessagepackCompiler.RunAsync(String input, String output, String conditionalSymbol, String resolverName, String namespace, Boolean useMapMode, String multipleIfDirectiveOutputSymbols) in d:\a\1\s\src\MessagePack.Generator\MessagepackCompiler.cs:line 30
   at ConsoleAppFramework.ConsoleAppEngine.RunCore(ConsoleAppContext ctx, Type type, MethodInfo methodInfo, String[] args, Int32 argsOffset)
    [MessagePackObject(true)]
    public class Stats
    {
        public int Str { get; }
        public int Con { get; }
        public int Dex { get; }
        public int Int { get; }
        public int Men { get; }
        public int Wit { get; }
        public int Cha { get; }
        public int Lck { get; }

        public Stats(int str, int con, int dex, int intelligence, int men, int wit, int cha, int lck)
        {
            Str = str;
            Con = con;
            Dex = dex;
            Int = intelligence; // <== Problem here. Compiler doesn't like 'Int' name 
            Men = men;
            Wit = wit;
            Cha = cha;
            Lck = lck;
        }
    } 

Is FindClosestBy returning wrong results?

Hey, thanks for your amazing work as usual. I wanted to test if the library might work out for my use case, but right now I'm not getting the result returned that I expected. Basically I expected it to find the person object with firstname == "realname" but I got the different one. Is this expected behavior? Thanks for your help!

grafik

  [MemoryTable("people"), MessagePackObject(true)]
  public class PersonModel
  {
      [SecondaryKey(0), NonUnique]
      [SecondaryKey(1, keyOrder: 1), NonUnique]
      public string LastName { get; set; }

      [SecondaryKey(2), NonUnique]
      [SecondaryKey(1, keyOrder: 0), NonUnique]
      public string FirstName { get; set; }

      [PrimaryKey] public string RandomId { get; set; }
  }

[PrimaryKey],[SecondaryKey] Attributes for external type?

How to add these attributes to the types from other assembly, which could not modify their sources?
Or, how to add these attributes to type types generated by other code generator? Class attributes are ok for generated partial class, but it's hard to add attribute to properties.

Null string check missing

I have started testing MasterMemory and while testing different modification of my models structures - to ensure it supports all possible modification, I came across a null string being read and sent to string.Intern making building the database impossible.

In InternStringResolver, there is no null check to the string that is sent to string.Intern.
Adding the null check didn't seem to cause any problem.

Dynamic Add From CSV

I can not find the method builder.AppendDynamic from generated code files. How can I fix it?

Adding items

Hi,

How can we add items if the database is initialized?
Would like to use your db as a secondary Index for searching where results get updated once per hour appx.

Thanks

RangeView to implement ICollection ?

I do not know if this is a good idea, but wouldn't it be better for RangeView to implement ICollection ? It already has a count method. This would improve performance when using Linq on RangeView as a IEnumerable.
This is useful for framework that needs abstraction.

OverflowException with large data

OverflowException occurs when trying to add large data to DatabaseBuilder.
Add large data using the ImmutableBuilder's Diff method does not throw OverflowException .

Is it possible to avoid this Exception by changing some settings?

Here is the source code I ran

var builder = new DatabaseBuilder();
var list = new List<Person>();
for (int i = 0; i < 100000000; i++)
{
    list.Add(new Person { PersonId = i, Age = i, Gender = (Gender)(i % 2), Name = "AAAAAAAAAAA" });
}
builder.Append(list);

ImmutableBuilder's Diff method does not throw OverflowException

var builder = new DatabaseBuilder();
var list = new List<Person>();

for (int i = 0; i < 100000000; i++)
{
    list.Add(new Person { PersonId = i, Age = i, Gender = (Gender)(i % 2), Name = "AAAAAAAAAAA" });
}

var firstData = list.First();
builder.Append(new List<Person>() { firstData });
byte[] dataA = builder.Build();
var db = new MemoryDatabase(dataA);

var builder1 = db.ToImmutableBuilder();
builder1.Diff(list.Skip(1).ToArray());
db = builder1.Build();

builder.Append(list);
MessagePack.MessagePackSerializationException   HResult=0x80131500   Message=Failed to serialize MasterMemorytest3.Model.Person[] value.   Source=MessagePack   スタック トレース:    場所 MessagePack.MessagePackSerializer.Serialize[T](MessagePackWriter& writer, T value, MessagePackSerializerOptions options)    場所 MessagePack.MessagePackSerializer.Serialize[T](IBufferWriter`1 writer, T value, MessagePackSerializerOptions options, CancellationToken cancellationToken)    場所 MasterMemory.DatabaseBuilderBase.AppendCore[T,TKey](IEnumerable`1 datasource, Func`2 indexSelector, IComparer`1 comparer)    場所 MasterMemorytest3.DatabaseBuilder.Append(IEnumerable`1 dataSource) (F:\\src\\source\\repos\\MasterMemorytest\\MasterMemorytest3\\MasterMemory\\DatabaseBuilder.cs):行 29    場所 MasterMemorytest3.Program.Test1() (F:\\src\\source\\repos\\MasterMemorytest\\MasterMemorytest3\\Program.cs):行 39    場所 MasterMemorytest3.Program.Main(String[] args) (F:\\src\\source\\repos\\MasterMemorytest\\MasterMemorytest3\\Program.cs):行 21    この例外は、最初にこの呼び出し履歴      [外部コード] でスローされました  内部例外 1: OverflowException: Arithmetic operation resulted in an overflow.

MemoryPack implementation ?

I guess you must have considered this :)
Issue would be compatibility between version. Or maybe create a new package with the MemoryPack implementation: MasterMemory.MemoryPack?

Share db between applications

Hi team, firstly this is awesome!

I have a question: I want to share database between 2 or more application, can I do that

I guess maybe I need a connectionstring or an address to make my applications understand where database is.

Have we support it yet or any road map for it

Thank you

Is it possible to storage an collection type in table

as title says,i have test store HashSet and it seems works fine,but I didn't see the information of the storage collection type in the sample, so I want to get some confirmation information here before using it in the production environment: Can the storage collection type be read normally when the table is not used as a query key?
And if possible, I also want to ask whether the collection type can be used as a query key by customizing the comparator?

Uncompression mode

Currently bin data is always lz4 compressed.
Add uncompression option for some reasons.

Doesn't work CLI tool on .NET6

MasterMemory.Generator CLI(dotnet-mmgen) does not work on .NET6.

It was not possible to find any compatible framework version
The framework 'Microsoft.NETCore.App', version '5.0.0' (arm64) was not found.
  - The following frameworks were found:
      6.0.5 at [/usr/local/share/dotnet/shared/Microsoft.NETCore.App]

and .NET5 SDK is End of support May 10, 2022.

EF Core support

Do you have plans to support Microsoft Entity Framawork Core?

FindManyRangeCore returns invalid range when data completely outside of min max.

Try the following test (added to tests\MasterMemory.Tests\DatabaseTest.cs)

Currently the db.SampleTable.FindRangeByAge( min: 2, max: 2) returns a range with Count 1 and pointing at the first element in the sorted list - which has age 9.

The problem is due to First / Last being used on empty element in FindManyRangeCore.

I have a suggested fix; which also improves the efficiency of FindManyRangeCore (in TableBase.cs) which I will attempt to upload. I am not very familiar with git however.

If the above behavior is by design - please let me know.

Steps to create:

        Sample[] CreateData()
        {
            // Id = Unique, PK
            // FirstName + LastName = Unique
            var data = new[]
            {
                new Sample { Id = 5, Age = 19, FirstName = "aaa", LastName = "foo" },
                new Sample { Id = 6, Age = 29, FirstName = "bbb", LastName = "foo" },
                new Sample { Id = 7, Age = 39, FirstName = "ccc", LastName = "foo" },
                new Sample { Id = 8, Age = 49, FirstName = "ddd", LastName = "foo" },
                new Sample { Id = 1, Age = 59, FirstName = "eee", LastName = "foo" },
                new Sample { Id = 2, Age = 89, FirstName = "aaa", LastName = "bar" },
                new Sample { Id = 3, Age = 79, FirstName = "be", LastName = "de" },
                new Sample { Id = 4, Age = 89, FirstName = "aaa", LastName = "tako" },
                new Sample { Id = 9, Age = 99, FirstName = "aaa", LastName = "ika" },
                new Sample { Id = 10, Age = 9, FirstName = "eee", LastName = "baz" },
            };
            return data;
        }

         [Fact]
        public void Ranges()
        {
            var builder = new DatabaseBuilder();
            builder.Append(CreateData());

            var bin = builder.Build();
            var db = new MemoryDatabase(bin);

            db.SampleTable.FindRangeByAge(2,2).Select(x=>x.Id).ToArray().Should().BeEquivalentTo( new int[] {} );     
            db.SampleTable.FindRangeByAge(30,50).Select(x=>x.Id).ToArray().Should().BeEquivalentTo( new int[] { 7, 8 } );     
            db.SampleTable.FindRangeByAge(100,100).Select(x=>x.Id).ToArray().Should().BeEquivalentTo( new int[] {} );     
        }

Bug: dotnet-mmgen throws a NullReferenceException when processing a [SecondaryKey] with a non-literal argument

If you run dotnet-mmgen on this class...

using MasterMemory;
using MessagePack;

namespace CorundumGames
{
    [MessagePackObject, MemoryTable("BugTable")]
    public sealed class BugEntry
    {
        private const int SecondaryKeyIndex = 0;

        [Key(0)]
        [PrimaryKey]
        public int Primary { get; }

        [Key(1)]
        [SecondaryKey(SecondaryKeyIndex)]
        public string Secondary { get; }
    }
}

...then it will fail with an error message that looks something like this:

Fail in console app running on Program.Execute
System.NullReferenceException: Object reference not set to an instance of an object.
   at MasterMemory.GeneratorCore.CodeGenerator.ExtractPropertyAttribute(PropertyDeclarationSyntax property) in /home/runner/work/MasterMemory/MasterMemory/src/MasterMemor
y.GeneratorCore/CodeGenerator.cs:line 205
   at MasterMemory.GeneratorCore.CodeGenerator.<CreateGenerationContext>b__4_5(PropertyDeclarationSyntax x) in /home/runner/work/MasterMemory/MasterMemory/src/MasterMemor
y.GeneratorCore/CodeGenerator.cs:line 165
   at System.Linq.Enumerable.SelectEnumerableIterator`2.ToArray()
   at System.Linq.Enumerable.ToArray[TSource](IEnumerable`1 source)
   at MasterMemory.GeneratorCore.CodeGenerator.CreateGenerationContext(String filePath)+MoveNext()
   at System.Collections.Generic.List`1.InsertRange(Int32 index, IEnumerable`1 collection)
   at MasterMemory.GeneratorCore.CodeGenerator.GenerateFile(String usingNamespace, String inputDirectory, String outputDirectory, String prefixClassName, Boolean addImmut
ableConstructor, Boolean throwIfKeyNotFound, Boolean forceOverwrite, Action`1 logger) in /home/runner/work/MasterMemory/MasterMemory/src/MasterMemory.GeneratorCore/CodeGe
nerator.cs:line 30
   at MasterMemory.Generator.Program.Execute(String inputDirectory, String outputDirectory, String usingNamespace, String prefixClassName, Boolean addImmutableConstructor
, Boolean returnNullIfKeyNotFound, Boolean forceOverwrite) in /home/runner/work/MasterMemory/MasterMemory/src/MasterMemory.Generator/Program.cs:line 31

However, running it on this (look at the [SecondaryKey])...

using MasterMemory;
using MessagePack;

namespace CorundumGames
{
    [MessagePackObject, MemoryTable("BugTable")]
    public sealed class BugEntry
    {
        [Key(0)]
        [PrimaryKey]
        public int Primary { get; }

        [Key(1)]
        [SecondaryKey(0)]
        public string Secondary { get; }
    }
}

...works as expected.

Request: Use Semantic Versioning

Semantic Versioning is a widely used convention in software development, distribution, and deployment.
I propose that MasterMemory adopt the semantic versioning conventions for future releases. If there are good reasons for not adopting this convention, MasterMemory should adopt a release labelling scheme that cannot be mistaken for semantic versioning.

Assembly Definition support

This package has no Assembly Definition. so, I cannot add MasterMemory attributes to classes in other assemblies in Unity.
Is supporting Assembly Definition planned?

I got this error when using it with ASP.NET Core(.NET Core 6)

Severity Code Description Project File Line Suppression State
Error CS0115 'MemoryDatabase.Init(Dictionary<string, (int offset, int count)>, ReadOnlyMemory, MessagePackSerializerOptions)': no suitable method found to override AspNetCoreWebApp C:\Codes\AspNetCoreWebApp\MasterMemory\MemoryDatabase.cs 33 Active

Severity Code Description Project File Line Suppression State
Error CS8138 Cannot reference 'System.Runtime.CompilerServices.TupleElementNamesAttribute' explicitly. Use the tuple syntax to define tuple names. AspNetCoreWebApp C:\Codes\AspNetCoreWebApp\MasterMemory\MemoryDatabase.cs 89 Active

ForeignKey

for example, define by attribute

[ForeignKey(0, typeof(ItemMaster), nameof(ItemMaster.ItemId))]
public int RewardItemId { get; }

automatically generate Exists validation and automatically generate navigation property.

Currently can not implements navigation proeprty so pending implementation.

Uncompilable code generated

The following code is not compilable when using the following code to initialize and query MasterMemory under .NET 7:

using MasterMemory;
using MasterMemoryDemo;
using MessagePack;

var builder = new DatabaseBuilder();
byte[] data = builder.Build();

// -----------------------

var db = new MemoryDatabase(data);


[MemoryTable("PortedMsisdns"), MessagePackObject(true)]
public class MsisdnInfo
{
    [PrimaryKey]
    public string Msisdn { get; set; }
    public string Operator { get; set; }
}

The error message from the compiler is

  MemoryDatabase.cs(32, 33): [CS0115] 'MemoryDatabase.Init(Dictionary<string, (int offset, int count)>, ReadOnlyMemory<byte>, MessagePackSerializerOptions)': no suitable method found to override
  MemoryDatabase.cs(16, 24): [CS0534] 'MemoryDatabase' does not implement inherited abstract member 'MemoryDatabaseBase.Init(Dictionary<string, (int offset, int count)>, ReadOnlyMemory<byte>, MessagePackSerializerOptions, int)'

ArgumentException with example code

Hi,

I just created fresh project in Unity 5.5, with empty GO and MB attached (in zip). This results in exception:

ArgumentException: does not implement right interface
System.Collections.Generic.Comparer1+DefaultComparer[ZeroFormatter.KeyTuple2[Gender,System.Int32]].Compare (KeyTuple2 x, KeyTuple2 y) (at /Users/builduser/buildslave/mono/build/mcs/class/corlib/System.Collections.Generic/Dictionary.cs:771)
System.Array.compare[KeyTuple2] (KeyTuple2 value1, KeyTuple2 value2, IComparer1 comparer) (at /Users/builduser/buildslave/mono/build/mcs/class/corlib/System.Collections.Generic/Dictionary.cs:319)
System.Array.qsort[KeyTuple2,Person] (ZeroFormatter.KeyTuple2[] keys, .Person[] items, Int32 low0, Int32 high0, IComparer1 comparer) (at /Users/builduser/buildslave/mono/build/mcs/class/corlib/System.Collections.Generic/Dictionary.cs:619) System.Array.Sort[KeyTuple2,Person] (ZeroFormatter.KeyTuple2[] keys, .Person[] items, Int32 index, Int32 length, IComparer1 comparer) (at /Users/builduser/buildslave/mono/build/mcs/class/corlib/System.Collections.Generic/Dictionary.cs:303)
Rethrow as InvalidOperationException: The comparer threw an exception.
System.Array.Sort[KeyTuple2,Person] (ZeroFormatter.KeyTuple2[] keys, .Person[] items, Int32 index, Int32 length, IComparer1 comparer) (at /Users/builduser/buildslave/mono/build/mcs/class/corlib/System.Collections.Generic/Dictionary.cs:303) MasterMemory.Memory2[ZeroFormatter.KeyTuple2[Gender,System.Int32],Person].FastSort (IEnumerable1 datasource, System.Func2 indexSelector, IComparer1 comparer) (at Assets/MasterMemory/Memory.cs:360)
MasterMemory.Memory2[ZeroFormatter.KeyTuple2[Gender,System.Int32],Person]..ctor (IEnumerable1 datasource, System.Func2 indexSelector, Boolean rootMemory) (at Assets/MasterMemory/Memory.cs:87)
MasterMemory.Memory2[ZeroFormatter.KeyTuple2[Gender,System.Int32],Person]..ctor (IEnumerable1 datasource, System.Func2 indexSelector) (at Assets/MasterMemory/Memory.cs:77)
MemoryTesting.Start () (at Assets/MemoryTesting.cs:71)

I am using unitypackage from releases for MasterMemory (0.1.0) and binary (dll) releases for ZeroFormatter (1.6.0)

MemoryTesting.zip

C# 9 record type support

Since C# 9 we have a great new type supporting immutability: records.
I saw that MessagePack now supports records, would be cool is MasterMemory supports it too.

Impl Task

  • Memory
    • Query Single Key
      • Find
      • FindNearest
      • FindRange
      • FindAll
    • Query Multiple Key(KeyTuple)
      • Find
      • FindNearest
      • FindRange
      • FindAll
    • Secondary Key
    • Index KeyTuple Deconstruction
  • View
    • RangeView
    • LookupView
    • DictionaryView
  • Database
    • Serialize
    • Deserialize
  • .NET Core
  • Unity
    • Create Project
    • Setup Cloudbuild
    • How Create KeyTupleComparer?
  • NuGet Upload
  • ReadMe

MemoryTableAttribute : MessagePackObjectAttribute

MasterMemory's design, MemoryTableAttribute always have to use MessagePackObjectAttirbute.
Implements two attributes is slightly painful.
If MemoryTableAttribute itself includes MessagePackObjectAttribute, we can use only one MemoryTableAttribute.

Is this a good use case for MasterMemory?

I'm considering using MasterMemory for some of my Unity game's save data, but I'd like to ask about my use case to make sure I understand what this library is designed for.

Here are some facts about my game's data:

  • I am using MessagePack-CSharp for serialization.
  • I do not intend to use MasterMemory for game design assets (e.g. difficulty, level design, game mechanics); for that I will just use standard ScriptableObjects and prefabs.
  • Most of my game's save data is only loaded once at the start of the game. This data is used every couple of minutes.
  • Part of my game's save data consists of local leaderboards for each game mode that are updated with one entry at a time whenever the player finishes a session. When that happens (usually once every few minutes), the data is saved to disk.
  • My game will have online leaderboards, but I haven't yet decided how it will be managed or structured.
  • Another part of my game's save data consists of input configuration, which is fairly complicated due to the nature of the input manager I use. This data is referenced whenever a controller is connected, disconnected, or assigned to a player. It is modified whenever the player customizes their controls (very rare) or when the player is assigned a controller (more common).

Given this information, do you believe MasterMemory would be well-suited for my game?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.