-
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
Expose ReferenceResolver and rename ReferenceHandling to ReferenceHandler #36829
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. |
@@ -22,7 +22,7 @@ public sealed class ReferenceHandling | |||
/// Metadata properties (`$id`, `$values`, and `$ref`) will not be consumed and therefore will be treated as regular JSON properties. | |||
/// The metadata properties can map to a real property on the returned object if the property names match, or will be added to the <see cref="JsonExtensionDataAttribute"/> overflow dictionary, if one exists; otherwise, they are ignored. | |||
/// </remarks> | |||
public static ReferenceHandling Default { get; } = new ReferenceHandling(PreserveReferencesHandling.None); | |||
public static ReferenceHandler? Default { get; } = null; |
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.
Is it OK to use null
as the "default" or as the "do not preserve" option?
As per early discussions with @ahsonkhan, people tend to prefer actually seeing the default option listed, so this would only mask null
value as the default, instead of having one instance that holds a ReferenceResolver
that is never used.
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.
If Default
is not functional, can we consider removing it from the API? The Default
property on JsonNamingPolicy
is internal.
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 the change regress existing benchmarks (both with and without the feature)?
@@ -190,11 +190,11 @@ public static partial class JsonSerializer | |||
public static object? Deserialize(ref System.Text.Json.Utf8JsonReader reader, System.Type returnType, System.Text.Json.JsonSerializerOptions? options = null) { throw null; } | |||
public static System.Threading.Tasks.ValueTask<object?> DeserializeAsync(System.IO.Stream utf8Json, System.Type returnType, System.Text.Json.JsonSerializerOptions? options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } | |||
public static System.Threading.Tasks.ValueTask<TValue> DeserializeAsync<TValue>(System.IO.Stream utf8Json, System.Text.Json.JsonSerializerOptions? options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } | |||
[return: System.Diagnostics.CodeAnalysis.MaybeNull] | |||
[return: System.Diagnostics.CodeAnalysis.MaybeNullAttribute] |
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.
Can you minimize the diff in this file by restricting changes just to the new API? We can follow up with a clean-up PR.
public abstract partial class ReferenceHandler | ||
{ | ||
protected ReferenceHandler() { } | ||
public static System.Text.Json.Serialization.ReferenceHandler? Default { get { throw null; } } |
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.
Why is this nullable?
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.
Because it holds a null
value on the implementation. As we discussed in another comment, this property can be just a facade for the default value for ReferenceHandler.
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 removed the property since as you mentioned in another comment, it is no longer functional, this still might need approval since we are changing the API.
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/DefaultReferenceResolver.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/DefaultReferenceResolver.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStack.cs
Outdated
Show resolved
Hide resolved
@@ -1256,7 +1256,7 @@ public static void IdIsNotFirstProperty() | |||
Assert.Equal("$.$id", ex.Path); | |||
} | |||
|
|||
[Fact] | |||
[Fact(Skip = "TODO: Decide whether update the test to no longer point to $id or try to append $id back to the JSON Path.")] |
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.
What's the limitation with setting the JSON path correctly?
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.
Before, AddReferenceOnDeserialize
behave as a TryAdd method, where you could know if the new object was successfully added or not afterwards and we could throw in the call site instead of throwing in the method.
Now, AddReference
is a void method that does not follow a TryAdd pattern so we should throw in its implementation, where the code is unaware of the state.
There are a few workaround to keep the behavior as it was before, e.g, we could set state.Current.JsonPropertyName = "$id"
before calling AddReference
and set it back to its original value after, but that seems quite ugly and no worth doing without discussion. That's why I left this comment 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.
Why would you have to set it back to the original value? Current
is for the specific frame, and we are doing a forward-only read.
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.
As we discussed offline, there is one case where we are off of $id
and we call AddReference
: for a preserved array the last metadata we will read before calling AddReference
would be $values
, i.e. the reader is at the StartArray
token when we call AddReference
on the IEnumerableDefaultConverter
.
As you suggested, we could potentially avoid the "hacky" thing of remember the last metadata if we call AddReference
right when we read the $id
property instead of waiting for $values
. This probably requires splitting HandleMetadata
implementation to instantiate the collection when we call HandleMetadata
from IEnumerableDefaultconverter
.
I have created issue #37168 to follow up on this.
src/libraries/System.Text.Json/tests/Serialization/ReferenceHandlerTests.cs
Show resolved
Hide resolved
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReferenceResolver.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Text.Json/tests/Serialization/ReferenceHandlerTests.cs
Show resolved
Hide resolved
This change does not regress performance on the out-of-box path. ReadresultsBefore:
Now:
summary
WriteresultsBefore:
Now:
summary
|
For ReferenceHandler.Preserve, allocation increased by 40 bytes on read/write, this is because we replaced ReadresultsBefore:
Now:
summary
WriteresultsBefore:
Now:
summary
|
bool shouldReadPreservedReferences = options.ReferenceHandling.ShouldReadPreservedReferences(); | ||
|
||
if (!state.SupportContinuation && !shouldReadPreservedReferences) | ||
if (!state.SupportContinuation && options.ReferenceHandler == null) |
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.
It looks like these 2 variables are always checked together. Although they could be separate in the future, consider collapsing then having state.SupportContinuation
consider options.ReferencHandler
factored in to prevent the &&
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.
As of today, state.SupportContinuation
and options.ReferenceHandler
need to go through the "slow path" since both relies on the state.Current.ObjectState
.
Is your suggestion related to refactor the code so we can do a fast path for ReferenceHandler
when there is no need for continuation? If that's the case I can try to tackle that in a follow-up PR.
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.
Not a fast path. Just having a single bool like
state.UseFastPath = !state.SupportContinuation && options.ReferenceHandler == null
@@ -148,7 +146,7 @@ protected static JsonConverter<TValue> GetValueConverter(ref WriteStack state) | |||
} | |||
|
|||
// Handle the metadata properties. | |||
if (shouldReadPreservedReferences && state.Current.ObjectState < StackFrameObjectState.PropertyValue) | |||
if (options.ReferenceHandler != null && state.Current.ObjectState < StackFrameObjectState.PropertyValue) |
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.
Cache these in a bool (since accessed elsewhere in same method)?
...es/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleMetadata.cs
Outdated
Show resolved
Hide resolved
internal sealed class PreserveReferenceResolver : ReferenceResolver | ||
{ | ||
private uint _referenceCount; | ||
private readonly Dictionary<string, object>? _referenceIdToObjectMap; |
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 assume ConcurrentDictionary
is not used is because we create a new instance for every (de)serialization call -- correct? i.e. we don't have to worry about multiple threads
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.
That's correct. We call CreateConverter
that returns a new instance every time we (de)serialize unless someone provides its own ReferenceHandler
implementation that does otherwise.
src/libraries/System.Text.Json/tests/Serialization/ReferenceHandlerTests.cs
Show resolved
Hide resolved
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs
Outdated
Show resolved
Hide resolved
…dler (dotnet#36829) * Expose ReferenceResolver and rename ReferenceHandling to ReferenceHandler * Address some feedback * Address feedback * Clean-up code * Change messages in string.resx * Add test for a badly implemented resolver * Address feedback.
Follow up to dotnet/runtime#37296 (comment) The property and the type ReferenceHandling were recently renamed to Referencehandler on dotnet/runtime#36829 Above PR was also ported to dotnet/runtime preview6 branch under dotnet/runtime#37296 therefore once this is into master, it should be ported to release/5.0-preview6 branch as well.
Address the following as it was detailed in #30820 (comment):
ReferenceHandling
to resemble a class.ReferenceResolver
class.ReferenceHandler<T> where T : ReferenceResolver
to allow users to safely provide their own resolver implementation that will be instantiated on each call to (De)Serialize.ReferenceHandler
abstract so if you override it you can opt-in for using a persistent resolver.This PR effectively closes #30820 as no more changes would be pending to address.