Add `JsonNode.ParseAsync` public API (#90006)
authorDoctorKrolic <70431552+DoctorKrolic@users.noreply.github.com>
Mon, 7 Aug 2023 10:52:08 +0000 (13:52 +0300)
committerGitHub <noreply@github.com>
Mon, 7 Aug 2023 10:52:08 +0000 (11:52 +0100)
* Add `JsonNode.ParseAsync` public API

* Regenerate ref assemblies

* Change implementation to use unrented document

src/libraries/System.Text.Json/ref/System.Text.Json.cs
src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.Parse.cs
src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonNode.Parse.cs
src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/ParseTests.cs

index 08b0639..251ede7 100644 (file)
@@ -774,7 +774,7 @@ namespace System.Text.Json.Nodes
         public static System.Text.Json.Nodes.JsonNode? Parse(System.ReadOnlySpan<byte> utf8Json, System.Text.Json.Nodes.JsonNodeOptions? nodeOptions = default(System.Text.Json.Nodes.JsonNodeOptions?), System.Text.Json.JsonDocumentOptions documentOptions = default(System.Text.Json.JsonDocumentOptions)) { throw null; }
         public static System.Text.Json.Nodes.JsonNode? Parse([System.Diagnostics.CodeAnalysis.StringSyntaxAttribute("Json")] string json, System.Text.Json.Nodes.JsonNodeOptions? nodeOptions = default(System.Text.Json.Nodes.JsonNodeOptions?), System.Text.Json.JsonDocumentOptions documentOptions = default(System.Text.Json.JsonDocumentOptions)) { throw null; }
         public static System.Text.Json.Nodes.JsonNode? Parse(ref System.Text.Json.Utf8JsonReader reader, System.Text.Json.Nodes.JsonNodeOptions? nodeOptions = default(System.Text.Json.Nodes.JsonNodeOptions?)) { throw null; }
-
+        public static System.Threading.Tasks.Task<System.Text.Json.Nodes.JsonNode?> ParseAsync(System.IO.Stream utf8Json, System.Text.Json.Nodes.JsonNodeOptions? nodeOptions = default(System.Text.Json.Nodes.JsonNodeOptions?), System.Text.Json.JsonDocumentOptions documentOptions = default(System.Text.Json.JsonDocumentOptions), System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
         [System.Diagnostics.CodeAnalysis.RequiresDynamicCodeAttribute("Creating JsonValue instances with non-primitive types requires generating code at runtime.")]
         [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute("Creating JsonValue instances with non-primitive types is not compatible with trimming. It can result in non-primitive types being serialized, which may have their members trimmed.")]
         public void ReplaceWith<T>(T value) { throw null; }
@@ -1217,12 +1217,12 @@ namespace System.Text.Json.Serialization.Metadata
         public static System.Text.Json.Serialization.Metadata.JsonTypeInfo<TCollection> CreateIReadOnlyDictionaryInfo<TCollection, TKey, TValue>(System.Text.Json.JsonSerializerOptions options, System.Text.Json.Serialization.Metadata.JsonCollectionInfoValues<TCollection> collectionInfo) where TCollection : System.Collections.Generic.IReadOnlyDictionary<TKey, TValue> where TKey : notnull { throw null; }
         public static System.Text.Json.Serialization.Metadata.JsonTypeInfo<TCollection> CreateISetInfo<TCollection, TElement>(System.Text.Json.JsonSerializerOptions options, System.Text.Json.Serialization.Metadata.JsonCollectionInfoValues<TCollection> collectionInfo) where TCollection : System.Collections.Generic.ISet<TElement> { throw null; }
         public static System.Text.Json.Serialization.Metadata.JsonTypeInfo<TCollection> CreateListInfo<TCollection, TElement>(System.Text.Json.JsonSerializerOptions options, System.Text.Json.Serialization.Metadata.JsonCollectionInfoValues<TCollection> collectionInfo) where TCollection : System.Collections.Generic.List<TElement> { throw null; }
-        public static System.Text.Json.Serialization.Metadata.JsonTypeInfo<Memory<TElement>> CreateMemoryInfo<TElement>(System.Text.Json.JsonSerializerOptions options, System.Text.Json.Serialization.Metadata.JsonCollectionInfoValues<Memory<TElement>> collectionInfo) { throw null; }
+        public static System.Text.Json.Serialization.Metadata.JsonTypeInfo<System.Memory<TElement>> CreateMemoryInfo<TElement>(System.Text.Json.JsonSerializerOptions options, System.Text.Json.Serialization.Metadata.JsonCollectionInfoValues<System.Memory<TElement>> collectionInfo) { throw null; }
         public static System.Text.Json.Serialization.Metadata.JsonTypeInfo<T> CreateObjectInfo<T>(System.Text.Json.JsonSerializerOptions options, System.Text.Json.Serialization.Metadata.JsonObjectInfoValues<T> objectInfo) where T : notnull { throw null; }
         public static System.Text.Json.Serialization.Metadata.JsonPropertyInfo CreatePropertyInfo<T>(System.Text.Json.JsonSerializerOptions options, System.Text.Json.Serialization.Metadata.JsonPropertyInfoValues<T> propertyInfo) { throw null; }
         public static System.Text.Json.Serialization.Metadata.JsonTypeInfo<TCollection> CreateQueueInfo<TCollection>(System.Text.Json.JsonSerializerOptions options, System.Text.Json.Serialization.Metadata.JsonCollectionInfoValues<TCollection> collectionInfo, System.Action<TCollection, object?> addFunc) where TCollection : System.Collections.IEnumerable { throw null; }
         public static System.Text.Json.Serialization.Metadata.JsonTypeInfo<TCollection> CreateQueueInfo<TCollection, TElement>(System.Text.Json.JsonSerializerOptions options, System.Text.Json.Serialization.Metadata.JsonCollectionInfoValues<TCollection> collectionInfo) where TCollection : System.Collections.Generic.Queue<TElement> { throw null; }
-        public static System.Text.Json.Serialization.Metadata.JsonTypeInfo<ReadOnlyMemory<TElement>> CreateReadOnlyMemoryInfo<TElement>(System.Text.Json.JsonSerializerOptions options, System.Text.Json.Serialization.Metadata.JsonCollectionInfoValues<ReadOnlyMemory<TElement>> collectionInfo) { throw null; }
+        public static System.Text.Json.Serialization.Metadata.JsonTypeInfo<System.ReadOnlyMemory<TElement>> CreateReadOnlyMemoryInfo<TElement>(System.Text.Json.JsonSerializerOptions options, System.Text.Json.Serialization.Metadata.JsonCollectionInfoValues<System.ReadOnlyMemory<TElement>> collectionInfo) { throw null; }
         public static System.Text.Json.Serialization.Metadata.JsonTypeInfo<TCollection> CreateStackInfo<TCollection>(System.Text.Json.JsonSerializerOptions options, System.Text.Json.Serialization.Metadata.JsonCollectionInfoValues<TCollection> collectionInfo, System.Action<TCollection, object?> addFunc) where TCollection : System.Collections.IEnumerable { throw null; }
         public static System.Text.Json.Serialization.Metadata.JsonTypeInfo<TCollection> CreateStackInfo<TCollection, TElement>(System.Text.Json.JsonSerializerOptions options, System.Text.Json.Serialization.Metadata.JsonCollectionInfoValues<TCollection> collectionInfo) where TCollection : System.Collections.Generic.Stack<TElement> { throw null; }
         public static System.Text.Json.Serialization.Metadata.JsonTypeInfo<T> CreateValueInfo<T>(System.Text.Json.JsonSerializerOptions options, System.Text.Json.Serialization.JsonConverter converter) { throw null; }
index f639fcc..e571988 100644 (file)
@@ -228,6 +228,24 @@ namespace System.Text.Json
             }
         }
 
