Expose ReferenceResolver and rename ReferenceHandling to ReferenceHandler (#36829)
authorDavid Cantu <dacantu@microsoft.com>
Tue, 2 Jun 2020 07:05:26 +0000 (00:05 -0700)
committerGitHub <noreply@github.com>
Tue, 2 Jun 2020 07:05:26 +0000 (00:05 -0700)
* 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.

27 files changed:
src/libraries/System.Text.Json/ref/System.Text.Json.cs
src/libraries/System.Text.Json/src/Resources/Strings.resx
src/libraries/System.Text.Json/src/System.Text.Json.csproj
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/DictionaryDefaultConverter.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IEnumerableDefaultConverter.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectDefaultConverter.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/DefaultReferenceResolver.cs [deleted file]
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleMetadata.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandlePropertyName.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleMetadata.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/PreserveReferenceHandler.cs [new file with mode: 0644]
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/PreserveReferenceResolver.cs [new file with mode: 0644]
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStack.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReferenceHandler.cs [moved from src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReferenceHandling.cs with 51% similarity]
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReferenceHandlerOfT.cs [new file with mode: 0644]
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReferenceResolver.cs [new file with mode: 0644]
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStack.cs
src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs
src/libraries/System.Text.Json/tests/Serialization/ConstructorTests/ConstructorTests.Exceptions.cs
src/libraries/System.Text.Json/tests/Serialization/OptionsTests.cs
src/libraries/System.Text.Json/tests/Serialization/ReferenceHandlerTests.Deserialize.cs [moved from src/libraries/System.Text.Json/tests/Serialization/ReferenceHandlingTests.Deserialize.cs with 98% similarity]
src/libraries/System.Text.Json/tests/Serialization/ReferenceHandlerTests.Serialize.cs [moved from src/libraries/System.Text.Json/tests/Serialization/ReferenceHandlingTests.Serialize.cs with 98% similarity]
src/libraries/System.Text.Json/tests/Serialization/ReferenceHandlerTests.cs [moved from src/libraries/System.Text.Json/tests/Serialization/ReferenceHandlingTests.cs with 81% similarity]
src/libraries/System.Text.Json/tests/Serialization/Stream.Collections.cs
src/libraries/System.Text.Json/tests/System.Text.Json.Tests.csproj

index 4aa8377..bcc2ffe 100644 (file)
@@ -222,7 +222,7 @@ namespace System.Text.Json
         public bool PropertyNameCaseInsensitive { get { throw null; } set { } }
         public System.Text.Json.JsonNamingPolicy? PropertyNamingPolicy { get { throw null; } set { } }
         public System.Text.Json.JsonCommentHandling ReadCommentHandling { get { throw null; } set { } }
-        public System.Text.Json.Serialization.ReferenceHandling ReferenceHandling { get { throw null; } set { } }
+        public System.Text.Json.Serialization.ReferenceHandler? ReferenceHandler { get { throw null; } set { } }
         public bool WriteIndented { get { throw null; } set { } }
         public System.Text.Json.Serialization.JsonConverter GetConverter(System.Type typeToConvert) { throw null; }
     }
@@ -535,10 +535,22 @@ namespace System.Text.Json.Serialization
         public override bool CanConvert(System.Type typeToConvert) { throw null; }
         public override System.Text.Json.Serialization.JsonConverter CreateConverter(System.Type typeToConvert, System.Text.Json.JsonSerializerOptions options) { throw null; }
     }
-    public sealed partial class ReferenceHandling
+    public abstract partial class ReferenceHandler
     {
-        internal ReferenceHandling() { }
-        public static System.Text.Json.Serialization.ReferenceHandling Default { get { throw null; } }
-        public static System.Text.Json.Serialization.ReferenceHandling Preserve { get { throw null; } }
+        protected ReferenceHandler() { }
+        public static System.Text.Json.Serialization.ReferenceHandler Preserve { get { throw null; } }
+        public abstract System.Text.Json.Serialization.ReferenceResolver CreateResolver();
+    }
+    public sealed partial class ReferenceHandler<T> : System.Text.Json.Serialization.ReferenceHandler where T : System.Text.Json.Serialization.ReferenceResolver, new()
+    {
+        public ReferenceHandler() { }
+        public override System.Text.Json.Serialization.ReferenceResolver CreateResolver() { throw null; }
+    }
+    public abstract partial 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);
     }
 }
index 44de8a1..1a029bf 100644 (file)
     <value>Either the JSON value is not in a supported format, or is out of bounds for a UInt16.</value>
   </data>
   <data name="SerializerCycleDetected" xml:space="preserve">
-    <value>A possible object cycle was detected. This can either be due to a cycle or if the object depth is larger than the maximum allowed depth of {0}. Consider using ReferenceHandling.Preserve on JsonSerializerOptions to support cycles.</value>
+    <value>A possible object cycle was detected. This can either be due to a cycle or if the object depth is larger than the maximum allowed depth of {0}. Consider using ReferenceHandler.Preserve on JsonSerializerOptions to support cycles.</value>
   </data>
   <data name="EmptyStringToInitializeNumber" xml:space="preserve">
     <value>Expected a number, but instead got empty string.</value>
     <value>The '$id' and '$ref' metadata properties must be JSON strings. Current token type is '{0}'.</value>
   </data>
   <data name="MetadataInvalidPropertyWithLeadingDollarSign" xml:space="preserve">
-    <value>Properties that start with '$' are not allowed on preserve mode, either escape the character or turn off preserve references by setting ReferenceHandling to ReferenceHandling.Default.</value>
+    <value>Properties that start with '$' are not allowed on preserve mode, either escape the character or turn off preserve references by setting ReferenceHandler to null.</value>
   </data>
   <data name="MultipleMembersBindWithConstructorParameter" xml:space="preserve">
     <value>Members '{0}' and '{1}' on type '{2}' cannot both bind with parameter '{3}' in constructor '{4}' on deserialization.</value>
   <data name="DefaultIgnoreConditionInvalid" xml:space="preserve">
     <value>The value cannot be 'JsonIgnoreCondition.Always'.</value>
   </data>
-</root>
\ No newline at end of file
+</root>
index 7c82fc4..0dce72c 100644 (file)
     <Compile Include="System\Text\Json\Serialization\Converters\Value\UInt32Converter.cs" />
     <Compile Include="System\Text\Json\Serialization\Converters\Value\UInt64Converter.cs" />
     <Compile Include="System\Text\Json\Serialization\Converters\Value\UriConverter.cs" />
-    <Compile Include="System\Text\Json\Serialization\DefaultReferenceResolver.cs" />
     <Compile Include="System\Text\Json\Serialization\JsonCamelCaseNamingPolicy.cs" />
     <Compile Include="System\Text\Json\Serialization\JsonClassInfo.cs" />
     <Compile Include="System\Text\Json\Serialization\JsonClassInfo.Cache.cs" />
     <Compile Include="System\Text\Json\Serialization\MetadataPropertyName.cs" />
     <Compile Include="System\Text\Json\Serialization\ParameterRef.cs" />
     <Compile Include="System\Text\Json\Serialization\PooledByteBufferWriter.cs" />
