diff --git a/Fluid.Tests/MiscFiltersTests.cs b/Fluid.Tests/MiscFiltersTests.cs index dcacced4..5eaf3363 100644 --- a/Fluid.Tests/MiscFiltersTests.cs +++ b/Fluid.Tests/MiscFiltersTests.cs @@ -520,6 +520,33 @@ public async Task JsonShouldHideMembers() Assert.Equal(expected, result.ToStringValue()); } + [Fact] + public async Task JsonShouldHandleCircularReferences() + { + var model = TestObjects.RecursiveReferenceObject; + var input = FluidValue.Create(model, TemplateOptions.Default); + var to = new TemplateOptions(); + to.MemberAccessStrategy.Register(); + + var result = await MiscFilters.Json(input, new FilterArguments(), new TemplateContext(to)); + + Assert.Equal("{\"Name\":\"Object1\",\"NodeRef\":{\"Name\":\"Child1\",\"NodeRef\":\"circular reference detected.\"}}", result.ToStringValue()); + } + + [Fact] + public async Task JsonShouldHandleCircularReferencesOnSiblingPropertiesSeparately() + { + var model = TestObjects.SiblingPropertiesHaveSameReferenceObject; + var input = FluidValue.Create(model, TemplateOptions.Default); + var to = new TemplateOptions(); + to.MemberAccessStrategy.Register(); + to.MemberAccessStrategy.Register(); + + var result = await MiscFilters.Json(input, new FilterArguments(), new TemplateContext(to)); + + Assert.Equal("{\"Name\":\"MultipleNode1\",\"Node1\":{\"Name\":\"Object1\",\"NodeRef\":{\"Name\":\"Child1\",\"NodeRef\":\"circular reference detected.\"}},\"Node2\":{\"Name\":\"Object1\",\"NodeRef\":{\"Name\":\"Child1\",\"NodeRef\":\"circular reference detected.\"}}}", result.ToStringValue()); + } + [Theory] [InlineData("", "", "", "0")] [InlineData(123456, "", "", "123456")] @@ -565,6 +592,57 @@ public async Task FormatString(object input, object[] args, string culture, stri Assert.Equal(expected, result.ToStringValue()); } + public static class TestObjects + { + public class Node + { + public string Name { get; set; } + public Node NodeRef { get; set; } + } + + public class MultipleNode + { + public string Name { get; set; } + + public Node Node1 { get; set; } + + public Node Node2 { get; set; } + } + + public static Node RecursiveReferenceObject + { + get + { + var parent = new Node + { + Name = "Object1", + }; + var child = new Node + { + Name = "Child1", + NodeRef = parent + }; + parent.NodeRef = child; + return parent; + } + } + + public static object SiblingPropertiesHaveSameReferenceObject + { + get + { + var n = RecursiveReferenceObject; + var m = new MultipleNode + { + Name = "MultipleNode1", + Node1 = n, + Node2 = n + }; + return m; + } + } + } + private class JsonAccessStrategy { public string Visible { get; set; } = "Visible"; diff --git a/Fluid/Filters/MiscFilters.cs b/Fluid/Filters/MiscFilters.cs index ada4e459..32cc1088 100644 --- a/Fluid/Filters/MiscFilters.cs +++ b/Fluid/Filters/MiscFilters.cs @@ -542,7 +542,7 @@ private static bool TryGetDateTimeInput(FluidValue input, TemplateContext contex return true; } - private static async ValueTask WriteJson(Utf8JsonWriter writer, FluidValue input, TemplateContext ctx) + private static async ValueTask WriteJson(Utf8JsonWriter writer, FluidValue input, TemplateContext ctx, HashSet stack = null) { switch (input.Type) { @@ -614,6 +614,12 @@ private static async ValueTask WriteJson(Utf8JsonWriter writer, FluidValue input value = access.Get(obj, name, ctx); } + stack ??= new HashSet(); + if (stack.Contains(value)) + { + value = "circular reference detected."; + } + var fluidValue = FluidValue.Create(value, ctx.Options); if (fluidValue.IsNil()) { @@ -621,7 +627,9 @@ private static async ValueTask WriteJson(Utf8JsonWriter writer, FluidValue input } writer.WritePropertyName(name); - await WriteJson(writer, fluidValue, ctx); + stack.Add(obj); + await WriteJson(writer, fluidValue, ctx, stack); + stack.Remove(obj); } writer.WriteEndObject();