Skip to content

Commit 0d973b5

Browse files
committed
Avoid materializing dictionaries when serializing
1 parent 660809c commit 0d973b5

3 files changed

Lines changed: 577 additions & 52 deletions

File tree

src/SharpYaml.Tests/Serialization/YamlOrderedDictionaryConverterTests.cs

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,20 @@ internal sealed class OrderedDictionaryObjectModel
1919
public OrderedDictionary<string, object> Items { get; set; } = new();
2020
}
2121

22+
internal sealed class OrderedDictionaryReferenceModel
23+
{
24+
public OrderedDictionary<string, int>? Primary { get; set; }
25+
26+
public OrderedDictionary<string, int>? Secondary { get; set; }
27+
}
28+
29+
internal sealed class OrderedDictionaryGenericReferenceModel
30+
{
31+
public OrderedDictionary<int, string>? Primary { get; set; }
32+
33+
public OrderedDictionary<int, string>? Secondary { get; set; }
34+
}
35+
2236
[YamlSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
2337
[YamlSerializable(typeof(OrderedDictionary<string, int>))]
2438
[YamlSerializable(typeof(OrderedDictionary<string, string>))]
@@ -128,6 +142,70 @@ public void Reflection_RoundTrip_PreservesOrder()
128142
CollectionAssert.AreEqual(originalKeys, resultKeys);
129143
}
130144

145+
[TestMethod]
146+
public void Reflection_RoundTrip_StringKeyOrderedDictionary_ShouldPreserveSharedReferences()
147+
{
148+
var shared = new OrderedDictionary<string, int>
149+
{
150+
{ "zebra", 1 },
151+
{ "apple", 2 },
152+
};
153+
154+
var payload = new OrderedDictionaryReferenceModel
155+
{
156+
Primary = shared,
157+
Secondary = shared,
158+
};
159+
160+
var options = new YamlSerializerOptions { ReferenceHandling = YamlReferenceHandling.Preserve };
161+
var yaml = YamlSerializer.Serialize(payload, options);
162+
163+
var anchor = ExtractAnchor(yaml, "Primary: &");
164+
StringAssert.Contains(yaml, $"Secondary: *{anchor}");
165+
166+
var result = YamlSerializer.Deserialize<OrderedDictionaryReferenceModel>(yaml, options);
167+
168+
Assert.IsNotNull(result);
169+
Assert.IsNotNull(result.Primary);
170+
Assert.IsNotNull(result.Secondary);
171+
Assert.IsTrue(ReferenceEquals(result.Primary, result.Secondary));
172+
173+
var keys = new List<string>(result.Primary.Keys);
174+
CollectionAssert.AreEqual(new[] { "zebra", "apple" }, keys);
175+
}
176+
177+
[TestMethod]
178+
public void Reflection_RoundTrip_GenericKeyOrderedDictionary_ShouldPreserveSharedReferences()
179+
{
180+
var shared = new OrderedDictionary<int, string>
181+
{
182+
{ 3, "three" },
183+
{ 1, "one" },
184+
};
185+
186+
var payload = new OrderedDictionaryGenericReferenceModel
187+
{
188+
Primary = shared,
189+
Secondary = shared,
190+
};
191+
192+
var options = new YamlSerializerOptions { ReferenceHandling = YamlReferenceHandling.Preserve };
193+
var yaml = YamlSerializer.Serialize(payload, options);
194+
195+
var anchor = ExtractAnchor(yaml, "Primary: &");
196+
StringAssert.Contains(yaml, $"Secondary: *{anchor}");
197+
198+
var result = YamlSerializer.Deserialize<OrderedDictionaryGenericReferenceModel>(yaml, options);
199+
200+
Assert.IsNotNull(result);
201+
Assert.IsNotNull(result.Primary);
202+
Assert.IsNotNull(result.Secondary);
203+
Assert.IsTrue(ReferenceEquals(result.Primary, result.Secondary));
204+
205+
var keys = new List<int>(result.Primary.Keys);
206+
CollectionAssert.AreEqual(new[] { 3, 1 }, keys);
207+
}
208+
131209
[TestMethod]
132210
public void Reflection_Deserialize_NullValue()
133211
{
@@ -284,4 +362,18 @@ public void SourceGen_Deserialize_WithCamelCaseNaming()
284362
Assert.IsNotNull(result);
285363
Assert.AreEqual(2, result.Count);
286364
}
365+
366+
private static string ExtractAnchor(string yaml, string prefix)
367+
{
368+
var anchorStart = yaml.IndexOf(prefix, StringComparison.Ordinal);
369+
Assert.IsTrue(anchorStart >= 0, $"Expected '{prefix}' in YAML.");
370+
anchorStart += prefix.Length;
371+
372+
var anchorEnd = yaml.IndexOf('\n', anchorStart);
373+
Assert.IsTrue(anchorEnd > anchorStart, $"Expected an anchor after '{prefix}'.");
374+
375+
var anchor = yaml.Substring(anchorStart, anchorEnd - anchorStart).Trim();
376+
Assert.AreNotEqual(string.Empty, anchor);
377+
return anchor;
378+
}
287379
}

