mbdavid / LiteDB

LiteDB - A .NET NoSQL Document Store in a single data file
http://www.litedb.org
MIT License
8.52k stars 1.24k forks source link

[BUG] Cannot use custom types in Dictionaries #2161

Closed MathieuDR closed 2 years ago

MathieuDR commented 2 years ago

Version Which LiteDB version/OS/.NET framework version are you using. (REQUIRED)

Describe the bug When having a dictionary with a custom key type (for example a strongly typed long) it will be unable to cast/convert into the corrext type, even when you have a mapping function for said custom key.

Code to Reproduce

Models

public partial struct DiscordUserId {
    public DiscordUserId(ulong value) : this((long)value){ }
    public ulong UlongValue => (ulong)Value;
        public long Value { get; }

        public DiscordUserId(long value)
        {
            Value = value;
        }

        public static readonly DiscordUserId Empty = new DiscordUserId(0);

        public bool Equals(DiscordUserId other) => this.Value.Equals(other.Value);
        public override bool Equals(object obj)
        {
            if (ReferenceEquals(null, obj)) return false;
            return obj is DiscordUserId other && Equals(other);
        }

        public override int GetHashCode() => Value.GetHashCode();

        public override string ToString() => Value.ToString();
        public static bool operator ==(DiscordUserId a, DiscordUserId b) => a.Equals(b);
        public static bool operator !=(DiscordUserId a, DiscordUserId b) => !(a == b);
        public int CompareTo(DiscordUserId other) => Value.CompareTo(other.Value);

        class DiscordUserIdSystemTextJsonConverter : System.Text.Json.Serialization.JsonConverter<DiscordUserId>
        {
            public override DiscordUserId Read(ref System.Text.Json.Utf8JsonReader reader, System.Type typeToConvert, System.Text.Json.JsonSerializerOptions options)
            {
                return new DiscordUserId(reader.GetInt32());
            }

            public override void Write(System.Text.Json.Utf8JsonWriter writer, DiscordUserId value, System.Text.Json.JsonSerializerOptions options)
            {
                writer.WriteNumberValue(value.Value);
            }
        }
}

public class TestModel {
    [BsonId]
    public ObjectId Id { get; set; }
    public DiscordUserId UserId { get; set; }
    public string Name { get; set; }
}

public class TestModelWithDictionary {
    [BsonId]
    public ObjectId Id { get; set; }
    public Dictionary<DiscordUserId, List<TestModel>> Dictionary { get; set; }
}

Mapping

BsonMapper.RegisterType(id => id.Value, bson => new DiscordUserId(bson.AsInt64));

Tests

[Fact]
    public void CanReadIdentityDict() {
        //Arrange
        using var db = _dbManager.GetDatabase();

        var model = new TestModel() {
            Name = "MyTestName",
            UserId = new DiscordUserId(513512)
        };

        var model2 = new TestModel() {
            Name = "MyTestName2",
            UserId = new DiscordUserId(5139512)
        };

        var model3 = new TestModel() {
            Name = "MyTestName3",
            UserId = new DiscordUserId(5139512)
        };

        var dict = new Dictionary<DiscordUserId, List<TestModel>>() {
            { model.UserId, new(){model} },
            { model2.UserId, new(){model2, model3} }
        };
        var toWrite = new TestModelWithDictionary() {
            Dictionary = dict
        };

        //Act
        var coll = db.GetCollection<TestModelWithDictionary>("Dicts");
        var written = coll.Insert(toWrite);
        var read = coll.FindAll().FirstOrDefault();

        //Assert
        read.Should().NotBeNull();
        read.Should().BeEquivalentTo(model, opts => opts.Excluding(x=> x.Id));
    }

Expected behavior The dictionary should be able to be recreated, especially since there is a mapping tool.

Screenshots/Stacktrace

System.InvalidCastException: Invalid cast from 'System.String' to 'DiscordBot.Common.Identities.DiscordUserId'.

System.InvalidCastException
Invalid cast from 'System.String' to 'DiscordBot.Common.Identities.DiscordUserId'.
   at System.Convert.DefaultToType(IConvertible value, Type targetType, IFormatProvider provider)
   at System.String.System.IConvertible.ToType(Type type, IFormatProvider provider)
   at System.Convert.ChangeType(Object value, Type conversionType, IFormatProvider provider)
   at LiteDB.BsonMapper.DeserializeDictionary(Type K, Type T, IDictionary dict, BsonDocument value)
   at LiteDB.BsonMapper.Deserialize(Type type, BsonValue value)
   at LiteDB.BsonMapper.DeserializeObject(EntityMapper entity, Object obj, BsonDocument value)
   at LiteDB.BsonMapper.Deserialize(Type type, BsonValue value)
   at LiteDB.LiteQueryable`1.<ToEnumerable>b__27_2(BsonDocument x)
   at System.Linq.Enumerable.SelectEnumerableIterator`2.MoveNext()
   at System.Linq.Enumerable.TryGetFirst[TSource](IEnumerable`1 source, Boolean& found)
   at System.Linq.Enumerable.FirstOrDefault[TSource](IEnumerable`1 source)
   at DiscordBot.ServicesTests.Data.IdentityTests.CanReadIdentityDict()

Additional context This method seems to be the 'killer' and should reference the correct mapping functions

 private void DeserializeDictionary(Type K, Type T, IDictionary dict, BsonDocument value)
    {
      bool isEnum = K.GetTypeInfo().IsEnum;
      foreach (KeyValuePair<string, BsonValue> element in value.GetElements())
      {
        object key = isEnum ? Enum.Parse(K, element.Key) : (K == typeof (Uri) ? (object) new Uri(element.Key) : Convert.ChangeType((object) element.Key, K));
        object obj = this.Deserialize(T, element.Value);
        dict.Add(key, obj);
      }
    }
MathieuDR commented 2 years ago

Seems to be a duplicate of #546