Skip to content

Commit a8a55e5

Browse files
committed
Fix Nullable<T> deserialization/serialization and add support for read-only collection and dictionary interface types
1 parent cb555ae commit a8a55e5

File tree

4 files changed

+85
-7
lines changed

4 files changed

+85
-7
lines changed

ValveKeyValue/ValveKeyValue.Test/TestFixtureSources.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ public static IEnumerable SupportedEnumerableTypesForDeserialization
1515
yield return CreateTestFixtureDataForGenericTest(typeof(Collection<string>));
1616
yield return CreateTestFixtureDataForGenericTest(typeof(IList<string>));
1717
yield return CreateTestFixtureDataForGenericTest(typeof(ICollection<string>));
18+
yield return CreateTestFixtureDataForGenericTest(typeof(IReadOnlyList<string>));
19+
yield return CreateTestFixtureDataForGenericTest(typeof(IReadOnlyCollection<string>));
20+
yield return CreateTestFixtureDataForGenericTest(typeof(IEnumerable<string>));
1821
yield return CreateTestFixtureDataForGenericTest(typeof(ObservableCollection<string>));
1922
}
2023
}

ValveKeyValue/ValveKeyValue.Test/Text/NullablePropertyDeserializationTestCase.cs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,34 @@ public void RequiredStringPropertyIsDefaultWhenMissingFromData()
8585

8686
#endregion
8787

88+
#region Deserialization - non-nullable properties missing from data
89+
90+
[Test]
91+
public void NonNullableStringPropertyIsNullWhenMissingFromData()
92+
{
93+
// Only "Name" is present, "Title" and "Count" are missing
94+
var vdf = "\"object\"\n{\n\t\"Name\"\t\"hello\"\n}";
95+
var obj = KVSerializer.Create(KVSerializationFormat.KeyValues1Text).Deserialize<NonNullableObject>(vdf);
96+
97+
Assert.That(obj.Name, Is.EqualTo("hello"));
98+
99+
// GetUninitializedObject zeroes all memory, so non-nullable string is null
100+
// This violates the non-nullable contract silently
101+
Assert.That(obj.Title, Is.Null);
102+
}
103+
104+
[Test]
105+
public void NonNullableIntPropertyIsZeroWhenMissingFromData()
106+
{
107+
var vdf = "\"object\"\n{\n\t\"Name\"\t\"hello\"\n}";
108+
var obj = KVSerializer.Create(KVSerializationFormat.KeyValues1Text).Deserialize<NonNullableObject>(vdf);
109+
110+
// Non-nullable int defaults to 0 when missing
111+
Assert.That(obj.Count, Is.EqualTo(0));
112+
}
113+
114+
#endregion
115+
88116
#region Serialization - nullable properties
89117

90118
[Test]
@@ -212,6 +240,13 @@ class RequiredObject
212240
public string? Name { get; set; }
213241
}
214242

243+
class NonNullableObject
244+
{
245+
public string Name { get; set; } = null!;
246+
public string Title { get; set; } = null!;
247+
public int Count { get; set; }
248+
}
249+
215250
#endregion
216251
}
217252
}

ValveKeyValue/ValveKeyValue.Test/Text/StronglyTypedCollectionDeserializationTestCase.cs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ internal class SimpleObject
2020
}
2121

2222
[TestFixture(typeof(Dictionary<string, SimpleObject>))]
23+
[TestFixture(typeof(IDictionary<string, SimpleObject>))]
2324
class StronglyTypedDictionaryDeserializationTestCase<TDictionary>
2425
where TDictionary : IDictionary<string, SimpleObject>
2526
{
@@ -52,6 +53,39 @@ public void CanDeserializeToObject()
5253
}
5354
}
5455