+        internal static async Task<JsonDocument> ParseAsyncCoreUnrented(
+            Stream utf8Json,
+            JsonDocumentOptions options = default,
+            CancellationToken cancellationToken = default)
+        {
+            ArraySegment<byte> drained = await ReadToEndAsync(utf8Json, cancellationToken).ConfigureAwait(false);
+            Debug.Assert(drained.Array != null);
+
+            byte[] owned = new byte[drained.Count];
+            Buffer.BlockCopy(drained.Array, 0, owned, 0, drained.Count);
+
+            // Holds document content, clear it before returning it.
+            drained.AsSpan().Clear();
+            ArrayPool<byte>.Shared.Return(drained.Array);
+
+            return ParseUnrented(owned.AsMemory(), options.GetReaderOptions());
+        }
+
         /// <summary>
         ///   Parses text representing a single JSON value into a JsonDocument.
         /// </summary>
index 750802b..2e3b083 100644 (file)
@@ -4,6 +4,8 @@
 using System.Diagnostics.CodeAnalysis;
 using System.IO;
 using System.Text.Json.Serialization.Converters;
+using System.Threading;
+using System.Threading.Tasks;
 
 namespace System.Text.Json.Nodes
 {
@@ -126,5 +128,34 @@ namespace System.Text.Json.Nodes
             JsonElement element = JsonElement.ParseValue(utf8Json, documentOptions);
             return JsonNodeConverter.Create(element, nodeOptions);
         }
