Skip to content
This repository was archived by the owner on Dec 24, 2022. It is now read-only.

Commit 9292a0e

Browse files
committed
Add HasCircularReferences / ToSafePartialObjectDictionary / ToSafeJson / ToSafeJsv to guard against circular refs
1 parent 7948841 commit 9292a0e

File tree

5 files changed

+153
-1
lines changed

5 files changed

+153
-1
lines changed

src/ServiceStack.Text/ReflectionExtensions.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1982,6 +1982,27 @@ private static ObjectDictionaryDefinition CreateObjectDictionaryDefinition(Type
19821982
}
19831983
return def;
19841984
}
1985+
1986+
public static Dictionary<string, object> ToSafePartialObjectDictionary<T>(this T instance)
1987+
{
1988+
var to = new Dictionary<string, object>();
1989+
var propValues = instance.ToObjectDictionary();
1990+
if (propValues != null)
1991+
{
1992+
foreach (var entry in propValues)
1993+
{
1994+
if (!TypeSerializer.HasCircularReferences(entry.Value))
1995+
{
1996+
to[entry.Key] = entry.Value.ToSafePartialObjectDictionary();
1997+
}
1998+
else
1999+
{
2000+
to[entry.Key] = entry.Value.ToString();
2001+
}
2002+
}
2003+
}
2004+
return to;
2005+
}
19852006
}
19862007

19872008
}

src/ServiceStack.Text/StringExtensions.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -511,6 +511,13 @@ public static string ToJsv<T>(this T obj)
511511
return TypeSerializer.SerializeToString(obj);
512512
}
513513

514+
public static string ToSafeJsv<T>(this T obj)
515+
{
516+
return TypeSerializer.HasCircularReferences(obj)
517+
? obj.ToSafePartialObjectDictionary().ToJsv()
518+
: obj.ToJsv();
519+
}
520+
514521
public static T FromJsv<T>(this string jsv)
515522
{
516523
return TypeSerializer.DeserializeFromString<T>(jsv);
@@ -523,6 +530,13 @@ public static string ToJson<T>(this T obj)
523530
: JsonSerializer.SerializeToString(obj);
524531
}
525532

533+
public static string ToSafeJson<T>(this T obj)
534+
{
535+
return TypeSerializer.HasCircularReferences(obj)
536+
? obj.ToSafePartialObjectDictionary().ToJson()
537+
: obj.ToJson();
538+
}
539+
526540
public static T FromJson<T>(this string json)
527541
{
528542
return JsonSerializer.DeserializeFromString<T>(json);

src/ServiceStack.Text/TypeSerializer.cs

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
//
1212

1313
using System;
14+
using System.Collections;
1415
using System.Collections.Generic;
1516
using System.Globalization;
1617
using System.IO;
@@ -253,7 +254,9 @@ public static string SerializeAndFormat<T>(this T instance)
253254
if (fn != null)
254255
return Dump(fn);
255256

256-
var dtoStr = SerializeToString(instance);
257+
var dtoStr = !HasCircularReferences(instance)
258+
? SerializeToString(instance)
259+
: SerializeToString(instance.ToSafePartialObjectDictionary());
257260
var formatStr = JsvFormatter.Format(dtoStr);
258261
return formatStr;
259262
}
@@ -275,6 +278,62 @@ public static string Dump(this Delegate fn)
275278
StringBuilderThreadStatic.ReturnAndFree(sb));
276279
return info;
277280
}
281+
282+
public static bool HasCircularReferences(object value)
283+
{
284+
return HasCircularReferences(value, null);
285+
}
286+
287+
private static bool HasCircularReferences(object value, Stack<object> parentValues)
288+
{
289+
var type = value != null ? value.GetType() : null;
290+
291+
if (type == null || !type.IsClass() || value is string)
292+
return false;
293+
294+
if (parentValues == null)
295+
{
296+
parentValues = new Stack<object>();
297+
parentValues.Push(value);
298+
}
299+
300+
var valueEnumerable = value as IEnumerable;
301+
if (valueEnumerable != null)
302+
{
303+
foreach (var item in valueEnumerable)
304+
{
305+
if (HasCircularReferences(item, parentValues))
306+
return true;
307+
}
308+
}
309+
else
310+
{
311+
var props = type.GetSerializableProperties();
312+
313+
foreach (var pi in props)
314+
{
315+
if (pi.GetIndexParameters().Length > 0)
316+
continue;
317+
318+
var mi = pi.GetGetMethod();
319+
var pValue = mi != null ? mi.Invoke(value, null) : null;
320+
if (pValue == null)
321+
continue;
322+
323+
if (parentValues.Contains(pValue))
324+
return true;
325+
326+
parentValues.Push(pValue);
327+
328+
if (HasCircularReferences(pValue, parentValues))
329+
return true;
330+
331+
parentValues.Pop();
332+
}
333+
}
334+
335+
return false;
336+
}
278337
}
279338

280339
public class JsvStringSerializer : IStringSerializer
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
using NUnit.Framework;
2+
3+
namespace ServiceStack.Text.Tests
4+
{
5+
public class DumpTests
6+
{
7+
public class Node
8+
{
9+
public Node(int id, params Node[] children)
10+
{
11+
Id = id;
12+
Children = children;
13+
}
14+
15+
public int Id { get; set; }
16+
17+
public Node[] Children { get; set; }
18+
}
19+
20+
[Test]
21+
public void Can_detect_Circular_References_in_models()
22+
{
23+
var node = new Node(1,
24+
new Node(11, new Node(111)),
25+
new Node(12, new Node(121)));
26+
27+
Assert.That(!TypeSerializer.HasCircularReferences(node));
28+
29+
var root = new Node(1,
30+
new Node(11));
31+
32+
var cyclicalNode = new Node(1, root);
33+
root.Children[0].Children = new[] { cyclicalNode };
34+
35+
Assert.That(TypeSerializer.HasCircularReferences(root));
36+
}
37+
38+
[Test]
39+
public void Can_PrintDump_ToSafeJson_ToSafeJsv()
40+
{
41+
var node = new Node(1,
42+
new Node(11, new Node(111)),
43+
new Node(12, new Node(121)));
44+
45+
var root = new Node(1,
46+
new Node(11, new Node(111)),
47+
node);
48+
49+
var cyclicalNode = new Node(1, root);
50+
root.Children[0].Children[0].Children = new[] { cyclicalNode };
51+
52+
root.PrintDump();
53+
root.ToSafeJson().Print();
54+
root.ToSafeJsv().Print();
55+
}
56+
}
57+
}

tests/ServiceStack.Text.Tests/ServiceStack.Text.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@
186186
<ItemGroup>
187187
<Compile Include="AutoMappingObjectDictionaryTests.cs" />
188188
<Compile Include="CustomCultureInfoTests.cs" />
189+
<Compile Include="DumpTests.cs" />
189190
<Compile Include="DynamicModels\ODataTests.cs" />
190191
<Compile Include="EnumerableTests.cs" />
191192
<Compile Include="AttributeTests.cs" />

0 commit comments

Comments
 (0)