56+
[TestFixture(typeof(IReadOnlyDictionary<string, SimpleObject>))]
57+
class StronglyTypedReadOnlyDictionaryDeserializationTestCase<TDictionary>
58+
where TDictionary : IReadOnlyDictionary<string, SimpleObject>
59+
{
60+
[Test]
61+
public void CanDeserializeToObject()
62+
{
63+
StronglyTypedCollectionDeserializationTestCase.RootObject<TDictionary> rootObject;
64+
65+
using (var resourceStream = TestDataHelper.OpenResource("Text.list_of_objects.vdf"))
66+
{
67+
rootObject = KVSerializer.Create(KVSerializationFormat.KeyValues1Text)
68+
.Deserialize<StronglyTypedCollectionDeserializationTestCase.RootObject<TDictionary>>(resourceStream);
69+
}
70+
71+
Assert.That(rootObject, Is.Not.Null);
72+
Assert.That(rootObject.Numbers, Is.Not.Null);
73+
Assert.That(rootObject.Numbers, Is.InstanceOf<TDictionary>());
74+
Assert.That(rootObject.Numbers, Has.Count.EqualTo(3));
75+
Assert.That(rootObject.Numbers["0"], Is.Not.Null);
76+
Assert.That(rootObject.Numbers["0"].Name, Is.EqualTo("zero"));
77+
Assert.That(rootObject.Numbers["0"].Value, Is.EqualTo("nothing"));
78+
79+
Assert.That(rootObject.Numbers["1"], Is.Not.Null);
80+
Assert.That(rootObject.Numbers["1"].Name, Is.EqualTo("one"));
81+
Assert.That(rootObject.Numbers["1"].Value, Is.EqualTo("a bit"));
82+
83+
Assert.That(rootObject.Numbers["2"], Is.Not.Null);
84+
Assert.That(rootObject.Numbers["2"].Name, Is.EqualTo("two"));
85+
Assert.That(rootObject.Numbers["2"].Value, Is.EqualTo("a bit more"));
86+
}
87+
}
88+
5589
[TestFixture(typeof(SimpleObject[]))]
5690
[TestFixture(typeof(List<SimpleObject>))]
5791
class StronglyTypedCollectionDeserializationTestCase<TCollection>

ValveKeyValue/ValveKeyValue/ObjectCopier.cs

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,9 @@ static object MakeLookup(
231231
{
232232
[typeof(List<>)] = (type, values, reflector) => InvokeGeneric(nameof(MakeList), type.GetGenericArguments()[0], new object[] { values, reflector }),
233233
[typeof(IList<>)] = (type, values, reflector) => InvokeGeneric(nameof(MakeList), type.GetGenericArguments()[0], new object[] { values, reflector }),
234+
[typeof(IReadOnlyList<>)] = (type, values, reflector) => InvokeGeneric(nameof(MakeList), type.GetGenericArguments()[0], new object[] { values, reflector }),
235+
[typeof(IReadOnlyCollection<>)] = (type, values, reflector) => InvokeGeneric(nameof(MakeList), type.GetGenericArguments()[0], new object[] { values, reflector }),
236+
[typeof(IEnumerable<>)] = (type, values, reflector) => InvokeGeneric(nameof(MakeList), type.GetGenericArguments()[0], new object[] { values, reflector }),
234237
[typeof(Collection<>)] = (type, values, reflector) => InvokeGeneric(nameof(MakeCollection), type.GetGenericArguments()[0], new object[] { values, reflector }),
235238
[typeof(ICollection<>)] = (type, values, reflector) => InvokeGeneric(nameof(MakeCollection), type.GetGenericArguments()[0], new object[] { values, reflector }),
236239
[typeof(ObservableCollection<>)] = (type, values, reflector) => InvokeGeneric(nameof(MakeObservableCollection), type.GetGenericArguments()[0], new object[] { values, reflector }),
@@ -352,12 +355,9 @@ static bool IsDictionary(Type type)
352355
}
353356

354357
var genericType = type.GetGenericTypeDefinition();
355-
if (genericType != typeof(Dictionary<,>))
356-
{
357-
return false;
358-
}
359-
360-
return true;
358+
return genericType == typeof(Dictionary<,>)
359+
|| genericType == typeof(IDictionary<,>)
360+
|| genericType == typeof(IReadOnlyDictionary<,>);
361361
}
362362

363363
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2060", Justification = "Analysis cannot follow MakeGenericMethod but we should be clear by here anyway.")]
@@ -367,9 +367,15 @@ static object MakeDictionary(
367367
KVObject kv,
368368
IObjectReflector reflector)
369369
{
370-
var dictionary = Activator.CreateInstance(type);
371370
var genericArguments = type.GetGenericArguments();
372371

372+
// For interface types (IDictionary<,>, IReadOnlyDictionary<,>), use concrete Dictionary<,>
373+
var concreteType = type.GetGenericTypeDefinition() == typeof(Dictionary<,>)
374+
? type
375+
: typeof(Dictionary<,>).MakeGenericType(genericArguments);
376+
377+
var dictionary = Activator.CreateInstance(concreteType);
378+
373379
var method = typeof(ObjectCopier)
374380
.GetMethod(nameof(FillDictionary), BindingFlags.Static | BindingFlags.NonPublic)!;
375381
method.MakeGenericMethod(genericArguments)

0 commit comments

Comments
 (0)