+
+        /// <summary>
+        ///   Parse a <see cref="Stream"/> as UTF-8 encoded data representing a single JSON value into a
+        ///   <see cref="JsonNode"/>.  The Stream will be read to completion.
+        /// </summary>
+        /// <param name="utf8Json">JSON text to parse.</param>
+        /// <param name="nodeOptions">Options to control the node behavior after parsing.</param>
+        /// <param name="documentOptions">Options to control the document behavior during parsing.</param>
+        /// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
+        /// <returns>
+        ///   A <see cref="Task"/> to produce a <see cref="JsonNode"/> representation of the JSON value.
+        /// </returns>
+        /// <exception cref="JsonException">
+        ///   <paramref name="utf8Json"/> does not represent a valid single JSON value.
+        /// </exception>
+        public static async Task<JsonNode?> ParseAsync(
+            Stream utf8Json,
+            JsonNodeOptions? nodeOptions = null,
+            JsonDocumentOptions documentOptions = default,
+            CancellationToken cancellationToken = default)
+        {
+            if (utf8Json is null)
+            {
+                ThrowHelper.ThrowArgumentNullException(nameof(utf8Json));
+            }
+
+            JsonDocument document = await JsonDocument.ParseAsyncCoreUnrented(utf8Json, documentOptions, cancellationToken).ConfigureAwait(false);
+            return JsonNodeConverter.Create(document.RootElement, nodeOptions);
+        }
     }
 }
index 44139de..b8594d8 100644 (file)
@@ -4,6 +4,7 @@
 using System.IO;
 using System.Reflection;
 using System.Text.Json.Serialization.Tests;
+using System.Threading.Tasks;
 using Xunit;
 
 namespace System.Text.Json.Nodes.Tests
@@ -125,15 +126,16 @@ namespace System.Text.Json.Nodes.Tests
         }
 
         [Fact]
-        public static void NullReference_Fail()
+        public static async Task NullReference_Fail()
         {
             Assert.Throws<ArgumentNullException>(() => JsonSerializer.Deserialize<JsonNode>((string)null));
             Assert.Throws<ArgumentNullException>(() => JsonNode.Parse((string)null));
             Assert.Throws<ArgumentNullException>(() => JsonNode.Parse((Stream)null));
+            await Assert.ThrowsAsync<ArgumentNullException>(() => JsonNode.ParseAsync(null));
         }
 
         [Fact]
-        public static void NullLiteral()
+        public static async Task NullLiteral()
         {
             Assert.Null(JsonSerializer.Deserialize<JsonNode>("null"));
             Assert.Null(JsonNode.Parse("null"));
@@ -141,11 +143,13 @@ namespace System.Text.Json.Nodes.Tests
             using (MemoryStream stream = new MemoryStream("null"u8.ToArray()))
             {
                 Assert.Null(JsonNode.Parse(stream));
+                stream.Position = 0;
+                Assert.Null(await JsonNode.ParseAsync(stream));
             }
         }
 
         [Fact]
