-
Notifications
You must be signed in to change notification settings - Fork 4.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add option to ignore reference cycles on serialization #46101
Conversation
Note regarding the This serves as a reminder for when your PR is modifying a ref *.cs file and adding/modifying public APIs, to please make sure the API implementation in the src *.cs file is documented with triple slash comments, so the PR reviewers can sign off that change. |
...s/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleMetadata.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReferenceEqualsWrapper.cs
Outdated
Show resolved
Hide resolved
I think this has already been discussed in the parent issue, but as a json user, assuming I didn't want to preserve references I would prefer it if the serializer threw an exception rather than silently ignoring data (because there's likely a bug in my code). |
@eiriktsarpalis this is not changing the default behavior that throws an exception when MaxDepth is reached. You have to opt-in for ignoring reference loops as shown in the description's code snippet. |
@jozkee have you evaluated what it would take to improve the existing extensibility model to support ignore? That would either be an alternative to this feature or perhaps complementary to handle scenarios with different requirements. |
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.WriteCore.cs
Outdated
Show resolved
Hide resolved
@steveharter I was thinking that we could add a ReferenceCycleStrategy flag that indicates the semantics that will be used and add a couple methods to ReferenceResolver for such purpose. public abstract class ReferenceResolver
{
protected ReferenceResolver() { }
public abstract void AddReference(string referenceId, object value);
public abstract string GetReference(object value, out bool alreadyExists);
public abstract object ResolveReference(string referenceId);
+ // Pushes the value to the stack of references.
+ // Returns true if the reference already exists; otherwise, false.
+ public virtual bool PushReferenceForCycleDetection(object value);
+
+ // Pops the last reference on the stack of references once serialization of the value is complete.
+ public virtual void PopReferenceForCycleDetection();
}
public abstract class ReferenceHandler
{
public static ReferenceHandler Preserve { get { throw null; } }
public static ReferenceHandler IgnoreCycle { get { throw null; } }
public abstract ReferenceResolver CreateResolver();
+ public virtual JsonReferenceCycleStrategy ReferenceCycleStrategy => JsonReferenceCycleStrategy.None;
}
+public enum JsonReferenceCycleStrategy
+{
+ // Use preserve semantics ($id and $ref) on (de)serialization.
+ None,
+ // Use ignore semantics (treat the value as null on cycle detection) on serialization.
+ Ignore
+} EDIT: As we discussed offline, this prototype breaks the single responsibility principle in |
9e51bd8
to
de74d5f
Compare
-public abstract class ReferenceResolver
+public abstract class ReferenceResolver : ReferenceResolverBase
{
public abstract void AddReference(string referenceId, object value);
public abstract string GetReference(object value, out bool alreadyExists);
public abstract object ResolveReference(string referenceId);
}
+public abstract class ReferenceResolverBase {}
+public abstract class IgnoreReferenceResolver : ReferenceResolverBase
+{
+ public abstract void PopReferenceForCycleDetection() => throw new InvalidOperationException();
+ public abstract void PushReferenceForCycleDetection(object value) => throw new InvalidOperationException();
+ public abstract bool ContainsReferenceForCycleDetection(object value) => throw new InvalidOperationException();
+}
// Binary breaking change.
-public sealed class ReferenceHandler<T> : ReferenceHandler where T : ReferenceResolver, new()
+public sealed class ReferenceHandler<T> : ReferenceHandler where T : ReferenceResolverBase, new()
{
- public override ReferenceResolver CreateResolver() { throw null; }
+ public override ReferenceResolverBase CreateResolver() { throw null; }
}
public abstract partial class ReferenceHandler
{
protected ReferenceHandler() { }
public static ReferenceHandler Preserve { get; }
public static ReferenceHandler IgnoreCycle { get; }
- public abstract ReferenceResolver CreateResolver();
+ public abstract ReferenceResolverBase CreateResolver();
} I was thinking a bit more about how an uber class could help us to keep single responsibility on each resolver and thought that we could probably add
However the downside of such design is that it would introduce a binary breaking change on users that already implement their own @steveharter @ahsonkhan @layomia @eiriktsarpalis thoughts? |
Update: addressed feedback and changed feature behavior to treat reference loops as null values in order to not alter the length of collections. |
b389da0
to
1905462
Compare
Fixed a perf regression caused by using Lines 91 to 92 in 2e55d73
New internal field runtime/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs Lines 491 to 496 in 1905462
|
BenchmarkDotNet=v0.12.1.1466-nightly, OS=Windows 10.0.18363.1316 (1909/November2019Update/19H2)
Intel Core i7-9750H CPU 2.60GHz, 1 CPU, 12 logical and 6 physical cores
.NET SDK=5.0.100
[Host] : .NET 5.0.0 (5.0.20.51904), X64 RyuJIT
Job-ABOOKC : .NET 6.0.0 (42.42.42.42424), X64 RyuJIT
Job-QJROBB : .NET 6.0.0 (42.42.42.42424), X64 RyuJIT
PowerPlanMode=00000000-0000-0000-0000-000000000000 Arguments=/p:DebugType=portable IterationTime=250.0000 ms
MaxIterationCount=20 MinIterationCount=15 WarmupCount=1
|
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/IgnoreReferenceResolver.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/IgnoreReferenceResolver.cs
Show resolved
Hide resolved
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReferenceHandlingStrategy.cs
Show resolved
Hide resolved
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReferenceHandler.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReferenceHandler.cs
Show resolved
Hide resolved
new JsonSerializerOptions { ReferenceHandler = ReferenceHandler.IgnoreCycle }; | ||
|
||
[Fact] | ||
public async Task IgnoreCycles_OnObject() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For this and the following collection-related types, I notice that the element types are all object
. Do we have any with strongly typed class-type, value-type, and nullable-value-type elements?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added more types that contain a property with object or a concrete type. It should cover boxed classes, non-boxed classes and boxed structs.
I am not testing non-boxed value types since those are ignored/never tracked in the stack of references and Nullable<T>
can't be boxed.
var root = new EmptyClassWithExtensionProperty(); | ||
root.MyOverflow.Add("root", root); | ||
await Test_Serialize_And_SerializeAsync(root, @"{""root"":null}", s_optionsIgnoreCycles); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does Newtonsoft.Json
have the same behavior here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, Newtonsoft also detects loops in the elements of the extension data.
src/libraries/System.Text.Json/tests/Serialization/ReferenceHandlerTests.IgnoreCycle.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Text.Json/tests/Serialization/ReferenceHandlerTests.IgnoreCycle.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Text.Json/tests/Serialization/ReferenceHandlerTests.IgnoreCycle.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoOfT.cs
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
Awesome! 💯 @jozkee, please add a comment to dotnet/core#5889 to capture this new feature in the Preview 2 release notes. |
@jozkee thank you very much, I know there was a lot of churn/heat regarding this :) |
On further investigation this really seems like a payload building problem, since it didn't repro on the same combos in master after you merged. Probably can ignore, but please do be cautious about this sort of thing as it wasn't related to the linked issues. |
Fixes #40099
This is a draft implementing a new API for
ReferenceHandler
capable of detect circularity on the input value of JsonSerializer.Serialize. A sample usage scenario would be the following:Note that reference cycles are broken by null tokens in JSON.