src/SharpYaml.Tests/Serialization/YamlSerializerCollectionSupportReflectionTests.cs

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,68 @@ public void Deserialize_IReadOnlyDictionaryWithEnumKeys_ShouldParseKeys()
6060
Assert.AreEqual(2, result[TestColor.Green]);
6161
}
6262

63+
[TestMethod]
64+
public void RoundTrip_IDictionaryNonDictionaryImplementation_ShouldPreserveSharedReferences()
65+
{
66+
var shared = new SortedDictionary<int, string>
67+
{
68+
[2] = "two",
69+
[1] = "one",
70+
};
71+
72+
var payload = new DictionaryInterfacePayload
73+
{
74+
Primary = shared,
75+
Secondary = shared,
76+
};
77+
78+
var options = new YamlSerializerOptions { ReferenceHandling = YamlReferenceHandling.Preserve };
79+
var yaml = YamlSerializer.Serialize(payload, options);
80+
81+
var anchor = ExtractAnchor(yaml, "Primary: &");
82+
StringAssert.Contains(yaml, $"Secondary: *{anchor}");
83+
84+
var result = YamlSerializer.Deserialize<DictionaryInterfacePayload>(yaml, options);
85+
86+
Assert.IsNotNull(result);
87+
Assert.IsNotNull(result.Primary);
88+
Assert.IsNotNull(result.Secondary);
89+
Assert.IsTrue(ReferenceEquals(result.Primary, result.Secondary));
90+
Assert.AreEqual("one", result.Primary[1]);
91+
Assert.AreEqual("two", result.Primary[2]);
92+
}
93+
94+
[TestMethod]
95+
public void RoundTrip_IReadOnlyDictionaryNonDictionaryImplementation_ShouldPreserveSharedReferences()
96+
{
97+
var shared = new SortedDictionary<TestColor, int>
98+
{
99+
[TestColor.Green] = 2,
100+
[TestColor.Red] = 1,
101+
};
102+
103+
var payload = new ReadOnlyDictionaryInterfacePayload
104+
{
105+
Primary = shared,
106+
Secondary = shared,
107+
};
108+
109+
var options = new YamlSerializerOptions { ReferenceHandling = YamlReferenceHandling.Preserve };
110+
var yaml = YamlSerializer.Serialize(payload, options);
111+
112+
var anchor = ExtractAnchor(yaml, "Primary: &");
113+
StringAssert.Contains(yaml, $"Secondary: *{anchor}");
114+
115+
var result = YamlSerializer.Deserialize<ReadOnlyDictionaryInterfacePayload>(yaml, options);
116+
117+
Assert.IsNotNull(result);
118+
Assert.IsNotNull(result.Primary);
119+
Assert.IsNotNull(result.Secondary);
120+
Assert.IsTrue(ReferenceEquals(result.Primary, result.Secondary));
121+
Assert.AreEqual(1, result.Primary[TestColor.Red]);
122+
Assert.AreEqual(2, result.Primary[TestColor.Green]);
123+
}
124+
63125
[TestMethod]
64126
public void Serialize_DictionaryWithGuidKeys_ShouldUseInvariantFormat()
65127
{
@@ -164,4 +226,32 @@ private sealed class ImmutableArrayAnchorPayload
164226

165227
public ImmutableArray<int> Other { get; set; }
166228
}
229+
230+
private sealed class DictionaryInterfacePayload
231+
{
232+
public IDictionary<int, string>? Primary { get; set; }
233+
234+
public IDictionary<int, string>? Secondary { get; set; }
235+
}
236+
237+
private sealed class ReadOnlyDictionaryInterfacePayload
238+
{
239+
public IReadOnlyDictionary<TestColor, int>? Primary { get; set; }
240+
241+
public IReadOnlyDictionary<TestColor, int>? Secondary { get; set; }
242+
}
243+
244+
private static string ExtractAnchor(string yaml, string prefix)
245+
{
246+
var anchorStart = yaml.IndexOf(prefix, StringComparison.Ordinal);
247+
Assert.IsTrue(anchorStart >= 0, $"Expected '{prefix}' in YAML.");
248+
anchorStart += prefix.Length;
249+
250+
var anchorEnd = yaml.IndexOf('\n', anchorStart);
251+
Assert.IsTrue(anchorEnd > anchorStart, $"Expected an anchor after '{prefix}'.");
252+
253+
var anchor = yaml.Substring(anchorStart, anchorEnd - anchorStart).Trim();
254+
Assert.AreNotEqual(string.Empty, anchor);
255+
return anchor;
256+
}
167257
}

0 commit comments

Comments
 (0)