Skip to content

[BUG] Cannot use custom types in Dictionaries #2161

@MathieuDR

Description

@MathieuDR

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

  • .NET 6
  • LiteDB 5.0.11
  • PopOS (Ubuntu)

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);
      }
    }

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions