SamboyCoding / Tomlet

Zero-Dependency, model-based TOML De/Serializer for .NET
MIT License
154 stars 29 forks source link

Problem serializing (and maybe deserializing?) dynamic types #42

Open levicki opened 1 month ago

levicki commented 1 month ago

Let's start with simple stuff:

// Example 1
namespace DynamicDictionaryTest
{
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Dynamic;
    using Tomlet;
    using Tomlet.Attributes;

    public class GameState
    {
        public Dictionary<string, dynamic> GameVars = new Dictionary<string, dynamic>();
    }

    internal class Program
    {
        public static GameState State = new GameState();

        static void Main(string[] args)
        {
            State.GameVars["GAME1_BASE_LEVEL1_AVAILABLE"] = true;
            State.GameVars["GAME2_DLC1_LEVEL2_TREASURE_FOUND"] = 3;
            State.GameVars["GAME3_GLOBAL_PLAYER_FERTILITY"] = 0.98;

            string tomlString = TomletMain.TomlStringFrom(State);
        }
    }
}

This produces the following exception:

System.Reflection.TargetInvocationException
  HResult=0x80131604
  Message=Exception has been thrown by the target of an invocation.
  Source=mscorlib
  StackTrace:
   at System.RuntimeMethodHandle.InvokeMethod(Object target, Object[] arguments, Signature sig, Boolean constructor)
   at System.Reflection.RuntimeMethodInfo.UnsafeInvokeInternal(Object obj, Object[] parameters, Object[] arguments) in f:\dd\ndp\clr\src\BCL\system\reflection\methodinfo.cs:line 761
   at System.Delegate.DynamicInvokeImpl(Object[] args) in f:\dd\ndp\clr\src\BCL\system\delegate.cs:line 123
   at Tomlet.TomlSerializationMethods.<>c__DisplayClass12_1.<GetSerializer>b__0(Object dict) in /_/Tomlet/TomlSerializationMethods.cs:line 133
   at Tomlet.TomlCompositeSerializer.<>c__DisplayClass0_2.<For>b__8(Object instance) in /_/Tomlet/TomlCompositeSerializer.cs:line 60
   at Tomlet.TomletMain.ValueFrom(Type type, Object t, TomlSerializerOptions options) in /_/Tomlet/TomletMain.cs:line 58
   at Tomlet.TomletMain.DocumentFrom(Type type, Object t, TomlSerializerOptions options) in /_/Tomlet/TomletMain.cs:line 81
   at Tomlet.TomletMain.DocumentFrom[T](T t, TomlSerializerOptions options) in /_/Tomlet/TomletMain.cs:line 67
   at Tomlet.TomletMain.TomlStringFrom[T](T t, TomlSerializerOptions options) in /_/Tomlet/TomletMain.cs:line 85
   at DynamicDictionaryTest.Program.Main(String[] args) in C:\WORK\Projects\C#\ConsoleApp10\ConsoleApp10\Program.cs:line 137

  This exception was originally thrown at this call stack:
    System.Reflection.RtFieldInfo.CheckConsistency(object) in fieldinfo.cs
    System.Reflection.RtFieldInfo.GetValue(object) in fieldinfo.cs
    Tomlet.TomlCompositeSerializer.For.AnonymousMethod__8(object) in TomlCompositeSerializer.cs
    Tomlet.TomlSerializationMethods.RegisterSerializer.__ObjectAcceptingSerializer|0(object) in TomlSerializationMethods.cs
    Tomlet.TomlSerializationMethods.GenericDictionarySerializer<TKey, TValue>(System.Collections.Generic.Dictionary<TKey, TValue>, Tomlet.TomlSerializerOptions) in TomlSerializationMethods.cs

Inner Exception 1:
ArgumentException: Field 'GameVars' defined on type 'DynamicDictionaryTest.GameState' is not a field on the target object which is of type 'System.Boolean'.

If you do not wrap the Dictionary<string, dynamic> into a GameState class but put it as a top object for serialization:

// Example 2
namespace DynamicDictionaryTest
{
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Dynamic;
    using Tomlet;
    using Tomlet.Attributes;

    internal class Program
    {
        public static Dictionary<string, dynamic> GameVars = new Dictionary<string, dynamic>();

        static void Main(string[] args)
        {
            GameVars["GAME1_BASE_LEVEL1_AVAILABLE"] = true;
            GameVars["GAME2_DLC1_LEVEL2_TREASURE_FOUND"] = 3;
            GameVars["GAME3_GLOBAL_PLAYER_FERTILITY"] = 0.98;

            string tomlString = TomletMain.TomlStringFrom(GameVars);
        }
    }
}

Serialization kind of works (i.e. doesn't crash), but produces nonsensical / useless output:

GAME1_BASE_LEVEL1_AVAILABLE = {  }
GAME2_DLC1_LEVEL2_TREASURE_FOUND = {  }
GAME3_GLOBAL_PLAYER_FERTILITY = {  }

Am I wrong expecting this to produce proper values at the TOML file level outside of any table instead of bunch of empty inline tables?

Also, I expected the first case with GameVars inside of GameState class to produce:

[GameVars]
GAME1_BASE_LEVEL1_AVAILABLE = true
GAME2_DLC1_LEVEL2_TREASURE_FOUND = 3
GAME3_GLOBAL_PLAYER_FERTILITY = 0.98

Hopefully this is easy to fix?

levicki commented 1 month ago

To clarify, doing serialization using Newtonsoft.JSON produces the following output:

Example 1 image

Example 2 image

levicki commented 1 month ago

Workaround for the problem (partial, very inelegant, and without error handling):

TomletMain.RegisterMapper<Dictionary<string, dynamic>>(
    s => {
        TomlTable Result = new TomlTable();
        foreach (var kvp in s) {
            TomlValue Value = null;
            TypeCode tc = Type.GetTypeCode(kvp.Value.GetType());
            switch (tc) {
            case TypeCode.Boolean:
                Value = TomlBoolean.ValueOf(kvp.Value);
                break;
            case TypeCode.Byte:
            case TypeCode.SByte:
            case TypeCode.Int16:
            case TypeCode.UInt16:
            case TypeCode.Int32:
            case TypeCode.UInt32:
            case TypeCode.Int64:
            case TypeCode.UInt64:
                Value = new TomlLong(kvp.Value);
                break;
            case TypeCode.Single:
            case TypeCode.Double:
            case TypeCode.Decimal:
                Value = new TomlDouble(kvp.Value);
                break;
            case TypeCode.DateTime:
                Value = new TomlLocalDateTime(kvp.Value);
                break;
            case TypeCode.String:
            case TypeCode.Char:
                Value = new TomlString(kvp.Value);
                break;
            default:
                // TODO: throw proper exception about type unsupported by TOML
                break;
            }
            Result.PutValue(kvp.Key, Value);
        }
        return Result;
    },
    d => {
        Dictionary<string, dynamic> Result = new Dictionary<string, dynamic>();
        TomlDocument doc = d as TomlDocument;
        foreach (var kvp in doc) {
            Result.Add(kvp.Key, kvp.Value);
        }
        return Result;
    }
);