+    <Compile Include="System\Text\Json\Serialization\PreserveReferenceHandler.cs" />
+    <Compile Include="System\Text\Json\Serialization\PreserveReferenceResolver.cs" />
     <Compile Include="System\Text\Json\Serialization\PropertyRef.cs" />
     <Compile Include="System\Text\Json\Serialization\ReadStack.cs" />
     <Compile Include="System\Text\Json\Serialization\ReadStackFrame.cs" />
-    <Compile Include="System\Text\Json\Serialization\ReferenceHandling.cs" />
+    <Compile Include="System\Text\Json\Serialization\ReferenceHandler.cs" />
+    <Compile Include="System\Text\Json\Serialization\ReferenceHandlerOfT.cs" />
+    <Compile Include="System\Text\Json\Serialization\ReferenceResolver.cs" />
     <Compile Include="System\Text\Json\Serialization\ReflectionEmitMemberAccessor.cs" />
     <Compile Include="System\Text\Json\Serialization\ReflectionMemberAccessor.cs" />
     <Compile Include="System\Text\Json\Serialization\StackFrameObjectState.cs" />
index 4603b59..77c8ff8 100644 (file)
@@ -69,9 +69,7 @@ namespace System.Text.Json.Serialization.Converters
             ref ReadStack state,
             [MaybeNullWhen(false)] out TCollection value)
         {
-            bool shouldReadPreservedReferences = options.ReferenceHandling.ShouldReadPreservedReferences();
-
-            if (!state.SupportContinuation && !shouldReadPreservedReferences)
+            if (state.UseFastPath)
             {
                 // Fast path that avoids maintaining state variables and dealing with preserved references.
 
@@ -148,7 +146,8 @@ namespace System.Text.Json.Serialization.Converters
                 }
 
                 // Handle the metadata properties.
-                if (shouldReadPreservedReferences && state.Current.ObjectState < StackFrameObjectState.PropertyValue)
+                bool preserveReferences = options.ReferenceHandler != null;
+                if (preserveReferences && state.Current.ObjectState < StackFrameObjectState.PropertyValue)
                 {
                     if (JsonSerializer.ResolveMetadata(this, ref reader, ref state))
                     {
@@ -175,10 +174,10 @@ namespace System.Text.Json.Serialization.Converters
                         Debug.Assert(CanHaveIdMetadata);
 
                         value = (TCollection)state.Current.ReturnValue!;
-                        if (!state.ReferenceResolver.AddReferenceOnDeserialize(state.Current.MetadataId, value))
-                        {
-                            ThrowHelper.ThrowJsonException_MetadataDuplicateIdFound(state.Current.MetadataId, ref state);
-                        }
+                        state.ReferenceResolver.AddReference(state.Current.MetadataId, value);
+                        // Clear metadata name, if the next read fails
+                        // we want to point the JSON path to the property's object.
+                        state.Current.JsonPropertyName = null;
                     }
 
                     state.Current.ObjectState = StackFrameObjectState.CreatedObject;
@@ -214,7 +213,7 @@ namespace System.Text.Json.Serialization.Converters
                         state.Current.PropertyState = StackFramePropertyState.Name;
 
                         // Verify property doesn't contain metadata.
-                        if (shouldReadPreservedReferences)
+                        if (preserveReferences)
                         {
                             ReadOnlySpan<byte> propertyName = reader.GetSpan();
                             if (propertyName.Length > 0 && propertyName[0] == '$')
@@ -275,7 +274,7 @@ namespace System.Text.Json.Serialization.Converters
                 state.Current.ProcessedStartToken = true;
                 writer.WriteStartObject();
 
-                if (options.ReferenceHandling.ShouldWritePreservedReferences())
+                if (options.ReferenceHandler != null)
                 {
                     if (JsonSerializer.WriteReferenceForObject(this, dictionary, ref state, writer) == MetadataPropertyName.Ref)
                     {
index 6803e9d..9dd215a 100644 (file)
@@ -40,9 +40,7 @@ namespace System.Text.Json.Serialization.Converters
             ref ReadStack state,
             [MaybeNullWhen(false)] out TCollection value)
         {
-            bool shouldReadPreservedReferences = options.ReferenceHandling.ShouldReadPreservedReferences();
-
-            if (!state.SupportContinuation && !shouldReadPreservedReferences)
+            if (state.UseFastPath)
             {
                 // Fast path that avoids maintaining state variables and dealing with preserved references.
 
@@ -91,13 +89,14 @@ namespace System.Text.Json.Serialization.Converters
             {
                 // Slower path that supports continuation and preserved references.
 
+                bool preserveReferences = options.ReferenceHandler != null;
                 if (state.Current.ObjectState == StackFrameObjectState.None)
                 {
                     if (reader.TokenType == JsonTokenType.StartArray)
                     {
                         state.Current.ObjectState = StackFrameObjectState.PropertyValue;
                     }
-                    else if (shouldReadPreservedReferences)
+                    else if (preserveReferences)
                     {
                         if (reader.TokenType != JsonTokenType.StartObject)
                         {
@@ -113,7 +112,7 @@ namespace System.Text.Json.Serialization.Converters
                 }
 
                 // Handle the metadata properties.
-                if (shouldReadPreservedReferences && state.Current.ObjectState < StackFrameObjectState.PropertyValue)
+                if (preserveReferences && state.Current.ObjectState < StackFrameObjectState.PropertyValue)
                 {
                     if (JsonSerializer.ResolveMetadata(this, ref reader, ref state))
                     {
@@ -137,10 +136,17 @@ namespace System.Text.Json.Serialization.Converters
                     if (state.Current.MetadataId != null)
                     {
                         value = (TCollection)state.Current.ReturnValue!;
-                        if (!state.ReferenceResolver.AddReferenceOnDeserialize(state.Current.MetadataId, value))
-                        {
-                            ThrowHelper.ThrowJsonException_MetadataDuplicateIdFound(state.Current.MetadataId, ref state);
-                        }
+
+                        // TODO: https://github.com/dotnet/runtime/issues/37168
+                        //Separate logic for IEnumerable to call AddReference when the reader is at `$id`, in order to avoid remembering the last metadata.
+
+                        // Remember the prior metadata and temporarily use '$id' to write it in the path in case AddReference throws
+                        // in this case, the last property seen will be '$values' when we reach this point.
+                        byte[]? lastMetadataProperty = state.Current.JsonPropertyName;
+                        state.Current.JsonPropertyName = JsonSerializer.s_idPropertyName;
+
+                        state.ReferenceResolver.AddReference(state.Current.MetadataId, value);
+                        state.Current.JsonPropertyName = lastMetadataProperty;
                     }
 
                     state.Current.JsonPropertyInfo = state.Current.JsonClassInfo.ElementClassInfo!.PropertyInfoForClassInfo;
@@ -247,13 +253,11 @@ namespace System.Text.Json.Serialization.Converters
             }
             else
             {
-                bool shouldWritePreservedReferences = options.ReferenceHandling.ShouldWritePreservedReferences();
-
                 if (!state.Current.ProcessedStartToken)
                 {
                     state.Current.ProcessedStartToken = true;
 
-                    if (!shouldWritePreservedReferences)
+                    if (options.ReferenceHandler == null)
                     {
                         writer.WriteStartArray();
                     }
index 10327bc..c6624a3 100644 (file)
@@ -14,10 +14,9 @@ namespace System.Text.Json.Serialization.Converters
     {
         internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, ref ReadStack state, [MaybeNullWhen(false)] out T value)
         {
-            bool shouldReadPreservedReferences = options.ReferenceHandling.ShouldReadPreservedReferences();
             object obj;
 
-            if (!state.SupportContinuation && !shouldReadPreservedReferences)
+            if (state.UseFastPath)
             {
                 // Fast path that avoids maintaining state variables and dealing with preserved references.
 
@@ -76,7 +75,7 @@ namespace System.Text.Json.Serialization.Converters
                 // Handle the metadata properties.
                 if (state.Current.ObjectState < StackFrameObjectState.PropertyValue)
                 {
-                    if (shouldReadPreservedReferences)
+                    if (options.ReferenceHandler != null)
                     {
                         if (JsonSerializer.ResolveMetadata(this, ref reader, ref state))
                         {
@@ -106,10 +105,10 @@ namespace System.Text.Json.Serialization.Converters
                     obj = state.Current.JsonClassInfo.CreateObject!()!;
                     if (state.Current.MetadataId != null)
                     {
-                        if (!state.ReferenceResolver.AddReferenceOnDeserialize(state.Current.MetadataId, obj))
-                        {
-                            ThrowHelper.ThrowJsonException_MetadataDuplicateIdFound(state.Current.MetadataId, ref state);
-                        }
+                        state.ReferenceResolver.AddReference(state.Current.MetadataId, obj);
+                        // Clear metadata name, if the next read fails
+                        // we want to point the JSON path to the property's object.
+                        state.Current.JsonPropertyName = null;
                     }
 
                     state.Current.ReturnValue = obj;
@@ -239,7 +238,7 @@ namespace System.Text.Json.Serialization.Converters
             {
                 writer.WriteStartObject();
 
-                if (options.ReferenceHandling.ShouldWritePreservedReferences())
+                if (options.ReferenceHandler != null)
                 {
                     if (JsonSerializer.WriteReferenceForObject(this, objectValue, ref state, writer) == MetadataPropertyName.Ref)
                     {
@@ -294,7 +293,7 @@ namespace System.Text.Json.Serialization.Converters
                 {
                     writer.WriteStartObject();
 
-                    if (options.ReferenceHandling.ShouldWritePreservedReferences())
+                    if (options.ReferenceHandler != null)
                     {
                         if (JsonSerializer.WriteReferenceForObject(this, objectValue, ref state, writer) == MetadataPropertyName.Ref)
                         {
index 1af7ab0..89ecc99 100644 (file)
@@ -21,10 +21,9 @@ namespace System.Text.Json.Serialization.Converters
     {
         internal sealed override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, ref ReadStack state, [MaybeNullWhen(false)] out T value)
         {
-            bool shouldReadPreservedReferences = options.ReferenceHandling.ShouldReadPreservedReferences();
             object obj;
 
-            if (!state.SupportContinuation && !shouldReadPreservedReferences)
+            if (state.UseFastPath)
             {
                 // Fast path that avoids maintaining state variables.
 
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/DefaultReferenceResolver.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/DefaultReferenceResolver.cs
deleted file mode 100644 (file)
index 06f4ed6..0000000
+++ /dev/null
@@ -1,87 +0,0 @@
-// Licensed to the.NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-// See the LICENSE file in the project root for more information.
-
-using System.Collections.Generic;
-
-namespace System.Text.Json.Serialization
-{
-    /// <summary>
-    /// The default ReferenceResolver implementation to handle duplicate object references.
-    /// </summary>
-    /// <remarks>
-    /// It is currently a struct to save one unnecessary allcation while (de)serializing.
-    /// If we choose to expose the ReferenceResolver in a future, we may need to create an abstract class/interface and change this type to become a class that inherits from that abstract class/interface.
-    /// </remarks>
-    internal struct DefaultReferenceResolver
-    {
-        private uint _referenceCount;
-        private readonly Dictionary<string, object>? _referenceIdToObjectMap;
-        private readonly Dictionary<object, string>? _objectToReferenceIdMap;
-
-        public DefaultReferenceResolver(bool writing)
-        {
-            _referenceCount = default;
-
-            if (writing)
-            {
-                // Comparer used here to always do a Reference Equality comparison on serialization which is where we use the objects as the TKey in our dictionary.
-                _objectToReferenceIdMap = new Dictionary<object, string>(ReferenceEqualityComparer.Instance);
-                _referenceIdToObjectMap = null;
-            }
-            else
-            {
-                _referenceIdToObjectMap = new Dictionary<string, object>();
-                _objectToReferenceIdMap = null;
-            }
-        }
-
-        /// <summary>
-        /// Adds an entry to the bag of references using the specified id and value.
-        /// This method gets called when an $id metadata property from a JSON object is read.
-        /// </summary>
-        /// <param name="referenceId">The identifier of the respective JSON object or array.</param>
-        /// <param name="value">The value of the respective CLR reference type object that results from parsing the JSON object.</param>
-        /// <returns>True if the value was successfully added, false otherwise.</returns>
-        public bool AddReferenceOnDeserialize(string referenceId, object value)
-        {
-            return JsonHelpers.TryAdd(_referenceIdToObjectMap!, referenceId, value);
-        }
-
-        /// <summary>
-        /// Gets the reference id of the specified value if exists; otherwise a new id is assigned.
-        /// This method gets called before a CLR object is written so we can decide whether to write $id and the rest of its properties or $ref and step into the next object.
-        /// The first $id value will be 1.
-        /// </summary>
-        /// <param name="value">The value of the CLR reference type object to get or add an id for.</param>
-        /// <param name="referenceId">The id realated to the object.</param>
-        /// <returns></returns>
-        public bool TryGetOrAddReferenceOnSerialize(object value, out string referenceId)
-        {
-            bool result = _objectToReferenceIdMap!.TryGetValue(value, out referenceId!);
-            if (!result)
-            {
-                _referenceCount++;
-                referenceId = _referenceCount.ToString();
-                _objectToReferenceIdMap.Add(value, referenceId);
-            }
-            return result;
-        }
-
-        /// <summary>
-        /// Resolves the CLR reference type object related to the specified reference id.
-        /// This method gets called when $ref metadata property is read.
-        /// </summary>
-        /// <param name="referenceId">The id related to the returned object.</param>
-        /// <returns></returns>
-        public object ResolveReferenceOnDeserialize(string referenceId)
-        {
-            if (!_referenceIdToObjectMap!.TryGetValue(referenceId, out object? value))
-            {
-                ThrowHelper.ThrowJsonException_MetadataReferenceNotFound(referenceId);
-            }
-
-            return value;
-        }
-    }
-}
index edb6aeb..3375512 100644 (file)
@@ -10,6 +10,9 @@ namespace System.Text.Json
 {
     public static partial class JsonSerializer
     {
+        internal static readonly byte[] s_idPropertyName
+            = new byte[] { (byte)'$', (byte)'i', (byte)'d' };
+
         /// <summary>
         /// Returns true if successful, false is the reader ran out of buffer.
         /// Sets state.Current.ReturnValue to the $ref target for MetadataRefProperty cases.
@@ -123,7 +126,7 @@ namespace System.Text.Json
                 string key = reader.GetString()!;
 
                 // todo: https://github.com/dotnet/runtime/issues/32354
-                state.Current.ReturnValue = state.ReferenceResolver.ResolveReferenceOnDeserialize(key);
+                state.Current.ReturnValue = state.ReferenceResolver.ResolveReference(key);
                 state.Current.ObjectState = StackFrameObjectState.ReadAheadRefEndObject;
             }
             else if (state.Current.ObjectState == StackFrameObjectState.ReadIdValue)
@@ -135,9 +138,6 @@ namespace System.Text.Json
 
                 state.Current.MetadataId = reader.GetString();
 
-                // Clear the MetadataPropertyName since we are done processing Id.
-                state.Current.JsonPropertyName = default;
-
                 if (converter.ClassType == ClassType.Enumerable)
                 {
                     // Need to Read $values property name.
@@ -184,6 +184,8 @@ namespace System.Text.Json
             {
                 if (reader.TokenType != JsonTokenType.PropertyName)
                 {
+                    // Missing $values, JSON path should point to the property's object.
+                    state.Current.JsonPropertyName = null;
                     ThrowHelper.ThrowJsonException_MetadataPreservedArrayValuesNotFound(converter.TypeToConvert);
                 }
 
index b7b4db7..7ed9da9 100644 (file)
@@ -84,7 +84,7 @@ namespace System.Text.Json
                 unescapedPropertyName = propertyName;
             }
 
-            if (options.ReferenceHandling.ShouldReadPreservedReferences())
+            if (options.ReferenceHandler != null)
             {
                 if (propertyName.Length > 0 && propertyName[0] == '$')
                 {
index 0b5061e..7d5b504 100644 (file)
@@ -20,28 +20,33 @@ namespace System.Text.Json
             ref WriteStack state,
             Utf8JsonWriter writer)
         {
-            MetadataPropertyName metadataToWrite;
+            MetadataPropertyName writtenMetadataName;
 
             // If the jsonConverter supports immutable dictionaries or value types, don't write any metadata
             if (!jsonConverter.CanHaveIdMetadata || jsonConverter.IsValueType)
             {
-                metadataToWrite = MetadataPropertyName.NoMetadata;
-            }
-            else if (state.ReferenceResolver.TryGetOrAddReferenceOnSerialize(currentValue, out string referenceId))
-            {
-                Debug.Assert(referenceId != null);
-                writer.WriteString(s_metadataRef, referenceId);
-                writer.WriteEndObject();
-                metadataToWrite = MetadataPropertyName.Ref;
+                writtenMetadataName = MetadataPropertyName.NoMetadata;
             }
             else
             {
+
+                string referenceId = state.ReferenceResolver.GetReference(currentValue, out bool alreadyExists);
                 Debug.Assert(referenceId != null);
-                writer.WriteString(s_metadataId, referenceId);
-                metadataToWrite = MetadataPropertyName.Id;
+
+                if (alreadyExists)
+                {
+                    writer.WriteString(s_metadataRef, referenceId);
+                    writer.WriteEndObject();
+                    writtenMetadataName = MetadataPropertyName.Ref;
+                }
+                else
+                {
+                    writer.WriteString(s_metadataId, referenceId);
+                    writtenMetadataName = MetadataPropertyName.Id;
+                }
             }
 
-            return metadataToWrite;
+            return writtenMetadataName;
         }
 
         internal static MetadataPropertyName WriteReferenceForCollection(
@@ -50,32 +55,37 @@ namespace System.Text.Json
             ref WriteStack state,
             Utf8JsonWriter writer)
         {
-            MetadataPropertyName metadataToWrite;
+            MetadataPropertyName writtenMetadataName;
 
             // If the jsonConverter supports immutable enumerables or value type collections, don't write any metadata
             if (!jsonConverter.CanHaveIdMetadata || jsonConverter.IsValueType)
             {
                 writer.WriteStartArray();
-                metadataToWrite = MetadataPropertyName.NoMetadata;
-            }
-            else if (state.ReferenceResolver.TryGetOrAddReferenceOnSerialize(currentValue, out string referenceId))
-            {
-                Debug.Assert(referenceId != null);
-                writer.WriteStartObject();
-                writer.WriteString(s_metadataRef, referenceId);
-                writer.WriteEndObject();
-                metadataToWrite = MetadataPropertyName.Ref;
+                writtenMetadataName = MetadataPropertyName.NoMetadata;
             }
             else
             {
-                Debug.Assert(referenceId != null);
-                writer.WriteStartObject();
-                writer.WriteString(s_metadataId, referenceId);
-                writer.WriteStartArray(s_metadataValues);
-                metadataToWrite = MetadataPropertyName.Id;
+                string referenceId = state.ReferenceResolver.GetReference(currentValue, out bool alreadyExists);
+
+                if (alreadyExists)
+                {
+                    Debug.Assert(referenceId != null);
+                    writer.WriteStartObject();
+                    writer.WriteString(s_metadataRef, referenceId);
+                    writer.WriteEndObject();
+                    writtenMetadataName = MetadataPropertyName.Ref;
+                }
+                else
+                {
+                    Debug.Assert(referenceId != null);
+                    writer.WriteStartObject();
+                    writer.WriteString(s_metadataId, referenceId);
+                    writer.WriteStartArray(s_metadataValues);
+                    writtenMetadataName = MetadataPropertyName.Id;
+                }
             }
 
-            return metadataToWrite;
+            return writtenMetadataName;
         }
     }
 }
index 05b2aec..98587cb 100644 (file)
@@ -27,7 +27,7 @@ namespace System.Text.Json
         private JsonNamingPolicy? _dictionaryKeyPolicy;
         private JsonNamingPolicy? _jsonPropertyNamingPolicy;
         private JsonCommentHandling _readCommentHandling;
-        private ReferenceHandling _referenceHandling = ReferenceHandling.Default;
+        private ReferenceHandler? _referenceHandler;
         private JavaScriptEncoder? _encoder = null;
         private JsonIgnoreCondition _defaultIgnoreCondition;
 
@@ -66,7 +66,7 @@ namespace System.Text.Json
             _dictionaryKeyPolicy = options._dictionaryKeyPolicy;
             _jsonPropertyNamingPolicy = options._jsonPropertyNamingPolicy;
             _readCommentHandling = options._readCommentHandling;
-            _referenceHandling = options._referenceHandling;
+            _referenceHandler = options._referenceHandler;
             _encoder = options._encoder;
             _defaultIgnoreCondition = options._defaultIgnoreCondition;
 
@@ -404,16 +404,15 @@ namespace System.Text.Json
         }
 
         /// <summary>
-        /// Defines how references are treated when reading and writing JSON, this is convenient to deal with circularity.
+        /// Configures how object references are handled when reading and writing JSON.
         /// </summary>
-        public ReferenceHandling ReferenceHandling
+        public ReferenceHandler? ReferenceHandler
         {
-            get => _referenceHandling;
+            get => _referenceHandler;
             set
             {
                 VerifyMutable();
-
-                _referenceHandling = value ?? throw new ArgumentNullException(nameof(value));
+                _referenceHandler = value;
             }
         }
 
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/PreserveReferenceHandler.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/PreserveReferenceHandler.cs
new file mode 100644 (file)
index 0000000..a92b1ba
--- /dev/null
@@ -0,0 +1,13 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace System.Text.Json.Serialization
+{
+    internal sealed class PreserveReferenceHandler : ReferenceHandler
+    {
+        public override ReferenceResolver CreateResolver() => throw new InvalidOperationException();
+
+        internal override ReferenceResolver CreateResolver(bool writing) => new PreserveReferenceResolver(writing);
+    }
+}
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/PreserveReferenceResolver.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/PreserveReferenceResolver.cs
new file mode 100644 (file)
index 0000000..dd9469f
--- /dev/null
@@ -0,0 +1,73 @@
+// Licensed to the.NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Collections.Generic;
+using System.Diagnostics;
+
+namespace System.Text.Json.Serialization
+{
+    /// <summary>
+    /// The default ReferenceResolver implementation to handle duplicate object references.
+    /// </summary>
+    internal sealed class PreserveReferenceResolver : ReferenceResolver
+    {
+        private uint _referenceCount;
+        private readonly Dictionary<string, object>? _referenceIdToObjectMap;
+        private readonly Dictionary<object, string>? _objectToReferenceIdMap;
+
+        public PreserveReferenceResolver(bool writing)
+        {
+            if (writing)
+            {
+                // Comparer used here does a reference equality comparison on serialization, which is where we use the objects as the dictionary keys.
+                _objectToReferenceIdMap = new Dictionary<object, string>(ReferenceEqualityComparer.Instance);
+            }
+            else
+            {
+                _referenceIdToObjectMap = new Dictionary<string, object>();
+            }
+        }
+
+        public override void AddReference(string referenceId, object value)
+        {
+            Debug.Assert(_referenceIdToObjectMap != null);
+
+            if (!JsonHelpers.TryAdd(_referenceIdToObjectMap, referenceId, value))
+            {
+                ThrowHelper.ThrowJsonException_MetadataDuplicateIdFound(referenceId);
+            }
+        }
+
+        public override string GetReference(object value, out bool alreadyExists)
+        {
+            Debug.Assert(_objectToReferenceIdMap != null);
+
+            if (_objectToReferenceIdMap.TryGetValue(value, out string? referenceId))
+            {
+                alreadyExists = true;
+            }
+            else
+            {
+                _referenceCount++;
+                referenceId = _referenceCount.ToString();
+                _objectToReferenceIdMap.Add(value, referenceId);
+                alreadyExists = false;
+            }
+
+            return referenceId;
+        }
+
+        public override object ResolveReference(string referenceId)
+        {
+            Debug.Assert(_referenceIdToObjectMap != null);
+
+            if (!_referenceIdToObjectMap.TryGetValue(referenceId, out object? value))
+            {
+                ThrowHelper.ThrowJsonException_MetadataReferenceNotFound(referenceId);
+            }
+
+            return value;
+        }
+    }
+}
index 8914c9c..80db2c4 100644 (file)
@@ -47,13 +47,18 @@ namespace System.Text.Json
         public bool ReadAhead;
 
         // The bag of preservable references.
-        public DefaultReferenceResolver ReferenceResolver;
+        public ReferenceResolver ReferenceResolver;
 
         /// <summary>
         /// Whether we need to read ahead in the inner read loop.
         /// </summary>
         public bool SupportContinuation;
 
+        /// <summary>
+        /// Whether we can read without the need of saving state for stream and preserve references cases.
+        /// </summary>
+        public bool UseFastPath;
+
         private void AddCurrent()
         {
             if (_previous == null)
@@ -84,12 +89,14 @@ namespace System.Text.Json
             // The initial JsonPropertyInfo will be used to obtain the converter.
             Current.JsonPropertyInfo = jsonClassInfo.PropertyInfoForClassInfo;
 
-            if (options.ReferenceHandling.ShouldReadPreservedReferences())
+            bool preserveReferences = options.ReferenceHandler != null;
+            if (preserveReferences)
             {
-                ReferenceResolver = new DefaultReferenceResolver(writing: false);
+                ReferenceResolver = options.ReferenceHandler!.CreateResolver(writing: false);
             }
 
             SupportContinuation = supportContinuation;
+            UseFastPath = !supportContinuation && !preserveReferences;
         }
 
         public void Push()
@@ -244,8 +251,10 @@ namespace System.Text.Json
                         return;
                     }
 
-                    // Once all elements are read, the exception is not within the array.
-                    if (frame.ObjectState < StackFrameObjectState.ReadElements)
+                    // For continuation scenarios only, before or after all elements are read, the exception is not within the array.
+                    if (frame.ObjectState == StackFrameObjectState.None ||
+                        frame.ObjectState == StackFrameObjectState.CreatedObject ||
+                        frame.ObjectState == StackFrameObjectState.ReadElements)
                     {
                         sb.Append('[');
                         sb.Append(GetCount(enumerable));
@@ -9,22 +9,9 @@ namespace System.Text.Json.Serialization
     /// <summary>
     /// This class defines how the <see cref="JsonSerializer"/> deals with references on serialization and deserialization.
     /// </summary>
-    public sealed class ReferenceHandling
+    public abstract class ReferenceHandler
     {
         /// <summary>
-        /// Serialization does not support objects with cycles and does not preserve duplicate references. Metadata properties will not be written when serializing reference types and will be treated as regular properties on deserialize.
-        /// </summary>
-        /// <remarks>
-        /// * On Serialize:
-        /// Treats duplicate object references as if they were unique and writes all their properties.
-        /// The serializer throws a <see cref="JsonException"/> if an object contains a cycle.
-        /// * On Deserialize:
-        /// 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);
-
-        /// <summary>
         /// Metadata properties will be honored when deserializing JSON objects and arrays into reference types and written when serializing reference types. This is necessary to create round-trippable JSON from objects that contain cycles or duplicate references.
         /// </summary>
         /// <remarks>
@@ -36,7 +23,7 @@ namespace System.Text.Json.Serialization
         /// No metadata properties are written for value types.
         /// * On Deserialize:
         /// The metadata properties within the JSON that are used to preserve duplicated references and cycles will be honored as long as they are well-formed**.
-        /// For JSON objects that don't contain any metadata properties, the deserialization behavior is identical to <see cref="ReferenceHandling.Default"/>.
+        /// For JSON objects that don't contain any metadata properties, the deserialization behavior is identical to <see langword="null"/>.
         /// For value types:
         ///   * The `$id` metadata property is ignored.
         ///   * A <see cref="JsonException"/> is thrown if a `$ref` metadata property is found within the JSON object.
@@ -51,47 +38,18 @@ namespace System.Text.Json.Serialization
         ///   7) The `$values` metadata property is only valid when referring to enumerable types.
         /// If the JSON is not well-formed, a <see cref="JsonException"/> is thrown.
         /// </remarks>
-        public static ReferenceHandling Preserve { get; } = new ReferenceHandling(PreserveReferencesHandling.All);
-
-        private readonly bool _shouldReadPreservedReferences;
-        private readonly bool _shouldWritePreservedReferences;
+        public static ReferenceHandler Preserve { get; } = new PreserveReferenceHandler();
 
         /// <summary>
-        /// Creates a new instance of <see cref="ReferenceHandling"/> using the specified <paramref name="handling"/>
+        /// Returns the <see cref="ReferenceResolver "/> used for each serialization call.
         /// </summary>
-        /// <param name="handling">The specified behavior for write/read preserved references.</param>
-        private ReferenceHandling(PreserveReferencesHandling handling) : this(handling, handling) { }
-
-        // For future, someone may want to define their own custom Handler with different behaviors of PreserveReferenceHandling on Serialize vs Deserialize.
-        private ReferenceHandling(PreserveReferencesHandling preserveHandlingOnSerialize, PreserveReferencesHandling preserveHandlingOnDeserialize)
-        {
-            _shouldReadPreservedReferences = preserveHandlingOnDeserialize == PreserveReferencesHandling.All;
-            _shouldWritePreservedReferences = preserveHandlingOnSerialize == PreserveReferencesHandling.All;
-        }
+        /// <returns>The resolver to use for serialization and deserialization.</returns>
+        public abstract ReferenceResolver CreateResolver();
 
-        internal bool ShouldReadPreservedReferences()
-        {
-            return _shouldReadPreservedReferences;
-        }
-
-        internal bool ShouldWritePreservedReferences()
-        {
-            return _shouldWritePreservedReferences;
-        }
-    }
-
-    /// <summary>
-    /// Defines behaviors to preserve references of JSON complex types.
-    /// </summary>
-    internal enum PreserveReferencesHandling
-    {
-        /// <summary>
-        /// Preserved objects and arrays will not be written/read.
-        /// </summary>
-        None = 0,
         /// <summary>
-        /// Preserved objects and arrays will be written/read.
+        /// Optimization for the resolver used when <see cref="Preserve"/> is set in <see cref="JsonSerializerOptions.ReferenceHandler"/>;
+        /// we pass a flag signaling whether this is called from serialization or deserialization to save one dictionary instantiation.
         /// </summary>
-        All = 1,
+        internal virtual ReferenceResolver CreateResolver(bool writing) => CreateResolver();
     }
 }
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReferenceHandlerOfT.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReferenceHandlerOfT.cs
new file mode 100644 (file)
index 0000000..18fb450
--- /dev/null
@@ -0,0 +1,20 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace System.Text.Json.Serialization
+{
+    /// <summary>
+    /// This class defines how the <see cref="JsonSerializer"/> deals with references on serialization and deserialization.
+    /// </summary>
+    /// <typeparam name="T">The type of the <see cref="ReferenceResolver"/> to create on each serialization or deserialization call.</typeparam>
+    public sealed class ReferenceHandler<T> : ReferenceHandler
+        where T: ReferenceResolver, new()
+    {
+        /// <summary>
+        /// Creates a new <see cref="ReferenceResolver"/> of type <typeparamref name="T"/> used for each serialization call.
+        /// </summary>
+        /// <returns>The new resolver to use for serialization and deserialization.</returns>
+        public override ReferenceResolver CreateResolver() => new T();
+    }
+}
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReferenceResolver.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReferenceResolver.cs
new file mode 100644 (file)
index 0000000..a5a0147
--- /dev/null
@@ -0,0 +1,38 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace System.Text.Json.Serialization
+{
+    /// <summary>
+    /// This class defines how the <see cref="JsonSerializer"/> deals with references on serialization and deserialization.
+    /// Defines the core behavior of preserving references on serialization and deserialization.
+    /// </summary>
+    public abstract class ReferenceResolver
+    {
+        /// <summary>
+        /// Adds an entry to the bag of references using the specified id and value.
+        /// This method gets called when an $id metadata property from a JSON object is read.
+        /// </summary>
+        /// <param name="referenceId">The identifier of the respective JSON object or array.</param>
+        /// <param name="value">The value of the respective CLR reference type object that results from parsing the JSON object.</param>
+        public abstract void AddReference(string referenceId, object value);
+
+        /// <summary>
+        /// Gets the reference identifier of the specified value if exists; otherwise a new id is assigned.
+        /// This method gets called before a CLR object is written so we can decide whether to write $id and enumerate the rest of its properties or $ref and step into the next object.
+        /// </summary>
+        /// <param name="value">The value of the CLR reference type object to get an id for.</param>
+        /// <param name="alreadyExists">When this method returns, <see langword="true"/> if a reference to value already exists; otherwise, <see langword="false"/>.</param>
+        /// <returns>The reference id for the specified object.</returns>
+        public abstract string GetReference(object value, out bool alreadyExists);
+
+        /// <summary>
+        /// Returns the CLR reference type object related to the specified reference id.
+        /// This method gets called when $ref metadata property is read.
+        /// </summary>
+        /// <param name="referenceId">The reference id related to the returned object.</param>
+        /// <returns>The reference type object related to specified reference id.</returns>
+        public abstract object ResolveReference(string referenceId);
+    }
+}
index 16df62f..e2efb64 100644 (file)
@@ -35,7 +35,7 @@ namespace System.Text.Json
         public bool IsContinuation => _continuationCount != 0;
 
         // The bag of preservable references.
-        public DefaultReferenceResolver ReferenceResolver;
+        public ReferenceResolver ReferenceResolver;
 
         /// <summary>
         /// Internal flag to let us know that we need to read ahead in the inner read loop.
@@ -77,9 +77,10 @@ namespace System.Text.Json
                 Current.DeclaredJsonPropertyInfo = jsonClassInfo.PropertyInfoForClassInfo;
             }
 
-            if (options.ReferenceHandling.ShouldWritePreservedReferences())
+            bool preserveReferences = options.ReferenceHandler != null;
+            if (preserveReferences)
             {
-                ReferenceResolver = new DefaultReferenceResolver(writing: true);
+                ReferenceResolver = options.ReferenceHandler!.CreateResolver(writing: true);
             }
 
             SupportContinuation = supportContinuation;
index 0d7bd68..71d3f2a 100644 (file)
@@ -509,11 +509,8 @@ namespace System.Text.Json
 
         [DoesNotReturn]
         [MethodImpl(MethodImplOptions.NoInlining)]
-        public static void ThrowJsonException_MetadataDuplicateIdFound(string id, ref ReadStack state)
+        public static void ThrowJsonException_MetadataDuplicateIdFound(string id)
         {
-            // Set so JsonPath throws exception with $id in it.
-            state.Current.JsonPropertyName = JsonSerializer.s_metadataId.EncodedUtf8Bytes.ToArray();
-
             ThrowJsonException(SR.Format(SR.MetadataDuplicateIdFound, id));
         }
 
index e47b1e9..1f6c8ad 100644 (file)
@@ -68,7 +68,7 @@ namespace System.Text.Json.Serialization.Tests
 
             // Metadata not supported with preserve ref feature on.
 
-            var options = new JsonSerializerOptions { ReferenceHandling = ReferenceHandling.Preserve };
+            var options = new JsonSerializerOptions { ReferenceHandler = ReferenceHandler.Preserve };
 
             NotSupportedException ex = Assert.Throws<NotSupportedException>(
                 () => Serializer.Deserialize<Employee>(json, options));
@@ -102,7 +102,7 @@ namespace System.Text.Json.Serialization.Tests
 
             // Metadata not supported with preserve ref feature on.
 
-            var options = new JsonSerializerOptions { ReferenceHandling = ReferenceHandling.Preserve };
+            var options = new JsonSerializerOptions { ReferenceHandler = ReferenceHandler.Preserve };
 
             NotSupportedException ex = Assert.Throws<NotSupportedException>(() => Serializer.Deserialize<Employee>(json, options));
             string exStr = ex.ToString();
index e86537c..2d5c663 100644 (file)
@@ -467,13 +467,13 @@ namespace System.Text.Json.Serialization.Tests
 
             // Perform serialization with options, after which it will be locked.
             JsonSerializer.Serialize("1", options);
-            Assert.Throws<InvalidOperationException>(() => options.ReferenceHandling = ReferenceHandling.Preserve);
+            Assert.Throws<InvalidOperationException>(() => options.ReferenceHandler = ReferenceHandler.Preserve);
 
             var newOptions = new JsonSerializerOptions(options);
             VerifyOptionsEqual(options, newOptions);
 
             // No exception is thrown on mutating the new options instance because it is "unlocked".
-            newOptions.ReferenceHandling = ReferenceHandling.Preserve;
+            newOptions.ReferenceHandler = ReferenceHandler.Preserve;
         }
 
         [Fact]
@@ -582,9 +582,9 @@ namespace System.Text.Json.Serialization.Tests
                     options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
                     options.DictionaryKeyPolicy = new SimpleSnakeCasePolicy();
                 }
-                else if (propertyType == typeof(ReferenceHandling))
+                else if (propertyType == typeof(ReferenceHandler))
                 {
-                    options.ReferenceHandling = ReferenceHandling.Preserve;
+                    options.ReferenceHandler = ReferenceHandler.Preserve;
                 }
                 else if (propertyType.IsValueType)
                 {
@@ -9,9 +9,9 @@ using Xunit;
 
 namespace System.Text.Json.Serialization.Tests
 {
-    public static partial class ReferenceHandlingTests
+    public static partial class ReferenceHandlerTests
     {
-        private static readonly JsonSerializerOptions s_deserializerOptionsPreserve = new JsonSerializerOptions { ReferenceHandling = ReferenceHandling.Preserve };
+        private static readonly JsonSerializerOptions s_deserializerOptionsPreserve = new JsonSerializerOptions { ReferenceHandler = ReferenceHandler.Preserve };
 
         private class EmployeeWithContacts
         {
@@ -636,7 +636,7 @@ namespace System.Text.Json.Serialization.Tests
 
             var options = new JsonSerializerOptions
             {
-                ReferenceHandling = ReferenceHandling.Preserve,
+                ReferenceHandler = ReferenceHandler.Preserve,
                 Converters = { new ListOfEmployeeConverter() }
             };
             Employee angela = JsonSerializer.Deserialize<Employee>(json, options);
@@ -1199,7 +1199,7 @@ namespace System.Text.Json.Serialization.Tests
         [MemberData(nameof(ReadSuccessCases))]
         public static void ReadTestClassesWithExtensionOption(Type classType, byte[] data)
         {
-            var options = new JsonSerializerOptions { ReferenceHandling = ReferenceHandling.Preserve };
+            var options = new JsonSerializerOptions { ReferenceHandler = ReferenceHandler.Preserve };
             object obj = JsonSerializer.Deserialize(data, classType, options);
             Assert.IsAssignableFrom<ITestClass>(obj);
             ((ITestClass)obj).Verify();
@@ -1276,6 +1276,32 @@ namespace System.Text.Json.Serialization.Tests
             Assert.Contains("'1'", ex.Message);
         }
 
+        class ClassWithTwoListProperties
+        {
+            public List<string> List1 { get; set; }
+            public List<string> List2 { get; set; }
+        }
+
+        [Fact]
+        public static void DuplicatedIdArray()
+        {
+            string json = @"{
+                ""List1"": {
+                        ""$id"": ""1"",
+                        ""$values"": []
+                    },
+                ""List2"": {
+                        ""$id"": ""1"",
+                        ""$values"": []
+                    }
+            }";
+
+            JsonException ex = Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<ClassWithTwoListProperties>(json, s_deserializerOptionsPreserve));
+
+            Assert.Equal("$.List2.$id", ex.Path);
+            Assert.Contains("'1'", ex.Message);
+        }
+
         [Theory]
         [InlineData(@"{""$id"":""A"", ""Manager"":{""$ref"":""A""}}")]
         [InlineData(@"{""$id"":""00000000-0000-0000-0000-000000000000"", ""Manager"":{""$ref"":""00000000-0000-0000-0000-000000000000""}}")]
@@ -9,9 +9,9 @@ using Xunit;
 
 namespace System.Text.Json.Serialization.Tests
 {
-    public static partial class ReferenceHandlingTests
+    public static partial class ReferenceHandlerTests
     {
-        private static readonly JsonSerializerOptions s_serializerOptionsPreserve = new JsonSerializerOptions { ReferenceHandling = ReferenceHandling.Preserve };
+        private static readonly JsonSerializerOptions s_serializerOptionsPreserve = new JsonSerializerOptions { ReferenceHandler = ReferenceHandler.Preserve };
         private static readonly JsonSerializerSettings s_newtonsoftSerializerSettingsPreserve = new JsonSerializerSettings { PreserveReferencesHandling = PreserveReferencesHandling.All, ReferenceLoopHandling = ReferenceLoopHandling.Serialize };
 
         private class Employee
@@ -3,13 +3,15 @@
 // See the LICENSE file in the project root for more information.
 
 using System.Collections.Generic;
+using System.Collections.Immutable;
 using System.Text.Encodings.Web;
+using System.Text.Json.Tests;
 using Newtonsoft.Json;
 using Xunit;
 
 namespace System.Text.Json.Serialization.Tests
 {
-    public static partial class ReferenceHandlingTests
+    public static partial class ReferenceHandlerTests
     {
 
         [Fact]
@@ -21,12 +23,6 @@ namespace System.Text.Json.Serialization.Tests
             JsonException ex = Assert.Throws<JsonException>(() => JsonSerializer.Serialize(a));
         }
 
-        [Fact]
-        public static void ThrowWhenPassingNullToReferenceHandling()
-        {
-            Assert.Throws<ArgumentNullException>(() => new JsonSerializerOptions { ReferenceHandling = null });
-        }
-
         #region Root Object
         [Fact]
         public static void ObjectLoop()
@@ -203,7 +199,7 @@ namespace System.Text.Json.Serialization.Tests
             var optionsWithEncoder = new JsonSerializerOptions
             {
                 Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
-                ReferenceHandling = ReferenceHandling.Preserve
+                ReferenceHandler = ReferenceHandler.Preserve
             };
             json = JsonSerializer.Serialize(obj, optionsWithEncoder);
             Assert.StartsWith("{\"$id\":\"1\",", json);
@@ -393,7 +389,7 @@ namespace System.Text.Json.Serialization.Tests
             var optionsWithEncoder = new JsonSerializerOptions
             {
                 Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
-                ReferenceHandling = ReferenceHandling.Preserve
+                ReferenceHandler = ReferenceHandler.Preserve
             };
             json = JsonSerializer.Serialize(obj, optionsWithEncoder);
             Assert.Equal("{\"$id\":\"1\",\"A\u0467\":1}", json);
@@ -536,5 +532,171 @@ namespace System.Text.Json.Serialization.Tests
             Assert.Same(rootCopy[0], rootCopy[1]);
         }
         #endregion
+
+        #region ReferenceResolver
+        [Fact]
+        public static void CustomReferenceResolver()
+        {
+            string json = @"[
+  {
+    ""$id"": ""0b64ffdf-d155-44ad-9689-58d9adb137f3"",
+    ""Name"": ""John Smith"",
+    ""Spouse"": {
+      ""$id"": ""ae3c399c-058d-431d-91b0-a36c266441b9"",
+      ""Name"": ""Jane Smith"",
+      ""Spouse"": {
+        ""$ref"": ""0b64ffdf-d155-44ad-9689-58d9adb137f3""
+      }
+    }
+  },
+  {
+    ""$ref"": ""ae3c399c-058d-431d-91b0-a36c266441b9""
+  }
+]";
+            var options = new JsonSerializerOptions
+            {
+                WriteIndented = true,
+                ReferenceHandler = new ReferenceHandler<GuidReferenceResolver>()
+            };
+            ImmutableArray<PersonReference> people = JsonSerializer.Deserialize<ImmutableArray<PersonReference>>(json, options);
+
+            Assert.Equal(2, people.Length);
+
+            PersonReference john = people[0];
+            PersonReference jane = people[1];
+
+            Assert.Same(john, jane.Spouse);
+            Assert.Same(jane, john.Spouse);
+
+            Assert.Equal(json, JsonSerializer.Serialize(people, options));
+        }
+
+        [Fact]
+        public static void CustomReferenceResolverPersistent()
+        {
+            var options = new JsonSerializerOptions
+            {
+                WriteIndented = true,
+                ReferenceHandler = new PresistentGuidReferenceHandler
+                {
+                    // Re-use the same resolver instance across all (de)serialiations based on this options instance.
+                    PersistentResolver = new GuidReferenceResolver()
+                }
+            };
+
+            string json =
+@"[
+  {
+    ""$id"": ""0b64ffdf-d155-44ad-9689-58d9adb137f3"",
+    ""Name"": ""John Smith"",
+    ""Spouse"": {
+      ""$id"": ""ae3c399c-058d-431d-91b0-a36c266441b9"",
+      ""Name"": ""Jane Smith"",
+      ""Spouse"": {
+        ""$ref"": ""0b64ffdf-d155-44ad-9689-58d9adb137f3""
+      }
+    }
+  },
+  {
+    ""$ref"": ""ae3c399c-058d-431d-91b0-a36c266441b9""
+  }
+]";
+            ImmutableArray<PersonReference> firstListOfPeople = JsonSerializer.Deserialize<ImmutableArray<PersonReference>>(json, options);
+
+            json =
+@"[
+  {
+    ""$ref"": ""0b64ffdf-d155-44ad-9689-58d9adb137f3""
+  },
+  {
+    ""$ref"": ""ae3c399c-058d-431d-91b0-a36c266441b9""
+  }
+]";
+            ImmutableArray<PersonReference> secondListOfPeople = JsonSerializer.Deserialize<ImmutableArray<PersonReference>>(json, options);
+
+            Assert.Same(firstListOfPeople[0], secondListOfPeople[0]);
+            Assert.Same(firstListOfPeople[1], secondListOfPeople[1]);
+            Assert.Same(firstListOfPeople[0].Spouse, secondListOfPeople[0].Spouse);
+            Assert.Same(firstListOfPeople[1].Spouse, secondListOfPeople[1].Spouse);
+
+            Assert.Equal(json, JsonSerializer.Serialize(secondListOfPeople, options));
+        }
+
+        internal class PresistentGuidReferenceHandler : ReferenceHandler
+        {
+            public ReferenceResolver PersistentResolver { get; set; }
+            public override ReferenceResolver CreateResolver() => PersistentResolver;
+        }
+
+        public class GuidReferenceResolver : ReferenceResolver
+        {
+            private readonly IDictionary<Guid, PersonReference> _people = new Dictionary<Guid, PersonReference>();
+
+            public override object ResolveReference(string referenceId)
+            {
+                Guid id = new Guid(referenceId);
+
+                PersonReference p;
+                _people.TryGetValue(id, out p);
+
+                return p;
+            }
+
+            public override string GetReference(object value, out bool alreadyExists)
+            {
+                PersonReference p = (PersonReference)value;
+
+                alreadyExists = _people.ContainsKey(p.Id);
+                _people[p.Id] = p;
+
+                return p.Id.ToString();
+            }
+
+            public override void AddReference(string reference, object value)
+            {
+                Guid id = new Guid(reference);
+                PersonReference person = (PersonReference)value;
+                person.Id = id;
+                _people[id] = person;
+            }
+        }
+
+        [Fact]
+        public static void TestBadReferenceResolver()
+        {
+            var options = new JsonSerializerOptions { ReferenceHandler = new ReferenceHandler<BadReferenceResolver>() };
+
+            PersonReference angela = new PersonReference { Name = "Angela" };
+            PersonReference bob = new PersonReference { Name = "Bob" };
+
+            angela.Spouse = bob;
+            bob.Spouse = angela;
+
+            // Nothing is preserved, hence MaxDepth will be reached.
+            Assert.Throws<JsonException>(() => JsonSerializer.Serialize(angela, options));
+        }
+
+        class BadReferenceResolver : ReferenceResolver
+        {
+            private int _count;
+            public override void AddReference(string referenceId, object value)
+            {
+                throw new NotImplementedException();
+            }
+
+            public override string GetReference(object value, out bool alreadyExists)
+            {
+                alreadyExists = false;
+                _count++;
+
+                return _count.ToString();
+            }
+
+            public override object ResolveReference(string referenceId)
+            {
+                throw new NotImplementedException();
+            }
+        }
+        #endregion
     }
 }
index b90b50d..0a07521 100644 (file)
@@ -47,7 +47,7 @@ namespace System.Text.Json.Serialization.Tests
 
                 var optionsWithPreservedReferenceHandling = new JsonSerializerOptions(options)
                 {
-                    ReferenceHandling = ReferenceHandling.Preserve
+                    ReferenceHandler = ReferenceHandler.Preserve
                 };
 
                 object obj = GetPopulatedCollection<TElement>(type, thresholdSize);
@@ -98,7 +98,7 @@ namespace System.Text.Json.Serialization.Tests
 
                 // TODO: https://github.com/dotnet/runtime/issues/35611.
                 // Can't control order of dictionary elements when serializing, so reference metadata might not match up.
-                if (!(DictionaryTypes<TElement>().Contains(type) && options.ReferenceHandling == ReferenceHandling.Preserve))
+                if(!(DictionaryTypes<TElement>().Contains(type) && options.ReferenceHandler == ReferenceHandler.Preserve))
                 {
                     JsonTestHelper.AssertJsonEqual(expectedJson, serialized);
                 }
index 2693078..ddfccac 100644 (file)
     <Compile Include="Serialization\PropertyVisibilityTests.cs" />
     <Compile Include="Serialization\ReadScenarioTests.cs" />
     <Compile Include="Serialization\ReadValueTests.cs" />
-    <Compile Include="Serialization\ReferenceHandlingTests.cs" />
-    <Compile Include="Serialization\ReferenceHandlingTests.Deserialize.cs" />
-    <Compile Include="Serialization\ReferenceHandlingTests.Serialize.cs" />
+    <Compile Include="Serialization\ReferenceHandlerTests.cs" />
+    <Compile Include="Serialization\ReferenceHandlerTests.Deserialize.cs" />
+    <Compile Include="Serialization\ReferenceHandlerTests.Serialize.cs" />
     <Compile Include="Serialization\SampleTestData.OrderPayload.cs" />
     <Compile Include="Serialization\SerializationWrapper.cs" />
     <Compile Include="Serialization\SpanTests.cs" />