-        public static void InternalValueFields()
+        public static async Task InternalValueFields()
         {
             // Use reflection to inspect the internal state of the 3 fields that hold values.
             // There is not another way to verify, and using a debug watch causes nodes to be created.
@@ -196,19 +200,51 @@ namespace System.Text.Json.Nodes.Tests
                     Assert.Equal(SimpleTestClass.s_json.StripWhitespace(), actual);
                 }
             }
+
+            using (MemoryStream stream = new MemoryStream(SimpleTestClass.s_data))
+            {
+                // Only JsonElement is present.
+                JsonNode node = await JsonNode.ParseAsync(stream);
+                object jsonDictionary = jsonDictionaryField.GetValue(node);
+                Assert.Null(jsonDictionary); // Value is null until converted from JsonElement.
+                Assert.NotNull(elementField.GetValue(node));
+                Test();
+
+                // Cause the single JsonElement to expand into individual JsonElement nodes.
+                Assert.Equal(1, node.AsObject()["MyInt16"].GetValue<int>());
+                Assert.Null(elementField.GetValue(node));
+
+                jsonDictionary = jsonDictionaryField.GetValue(node);
+                Assert.NotNull(jsonDictionary);
+
+                Assert.NotNull(listField.GetValue(jsonDictionary));
+                Assert.NotNull(dictionaryField.GetValue(jsonDictionary)); // The dictionary threshold was reached.
+                Test();
+
+                void Test()
+                {
+                    string actual = node.ToJsonString();
+
+                    // Replace the escaped "+" sign used with DateTimeOffset.
+                    actual = actual.Replace("\\u002B", "+");
+
+                    Assert.Equal(SimpleTestClass.s_json.StripWhitespace(), actual);
+                }
+            }
         }
 
         [Fact]
-        public static void ReadSimpleObjectWithTrailingTrivia()
+        public static async Task ReadSimpleObjectWithTrailingTrivia()
         {
             byte[] data = Encoding.UTF8.GetBytes(SimpleTestClass.s_json + " /* Multi\r\nLine Comment */\t");
-            using (MemoryStream stream = new MemoryStream(data))
+
+            var options = new JsonDocumentOptions
             {
-                var options = new JsonDocumentOptions
-                {
-                    CommentHandling = JsonCommentHandling.Skip
-                };
+                CommentHandling = JsonCommentHandling.Skip
+            };
 
+            using (MemoryStream stream = new MemoryStream(data))
+            {
                 JsonNode node = JsonNode.Parse(stream, nodeOptions: null, options);
 
                 string actual = node.ToJsonString();
@@ -217,16 +253,33 @@ namespace System.Text.Json.Nodes.Tests
 
                 Assert.Equal(SimpleTestClass.s_json.StripWhitespace(), actual);
             }
+
+            using (MemoryStream stream = new MemoryStream(data))
+            {
+                JsonNode node = await JsonNode.ParseAsync(stream, nodeOptions: null, options);
+
+                string actual = node.ToJsonString();
+                // Replace the escaped "+" sign used with DateTimeOffset.
+                actual = actual.Replace("\\u002B", "+");
+
+                Assert.Equal(SimpleTestClass.s_json.StripWhitespace(), actual);
+            }
         }
 
         [Fact]
-        public static void ReadPrimitives()
+        public static async Task ReadPrimitives()
         {
             using (MemoryStream stream = new MemoryStream(Encoding.UTF8.GetBytes(@"1")))
             {
                 int i = JsonNode.Parse(stream).AsValue().GetValue<int>();
                 Assert.Equal(1, i);
             }
+
+            using (MemoryStream stream = new MemoryStream(Encoding.UTF8.GetBytes(@"1")))
+            {
+                int i = (await JsonNode.ParseAsync(stream)).AsValue().GetValue<int>();
+                Assert.Equal(1, i);
+            }
         }
 
         [Fact]