diff --git a/src/libraries/System.Net.Http.Json/Directory.Build.props b/src/libraries/System.Net.Http.Json/Directory.Build.props new file mode 100644 index 0000000000000..749d7fc1c6b56 --- /dev/null +++ b/src/libraries/System.Net.Http.Json/Directory.Build.props @@ -0,0 +1,7 @@ + + + + Open + true + + diff --git a/src/libraries/System.Net.Http.Json/System.Net.Http.Json.sln b/src/libraries/System.Net.Http.Json/System.Net.Http.Json.sln new file mode 100644 index 0000000000000..9efc47bab2760 --- /dev/null +++ b/src/libraries/System.Net.Http.Json/System.Net.Http.Json.sln @@ -0,0 +1,60 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29806.167 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Net.Http.Json", "src\System.Net.Http.Json.csproj", "{1D422B1D-D7C4-41B9-862D-EB3D98DF37DE}" + ProjectSection(ProjectDependencies) = postProject + {132BF813-FC40-4D39-8B6F-E55D7633F0ED} = {132BF813-FC40-4D39-8B6F-E55D7633F0ED} + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Net.Http.Json", "ref\System.Net.Http.Json.csproj", "{132BF813-FC40-4D39-8B6F-E55D7633F0ED}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{E107E9C1-E893-4E87-987E-04EF0DCEAEFD}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ref", "ref", "{2E666815-2EDB-464B-9DF6-380BF4789AD4}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{1B471D80-205C-4E9C-8D36-601275080642}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Net.Http.Json.Functional.Tests", "tests\FunctionalTests\System.Net.Http.Json.Functional.Tests.csproj", "{DC607A29-7C8D-4933-9AEB-23CF696B2BC6}" + ProjectSection(ProjectDependencies) = postProject + {1D422B1D-D7C4-41B9-862D-EB3D98DF37DE} = {1D422B1D-D7C4-41B9-862D-EB3D98DF37DE} + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Net.Http.Json.Unit.Tests", "tests\UnitTests\System.Net.Http.Json.Unit.Tests.csproj", "{54A8AEE1-BEF2-454A-B1A1-548D73F25F0E}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {1D422B1D-D7C4-41B9-862D-EB3D98DF37DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1D422B1D-D7C4-41B9-862D-EB3D98DF37DE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1D422B1D-D7C4-41B9-862D-EB3D98DF37DE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1D422B1D-D7C4-41B9-862D-EB3D98DF37DE}.Release|Any CPU.Build.0 = Release|Any CPU + {132BF813-FC40-4D39-8B6F-E55D7633F0ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {132BF813-FC40-4D39-8B6F-E55D7633F0ED}.Debug|Any CPU.Build.0 = Debug|Any CPU + {132BF813-FC40-4D39-8B6F-E55D7633F0ED}.Release|Any CPU.ActiveCfg = Release|Any CPU + {132BF813-FC40-4D39-8B6F-E55D7633F0ED}.Release|Any CPU.Build.0 = Release|Any CPU + {DC607A29-7C8D-4933-9AEB-23CF696B2BC6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DC607A29-7C8D-4933-9AEB-23CF696B2BC6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DC607A29-7C8D-4933-9AEB-23CF696B2BC6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DC607A29-7C8D-4933-9AEB-23CF696B2BC6}.Release|Any CPU.Build.0 = Release|Any CPU + {54A8AEE1-BEF2-454A-B1A1-548D73F25F0E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {54A8AEE1-BEF2-454A-B1A1-548D73F25F0E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {54A8AEE1-BEF2-454A-B1A1-548D73F25F0E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {54A8AEE1-BEF2-454A-B1A1-548D73F25F0E}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {1D422B1D-D7C4-41B9-862D-EB3D98DF37DE} = {E107E9C1-E893-4E87-987E-04EF0DCEAEFD} + {132BF813-FC40-4D39-8B6F-E55D7633F0ED} = {2E666815-2EDB-464B-9DF6-380BF4789AD4} + {DC607A29-7C8D-4933-9AEB-23CF696B2BC6} = {1B471D80-205C-4E9C-8D36-601275080642} + {54A8AEE1-BEF2-454A-B1A1-548D73F25F0E} = {1B471D80-205C-4E9C-8D36-601275080642} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {5100F629-0FAB-4C6F-9A54-95AE9565EE0D} + EndGlobalSection +EndGlobal diff --git a/src/libraries/System.Net.Http.Json/pkg/System.Net.Http.Json.pkgproj b/src/libraries/System.Net.Http.Json/pkg/System.Net.Http.Json.pkgproj new file mode 100644 index 0000000000000..eba4cc39e9f90 --- /dev/null +++ b/src/libraries/System.Net.Http.Json/pkg/System.Net.Http.Json.pkgproj @@ -0,0 +1,10 @@ + + + + + net461;netcoreapp2.0;uap10.0.16299;$(AllXamarinFrameworks) + + + + + \ No newline at end of file diff --git a/src/libraries/System.Net.Http.Json/ref/System.Net.Http.Json.cs b/src/libraries/System.Net.Http.Json/ref/System.Net.Http.Json.cs new file mode 100644 index 0000000000000..bdb7399a2f6dc --- /dev/null +++ b/src/libraries/System.Net.Http.Json/ref/System.Net.Http.Json.cs @@ -0,0 +1,44 @@ +// 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. +// ------------------------------------------------------------------------------ +// Changes to this file must follow the https://aka.ms/api-review process. +// ------------------------------------------------------------------------------ + +namespace System.Net.Http.Json +{ + public static partial class HttpClientJsonExtensions + { + public static System.Threading.Tasks.Task GetFromJsonAsync(this System.Net.Http.HttpClient client, string? requestUri, System.Type type, System.Text.Json.JsonSerializerOptions? options, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public static System.Threading.Tasks.Task GetFromJsonAsync(this System.Net.Http.HttpClient client, string? requestUri, System.Type type, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public static System.Threading.Tasks.Task GetFromJsonAsync(this System.Net.Http.HttpClient client, System.Uri? requestUri, System.Type type, System.Text.Json.JsonSerializerOptions? options, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public static System.Threading.Tasks.Task GetFromJsonAsync(this System.Net.Http.HttpClient client, System.Uri? requestUri, System.Type type, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public static System.Threading.Tasks.Task GetFromJsonAsync(this System.Net.Http.HttpClient client, string? requestUri, System.Text.Json.JsonSerializerOptions? options, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public static System.Threading.Tasks.Task GetFromJsonAsync(this System.Net.Http.HttpClient client, string? requestUri, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public static System.Threading.Tasks.Task GetFromJsonAsync(this System.Net.Http.HttpClient client, System.Uri? requestUri, System.Text.Json.JsonSerializerOptions? options, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public static System.Threading.Tasks.Task GetFromJsonAsync(this System.Net.Http.HttpClient client, System.Uri? requestUri, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public static System.Threading.Tasks.Task PostAsJsonAsync(this System.Net.Http.HttpClient client, string? requestUri, TValue value, System.Text.Json.JsonSerializerOptions? options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public static System.Threading.Tasks.Task PostAsJsonAsync(this System.Net.Http.HttpClient client, string? requestUri, TValue value, System.Threading.CancellationToken cancellationToken) { throw null; } + public static System.Threading.Tasks.Task PostAsJsonAsync(this System.Net.Http.HttpClient client, System.Uri? requestUri, TValue value, System.Text.Json.JsonSerializerOptions? options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public static System.Threading.Tasks.Task PostAsJsonAsync(this System.Net.Http.HttpClient client, System.Uri? requestUri, TValue value, System.Threading.CancellationToken cancellationToken) { throw null; } + public static System.Threading.Tasks.Task PutAsJsonAsync(this System.Net.Http.HttpClient client, string? requestUri, TValue value, System.Text.Json.JsonSerializerOptions? options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public static System.Threading.Tasks.Task PutAsJsonAsync(this System.Net.Http.HttpClient client, string? requestUri, TValue value, System.Threading.CancellationToken cancellationToken) { throw null; } + public static System.Threading.Tasks.Task PutAsJsonAsync(this System.Net.Http.HttpClient client, System.Uri? requestUri, TValue value, System.Text.Json.JsonSerializerOptions? options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public static System.Threading.Tasks.Task PutAsJsonAsync(this System.Net.Http.HttpClient client, System.Uri? requestUri, TValue value, System.Threading.CancellationToken cancellationToken) { throw null; } + } + public static partial class HttpContentJsonExtensions + { + public static System.Threading.Tasks.Task ReadFromJsonAsync(this System.Net.Http.HttpContent content, System.Type type, System.Text.Json.JsonSerializerOptions? options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public static System.Threading.Tasks.Task ReadFromJsonAsync(this System.Net.Http.HttpContent content, System.Text.Json.JsonSerializerOptions? options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + } + public sealed partial class JsonContent : System.Net.Http.HttpContent + { + internal JsonContent() { } + public System.Type ObjectType { get { throw null; } } + public object? Value { get { throw null; } } + public static System.Net.Http.Json.JsonContent Create(object? inputValue, System.Type inputType, System.Net.Http.Headers.MediaTypeHeaderValue? mediaType = null, System.Text.Json.JsonSerializerOptions? options = null) { throw null; } + public static System.Net.Http.Json.JsonContent Create(T inputValue, System.Net.Http.Headers.MediaTypeHeaderValue? mediaType = null, System.Text.Json.JsonSerializerOptions? options = null) { throw null; } + protected override System.Threading.Tasks.Task SerializeToStreamAsync(System.IO.Stream stream, System.Net.TransportContext? context) { throw null; } + protected override bool TryComputeLength(out long length) { throw null; } + } +} diff --git a/src/libraries/System.Net.Http.Json/ref/System.Net.Http.Json.csproj b/src/libraries/System.Net.Http.Json/ref/System.Net.Http.Json.csproj new file mode 100644 index 0000000000000..b59dd09ae596c --- /dev/null +++ b/src/libraries/System.Net.Http.Json/ref/System.Net.Http.Json.csproj @@ -0,0 +1,18 @@ + + + netstandard2.0;$(NetCoreAppCurrent) + enable + + + + + + + + + + + + + + diff --git a/src/libraries/System.Net.Http.Json/ref/System.Net.Http.Json.netcoreapp.cs b/src/libraries/System.Net.Http.Json/ref/System.Net.Http.Json.netcoreapp.cs new file mode 100644 index 0000000000000..49131cd7c3e38 --- /dev/null +++ b/src/libraries/System.Net.Http.Json/ref/System.Net.Http.Json.netcoreapp.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace System.Net.Http.Json +{ + public sealed partial class JsonContent : System.Net.Http.HttpContent + { + protected override System.Threading.Tasks.Task SerializeToStreamAsync(System.IO.Stream stream, System.Net.TransportContext? context, System.Threading.CancellationToken cancellationToken) { throw null; } + } +} diff --git a/src/libraries/System.Net.Http.Json/src/Resources/Strings.resx b/src/libraries/System.Net.Http.Json/src/Resources/Strings.resx new file mode 100644 index 0000000000000..1722e93d5009d --- /dev/null +++ b/src/libraries/System.Net.Http.Json/src/Resources/Strings.resx @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Offset and length were out of bounds for the array or count is greater than the number of elements from index to the end of the source collection. + + + The character set provided in ContentType is invalid. + + + The character set provided in ContentType is not supported. + + + The provided ContentType is not supported; the supported types are 'application/json' and the structured syntax suffix 'application/+json'. + + + The specified type {0} must derive from the specific value's type {1}. + + diff --git a/src/libraries/System.Net.Http.Json/src/System.Net.Http.Json.csproj b/src/libraries/System.Net.Http.Json/src/System.Net.Http.Json.csproj new file mode 100644 index 0000000000000..04dd510bb1c79 --- /dev/null +++ b/src/libraries/System.Net.Http.Json/src/System.Net.Http.Json.csproj @@ -0,0 +1,31 @@ + + + netstandard2.0;$(NetCoreAppCurrent) + enable + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/libraries/System.Net.Http.Json/src/System/ArraySegmentExtensions.netstandard.cs b/src/libraries/System.Net.Http.Json/src/System/ArraySegmentExtensions.netstandard.cs new file mode 100644 index 0000000000000..e5f89eeff710c --- /dev/null +++ b/src/libraries/System.Net.Http.Json/src/System/ArraySegmentExtensions.netstandard.cs @@ -0,0 +1,24 @@ +// 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 +{ + internal static class ArraySegmentExtensions + { + public static ArraySegment Slice(this ArraySegment arraySegment, int index) + { + return new ArraySegment(arraySegment.Array, arraySegment.Offset + index, arraySegment.Count - index); + } + + public static ArraySegment Slice(this ArraySegment arraySegment, int index, int count) + { + return new ArraySegment(arraySegment.Array, arraySegment.Offset + index, count); + } + + public static void CopyTo(this ArraySegment source, ArraySegment destination) + { + Array.Copy(source.Array, source.Offset, destination.Array, destination.Offset, source.Count); + } + } +} diff --git a/src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Get.cs b/src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Get.cs new file mode 100644 index 0000000000000..1f4d2494e7703 --- /dev/null +++ b/src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Get.cs @@ -0,0 +1,96 @@ +// 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.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace System.Net.Http.Json +{ + /// + /// Contains the extensions methods for using JSON as the content-type in HttpClient. + /// + public static partial class HttpClientJsonExtensions + { + public static Task GetFromJsonAsync(this HttpClient client, string? requestUri, Type type, JsonSerializerOptions? options, CancellationToken cancellationToken = default) + { + if (client == null) + { + throw new ArgumentNullException(nameof(client)); + } + + Task taskResponse = client.GetAsync(requestUri, HttpCompletionOption.ResponseHeadersRead, cancellationToken); + return GetFromJsonAsyncCore(taskResponse, type, options, cancellationToken); + } + + public static Task GetFromJsonAsync(this HttpClient client, Uri? requestUri, Type type, JsonSerializerOptions? options, CancellationToken cancellationToken = default) + { + if (client == null) + { + throw new ArgumentNullException(nameof(client)); + } + + Task taskResponse = client.GetAsync(requestUri, HttpCompletionOption.ResponseHeadersRead, cancellationToken); + return GetFromJsonAsyncCore(taskResponse, type, options, cancellationToken); + } + + public static Task GetFromJsonAsync(this HttpClient client, string? requestUri, JsonSerializerOptions? options, CancellationToken cancellationToken = default) + { + if (client == null) + { + throw new ArgumentNullException(nameof(client)); + } + + Task taskResponse = client.GetAsync(requestUri, HttpCompletionOption.ResponseHeadersRead, cancellationToken); + return GetFromJsonAsyncCore(taskResponse, options, cancellationToken); + } + + public static Task GetFromJsonAsync(this HttpClient client, Uri? requestUri, JsonSerializerOptions? options, CancellationToken cancellationToken = default) + { + if (client == null) + { + throw new ArgumentNullException(nameof(client)); + } + + Task taskResponse = client.GetAsync(requestUri, HttpCompletionOption.ResponseHeadersRead, cancellationToken); + return GetFromJsonAsyncCore(taskResponse, options, cancellationToken); + } + + public static Task GetFromJsonAsync(this HttpClient client, string? requestUri, Type type, CancellationToken cancellationToken = default) + => client.GetFromJsonAsync(requestUri, type, options: null, cancellationToken); + + public static Task GetFromJsonAsync(this HttpClient client, Uri? requestUri, Type type, CancellationToken cancellationToken = default) + => client.GetFromJsonAsync(requestUri, type, options: null, cancellationToken); + + public static Task GetFromJsonAsync(this HttpClient client, string? requestUri, CancellationToken cancellationToken = default) + => client.GetFromJsonAsync(requestUri, options: null, cancellationToken); + + public static Task GetFromJsonAsync(this HttpClient client, Uri? requestUri, CancellationToken cancellationToken = default) + => client.GetFromJsonAsync(requestUri, options: null, cancellationToken); + + private static async Task GetFromJsonAsyncCore(Task taskResponse, Type type, JsonSerializerOptions? options, CancellationToken cancellationToken) + { + using (HttpResponseMessage response = await taskResponse.ConfigureAwait(false)) + { + response.EnsureSuccessStatusCode(); + // Nullable forgiving reason: + // GetAsync will usually return Content as not-null. + // If Content happens to be null, the extension will throw. + return await response.Content!.ReadFromJsonAsync(type, options, cancellationToken).ConfigureAwait(false); + } + } + + private static async Task GetFromJsonAsyncCore(Task taskResponse, JsonSerializerOptions? options, CancellationToken cancellationToken) + { + using (HttpResponseMessage response = await taskResponse.ConfigureAwait(false)) + { + response.EnsureSuccessStatusCode(); + // Nullable forgiving reason: + // GetAsync will usually return Content as not-null. + // If Content happens to be null, the extension will throw. + return await response.Content!.ReadFromJsonAsync(options, cancellationToken).ConfigureAwait(false); + } + } + } +} diff --git a/src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Post.cs b/src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Post.cs new file mode 100644 index 0000000000000..1ae586e864e32 --- /dev/null +++ b/src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Post.cs @@ -0,0 +1,41 @@ +// 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.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace System.Net.Http.Json +{ + public static partial class HttpClientJsonExtensions + { + public static Task PostAsJsonAsync(this HttpClient client, string? requestUri, TValue value, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) + { + if (client == null) + { + throw new ArgumentNullException(nameof(client)); + } + + JsonContent content = JsonContent.Create(value, mediaType: null, options); + return client.PostAsync(requestUri, content, cancellationToken); + } + + public static Task PostAsJsonAsync(this HttpClient client, Uri? requestUri, TValue value, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) + { + if (client == null) + { + throw new ArgumentNullException(nameof(client)); + } + + JsonContent content = JsonContent.Create(value, mediaType: null, options); + return client.PostAsync(requestUri, content, cancellationToken); + } + + public static Task PostAsJsonAsync(this HttpClient client, string? requestUri, TValue value, CancellationToken cancellationToken) + => client.PostAsJsonAsync(requestUri, value, options: null, cancellationToken); + + public static Task PostAsJsonAsync(this HttpClient client, Uri? requestUri, TValue value, CancellationToken cancellationToken) + => client.PostAsJsonAsync(requestUri, value, options: null, cancellationToken); + } +} diff --git a/src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Put.cs b/src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Put.cs new file mode 100644 index 0000000000000..cddd46443367c --- /dev/null +++ b/src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Put.cs @@ -0,0 +1,41 @@ +// 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.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace System.Net.Http.Json +{ + public static partial class HttpClientJsonExtensions + { + public static Task PutAsJsonAsync(this HttpClient client, string? requestUri, TValue value, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) + { + if (client == null) + { + throw new ArgumentNullException(nameof(client)); + } + + JsonContent content = JsonContent.Create(value, mediaType: null, options); + return client.PutAsync(requestUri, content, cancellationToken); + } + + public static Task PutAsJsonAsync(this HttpClient client, Uri? requestUri, TValue value, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) + { + if (client == null) + { + throw new ArgumentNullException(nameof(client)); + } + + JsonContent content = JsonContent.Create(value, mediaType: null, options); + return client.PutAsync(requestUri, content, cancellationToken); + } + + public static Task PutAsJsonAsync(this HttpClient client, string? requestUri, TValue value, CancellationToken cancellationToken) + => client.PutAsJsonAsync(requestUri, value, options: null, cancellationToken); + + public static Task PutAsJsonAsync(this HttpClient client, Uri? requestUri, TValue value, CancellationToken cancellationToken) + => client.PutAsJsonAsync(requestUri, value, options: null, cancellationToken); + } +} diff --git a/src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/HttpContentJsonExtensions.cs b/src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/HttpContentJsonExtensions.cs new file mode 100644 index 0000000000000..6d1d309760f3a --- /dev/null +++ b/src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/HttpContentJsonExtensions.cs @@ -0,0 +1,114 @@ +// 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.Diagnostics; +using System.IO; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace System.Net.Http.Json +{ + public static class HttpContentJsonExtensions + { + public static Task ReadFromJsonAsync(this HttpContent content, Type type, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) + { + ValidateContent(content); + Debug.Assert(content.Headers.ContentType != null); + Encoding? sourceEncoding = JsonContent.GetEncoding(content.Headers.ContentType.CharSet); + + return ReadFromJsonAsyncCore(content, type, sourceEncoding, options ?? JsonContent.DefaultSerializerOptions, cancellationToken); + } + + public static Task ReadFromJsonAsync(this HttpContent content, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) + { + ValidateContent(content); + Debug.Assert(content.Headers.ContentType != null); + Encoding? sourceEncoding = JsonContent.GetEncoding(content.Headers.ContentType.CharSet); + + return ReadFromJsonAsyncCore(content, sourceEncoding, options ?? JsonContent.DefaultSerializerOptions, cancellationToken); + } + + private static async Task ReadFromJsonAsyncCore(HttpContent content, Type type, Encoding? sourceEncoding, JsonSerializerOptions? options, CancellationToken cancellationToken) + { + Stream contentStream = await content.ReadAsStreamAsync().ConfigureAwait(false); + + // Wrap content stream into a transcoding stream that buffers the data transcoded from the sourceEncoding to utf-8. + if (sourceEncoding != null && sourceEncoding != Encoding.UTF8) + { + contentStream = new TranscodingReadStream(contentStream, sourceEncoding); + } + + using (contentStream) + { + return await JsonSerializer.DeserializeAsync(contentStream, type, options, cancellationToken).ConfigureAwait(false); + } + } + + private static async Task ReadFromJsonAsyncCore(HttpContent content, Encoding? sourceEncoding, JsonSerializerOptions? options, CancellationToken cancellationToken) + { + Stream contentStream = await content.ReadAsStreamAsync().ConfigureAwait(false); + + // Wrap content stream into a transcoding stream that buffers the data transcoded from the sourceEncoding to utf-8. + if (sourceEncoding != null && sourceEncoding != Encoding.UTF8) + { + contentStream = new TranscodingReadStream(contentStream, sourceEncoding); + } + + using (contentStream) + { + return await JsonSerializer.DeserializeAsync(contentStream, options, cancellationToken).ConfigureAwait(false); + } + } + + private static void ValidateContent(HttpContent content) + { + if (content == null) + { + throw new ArgumentNullException(nameof(content)); + } + + string? mediaType = content.Headers.ContentType?.MediaType; + + if (mediaType == null || + !mediaType.Equals(JsonContent.JsonMediaType, StringComparison.OrdinalIgnoreCase) && + !IsValidStructuredSyntaxJsonSuffix(mediaType.AsSpan())) + { + throw new NotSupportedException(SR.ContentTypeNotSupported); + } + } + + private static bool IsValidStructuredSyntaxJsonSuffix(ReadOnlySpan mediaType) + { + int index = 0; + int typeLength = mediaType.IndexOf('/'); + + ReadOnlySpan type = mediaType.Slice(index, typeLength); + if (typeLength < 0 || + type.CompareTo(JsonContent.JsonType.AsSpan(), StringComparison.OrdinalIgnoreCase) != 0) + { + return false; + } + + index += typeLength + 1; + int suffixStart = mediaType.Slice(index).IndexOf('+'); + + // Empty prefix subtype ("application/+json") not allowed. + if (suffixStart <= 0) + { + return false; + } + + index += suffixStart + 1; + ReadOnlySpan suffix = mediaType.Slice(index); + if (suffix.CompareTo(JsonContent.JsonSubtype.AsSpan(), StringComparison.OrdinalIgnoreCase) != 0) + { + return false; + } + + return true; + } + } +} diff --git a/src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/JsonContent.cs b/src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/JsonContent.cs new file mode 100644 index 0000000000000..a84d375b137c1 --- /dev/null +++ b/src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/JsonContent.cs @@ -0,0 +1,114 @@ +// 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.Diagnostics; +using System.IO; +using System.Net.Http.Headers; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace System.Net.Http.Json +{ + public sealed partial class JsonContent : HttpContent + { + internal const string JsonMediaType = "application/json"; + internal const string JsonType = "application"; + internal const string JsonSubtype = "json"; + private static MediaTypeHeaderValue DefaultMediaType + => new MediaTypeHeaderValue(JsonMediaType) { CharSet = "utf-8" }; + + internal static JsonSerializerOptions DefaultSerializerOptions + => new JsonSerializerOptions { PropertyNameCaseInsensitive = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; + + private readonly JsonSerializerOptions? _jsonSerializerOptions; + public Type ObjectType { get; } + public object? Value { get; } + + private JsonContent(object? inputValue, Type inputType, MediaTypeHeaderValue? mediaType, JsonSerializerOptions? options) + { + if (inputType == null) + { + throw new ArgumentNullException(nameof(inputType)); + } + + if (inputValue != null && !inputType.IsAssignableFrom(inputValue.GetType())) + { + throw new ArgumentException(SR.Format(SR.SerializeWrongType, inputType, inputValue.GetType())); + } + + Value = inputValue; + ObjectType = inputType; + Headers.ContentType = mediaType ?? DefaultMediaType; + _jsonSerializerOptions = options ?? DefaultSerializerOptions; + } + + public static JsonContent Create(T inputValue, MediaTypeHeaderValue? mediaType = null, JsonSerializerOptions? options = null) + => Create(inputValue, typeof(T), mediaType, options); + + public static JsonContent Create(object? inputValue, Type inputType, MediaTypeHeaderValue? mediaType = null, JsonSerializerOptions? options = null) + => new JsonContent(inputValue, inputType, mediaType, options); + + protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context) + => SerializeToStreamAsyncCore(stream, CancellationToken.None); + + protected override bool TryComputeLength(out long length) + { + length = 0; + return false; + } + + private async Task SerializeToStreamAsyncCore(Stream targetStream, CancellationToken cancellationToken) + { + Encoding? targetEncoding = GetEncoding(Headers.ContentType?.CharSet); + + // Wrap provided stream into a transcoding stream that buffers the data transcoded from utf-8 to the targetEncoding. + if (targetEncoding != null && targetEncoding != Encoding.UTF8) + { + using (TranscodingWriteStream transcodingStream = new TranscodingWriteStream(targetStream, targetEncoding)) + { + await JsonSerializer.SerializeAsync(transcodingStream, Value, ObjectType, _jsonSerializerOptions, cancellationToken).ConfigureAwait(false); + // The transcoding streams use Encoders and Decoders that have internal buffers. We need to flush these + // when there is no more data to be written. Stream.FlushAsync isn't suitable since it's + // acceptable to Flush a Stream (multiple times) prior to completion. + await transcodingStream.FinalWriteAsync(cancellationToken).ConfigureAwait(false); + } + } + else + { + await JsonSerializer.SerializeAsync(targetStream, Value, ObjectType, _jsonSerializerOptions, cancellationToken).ConfigureAwait(false); + } + } + + internal static Encoding? GetEncoding(string? charset) + { + Encoding? encoding = null; + + if (charset != null) + { + try + { + // Remove at most a single set of quotes. + if (charset.Length > 2 && charset[0] == '\"' && charset[charset.Length - 1] == '\"') + { + encoding = Encoding.GetEncoding(charset.Substring(1, charset.Length - 2)); + } + else + { + encoding = Encoding.GetEncoding(charset); + } + } + catch (ArgumentException e) + { + throw new InvalidOperationException(SR.CharSetInvalid, e); + } + + Debug.Assert(encoding != null); + } + + return encoding; + } + } +} diff --git a/src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/JsonContent.netcoreapp.cs b/src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/JsonContent.netcoreapp.cs new file mode 100644 index 0000000000000..a669b915a0aa8 --- /dev/null +++ b/src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/JsonContent.netcoreapp.cs @@ -0,0 +1,16 @@ +// 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.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace System.Net.Http.Json +{ + public sealed partial class JsonContent + { + protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context, CancellationToken cancellationToken) + => SerializeToStreamAsyncCore(stream, cancellationToken); + } +} diff --git a/src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/TranscodingReadStream.cs b/src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/TranscodingReadStream.cs new file mode 100644 index 0000000000000..e78546ad852f2 --- /dev/null +++ b/src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/TranscodingReadStream.cs @@ -0,0 +1,228 @@ +// 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. + +// Taken from https://github.com/dotnet/aspnetcore/blob/master/src/Mvc/Mvc.Core/src/Formatters/TranscodingReadStream.cs + +using System.Buffers; +using System.Diagnostics; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace System.Net.Http.Json +{ + internal sealed class TranscodingReadStream : Stream + { + private static readonly int OverflowBufferSize = Encoding.UTF8.GetMaxByteCount(1); // The most number of bytes used to represent a single UTF char + + // Default size of the buffer that will hold the bytes from the underlying stream. + // Those bytes are expected to be encoded in the sourceEncoding passed into the .ctor. + internal const int MaxByteBufferSize = 4096; + + private readonly Stream _stream; + private readonly Decoder _decoder; + private readonly Encoder _encoder; + + private ArraySegment _byteBuffer; + private ArraySegment _charBuffer; + private ArraySegment _overflowBuffer; + private bool _disposed; + + public TranscodingReadStream(Stream input, Encoding sourceEncoding) + { + _stream = input; + + // The "count" in the buffer is the size of any content from a previous read. + // Initialize them to 0 since nothing has been read so far. + _byteBuffer = new ArraySegment(ArrayPool.Shared.Rent(MaxByteBufferSize), 0, count: 0); + + // Attempt to allocate a char buffer than can tolerate the worst-case scenario for this + // encoding. This would allow the byte -> char conversion to complete in a single call. + // The conversion process is tolerant of char buffer that is not large enough to convert all the bytes at once. + int maxCharBufferSize = sourceEncoding.GetMaxCharCount(MaxByteBufferSize); + _charBuffer = new ArraySegment(ArrayPool.Shared.Rent(maxCharBufferSize), 0, count: 0); + + _overflowBuffer = new ArraySegment(ArrayPool.Shared.Rent(OverflowBufferSize), 0, count: 0); + + _decoder = sourceEncoding.GetDecoder(); + _encoder = Encoding.UTF8.GetEncoder(); + } + + public override bool CanRead => true; + public override bool CanSeek => false; + public override bool CanWrite => false; + public override long Length => throw new NotSupportedException(); + + public override long Position + { + get => throw new NotSupportedException(); + set => throw new NotSupportedException(); + } + + internal int ByteBufferCount => _byteBuffer.Count; + internal int CharBufferCount => _charBuffer.Count; + internal int OverflowCount => _overflowBuffer.Count; + + public override int Read(byte[] buffer, int offset, int count) + => throw new NotSupportedException(); + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + + if (offset < 0) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + + if (buffer.Length - offset < count) + { + throw new ArgumentException(SR.Argument_InvalidOffLen); + } + + var readBuffer = new ArraySegment(buffer, offset, count); + return ReadAsyncCore(readBuffer, cancellationToken); + } + + private async Task ReadAsyncCore(ArraySegment readBuffer, CancellationToken cancellationToken) + { + if (readBuffer.Count == 0) + { + return 0; + } + + if (_overflowBuffer.Count > 0) + { + int bytesToCopy = Math.Min(readBuffer.Count, _overflowBuffer.Count); + _overflowBuffer.Slice(0, bytesToCopy).CopyTo(readBuffer); + + _overflowBuffer = _overflowBuffer.Slice(bytesToCopy); + + // If we have any overflow bytes, avoid complicating the remainder of the code, by returning as + // soon as we copy any content. + return bytesToCopy; + } + + bool shouldFlushEncoder = false; + // Only read more content from the input stream if we have exhausted all the buffered chars. + if (_charBuffer.Count == 0) + { + int bytesRead = await ReadInputChars(cancellationToken).ConfigureAwait(false); + shouldFlushEncoder = bytesRead == 0 && _byteBuffer.Count == 0; + } + + bool completed = false; + int charsRead = default; + int bytesWritten = default; + // Since Convert() could fail if the destination buffer cannot fit at least one encoded char. + // If the destination buffer is smaller than GetMaxByteCount(1), we avoid encoding to the destination and we use the overflow buffer instead. + if (readBuffer.Count > OverflowBufferSize || _charBuffer.Count == 0) + { + _encoder.Convert(_charBuffer.Array!, _charBuffer.Offset, _charBuffer.Count, readBuffer.Array!, readBuffer.Offset, readBuffer.Count, + flush: shouldFlushEncoder, out charsRead, out bytesWritten, out completed); + } + + _charBuffer = _charBuffer.Slice(charsRead); + + if (completed || bytesWritten > 0) + { + return bytesWritten; + } + + _encoder.Convert(_charBuffer.Array!, _charBuffer.Offset, _charBuffer.Count, _overflowBuffer.Array!, byteIndex: 0, _overflowBuffer.Array!.Length, + flush: shouldFlushEncoder, out int overFlowChars, out int overflowBytes, out completed); + + Debug.Assert(overflowBytes > 0 && overFlowChars > 0, "We expect writes to the overflow buffer to always succeed since it is large enough to accommodate at least one char."); + + _charBuffer = _charBuffer.Slice(overFlowChars); + + // readBuffer: [ 0, 0, ], overflowBuffer: [ 7, 13, 34, ] + // Fill up the readBuffer to capacity, so the result looks like so: + // readBuffer: [ 7, 13 ], overflowBuffer: [ 34 ] + Debug.Assert(readBuffer.Count < overflowBytes); + _overflowBuffer.Array.AsSpan(0, readBuffer.Count).CopyTo(readBuffer); + + Debug.Assert(_overflowBuffer.Array != null); + + _overflowBuffer = new ArraySegment(_overflowBuffer.Array, readBuffer.Count, overflowBytes - readBuffer.Count); + + Debug.Assert(_overflowBuffer.Count > 0); + + return readBuffer.Count; + } + + private async Task ReadInputChars(CancellationToken cancellationToken) + { + // If we had left-over bytes from a previous read, move it to the start of the buffer and read content into + // the segment that follows. + Debug.Assert(_byteBuffer.Array != null); + Buffer.BlockCopy(_byteBuffer.Array, _byteBuffer.Offset, _byteBuffer.Array, 0, _byteBuffer.Count); + + int offset = _byteBuffer.Count; + int count = _byteBuffer.Array.Length - _byteBuffer.Count; + + int bytesRead = await _stream.ReadAsync(_byteBuffer.Array, offset, count, cancellationToken).ConfigureAwait(false); + + _byteBuffer = new ArraySegment(_byteBuffer.Array, 0, offset + bytesRead); + + Debug.Assert(_byteBuffer.Array != null); + Debug.Assert(_charBuffer.Array != null); + Debug.Assert(_charBuffer.Count == 0, "We should only expect to read more input chars once all buffered content is read"); + + _decoder.Convert(_byteBuffer.Array, _byteBuffer.Offset, _byteBuffer.Count, _charBuffer.Array, charIndex: 0, _charBuffer.Array.Length, + flush: bytesRead == 0, out int bytesUsed, out int charsUsed, out _); + + // We flush only when the stream is exhausted and there are no pending bytes in the buffer. + Debug.Assert(bytesRead != 0 || _byteBuffer.Count - bytesUsed == 0); + + _byteBuffer = _byteBuffer.Slice(bytesUsed); + _charBuffer = new ArraySegment(_charBuffer.Array, 0, charsUsed); + + return bytesRead; + } + + public override void Flush() + => throw new NotSupportedException(); + + public override long Seek(long offset, SeekOrigin origin) + => throw new NotSupportedException(); + + public override void SetLength(long value) + => throw new NotSupportedException(); + + public override void Write(byte[] buffer, int offset, int count) + => throw new NotSupportedException(); + + protected override void Dispose(bool disposing) + { + if (!_disposed) + { + _disposed = true; + + Debug.Assert(_charBuffer.Array != null); + ArrayPool.Shared.Return(_charBuffer.Array); + _charBuffer = default; + + Debug.Assert(_byteBuffer.Array != null); + ArrayPool.Shared.Return(_byteBuffer.Array); + _byteBuffer = default; + + Debug.Assert(_overflowBuffer.Array != null); + ArrayPool.Shared.Return(_overflowBuffer.Array); + _overflowBuffer = default; + + _stream.Dispose(); + } + } + } +} diff --git a/src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/TranscodingWriteStream.cs b/src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/TranscodingWriteStream.cs new file mode 100644 index 0000000000000..1135ad4bc1142 --- /dev/null +++ b/src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/TranscodingWriteStream.cs @@ -0,0 +1,159 @@ +// 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. + +// Taken from https://github.com/dotnet/aspnetcore/blob/master/src/Mvc/Mvc.Core/src/Formatters/TranscodingWriteStream.cs + +using System.Buffers; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace System.Net.Http.Json +{ + internal sealed class TranscodingWriteStream : Stream + { + // Default size of the char buffer that will hold the passed-in bytes when decoded from UTF-8. + // The buffer holds them and then they are encoded to the targetEncoding and written to the underlying stream. + internal const int MaxCharBufferSize = 4096; + // Upper bound that limits the byte buffer size to prevent an encoding that has a very poor worst-case scenario. + internal const int MaxByteBufferSize = 4 * MaxCharBufferSize; + private readonly int _maxByteBufferSize; + + private readonly Stream _stream; + private readonly Decoder _decoder; + private readonly Encoder _encoder; + private char[] _charBuffer; + private int _charsDecoded; + private bool _disposed; + + public TranscodingWriteStream(Stream stream, Encoding targetEncoding) + { + _stream = stream; + + _charBuffer = ArrayPool.Shared.Rent(MaxCharBufferSize); + + // Attempt to allocate a byte buffer than can tolerate the worst-case scenario for this + // encoding. This would allow the char -> byte conversion to complete in a single call. + // However limit the buffer size to prevent an encoding that has a very poor worst-case scenario. + _maxByteBufferSize = Math.Min(MaxByteBufferSize, targetEncoding.GetMaxByteCount(MaxCharBufferSize)); + + _decoder = Encoding.UTF8.GetDecoder(); + _encoder = targetEncoding.GetEncoder(); + } + + public override bool CanRead => false; + public override bool CanSeek => false; + public override bool CanWrite => true; + public override long Length => throw new NotSupportedException(); + public override long Position { get; set; } + + public override void Flush() + => throw new NotSupportedException(); + + public override Task FlushAsync(CancellationToken cancellationToken) + => _stream.FlushAsync(cancellationToken); + + public override int Read(byte[] buffer, int offset, int count) + => throw new NotSupportedException(); + + public override long Seek(long offset, SeekOrigin origin) + => throw new NotSupportedException(); + + public override void SetLength(long value) + => throw new NotSupportedException(); + + public override void Write(byte[] buffer, int offset, int count) + => throw new NotSupportedException(); + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + + if (offset < 0) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + + if (buffer.Length - offset < count) + { + throw new ArgumentException(SR.Argument_InvalidOffLen); + } + + var bufferSegment = new ArraySegment(buffer, offset, count); + return WriteAsyncCore(bufferSegment, cancellationToken); + } + + private async Task WriteAsyncCore(ArraySegment bufferSegment, CancellationToken cancellationToken) + { + bool decoderCompleted = false; + + while (!decoderCompleted) + { + _decoder.Convert(bufferSegment.Array!, bufferSegment.Offset, bufferSegment.Count, _charBuffer, _charsDecoded, _charBuffer.Length - _charsDecoded, + flush: false, out int bytesDecoded, out int charsDecoded, out decoderCompleted); + + _charsDecoded += charsDecoded; + bufferSegment = bufferSegment.Slice(bytesDecoded); + await WriteBufferAsync(cancellationToken).ConfigureAwait(false); + } + } + + private async Task WriteBufferAsync(CancellationToken cancellationToken) + { + bool encoderCompleted = false; + int charsWritten = 0; + byte[] byteBuffer = ArrayPool.Shared.Rent(_maxByteBufferSize); + + while (!encoderCompleted && charsWritten < _charsDecoded) + { + _encoder.Convert(_charBuffer, charsWritten, _charsDecoded - charsWritten, byteBuffer, byteIndex: 0, byteBuffer.Length, + flush: false, out int charsEncoded, out int bytesUsed, out encoderCompleted); + + await _stream.WriteAsync(byteBuffer, 0, bytesUsed, cancellationToken).ConfigureAwait(false); + charsWritten += charsEncoded; + } + + ArrayPool.Shared.Return(byteBuffer); + + // At this point, we've written all the buffered chars to the underlying Stream. + _charsDecoded = 0; + } + + protected override void Dispose(bool disposing) + { + if (!_disposed) + { + _disposed = true; + ArrayPool.Shared.Return(_charBuffer); + _charBuffer = null!; + } + } + + public async Task FinalWriteAsync(CancellationToken cancellationToken) + { + // Flush the encoder. + byte[] byteBuffer = ArrayPool.Shared.Rent(_maxByteBufferSize); + bool encoderCompleted = false; + + while (!encoderCompleted) + { + _encoder.Convert(Array.Empty(), 0, 0, byteBuffer, 0, byteBuffer.Length, + flush: true, out _, out int bytesUsed, out encoderCompleted); + + await _stream.WriteAsync(byteBuffer, 0, bytesUsed, cancellationToken).ConfigureAwait(false); + } + + ArrayPool.Shared.Return(byteBuffer); + } + } +} diff --git a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpClientJsonExtensionsTests.cs b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpClientJsonExtensionsTests.cs new file mode 100644 index 0000000000000..4186235c864be --- /dev/null +++ b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpClientJsonExtensionsTests.cs @@ -0,0 +1,206 @@ +// 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.Threading.Tasks; +using Xunit; +using System.Net.Test.Common; +using System.Text.Json; +using System.Linq; +using System.Collections.Generic; +using System.Threading; + +namespace System.Net.Http.Json.Functional.Tests +{ + public class HttpClientJsonExtensionsTests + { + private static readonly JsonSerializerOptions s_defaultSerializerOptions + = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + + [Fact] + public async Task TestGetFromJsonAsync() + { + const string json = @"{""Name"":""David"",""Age"":24}"; + const int NumRequests = 4; + HttpHeaderData header = new HttpHeaderData("Content-Type", "application/json"); + List headers = new List { header }; + + await LoopbackServer.CreateClientAndServerAsync( + async uri => + { + using (HttpClient client = new HttpClient()) + { + Person per = (Person)await client.GetFromJsonAsync(uri, typeof(Person)); + per.Validate(); + + per = (Person)await client.GetFromJsonAsync(uri.ToString(), typeof(Person)); + per.Validate(); + + per = await client.GetFromJsonAsync(uri); + per.Validate(); + + per = await client.GetFromJsonAsync(uri.ToString()); + per.Validate(); + } + }, + async server => + { + for (int i = 0; i < NumRequests; i++) + { + await server.HandleRequestAsync(content: json, headers: headers); + } + }); + } + + [Fact] + public async Task TestGetFromJsonAsyncUnsuccessfulResponseAsync() + { + const int NumRequests = 2; + await LoopbackServer.CreateClientAndServerAsync( + async uri => + { + using (HttpClient client = new HttpClient()) + { + await Assert.ThrowsAsync(() => client.GetFromJsonAsync(uri, typeof(Person))); + await Assert.ThrowsAsync(() => client.GetFromJsonAsync(uri)); + } + }, + async server => + { + for (int i = 0; i < NumRequests; i++) + { + await server.HandleRequestAsync(statusCode: HttpStatusCode.InternalServerError); + } + }); + } + + [Fact] + public async Task TestPostAsJsonAsync() + { + const int NumRequests = 4; + await LoopbackServer.CreateClientAndServerAsync( + async uri => + { + using (HttpClient client = new HttpClient()) + { + Person person = Person.Create(); + + using HttpResponseMessage response = await client.PostAsJsonAsync(uri.ToString(), person); + Assert.True(response.StatusCode == HttpStatusCode.OK); + + using HttpResponseMessage response2 = await client.PostAsJsonAsync(uri, person); + Assert.True(response2.StatusCode == HttpStatusCode.OK); + + using HttpResponseMessage response3 = await client.PostAsJsonAsync(uri.ToString(), person, CancellationToken.None); + Assert.True(response3.StatusCode == HttpStatusCode.OK); + + using HttpResponseMessage response4 = await client.PostAsJsonAsync(uri, person, CancellationToken.None); + Assert.True(response4.StatusCode == HttpStatusCode.OK); + } + }, + async server => { + for (int i = 0; i < NumRequests; i++) + { + HttpRequestData request = await server.HandleRequestAsync(); + ValidateRequest(request); + Person per = JsonSerializer.Deserialize(request.Body, s_defaultSerializerOptions); + per.Validate(); + } + }); + } + + [Fact] + public async Task TestPutAsJsonAsync() + { + const int NumRequests = 4; + await LoopbackServer.CreateClientAndServerAsync( + async uri => + { + using (HttpClient client = new HttpClient()) + { + Person person = Person.Create(); + Type typePerson = typeof(Person); + + using HttpResponseMessage response = await client.PutAsJsonAsync(uri.ToString(), person); + Assert.True(response.StatusCode == HttpStatusCode.OK); + + using HttpResponseMessage response2 = await client.PutAsJsonAsync(uri, person); + Assert.True(response2.StatusCode == HttpStatusCode.OK); + + using HttpResponseMessage response3 = await client.PutAsJsonAsync(uri.ToString(), person, CancellationToken.None); + Assert.True(response3.StatusCode == HttpStatusCode.OK); + + using HttpResponseMessage response4 = await client.PutAsJsonAsync(uri, person, CancellationToken.None); + Assert.True(response4.StatusCode == HttpStatusCode.OK); + } + }, + async server => { + for (int i = 0; i < NumRequests; i++) + { + HttpRequestData request = await server.HandleRequestAsync(); + ValidateRequest(request); + Person obj = JsonSerializer.Deserialize(request.Body, s_defaultSerializerOptions); + obj.Validate(); + } + }); + } + + [Fact] + public void TestHttpClientIsNullAsync() + { + HttpClient client = null; + string uriString = "http://example.com"; + Uri uri = new Uri(uriString); + + AssertExtensions.Throws("client", () => client.GetFromJsonAsync(uriString, typeof(Person))); + AssertExtensions.Throws("client", () => client.GetFromJsonAsync(uri, typeof(Person))); + AssertExtensions.Throws("client", () => client.GetFromJsonAsync(uriString)); + AssertExtensions.Throws("client", () => client.GetFromJsonAsync(uri)); + + AssertExtensions.Throws("client", () => client.PostAsJsonAsync(uriString, null)); + AssertExtensions.Throws("client", () => client.PostAsJsonAsync(uri, null)); + + AssertExtensions.Throws("client", () => client.PutAsJsonAsync(uriString, null)); + AssertExtensions.Throws("client", () => client.PutAsJsonAsync(uri, null)); + } + + private void ValidateRequest(HttpRequestData requestData) + { + HttpHeaderData contentType = requestData.Headers.Where(x => x.Name == "Content-Type").First(); + Assert.Equal("application/json; charset=utf-8", contentType.Value); + } + + [Fact] + public async Task AllowNullRequesturlAsync() + { + const int NumRequests = 4; + await LoopbackServer.CreateClientAndServerAsync( + async uri => + { + using (HttpClient client = new HttpClient()) + { + client.BaseAddress = uri; + + Person per = Assert.IsType(await client.GetFromJsonAsync((string)null, typeof(Person))); + per = Assert.IsType(await client.GetFromJsonAsync((Uri)null, typeof(Person))); + + per = await client.GetFromJsonAsync((string)null); + per = await client.GetFromJsonAsync((Uri)null); + } + }, + async server => { + List headers = new List { new HttpHeaderData("Content-Type", "application/json") }; + string json = Person.Create().Serialize(); + + for (int i = 0; i < NumRequests; i++) + { + await server.HandleRequestAsync(content: json, headers: headers); + } + }); + } + } +} diff --git a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpContentJsonExtensionsTests.cs b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpContentJsonExtensionsTests.cs new file mode 100644 index 0000000000000..d381247878f2d --- /dev/null +++ b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpContentJsonExtensionsTests.cs @@ -0,0 +1,287 @@ +// 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; +using System.IO; +using System.Net.Test.Common; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using Xunit; + +namespace System.Net.Http.Json.Functional.Tests +{ + public class HttpContentJsonExtensionsTests + { + private readonly List _headers = new List { new HttpHeaderData("Content-Type", "application/json") }; + + [Fact] + public void ThrowOnNull() + { + HttpContent content = null; + AssertExtensions.Throws("content", () => content.ReadFromJsonAsync()); + AssertExtensions.Throws("content", () => content.ReadFromJsonAsync(typeof(Person))); + } + + [Fact] + public async Task HttpContentGetThenReadFromJsonAsync() + { + const int NumRequests = 2; + await LoopbackServer.CreateClientAndServerAsync( + async uri => + { + using (HttpClient client = new HttpClient()) + { + var request = new HttpRequestMessage(HttpMethod.Get, uri); + HttpResponseMessage response = await client.SendAsync(request); + object obj = await response.Content.ReadFromJsonAsync(typeof(Person)); + Person per = Assert.IsType(obj); + per.Validate(); + + request = new HttpRequestMessage(HttpMethod.Get, uri); + response = await client.SendAsync(request); + per = await response.Content.ReadFromJsonAsync(); + per.Validate(); + } + }, + async server => { + for (int i = 0; i < NumRequests; i++) + { + HttpRequestData req = await server.HandleRequestAsync(headers: _headers, content: Person.Create().Serialize()); + } + }); + } + + [Fact] + public async Task HttpContentReturnValueIsNull() + { + const int NumRequests = 2; + await LoopbackServer.CreateClientAndServerAsync( + async uri => + { + using (HttpClient client = new HttpClient()) + { + var request = new HttpRequestMessage(HttpMethod.Get, uri); + HttpResponseMessage response = await client.SendAsync(request); + object obj = await response.Content.ReadFromJsonAsync(typeof(Person)); + Assert.Null(obj); + + request = new HttpRequestMessage(HttpMethod.Get, uri); + response = await client.SendAsync(request); + Person per = await response.Content.ReadFromJsonAsync(); + Assert.Null(per); + } + }, + async server => { + for (int i = 0; i < NumRequests; i++) + { + HttpRequestData req = await server.HandleRequestAsync(headers: _headers, content: "null"); + } + }); + } + + [Fact] + public async Task TestReadFromJsonNoMessageBodyAsync() + { + await LoopbackServer.CreateClientAndServerAsync( + async uri => + { + using (HttpClient client = new HttpClient()) + { + var request = new HttpRequestMessage(HttpMethod.Get, uri); + HttpResponseMessage response = await client.SendAsync(request); + + // As of now, we pass the message body to the serializer even when its empty which causes the serializer to throw. + JsonException ex = await Assert.ThrowsAsync(() => response.Content.ReadFromJsonAsync(typeof(Person))); + Assert.Contains("Path: $ | LineNumber: 0 | BytePositionInLine: 0", ex.Message); + } + }, + server => server.HandleRequestAsync(headers: _headers)); + } + + [Fact] + public async Task TestReadFromJsonNoContentTypeAsync() + { + await LoopbackServer.CreateClientAndServerAsync( + async uri => + { + using (HttpClient client = new HttpClient()) + { + var request = new HttpRequestMessage(HttpMethod.Get, uri); + HttpResponseMessage response = await client.SendAsync(request); + + await Assert.ThrowsAsync(() => response.Content.ReadFromJsonAsync()); + } + }, + server => server.HandleRequestAsync(content: "{}")); + } + + [Fact] + public async Task TestGetFromJsonQuotedCharSetAsync() + { + List customHeaders = new List + { + new HttpHeaderData("Content-Type", "application/json; charset=\"utf-8\"") + }; + + await LoopbackServer.CreateClientAndServerAsync( + async uri => + { + using (HttpClient client = new HttpClient()) + { + var request = new HttpRequestMessage(HttpMethod.Get, uri); + HttpResponseMessage response = await client.SendAsync(request); + + Person person = await response.Content.ReadFromJsonAsync(); + person.Validate(); + } + }, + server => server.HandleRequestAsync(headers: customHeaders, content: Person.Create().Serialize())); + } + + [Fact] + public async Task TestGetFromJsonThrowOnInvalidCharSetAsync() + { + List customHeaders = new List + { + new HttpHeaderData("Content-Type", "application/json; charset=\"foo-bar\"") + }; + + await LoopbackServer.CreateClientAndServerAsync( + async uri => + { + using (HttpClient client = new HttpClient()) + { + var request = new HttpRequestMessage(HttpMethod.Get, uri); + HttpResponseMessage response = await client.SendAsync(request); + + InvalidOperationException ex = await Assert.ThrowsAsync(() => response.Content.ReadFromJsonAsync()); + Assert.IsType(ex.InnerException); + } + }, + server => server.HandleRequestAsync(headers: customHeaders, content: Person.Create().Serialize())); + } + + [Fact] + public async Task TestGetFromJsonAsyncTextPlainUtf16Async() + { + const string json = @"{""Name"":""David"",""Age"":24}"; + const int NumRequests = 2; + await LoopbackServer.CreateClientAndServerAsync( + async uri => + { + using (HttpClient client = new HttpClient()) + { + var request = new HttpRequestMessage(HttpMethod.Get, uri); + HttpResponseMessage response = await client.SendAsync(request); + + Person per = Assert.IsType(await response.Content.ReadFromJsonAsync(typeof(Person))); + per.Validate(); + + request = new HttpRequestMessage(HttpMethod.Get, uri); + response = await client.SendAsync(request); + + per = await response.Content.ReadFromJsonAsync(); + per.Validate(); + } + }, + async server => + { + byte[] utf16Content = Encoding.Unicode.GetBytes(json); + byte[] bytes = + Encoding.ASCII.GetBytes( + $"HTTP/1.1 200 OK" + + $"\r\nContent-Type: application/json; charset=utf-16\r\n" + + $"Content-Length: {utf16Content.Length}\r\n" + + $"Connection:close\r\n\r\n"); + + + var buffer = new MemoryStream(); + buffer.Write(bytes, 0, bytes.Length); + buffer.Write(utf16Content, 0, utf16Content.Length); + + for (int i = 0; i < NumRequests; i++) + { + await server.AcceptConnectionSendCustomResponseAndCloseAsync(buffer.ToArray()); + } + }); + } + + [Fact] + public async Task EnsureDefaultJsonSerializerOptionsAsync() + { + await LoopbackServer.CreateClientAndServerAsync( + async uri => + { + using (HttpClient client = new HttpClient()) + { + var request = new HttpRequestMessage(HttpMethod.Get, uri); + HttpResponseMessage response = await client.SendAsync(request); + await response.Content.ReadFromJsonAsync(typeof(EnsureDefaultOptions)); + } + }, + server => server.HandleRequestAsync(headers: _headers, content: "{}")); + } + + [Theory] + [InlineData("application/json")] + [InlineData("Application/Json")] + [InlineData("application/foo+json")] // Structured Syntax Json Suffix + [InlineData("application/foo+Json")] + [InlineData("appLiCaTiOn/a+JsOn")] + public async Task TestValidMediaTypes(string mediaType) + { + List customHeaders = new List + { + new HttpHeaderData("Content-Type", $"{mediaType}; charset=\"utf-8\"") + }; + + await LoopbackServer.CreateClientAndServerAsync( + async uri => + { + using (HttpClient client = new HttpClient()) + { + var request = new HttpRequestMessage(HttpMethod.Get, uri); + HttpResponseMessage response = await client.SendAsync(request); + + Person person = await response.Content.ReadFromJsonAsync(); + person.Validate(); + } + }, + server => server.HandleRequestAsync(headers: customHeaders, content: Person.Create().Serialize())); + } + + [Theory] + [InlineData("text/plain")] + [InlineData("/")] + [InlineData("application/")] + [InlineData("application/+")] + [InlineData("application/+json")] // empty subtype before suffix is invalid. + [InlineData("application/problem+")] // no suffix after '+'. + [InlineData("application/problem+foo+json")] // more than one '+' not allowed. + public async Task TestInvalidMediaTypeAsync(string mediaType) + { + List customHeaders = new List + { + new HttpHeaderData("Content-Type", $"{mediaType}; charset=\"utf-8\"") + }; + + await LoopbackServer.CreateClientAndServerAsync( + async uri => + { + using (HttpClient client = new HttpClient()) + { + var request = new HttpRequestMessage(HttpMethod.Get, uri); + HttpResponseMessage response = await client.SendAsync(request); + + Exception ex = await Assert.ThrowsAsync(() => response.Content.ReadFromJsonAsync()); + Assert.Contains("application/json", ex.Message); + Assert.Contains("application/+json", ex.Message); + } + }, + server => server.HandleRequestAsync(headers: customHeaders, content: Person.Create().Serialize())); + } + } +} diff --git a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/JsonContentTests.cs b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/JsonContentTests.cs new file mode 100644 index 0000000000000..243f303f49cf5 --- /dev/null +++ b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/JsonContentTests.cs @@ -0,0 +1,227 @@ +// 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.Net.Http.Headers; +using System.Net.Test.Common; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using Xunit; + +namespace System.Net.Http.Json.Functional.Tests +{ + public class JsonContentTests + { + + private class Foo { } + private class Bar { } + + [Fact] + public void JsonContentObjectType() + { + Type fooType = typeof(Foo); + Foo foo = new Foo(); + + JsonContent content = JsonContent.Create(foo, fooType); + Assert.Equal(fooType, content.ObjectType); + Assert.Same(foo, content.Value); + + content = JsonContent.Create(foo); + Assert.Equal(fooType, content.ObjectType); + Assert.Same(foo, content.Value); + + object fooBoxed = foo; + + // ObjectType is the specified type when using the .ctor. + content = JsonContent.Create(fooBoxed, fooType); + Assert.Equal(fooType, content.ObjectType); + Assert.Same(fooBoxed, content.Value); + + // ObjectType is the declared type when using the factory method. + content = JsonContent.Create(fooBoxed); + Assert.Equal(typeof(object), content.ObjectType); + Assert.Same(fooBoxed, content.Value); + } + + [Fact] + public void TestJsonContentMediaType() + { + Type fooType = typeof(Foo); + Foo foo = new Foo(); + + // Use the default content-type if none is provided. + JsonContent content = JsonContent.Create(foo, fooType); + Assert.Equal("application/json", content.Headers.ContentType.MediaType); + Assert.Equal("utf-8", content.Headers.ContentType.CharSet); + + content = JsonContent.Create(foo); + Assert.Equal("application/json", content.Headers.ContentType.MediaType); + Assert.Equal("utf-8", content.Headers.ContentType.CharSet); + + // Use the specified MediaTypeHeaderValue if provided. + MediaTypeHeaderValue mediaType = MediaTypeHeaderValue.Parse("foo/bar; charset=utf-8"); + content = JsonContent.Create(foo, fooType, mediaType); + Assert.Same(mediaType, content.Headers.ContentType); + + content = JsonContent.Create(foo, mediaType: mediaType); + Assert.Same(mediaType, content.Headers.ContentType); + } + + [Fact] + public async Task SendQuotedCharsetAsync() + { + + await LoopbackServer.CreateClientAndServerAsync( + async uri => + { + using (HttpClient client = new HttpClient()) + { + JsonContent content = JsonContent.Create(null); + content.Headers.ContentType.CharSet = "\"utf-8\""; + + var request = new HttpRequestMessage(HttpMethod.Post, uri); + request.Content = content; + await client.SendAsync(request); + } + }, + async server => { + HttpRequestData req = await server.HandleRequestAsync(); + Assert.Equal("application/json; charset=\"utf-8\"", req.GetSingleHeaderValue("Content-Type")); + }); + } + + [Fact] + public void TestJsonContentContentTypeIsNotTheSameOnMultipleInstances() + { + JsonContent jsonContent1 = JsonContent.Create(null); + JsonContent jsonContent2 = JsonContent.Create(null); + + jsonContent1.Headers.ContentType.CharSet = "foo-bar"; + + Assert.NotEqual(jsonContent1.Headers.ContentType.CharSet, jsonContent2.Headers.ContentType.CharSet); + Assert.NotSame(jsonContent1.Headers.ContentType, jsonContent2.Headers.ContentType); + } + + [Fact] + public async Task JsonContentMediaTypeValidateOnServerAsync() + { + await LoopbackServer.CreateClientAndServerAsync( + async uri => + { + using (HttpClient client = new HttpClient()) + { + var request = new HttpRequestMessage(HttpMethod.Post, uri); + MediaTypeHeaderValue mediaType = MediaTypeHeaderValue.Parse("foo/bar; charset=utf-8"); + request.Content = JsonContent.Create(Person.Create(), mediaType: mediaType); + await client.SendAsync(request); + } + }, + async server => { + HttpRequestData req = await server.HandleRequestAsync(); + Assert.Equal("foo/bar; charset=utf-8", req.GetSingleHeaderValue("Content-Type")); + }); + } + + [Fact] + public void JsonContentMediaTypeDefaultIfNull() + { + Type fooType = typeof(Foo); + Foo foo = null; + + JsonContent content = JsonContent.Create(foo, fooType, mediaType: null); + Assert.Equal("application/json", content.Headers.ContentType.MediaType); + Assert.Equal("utf-8", content.Headers.ContentType.CharSet); + + content = JsonContent.Create(foo, mediaType: null); + Assert.Equal("application/json", content.Headers.ContentType.MediaType); + Assert.Equal("utf-8", content.Headers.ContentType.CharSet); + } + + [Fact] + public void JsonContentInputTypeIsNull() + => AssertExtensions.Throws("inputType", () => JsonContent.Create(null, inputType: null, mediaType: null)); + + [Fact] + public void JsonContentThrowsOnIncompatibleTypeAsync() + { + using (HttpClient client = new HttpClient()) + { + var foo = new Foo(); + Type typeOfBar = typeof(Bar); + + Exception ex = Assert.Throws(() => JsonContent.Create(foo, typeOfBar)); + + string strTypeOfBar = typeOfBar.ToString(); + Assert.Contains(strTypeOfBar, ex.Message); + + string afterInputTypeMessage = ex.Message.Split(strTypeOfBar.ToCharArray())[1]; + Assert.Contains(afterInputTypeMessage, ex.Message); + } + } + + [Fact] + public static async Task ValidateUtf16IsTranscodedAsync() + { + await LoopbackServer.CreateClientAndServerAsync( + async uri => + { + using (HttpClient client = new HttpClient()) + { + var request = new HttpRequestMessage(HttpMethod.Post, uri); + MediaTypeHeaderValue mediaType = MediaTypeHeaderValue.Parse("application/json; charset=utf-16"); + // Pass new options to avoid using the Default Web Options that use camelCase. + request.Content = JsonContent.Create(Person.Create(), mediaType: mediaType, options: new JsonSerializerOptions()); + await client.SendAsync(request); + } + }, + async server => { + HttpRequestData req = await server.HandleRequestAsync(); + Assert.Equal("application/json; charset=utf-16", req.GetSingleHeaderValue("Content-Type")); + Person per = JsonSerializer.Deserialize(Encoding.Unicode.GetString(req.Body)); + per.Validate(); + }); + } + + [Fact] + public async Task EnsureDefaultJsonSerializerOptionsAsync() + { + await LoopbackServer.CreateClientAndServerAsync( + async uri => + { + using (HttpClient client = new HttpClient()) + { + // EnsureDefaultOptions uses a JsonConverter where we validate the JsonSerializerOptions when not provided to JsonContent.Create. + EnsureDefaultOptions dummyObj = new EnsureDefaultOptions(); + var request = new HttpRequestMessage(HttpMethod.Post, uri); + request.Content = JsonContent.Create(dummyObj); + await client.SendAsync(request); + } + }, + server => server.HandleRequestAsync()); + } + + [Fact] + public async Task TestJsonContentNullContentTypeAsync() + { + await LoopbackServer.CreateClientAndServerAsync( + async uri => + { + using (HttpClient client = new HttpClient()) + { + var request = new HttpRequestMessage(HttpMethod.Post, uri); + MediaTypeHeaderValue mediaType = MediaTypeHeaderValue.Parse("application/json; charset=utf-16"); + JsonContent content = JsonContent.Create(Person.Create(), mediaType: mediaType); + content.Headers.ContentType = null; + + request.Content = content; + await client.SendAsync(request); + } + }, + async server => { + HttpRequestData req = await server.HandleRequestAsync(); + Assert.Equal(0, req.GetHeaderValueCount("Content-Type")); + }); + } + } +} diff --git a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/System.Net.Http.Json.Functional.Tests.csproj b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/System.Net.Http.Json.Functional.Tests.csproj new file mode 100644 index 0000000000000..ad6333b1aa6e5 --- /dev/null +++ b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/System.Net.Http.Json.Functional.Tests.csproj @@ -0,0 +1,40 @@ + + + $(NetCoreAppCurrent);$(NetFrameworkCurrent) + + + + + + + + + + Common\System\Net\Capability.Security.cs + + + Common\System\Net\Configuration.cs + + + Common\System\Net\Configuration.Certificates.cs + + + Common\System\Net\Configuration.Http.cs + + + Common\System\Net\Configuration.Security.cs + + + Common\System\Net\Http\LoopbackServer.cs + + + Common\System\Net\Http\LoopbackServer.AuthenticationHelpers.cs + + + Common\System\Net\Http\GenericLoopbackServer.cs + + + Common\System\Threading\Tasks\TaskTimeoutExtensions.cs + + + diff --git a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/TestClasses.cs b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/TestClasses.cs new file mode 100644 index 0000000000000..8dc962bae9155 --- /dev/null +++ b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/TestClasses.cs @@ -0,0 +1,66 @@ +// 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.Diagnostics.CodeAnalysis; +using System.Text.Json; +using System.Text.Json.Serialization; +using Xunit; + +namespace System.Net.Http.Json.Functional.Tests +{ + internal class Person + { + public int Age { get; set; } + public string Name { get; set; } + public Person Parent { get; set; } + + public void Validate() + { + Assert.Equal("David", Name); + Assert.Equal(24, Age); + Assert.Null(Parent); + } + + public static Person Create() + { + return new Person { Name = "David", Age = 24 }; + } + + public string Serialize() + { + return JsonSerializer.Serialize(this); + } + } + + internal class EnsureDefaultOptionsConverter : JsonConverter + { + public override EnsureDefaultOptions Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + AssertDefaultOptions(options); + + while (reader.TokenType != JsonTokenType.EndObject) + { + reader.Read(); + } + return new EnsureDefaultOptions(); + } + + public override void Write(Utf8JsonWriter writer, EnsureDefaultOptions value, JsonSerializerOptions options) + { + AssertDefaultOptions(options); + + writer.WriteStartObject(); + writer.WriteEndObject(); + } + + private static void AssertDefaultOptions(JsonSerializerOptions options) + { + Assert.True(options.PropertyNameCaseInsensitive); + Assert.Equal(JsonNamingPolicy.CamelCase, options.PropertyNamingPolicy); + } + } + + [JsonConverter(typeof(EnsureDefaultOptionsConverter))] + internal class EnsureDefaultOptions { } +} diff --git a/src/libraries/System.Net.Http.Json/tests/UnitTests/Resources/Strings.resx b/src/libraries/System.Net.Http.Json/tests/UnitTests/Resources/Strings.resx new file mode 100644 index 0000000000000..fcc6751c6d127 --- /dev/null +++ b/src/libraries/System.Net.Http.Json/tests/UnitTests/Resources/Strings.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Offset and length were out of bounds for the array or count is greater than the number of elements from index to the end of the source collection. + + \ No newline at end of file diff --git a/src/libraries/System.Net.Http.Json/tests/UnitTests/System.Net.Http.Json.Unit.Tests.csproj b/src/libraries/System.Net.Http.Json/tests/UnitTests/System.Net.Http.Json.Unit.Tests.csproj new file mode 100644 index 0000000000000..4b7b36a6be21f --- /dev/null +++ b/src/libraries/System.Net.Http.Json/tests/UnitTests/System.Net.Http.Json.Unit.Tests.csproj @@ -0,0 +1,22 @@ + + + $(NetCoreAppCurrent);$(NetFrameworkCurrent) + + + + + + + + ProductionCode\System\ArraySegmentExtensions.netstandard.cs + + + + + ProductionCode\System\Net\Http\Json\TranscodingReadStream.cs + + + ProductionCode\System\Net\Http\Json\TranscodingWriteStream.cs + + + diff --git a/src/libraries/System.Net.Http.Json/tests/UnitTests/TranscodingReadStreamTests.cs b/src/libraries/System.Net.Http.Json/tests/UnitTests/TranscodingReadStreamTests.cs new file mode 100644 index 0000000000000..3a8ee5840f7df --- /dev/null +++ b/src/libraries/System.Net.Http.Json/tests/UnitTests/TranscodingReadStreamTests.cs @@ -0,0 +1,296 @@ +// 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. + +// Taken from https://github.com/dotnet/aspnetcore/blob/master/src/Mvc/Mvc.Core/test/Formatters/TranscodingReadStreamTest.cs + +using System.Buffers; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using Xunit; + +namespace System.Net.Http.Json.Functional.Tests +{ + public class TranscodingReadStreamTest + { + [Fact] + public async Task ReadAsync_SingleByte() + { + // Arrange + string input = "Hello world"; + Encoding encoding = Encoding.Unicode; + using (TranscodingReadStream stream = new TranscodingReadStream(new MemoryStream(encoding.GetBytes(input)), encoding)) + { + var bytes = new byte[4]; + + // Act + int readBytes = await stream.ReadAsync(bytes, 0, 1); + + // Assert + Assert.Equal(1, readBytes); + Assert.Equal((byte)'H', bytes[0]); + Assert.Equal(0, bytes[1]); + + Assert.Equal(0, stream.ByteBufferCount); + Assert.Equal(0, stream.CharBufferCount); + Assert.Equal(10, stream.OverflowCount); + } + } + + [Fact] + public async Task ReadAsync_FillsBuffer() + { + // Arrange + string input = "Hello world"; + Encoding encoding = Encoding.Unicode; + using (TranscodingReadStream stream = new TranscodingReadStream(new MemoryStream(encoding.GetBytes(input)), encoding)) + { + byte[] bytes = new byte[3]; + byte[] expected = Encoding.UTF8.GetBytes(input.Substring(0, bytes.Length)); + + // Act + int readBytes = await stream.ReadAsync(bytes, 0, bytes.Length); + + // Assert + Assert.Equal(3, readBytes); + Assert.Equal(expected, bytes); + Assert.Equal(0, stream.ByteBufferCount); + Assert.Equal(0, stream.CharBufferCount); + Assert.Equal(8, stream.OverflowCount); + } + } + + [Fact] + public async Task ReadAsync_CompletedInSecondIteration() + { + // Arrange + string input = new string('A', 1024 + 10); + Encoding encoding = Encoding.Unicode; + using (TranscodingReadStream stream = new TranscodingReadStream(new MemoryStream(encoding.GetBytes(input)), encoding)) + { + var bytes = new byte[1024]; + byte[] expected = Encoding.UTF8.GetBytes(input.Substring(0, bytes.Length)); + + // Act + int readBytes = await stream.ReadAsync(bytes, 0, bytes.Length); + + // Assert + Assert.Equal(bytes.Length, readBytes); + Assert.Equal(expected, bytes); + Assert.Equal(0, stream.ByteBufferCount); + Assert.Equal(10, stream.CharBufferCount); + Assert.Equal(0, stream.OverflowCount); + + readBytes = await stream.ReadAsync(bytes, 0, bytes.Length); + Assert.Equal(10, readBytes); + Assert.Equal(0, stream.ByteBufferCount); + Assert.Equal(0, stream.CharBufferCount); + Assert.Equal(0, stream.OverflowCount); + } + } + + [Fact] + public async Task ReadAsync_WithOverflowBuffer() + { + // Arrange + // Test ensures that the overflow buffer works correctly + string input = "\u2600"; + Encoding encoding = Encoding.Unicode; + using (TranscodingReadStream stream = new TranscodingReadStream(new MemoryStream(encoding.GetBytes(input)), encoding)) + { + var bytes = new byte[1]; + byte[] expected = Encoding.UTF8.GetBytes(input); + + // Act + int readBytes = await stream.ReadAsync(bytes, 0, bytes.Length); + + // Assert + Assert.Equal(1, readBytes); + Assert.Equal(expected[0], bytes[0]); + Assert.Equal(0, stream.ByteBufferCount); + Assert.Equal(0, stream.CharBufferCount); + Assert.Equal(2, stream.OverflowCount); + + bytes = new byte[expected.Length - 1]; + readBytes = await stream.ReadAsync(bytes, 0, bytes.Length); + Assert.Equal(bytes.Length, readBytes); + Assert.Equal(0, stream.ByteBufferCount); + Assert.Equal(0, stream.CharBufferCount); + Assert.Equal(0, stream.OverflowCount); + + readBytes = await stream.ReadAsync(bytes, 0, bytes.Length); + Assert.Equal(0, readBytes); + } + } + + public static TheoryData ReadAsync_WithOverflowBuffer_AtBoundariesData(string encoding) + { + int maxCharBufferSize = Encoding.GetEncoding(encoding).GetMaxCharCount(TranscodingReadStream.MaxByteBufferSize); + + return new TheoryData + { + new string('a', maxCharBufferSize - 1) + '\u2600', + new string('a', maxCharBufferSize - 2) + '\u2600', + new string('a', maxCharBufferSize) + '\u2600', + }; + } + + [Theory] + [MemberData(nameof(ReadAsync_WithOverflowBuffer_AtBoundariesData), "utf-16")] + public Task ReadAsync_WithOverflowBuffer_WithBufferSize1(string input) => ReadAsync_WithOverflowBufferAtCharBufferBoundaries(input, bufferSize: 1); + + private static async Task ReadAsync_WithOverflowBufferAtCharBufferBoundaries(string input, int bufferSize) + { + // Arrange + // Test ensures that the overflow buffer works correctly + Encoding encoding = Encoding.Unicode; + using (TranscodingReadStream stream = new TranscodingReadStream(new MemoryStream(encoding.GetBytes(input)), encoding)) + { + byte[] expected = Encoding.UTF8.GetBytes(input); + + // Act + var buffer = new byte[bufferSize]; + var actual = new List(); + + while (await stream.ReadAsync(buffer, 0, bufferSize) != 0) + { + actual.AddRange(buffer); + } + + Assert.Equal(expected, actual); + } + } + + public static TheoryData ReadAsyncInputLatin(string encoding) + { + int maxCharBufferSize = Encoding.GetEncoding(encoding).GetMaxCharCount(TranscodingReadStream.MaxByteBufferSize); + return GetLatinTextInput(maxCharBufferSize, TranscodingReadStream.MaxByteBufferSize); + } + + public static TheoryData ReadAsyncInputUnicode(string encoding) + { + int maxCharBufferSize = Encoding.GetEncoding(encoding).GetMaxCharCount(TranscodingReadStream.MaxByteBufferSize); + return GetUnicodeText(maxCharBufferSize); + } + + internal static TheoryData GetLatinTextInput(int maxCharBufferSize, int maxByteBufferSize) + { + return new TheoryData + { + "Hello world", + string.Join(string.Empty, Enumerable.Repeat("AB", 9000)), + new string('A', count: maxByteBufferSize), + new string('A', count: maxCharBufferSize), + new string('A', count: maxByteBufferSize + 1), + new string('A', count: maxCharBufferSize + 1), + }; + } + + internal static TheoryData GetUnicodeText(int maxCharBufferSize) + { + return new TheoryData + { + new string('\u00c6', count: 7), + + new string('A', count: maxCharBufferSize - 1) + '\u00c6', + + "Ab\u0100\u0101\u0102\u0103\u0104\u0105\u0106\u014a\u014b\u014c\u014d\u014e\u014f\u0150\u0151\u0152\u0153\u0154\u0155\u0156\u0157\u0158\u0159\u015a\u015f\u0160\u0161\u0162\u0163\u0164\u0165\u0166\u0167\u0168\u0169\u016a\u016b\u016c\u016d\u016e\u016f\u0170\u0171\u0172\u0173\u0174\u0175\u0176\u0177\u0178\u0179\u017a\u017b\u017c\u017d\u017e\u017fAbc", + + "Abc\u0b90\u0b92\u0b93\u0b94\u0b95\u0b99\u0b9a\u0b9c\u0b9e\u0b9f\u0ba3\u0ba4\u0ba8\u0ba9\u0baa\u0bae\u0baf\u0bb0\u0bb1\u0bb2\u0bb3\u0bb4\u0bb5\u0bb7\u0bb8\u0bb9", + + "\u2600\u2601\u2602\u2603\u2604\u2605\u2606\u2607\u2608\u2609\u260a\u260b\u260c\u260d\u260e\u260f\u2610\u2611\u2612\u2613\u261a\u261b\u261c\u261d\u261e\u261f\u2620\u2621\u2622\u2623\u2624\u2625\u2626\u2627\u2628\u2629\u262a\u262b\u262c\u262d\u262e\u262f\u2630\u2631\u2632\u2633\u2634\u2635\u2636\u2637\u2638", + + new string('\u00c6', count: 64 * 1024), + + new string('\u00c6', count: 64 * 1024 + 1), + + "ping\u00fcino", + + new string('\u0904', count: maxCharBufferSize + 1), // This uses 3 bytes to represent in UTF8 + }; + } + + [Theory] + [MemberData(nameof(ReadAsyncInputLatin), "utf-32")] + [MemberData(nameof(ReadAsyncInputUnicode), "utf-32")] + public Task ReadAsync_Works_WhenInputIs_UTF32(string message) + { + Encoding sourceEncoding = Encoding.UTF32; + return ReadAsyncTest(sourceEncoding, message); + } + + [Theory] + [MemberData(nameof(ReadAsyncInputLatin), "utf-16")] + [MemberData(nameof(ReadAsyncInputUnicode), "utf-16")] + public Task ReadAsync_Works_WhenInputIs_Unicode(string message) + { + Encoding sourceEncoding = Encoding.Unicode; + return ReadAsyncTest(sourceEncoding, message); + } + + [Theory] + [MemberData(nameof(ReadAsyncInputLatin), "utf-7")] + [MemberData(nameof(ReadAsyncInputUnicode), "utf-7")] + public Task ReadAsync_Works_WhenInputIs_UTF7(string message) + { + Encoding sourceEncoding = Encoding.UTF7; + return ReadAsyncTest(sourceEncoding, message); + } + + [Theory] + [MemberData(nameof(ReadAsyncInputLatin), "iso-8859-1")] + public Task ReadAsync_Works_WhenInputIs_WesternEuropeanEncoding(string message) + { + // Arrange + Encoding sourceEncoding = Encoding.GetEncoding(28591); + return ReadAsyncTest(sourceEncoding, message); + } + + [Theory] + [MemberData(nameof(ReadAsyncInputLatin), "us-ascii")] + public Task ReadAsync_Works_WhenInputIs_ASCII(string message) + { + // Arrange + Encoding sourceEncoding = Encoding.ASCII; + return ReadAsyncTest(sourceEncoding, message); + } + + private static async Task ReadAsyncTest(Encoding sourceEncoding, string message) + { + string input = $"{{ \"Message\": \"{message}\" }}"; + var stream = new MemoryStream(sourceEncoding.GetBytes(input)); + + using (var transcodingStream = new TranscodingReadStream(stream, sourceEncoding)) + { + + object model = await JsonSerializer.DeserializeAsync(transcodingStream, typeof(TestModel)); + TestModel testModel = Assert.IsType(model); + + Assert.Equal(message, testModel.Message); + } + } + + public class TestModel + { + public string Message { get; set; } + } + + [Fact] + public async Task TestOneToOneTranscodingAsync() + { + Encoding sourceEncoding = Encoding.GetEncoding(28591); + string message = '"' + new string('A', TranscodingReadStream.MaxByteBufferSize - 2 + 1) + '"'; + + Stream stream = new MemoryStream(sourceEncoding.GetBytes(message)); + using (TranscodingReadStream transcodingStream = new TranscodingReadStream(stream, sourceEncoding)) + { + string deserializedMessage = await JsonSerializer.DeserializeAsync(transcodingStream); + Assert.Equal(message.Trim('"'), deserializedMessage); + } + } + } +} diff --git a/src/libraries/System.Net.Http.Json/tests/UnitTests/TranscodingWriteStreamTests.cs b/src/libraries/System.Net.Http.Json/tests/UnitTests/TranscodingWriteStreamTests.cs new file mode 100644 index 0000000000000..546f666c7c113 --- /dev/null +++ b/src/libraries/System.Net.Http.Json/tests/UnitTests/TranscodingWriteStreamTests.cs @@ -0,0 +1,96 @@ +// 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. + +// Taken from https://github.com/dotnet/aspnetcore/blob/master/src/Mvc/Mvc.Core/test/Formatters/TranscodingWriteStreamTest.cs + +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace System.Net.Http.Json.Functional.Tests +{ + public class TranscodingWriteStreamTest + { + public static TheoryData WriteAsyncInputLatin => + TranscodingReadStreamTest.GetLatinTextInput(TranscodingWriteStream.MaxCharBufferSize, TranscodingWriteStream.MaxByteBufferSize); + + public static TheoryData WriteAsyncInputUnicode => + TranscodingReadStreamTest.GetUnicodeText(TranscodingWriteStream.MaxCharBufferSize); + + [Theory] + [MemberData(nameof(WriteAsyncInputLatin))] + [MemberData(nameof(WriteAsyncInputUnicode))] + public Task WriteAsync_Works_WhenOutputIs_UTF32(string message) + { + Encoding targetEncoding = Encoding.UTF32; + return WriteAsyncTest(targetEncoding, message); + } + + [Theory] + [MemberData(nameof(WriteAsyncInputLatin))] + [MemberData(nameof(WriteAsyncInputUnicode))] + public Task WriteAsync_Works_WhenOutputIs_Unicode(string message) + { + Encoding targetEncoding = Encoding.Unicode; + return WriteAsyncTest(targetEncoding, message); + } + + [Theory] + [MemberData(nameof(WriteAsyncInputLatin))] + public Task WriteAsync_Works_WhenOutputIs_UTF7(string message) + { + Encoding targetEncoding = Encoding.UTF7; + return WriteAsyncTest(targetEncoding, message); + } + + [Theory] + [MemberData(nameof(WriteAsyncInputLatin))] + public Task WriteAsync_Works_WhenOutputIs_WesternEuropeanEncoding(string message) + { + // Arrange + Encoding targetEncoding = Encoding.GetEncoding(28591); + return WriteAsyncTest(targetEncoding, message); + } + + + [Theory] + [MemberData(nameof(WriteAsyncInputLatin))] + public Task WriteAsync_Works_WhenOutputIs_ASCII(string message) + { + // Arrange + Encoding targetEncoding = Encoding.ASCII; + return WriteAsyncTest(targetEncoding, message); + } + + private static async Task WriteAsyncTest(Encoding targetEncoding, string message) + { + string expected = $"{{\"Message\":\"{JavaScriptEncoder.Default.Encode(message)}\"}}"; + + var model = new TestModel { Message = message }; + var stream = new MemoryStream(); + + var transcodingStream = new TranscodingWriteStream(stream, targetEncoding); + await JsonSerializer.SerializeAsync(transcodingStream, model, model.GetType()); + // The transcoding streams use Encoders and Decoders that have internal buffers. We need to flush these + // when there is no more data to be written. Stream.FlushAsync isn't suitable since it's + // acceptable to Flush a Stream (multiple times) prior to completion. + await transcodingStream.FinalWriteAsync(default); + await transcodingStream.FlushAsync(); + + string actual = targetEncoding.GetString(stream.ToArray()); + Assert.Equal(expected, actual, StringComparer.OrdinalIgnoreCase); + } + + private class TestModel + { + public string Message { get; set; } + } + } +} diff --git a/src/libraries/pkg/baseline/packageIndex.json b/src/libraries/pkg/baseline/packageIndex.json index 7114331622978..6274acb2d8fe0 100644 --- a/src/libraries/pkg/baseline/packageIndex.json +++ b/src/libraries/pkg/baseline/packageIndex.json @@ -3320,6 +3320,12 @@ "4.2.0.0": "4.4.0" } }, + "System.Net.Http.Json": { + "InboxOn": {}, + "AssemblyVersionInPackageVersion": { + "5.0.0.0": "5.0.0" + } + }, "System.Net.Http.Rtc": { "StableVersions": [ "4.0.0", diff --git a/src/libraries/pkg/descriptions.json b/src/libraries/pkg/descriptions.json index 45937674b626f..88ca15427cc5f 100644 --- a/src/libraries/pkg/descriptions.json +++ b/src/libraries/pkg/descriptions.json @@ -1229,6 +1229,15 @@ "System.Net.HttpListener" ] }, + { + "Name": "System.Net.Http.Json", + "Description": "Provides extension methods for System.Net.Http.HttpClient and System.Net.Http.HttpContent that perform automatic serialization and deserialization using System.Text.Json.", + "CommonTypes": [ + "System.Net.Http.Json.HttpClientJsonExtensions", + "System.Net.Http.Json.HttpContentJsonExtensions", + "System.Net.Http.Json.JsonContent" + ] + }, { "Name": "System.Net.Requests", "Description": "Provides older classes (such as HttpWebRequest and HttpWebResponse) for sending HTTP requests and receiving HTTP responses from a resource identified by a URI. Developers should prefer the classes in the System.Net.Http package.",