From 2b4a82a7ad0532a3f313a9601a471d554929fcbe Mon Sep 17 00:00:00 2001 From: David Cantu Date: Thu, 5 Mar 2020 00:52:33 -0800 Subject: [PATCH 01/32] Add System.Net.Http.Json related projects --- .../Directory.Build.props | 7 + .../System.Net.Http.Json.sln | 53 +++++++ .../ref/System.Net.Http.Json.cs | 91 ++++++++++++ .../ref/System.Net.Http.Json.csproj | 22 +++ .../src/System.Net.Http.Json.csproj | 18 +++ .../Http/Json/HttpClientJsonExtensions.Get.cs | 132 ++++++++++++++++++ .../Json/HttpClientJsonExtensions.Post.cs | 24 ++++ .../Http/Json/HttpContentJsonExtensions.cs | 37 +++++ .../src/System/Net/Http/Json/JsonContent.cs | 87 ++++++++++++ .../HttpClientJsonExtensionsTests.cs | 27 ++++ ...stem.Net.Http.Json.Functional.Tests.csproj | 49 +++++++ 11 files changed, 547 insertions(+) create mode 100644 src/libraries/System.Net.Http.Json/Directory.Build.props create mode 100644 src/libraries/System.Net.Http.Json/System.Net.Http.Json.sln create mode 100644 src/libraries/System.Net.Http.Json/ref/System.Net.Http.Json.cs create mode 100644 src/libraries/System.Net.Http.Json/ref/System.Net.Http.Json.csproj create mode 100644 src/libraries/System.Net.Http.Json/src/System.Net.Http.Json.csproj create mode 100644 src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Get.cs create mode 100644 src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Post.cs create mode 100644 src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/HttpContentJsonExtensions.cs create mode 100644 src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/JsonContent.cs create mode 100644 src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpClientJsonExtensionsTests.cs create mode 100644 src/libraries/System.Net.Http.Json/tests/FunctionalTests/System.Net.Http.Json.Functional.Tests.csproj 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 00000000000000..465e1110d6b07f --- /dev/null +++ b/src/libraries/System.Net.Http.Json/Directory.Build.props @@ -0,0 +1,7 @@ + + + + Microsoft + true + + \ No newline at end of file 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 00000000000000..cb5094d589ea5d --- /dev/null +++ b/src/libraries/System.Net.Http.Json/System.Net.Http.Json.sln @@ -0,0 +1,53 @@ +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 +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 + 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} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {5100F629-0FAB-4C6F-9A54-95AE9565EE0D} + EndGlobalSection +EndGlobal 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 00000000000000..3d88de9f49cfc1 --- /dev/null +++ b/src/libraries/System.Net.Http.Json/ref/System.Net.Http.Json.cs @@ -0,0 +1,91 @@ +// 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. +// ------------------------------------------------------------------------------ + +using System.IO; +using System.Net.Http.Headers; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace System.Net.Http.Json +{ + public static class HttpClientJsonExtensions + { + public static Task GetFromJsonAsync( + this HttpClient client, + string requestUri, + Type type, + JsonSerializerOptions? options = null, + CancellationToken cancellationToken = default) + { + throw null; + } + + public static Task GetFromJsonAsync( + this HttpClient client, + Uri requestUri, + Type type, + JsonSerializerOptions? options = null, + CancellationToken cancellationToken = default) + { + throw null; + } + + public static Task GetFromJsonAsync( + this HttpClient client, + string requestUri, + JsonSerializerOptions? options = null, + CancellationToken cancellationToken = default) + { + throw null; + } + + public static Task GetFromJsonAsync( + this HttpClient client, + Uri requestUri, + JsonSerializerOptions? options = null, + CancellationToken cancellationToken = default) + { + throw null; + } + + public static Task PostAsJsonAsync( + this HttpClient client, + string requestUri, + Type type, + object? value, + JsonSerializerOptions? options = null, + CancellationToken cancellationToken = default) + { + throw null; + } + } + + public static class HttpContentJsonExtensions { } + + public class JsonContent : HttpContent + { + public Type ObjectType { get; } + public object? Value { get; } + + public static JsonContent Create(T value, JsonSerializerOptions options = null) { throw null; } + + public static JsonContent Create(T value, MediaTypeHeaderValue mediaType, JsonSerializerOptions options = null) { throw null; } + + public static JsonContent Create(T value, string mediaType, JsonSerializerOptions options = null) { throw null; } + + public JsonContent(Type type, object? value, JsonSerializerOptions options = null) { throw null; } + + public JsonContent(Type type, object? value, MediaTypeHeaderValue mediaType, JsonSerializerOptions options = null) { throw null; } + + public JsonContent(Type type, object? value, string mediaType, JsonSerializerOptions options = null) { throw null; } + + protected override Task SerializeToStreamAsync(Stream stream, 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 00000000000000..3102059dd7a502 --- /dev/null +++ b/src/libraries/System.Net.Http.Json/ref/System.Net.Http.Json.csproj @@ -0,0 +1,22 @@ + + + netstandard2.0 + enable + + + + + + + + + + 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 00000000000000..641e7f8a978a11 --- /dev/null +++ b/src/libraries/System.Net.Http.Json/src/System.Net.Http.Json.csproj @@ -0,0 +1,18 @@ + + + $(NetCoreAppCurrent) + enable + + + + + + + + + + + + + + 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 00000000000000..af7ac63733c506 --- /dev/null +++ b/src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Get.cs @@ -0,0 +1,132 @@ +// 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.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 + { + /// + /// TODO + /// + /// + /// + /// + /// + /// + /// + public static Task GetFromJsonAsync( + this HttpClient client, + string requestUri, + Type type, + JsonSerializerOptions? options = null, + CancellationToken cancellationToken = default) + { + Task taskResponse = client.GetAsync(requestUri, cancellationToken); + return ProcessTaskResponseAsync(taskResponse, type, options, cancellationToken); + } + + /// + /// TODO + /// + /// + /// + /// + /// + /// + /// + public static Task GetFromJsonAsync( + this HttpClient client, + Uri requestUri, + Type type, + JsonSerializerOptions? options = null, + CancellationToken cancellationToken = default) + { + Task taskResponse = client.GetAsync(requestUri, cancellationToken); + return ProcessTaskResponseAsync(taskResponse, type, options, cancellationToken); + } + + /// + /// TODO + /// + /// + /// + /// + /// + /// + /// + public static Task GetFromJsonAsync( + this HttpClient client, + string requestUri, + JsonSerializerOptions? options = null, + CancellationToken cancellationToken = default) + { + Task taskResponse = client.GetAsync(requestUri, cancellationToken); + return ProcessTaskResponseAsync(taskResponse, options, cancellationToken); + } + + /// + /// TODO + /// + /// + /// + /// + /// + /// + /// + public static Task GetFromJsonAsync( + this HttpClient client, + Uri requestUri, + JsonSerializerOptions? options = null, + CancellationToken cancellationToken = default) + { + Task taskResponse = client.GetAsync(requestUri, cancellationToken); + return ProcessTaskResponseAsync(taskResponse, options, cancellationToken); + } + + private static async Task ProcessTaskResponseAsync( + Task taskResponse, + JsonSerializerOptions? options = null, + CancellationToken cancellationToken = default) + { + Stream jsonStream = await GetUtf8StreamFromResponseAsync(taskResponse).ConfigureAwait(false); + TValue value = await JsonSerializer.DeserializeAsync(jsonStream, options, cancellationToken); + return value; + } + + private static async Task ProcessTaskResponseAsync( + Task taskResponse, + Type type, + JsonSerializerOptions? options = null, + CancellationToken cancellationToken = default) + { + Stream jsonStream = await GetUtf8StreamFromResponseAsync(taskResponse).ConfigureAwait(false); + object? value = await JsonSerializer.DeserializeAsync(jsonStream, type, options, cancellationToken); + return value; + } + + private static async Task GetUtf8StreamFromResponseAsync(Task taskResponse) + { + // Is CofigureAwait the right thing here? + HttpResponseMessage response = await taskResponse.ConfigureAwait(false); + + // TODO: Is there any other validation that we should do? + if (response.StatusCode != HttpStatusCode.OK) + { + throw new Exception(); + } + + // TODO: Validate that this is Utf8 or plain text. + + return await response.Content.ReadAsStreamAsync().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 00000000000000..cb790e83de41d2 --- /dev/null +++ b/src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Post.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. + +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, + Type type, + object? value, + JsonSerializerOptions? options = null, + CancellationToken cancellationToken = default) + { + throw null!; + } + } +} 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 00000000000000..081bba3441297a --- /dev/null +++ b/src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/HttpContentJsonExtensions.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +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) + //{ + // //Stream jsonStream = content.; + // object retValue = JsonSerializer.DeserializeAsync(content.ReadAsStreamAsync(), type, options, cancellationToken); + //} + + //public static Task ReadFromJsonAsync( + // this HttpContent content, + // JsonSerializerOptions? options = null, + // CancellationToken cancellationToken = default) + //{ + + //} + + //private static async Task ReadCore(HttpContent content, JsonSerializerOptions? options, CancellationToken cancellationToken) + //{ + // Stream utf8Stream = content. + //} + } +} 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 00000000000000..376dfb986af817 --- /dev/null +++ b/src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/JsonContent.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http.Headers; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace System.Net.Http.Json +{ + /// + /// TODO + /// + public class JsonContent : HttpContent + { + private const string JsonMediaType = "application/json"; + + private readonly byte[] _content; + private readonly int _offset; + private readonly int _count; + + // Is this the declared or the runtime type? + // if it is the declared type, then is weird that this does not honor the type passed-in to the constructor. + // if it is the runtime type, then is weird that this does not honor the T type in the Create method. + public Type ObjectType { get; } + + public object? Value { get; } + + public JsonContent(Type type, object? value, JsonSerializerOptions? options = null) + : this(type, value, new MediaTypeHeaderValue(JsonMediaType), options) { } + + public JsonContent(Type type, object? value, string mediaType, JsonSerializerOptions? options = null) + : this(type, value, new MediaTypeHeaderValue(mediaType), options) { } + + // What if someone passes a weird Content-Type? + // Should we set mediaType.CharSet = UTF-8? + public JsonContent(Type type, object? value, MediaTypeHeaderValue mediaType, JsonSerializerOptions? options = null) + : this(JsonSerializer.SerializeToUtf8Bytes(value, type, options), type, value, mediaType) { } + + private JsonContent(byte[] content, Type type, object? value, MediaTypeHeaderValue mediaType) + { + _content = content; + _offset = 0; + _count = content.Length; + + Value = value; + ObjectType = type; + Headers.ContentType = mediaType; + } + + public static JsonContent Create(T value, JsonSerializerOptions? options = null) + => Create(value, new MediaTypeHeaderValue(JsonMediaType), options); + + public static JsonContent Create(T value, string mediaType, JsonSerializerOptions? options = null) + => Create(value, new MediaTypeHeaderValue(mediaType), options); + + public static JsonContent Create(T value, MediaTypeHeaderValue mediaType, JsonSerializerOptions? options = null) + => new JsonContent(JsonSerializer.SerializeToUtf8Bytes(value, options), typeof(T), value, new MediaTypeHeaderValue(JsonMediaType)); + + /// + /// Serialize the HTTP content to a stream as an asynchronous operation. + /// + /// + /// + /// + protected override Task SerializeToStreamAsync(Stream stream, TransportContext context) + => stream.WriteAsync(_content, _offset, _count); + + // Should this method even exist? or we just call WriteAsync from above method without cancellationToken? + // UPDATE: This does not exists on netstandard + // protected override Task SerializeToStreamAsync(Stream stream, TransportContext context, CancellationToken cancellationToken) + // => stream.WriteAsync(_content, _offset, _count, cancellationToken); + + + /// + /// TODO + /// + /// + /// + protected override bool TryComputeLength(out long length) + { + throw new NotImplementedException(); + } + } +} 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 00000000000000..18e40917bcc4d3 --- /dev/null +++ b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpClientJsonExtensionsTests.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Xunit; +using System.Net.Test.Common; +using System.Text.Json; + +namespace System.Net.Http.Json.Functional.Tests +{ + public class HttpClientJsonExtensionsTests + { + [Fact] + public async Task TestGetFromJsonAsync() + { + await LoopbackServer.CreateClientAndServerAsync( + async uri => + { + using (HttpClient client = new HttpClient()) + { + var deserializedObj = await client.GetFromJsonAsync(uri, typeof(object)); + Assert.NotNull(deserializedObj); + Assert.IsType(deserializedObj); + } + }, + server => server.HandleRequestAsync(content: "{}")); + } + } +} 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 00000000000000..620c77ddc5e511 --- /dev/null +++ b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/System.Net.Http.Json.Functional.Tests.csproj @@ -0,0 +1,49 @@ + + + $(NetCoreAppCurrent) + + + + + + + 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 + + + \ No newline at end of file From 2bc9c90a6021a13f457f48cd6908ba854f0d71bb Mon Sep 17 00:00:00 2001 From: David Cantu Date: Tue, 10 Mar 2020 10:11:46 -0700 Subject: [PATCH 02/32] Add JsonContent, HttpContent extensions, and tests. --- .../ref/System.Net.Http.Json.cs | 104 +++------ .../src/System.Net.Http.Json.csproj | 2 + .../Http/Json/HttpClientJsonExtensions.Get.cs | 45 +--- .../Json/HttpClientJsonExtensions.Helpers.cs | 25 +++ .../Json/HttpClientJsonExtensions.Post.cs | 52 ++++- .../Http/Json/HttpClientJsonExtensions.Put.cs | 59 +++++ .../Http/Json/HttpContentJsonExtensions.cs | 113 +++++++--- .../src/System/Net/Http/Json/JsonContent.cs | 56 +++-- ...HttpClientJsonExtensionsTests.Formatter.cs | 91 ++++++++ .../HttpClientJsonExtensionsTests.cs | 201 +++++++++++++++++- .../HttpContentJsonExtensionsTests.cs | 66 ++++++ .../tests/FunctionalTests/JsonContentTests.cs | 107 ++++++++++ .../tests/FunctionalTests/PErson.cs | 33 +++ ...stem.Net.Http.Json.Functional.Tests.csproj | 4 + 14 files changed, 795 insertions(+), 163 deletions(-) create mode 100644 src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Helpers.cs create mode 100644 src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Put.cs create mode 100644 src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpClientJsonExtensionsTests.Formatter.cs create mode 100644 src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpContentJsonExtensionsTests.cs create mode 100644 src/libraries/System.Net.Http.Json/tests/FunctionalTests/JsonContentTests.cs create mode 100644 src/libraries/System.Net.Http.Json/tests/FunctionalTests/PErson.cs 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 index 3d88de9f49cfc1..c6139e321fe752 100644 --- 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 @@ -5,87 +5,39 @@ // Changes to this file must follow the https://aka.ms/api-review process. // ------------------------------------------------------------------------------ -using System.IO; -using System.Net.Http.Headers; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; - namespace System.Net.Http.Json { - public static class HttpClientJsonExtensions + public static partial class HttpClientJsonExtensions { - public static Task GetFromJsonAsync( - this HttpClient client, - string requestUri, - Type type, - JsonSerializerOptions? options = null, - CancellationToken cancellationToken = default) - { - throw null; - } - - public static Task GetFromJsonAsync( - this HttpClient client, - Uri requestUri, - Type type, - JsonSerializerOptions? options = null, - CancellationToken cancellationToken = default) - { - throw null; - } - - public static Task GetFromJsonAsync( - this HttpClient client, - string requestUri, - JsonSerializerOptions? options = null, - CancellationToken cancellationToken = default) - { - throw null; - } - - public static Task GetFromJsonAsync( - this HttpClient client, - Uri requestUri, - JsonSerializerOptions? options = null, - CancellationToken cancellationToken = default) - { - throw null; - } - - public static Task PostAsJsonAsync( - this HttpClient client, - string requestUri, - Type type, - object? value, - JsonSerializerOptions? options = null, - CancellationToken cancellationToken = default) - { - throw null; - } + public static System.Threading.Tasks.Task GetFromJsonAsync(this System.Net.Http.HttpClient client, string requestUri, 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 GetFromJsonAsync(this System.Net.Http.HttpClient client, System.Uri requestUri, 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 GetFromJsonAsync(this System.Net.Http.HttpClient client, string requestUri, System.Text.Json.JsonSerializerOptions? options = null, 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 = 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, System.Type type, object 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, System.Type type, object? 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, T 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, T 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, System.Type type, object? 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, System.Type type, object? 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, T 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, T value, System.Text.Json.JsonSerializerOptions options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } } - - public static class HttpContentJsonExtensions { } - - public class JsonContent : HttpContent + public static partial class HttpContentJsonExtensions { - public Type ObjectType { get; } - public object? Value { get; } - - public static JsonContent Create(T value, JsonSerializerOptions options = null) { throw null; } - - public static JsonContent Create(T value, MediaTypeHeaderValue mediaType, JsonSerializerOptions options = null) { throw null; } - - public static JsonContent Create(T value, string mediaType, JsonSerializerOptions options = null) { throw null; } - - public JsonContent(Type type, object? value, JsonSerializerOptions options = null) { throw null; } - - public JsonContent(Type type, object? value, MediaTypeHeaderValue mediaType, JsonSerializerOptions options = null) { throw null; } - - public JsonContent(Type type, object? value, string mediaType, JsonSerializerOptions options = null) { throw null; } - - protected override Task SerializeToStreamAsync(Stream stream, TransportContext context) { throw null; } - + 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 partial class JsonContent : System.Net.Http.HttpContent + { + public JsonContent(System.Type type, object? value, System.Net.Http.Headers.MediaTypeHeaderValue mediaType, System.Text.Json.JsonSerializerOptions options = null) { } + public JsonContent(System.Type type, object? value, string mediaType, System.Text.Json.JsonSerializerOptions options = null) { } + public JsonContent(System.Type type, object? value, System.Text.Json.JsonSerializerOptions options = null) { } + public System.Type ObjectType { get { throw null; } } + public object? Value { get { throw null; } } + public static System.Net.Http.Json.JsonContent Create(T value, System.Net.Http.Headers.MediaTypeHeaderValue mediaType, System.Text.Json.JsonSerializerOptions options = null) { throw null; } + public static System.Net.Http.Json.JsonContent Create(T value, string mediaType, System.Text.Json.JsonSerializerOptions options = null) { throw null; } + public static System.Net.Http.Json.JsonContent Create(T value, 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/src/System.Net.Http.Json.csproj b/src/libraries/System.Net.Http.Json/src/System.Net.Http.Json.csproj index 641e7f8a978a11..6dd887f8b8410e 100644 --- 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 @@ -5,7 +5,9 @@ + + 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 index af7ac63733c506..90a626c92b07ff 100644 --- 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 @@ -2,7 +2,6 @@ // 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.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -31,7 +30,7 @@ public static partial class HttpClientJsonExtensions CancellationToken cancellationToken = default) { Task taskResponse = client.GetAsync(requestUri, cancellationToken); - return ProcessTaskResponseAsync(taskResponse, type, options, cancellationToken); + return GetFromJsonAsyncCore(taskResponse, type, options, cancellationToken); } /// @@ -51,7 +50,7 @@ public static partial class HttpClientJsonExtensions CancellationToken cancellationToken = default) { Task taskResponse = client.GetAsync(requestUri, cancellationToken); - return ProcessTaskResponseAsync(taskResponse, type, options, cancellationToken); + return GetFromJsonAsyncCore(taskResponse, type, options, cancellationToken); } /// @@ -70,7 +69,7 @@ public static Task GetFromJsonAsync( CancellationToken cancellationToken = default) { Task taskResponse = client.GetAsync(requestUri, cancellationToken); - return ProcessTaskResponseAsync(taskResponse, options, cancellationToken); + return GetFromJsonAsyncCore(taskResponse, options, cancellationToken); } /// @@ -89,44 +88,22 @@ public static Task GetFromJsonAsync( CancellationToken cancellationToken = default) { Task taskResponse = client.GetAsync(requestUri, cancellationToken); - return ProcessTaskResponseAsync(taskResponse, options, cancellationToken); + return GetFromJsonAsyncCore(taskResponse, options, cancellationToken); } - private static async Task ProcessTaskResponseAsync( - Task taskResponse, - JsonSerializerOptions? options = null, - CancellationToken cancellationToken = default) + private static async Task GetFromJsonAsyncCore(Task taskResponse, Type type, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) { - Stream jsonStream = await GetUtf8StreamFromResponseAsync(taskResponse).ConfigureAwait(false); - TValue value = await JsonSerializer.DeserializeAsync(jsonStream, options, cancellationToken); - return value; - } + HttpResponseMessage response = await taskResponse.ConfigureAwait(false); + response.EnsureSuccessStatusCode(); - private static async Task ProcessTaskResponseAsync( - Task taskResponse, - Type type, - JsonSerializerOptions? options = null, - CancellationToken cancellationToken = default) - { - Stream jsonStream = await GetUtf8StreamFromResponseAsync(taskResponse).ConfigureAwait(false); - object? value = await JsonSerializer.DeserializeAsync(jsonStream, type, options, cancellationToken); - return value; + return await response.Content.ReadFromJsonAsync(type, options, cancellationToken).ConfigureAwait(false); } - - private static async Task GetUtf8StreamFromResponseAsync(Task taskResponse) + private static async Task GetFromJsonAsyncCore(Task taskResponse, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) { - // Is CofigureAwait the right thing here? HttpResponseMessage response = await taskResponse.ConfigureAwait(false); + response.EnsureSuccessStatusCode(); - // TODO: Is there any other validation that we should do? - if (response.StatusCode != HttpStatusCode.OK) - { - throw new Exception(); - } - - // TODO: Validate that this is Utf8 or plain text. - - return await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + return await response.Content.ReadFromJsonAsync(options, cancellationToken).ConfigureAwait(false); } } } diff --git a/src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Helpers.cs b/src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Helpers.cs new file mode 100644 index 00000000000000..5701e5f5644e1b --- /dev/null +++ b/src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Helpers.cs @@ -0,0 +1,25 @@ +// 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 + { + #region Post/Put helpers + private static JsonContent CreateJsonContent(Type type, object? value, JsonSerializerOptions? options) + { + return new JsonContent(type, value, options); + } + + private static JsonContent CreateJsonContent(T value, JsonSerializerOptions? options) + { + return JsonContent.Create(value, options); + } + #endregion + } +} 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 index cb790e83de41d2..e12829f73e551d 100644 --- 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 @@ -10,15 +10,51 @@ namespace System.Net.Http.Json { public static partial class HttpClientJsonExtensions { - public static Task PostAsJsonAsync( - this HttpClient client, - string requestUri, - Type type, - object? value, - JsonSerializerOptions? options = null, - CancellationToken cancellationToken = default) + // It would be nice to funnel object methods to the generic methods, but since I choose to make JsonContent.ObjectType = type arg on object signatures; we can't funnel + // and therefore validations need to be duplicated. + + public static Task PostAsJsonAsync(this HttpClient client, string requestUri, Type type, object? value, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) + { + if (client == null) + { + throw new ArgumentNullException(nameof(client)); + } + + JsonContent content = CreateJsonContent(type, value, options); + return client.PostAsync(requestUri, content, cancellationToken); + } + + public static Task PostAsJsonAsync(this HttpClient client, Uri requestUri, Type type, object? value, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) + { + if (client == null) + { + throw new ArgumentNullException(nameof(client)); + } + + JsonContent content = CreateJsonContent(type, value, options); + return client.PostAsync(requestUri, content, cancellationToken); + } + + public static Task PostAsJsonAsync(this HttpClient client, string requestUri, T value, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) { - throw null!; + if (client == null) + { + throw new ArgumentNullException(nameof(client)); + } + + JsonContent content = CreateJsonContent(value, options); + return client.PostAsync(requestUri, content, cancellationToken); + } + + public static Task PostAsJsonAsync(this HttpClient client, Uri requestUri, T value, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) + { + if (client == null) + { + throw new ArgumentNullException(nameof(client)); + } + + JsonContent content = CreateJsonContent(value, options); + return client.PostAsync(requestUri, content, 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 00000000000000..0e82bd3ac18731 --- /dev/null +++ b/src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Put.cs @@ -0,0 +1,59 @@ +// 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, + Type type, + object? value, + JsonSerializerOptions? options = null, + CancellationToken cancellationToken = default) + { + JsonContent content = CreateJsonContent(type, value, options); + return client.PutAsync(requestUri, content, cancellationToken); + } + + public static Task PutAsJsonAsync( + this HttpClient client, + Uri requestUri, + Type type, + object? value, + JsonSerializerOptions? options = null, + CancellationToken cancellationToken = default) + { + JsonContent content = CreateJsonContent(type, value, options); + return client.PutAsync(requestUri, content, cancellationToken); + } + + public static Task PutAsJsonAsync( + this HttpClient client, + string requestUri, + T value, + JsonSerializerOptions? options = null, + CancellationToken cancellationToken = default) + { + JsonContent content = CreateJsonContent(value, options); + return client.PutAsync(requestUri, content, cancellationToken); + } + + public static Task PutAsJsonAsync( + this HttpClient client, + Uri requestUri, + T value, + JsonSerializerOptions? options = null, + CancellationToken cancellationToken = default) + { + JsonContent content = CreateJsonContent(value, options); + return client.PutAsync(requestUri, content, 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 index 081bba3441297a..c8f33f1f12d44a 100644 --- 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 @@ -1,7 +1,7 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; +// 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; using System.Text.Json; using System.Threading; @@ -11,27 +11,88 @@ namespace System.Net.Http.Json { public static class HttpContentJsonExtensions { - //public static Task ReadFromJsonAsync( - // this HttpContent content, - // Type type, - // JsonSerializerOptions? options = null, - // CancellationToken cancellationToken = default) - //{ - // //Stream jsonStream = content.; - // object retValue = JsonSerializer.DeserializeAsync(content.ReadAsStreamAsync(), type, options, cancellationToken); - //} - - //public static Task ReadFromJsonAsync( - // this HttpContent content, - // JsonSerializerOptions? options = null, - // CancellationToken cancellationToken = default) - //{ - - //} - - //private static async Task ReadCore(HttpContent content, JsonSerializerOptions? options, CancellationToken cancellationToken) - //{ - // Stream utf8Stream = content. - //} + public static Task ReadFromJsonAsync( + this HttpContent content, + Type type, + JsonSerializerOptions? options = null, + CancellationToken cancellationToken = default) + { + return ReadFromJsonAsyncCore(content, type, options, cancellationToken); + } + + public static Task ReadFromJsonAsync( + this HttpContent content, + JsonSerializerOptions? options = null, + CancellationToken cancellationToken = default) + { + return ReadFromJsonAsyncCore(content, options, cancellationToken); + } + + private static async Task ReadFromJsonAsyncCore(HttpContent content, Type type, JsonSerializerOptions? options, CancellationToken cancellationToken) + { + byte[] contentBytes = await GetUtf8JsonBytesFromContentAsync(content, cancellationToken).ConfigureAwait(false); + // NOTE: I wanted to use DeserializeAsync and ReadAsStreamAsync, but if we need to transcode, we need the whole content, so it makes more sense to use ReadAsByteArrayAsync. + //Stream utf8Stream = await ReadStreamFromContent(content).ConfigureAwait(false); + //return await JsonSerializer.DeserializeAsync(utf8Stream, type, options, cancellationToken); + return JsonSerializer.Deserialize(contentBytes, type, options); + } + + private static async Task ReadFromJsonAsyncCore(HttpContent content, JsonSerializerOptions? options, CancellationToken cancellationToken) + { + byte[] contentBytes = await GetUtf8JsonBytesFromContentAsync(content, cancellationToken).ConfigureAwait(false); + return JsonSerializer.Deserialize(contentBytes, options)!; + } + + private static async Task GetUtf8JsonBytesFromContentAsync(HttpContent content, CancellationToken cancellationToken) + { + string? mediaType = content.Headers.ContentType?.MediaType; + // if Content-Type == null we assume it as "application/octet-stream" in accordance with section 7.2.1 of the HTTP spec. + // This is how Formatting API works + // And at the same time, it is contrary to how HttpContent.ReadAsStringAsync works. + // IMO, this should default to application/json + if (mediaType != "application/json" && + mediaType != "text/plain") + { + throw new NotSupportedException("The provided ContentType is not supported; the supported types are 'application/json' and 'text/plain'."); + } + + // https://source.dot.net/#System.Net.Http/System/Net/Http/HttpContent.cs,047409be2a4d70a8 + string? charset = content.Headers.ContentType!.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); + } + + // Byte-order-mark (BOM) characters may be present even if a charset was specified. + // bomLength = GetPreambleLength(buffer, encoding); + } + catch (ArgumentException e) + { + throw new InvalidOperationException("The character set provided in ContentType is invalid.", e); + } + } + + byte[] contentBytes = await content.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false); + + // Transcode to UTF-8. + if (encoding != null && encoding != Encoding.UTF8) + { + contentBytes = Encoding.Convert(encoding, Encoding.UTF8, contentBytes); + } + + return contentBytes; + } } } 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 index 376dfb986af817..a6f98f17331a63 100644 --- 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 @@ -1,11 +1,11 @@ -using System; -using System.Collections.Generic; +// 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.Linq; using System.Net.Http.Headers; using System.Text; using System.Text.Json; -using System.Threading; using System.Threading.Tasks; namespace System.Net.Http.Json @@ -21,26 +21,53 @@ public class JsonContent : HttpContent private readonly int _offset; private readonly int _count; - // Is this the declared or the runtime type? - // if it is the declared type, then is weird that this does not honor the type passed-in to the constructor. - // if it is the runtime type, then is weird that this does not honor the T type in the Create method. + private static MediaTypeHeaderValue CreateMediaType(string mediaTypeAsString) + { + //MediaTypeHeaderValue mediaType = new MediaTypeHeaderValue(mediaTypeAsString); // this one is used by the Formatting API. + MediaTypeHeaderValue mediaType = MediaTypeHeaderValue.Parse(mediaTypeAsString); + + // If the instantiated mediaType does not specify its CharSet, set UTF-8 by default. + if (mediaType.CharSet == null) + { + mediaType.CharSet = Encoding.UTF8.WebName; + } + + return mediaType; + } + + // When Create is callled, this is the typeof(T). + // When .ctor is called, this is the specified type argument. + // As per Formatting, this is always the declared type. public Type ObjectType { get; } public object? Value { get; } public JsonContent(Type type, object? value, JsonSerializerOptions? options = null) - : this(type, value, new MediaTypeHeaderValue(JsonMediaType), options) { } + : this(type, value, CreateMediaType(JsonMediaType), options) { } + /// + /// TODO + /// + /// + /// + /// The authoritative value of the request's content's Content-Type header. Can be null in which case the default content type will be used. + /// public JsonContent(Type type, object? value, string mediaType, JsonSerializerOptions? options = null) - : this(type, value, new MediaTypeHeaderValue(mediaType), options) { } + : this(type, value, CreateMediaType(mediaType?? throw new ArgumentNullException(nameof(mediaType))), options) { } // What if someone passes a weird Content-Type? // Should we set mediaType.CharSet = UTF-8? + // Formatting allows it. public JsonContent(Type type, object? value, MediaTypeHeaderValue mediaType, JsonSerializerOptions? options = null) - : this(JsonSerializer.SerializeToUtf8Bytes(value, type, options), type, value, mediaType) { } + : this(JsonSerializer.SerializeToUtf8Bytes(value, type, options), type, value, mediaType ?? throw new ArgumentNullException(nameof(mediaType))) { } private JsonContent(byte[] content, Type type, object? value, MediaTypeHeaderValue mediaType) { + if (mediaType == null) + { + throw new ArgumentNullException(nameof(mediaType)); + } + _content = content; _offset = 0; _count = content.Length; @@ -51,13 +78,13 @@ private JsonContent(byte[] content, Type type, object? value, MediaTypeHeaderVal } public static JsonContent Create(T value, JsonSerializerOptions? options = null) - => Create(value, new MediaTypeHeaderValue(JsonMediaType), options); + => Create(value, CreateMediaType(JsonMediaType), options); public static JsonContent Create(T value, string mediaType, JsonSerializerOptions? options = null) - => Create(value, new MediaTypeHeaderValue(mediaType), options); + => Create(value, CreateMediaType(mediaType ?? throw new ArgumentNullException(nameof(mediaType))), options); public static JsonContent Create(T value, MediaTypeHeaderValue mediaType, JsonSerializerOptions? options = null) - => new JsonContent(JsonSerializer.SerializeToUtf8Bytes(value, options), typeof(T), value, new MediaTypeHeaderValue(JsonMediaType)); + => new JsonContent(JsonSerializer.SerializeToUtf8Bytes(value, options), typeof(T), value, mediaType ?? throw new ArgumentNullException(nameof(mediaType))); /// /// Serialize the HTTP content to a stream as an asynchronous operation. @@ -81,7 +108,8 @@ protected override Task SerializeToStreamAsync(Stream stream, TransportContext c /// protected override bool TryComputeLength(out long length) { - throw new NotImplementedException(); + length = _count; + return true; } } } diff --git a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpClientJsonExtensionsTests.Formatter.cs b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpClientJsonExtensionsTests.Formatter.cs new file mode 100644 index 00000000000000..231b3a1913092c --- /dev/null +++ b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpClientJsonExtensionsTests.Formatter.cs @@ -0,0 +1,91 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +// 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; + +namespace System.Net.Http.Json.Functional.Tests +{ + // Tests taken from https://github.com/aspnet/AspNetWebStack/blob/master/test/System.Net.Http.Formatting.Test/HttpClientExtensionsTest.cs + public class HttpClientExtensionsTest + { + //private readonly MediaTypeFormatter _formatter = new MockMediaTypeFormatter { CallBase = true }; + private readonly HttpClient _client = new HttpClient(); + // TODO: Use this for JsonContent unit tests + //private readonly MediaTypeHeaderValue _mediaTypeHeader = MediaTypeHeaderValue.Parse("foo/bar; charset=utf-16"); + + //public HttpClientExtensionsTest() + //{ + // Mock handlerMock = new Mock { CallBase = true }; + // handlerMock + // .Setup(h => h.SendAsyncPublic(It.IsAny(), It.IsAny())) + // .Returns((HttpRequestMessage request, CancellationToken _) => Task.FromResult(new HttpResponseMessage() { RequestMessage = request })); + + // _client = new HttpClient(handlerMock.Object); + //} + + [Fact] + public async Task PostAsJsonAsync_String_WhenClientIsNull_ThrowsException() + { + HttpClient client = null; + + ArgumentNullException ex = await Assert.ThrowsAsync(() => client.PostAsJsonAsync("http://www.example.com", new object())); + Assert.Equal("client", ex.ParamName); + } + + [Fact] + public void PostAsJsonAsync_String_WhenUriIsNull_ThrowsExceptionAsync() + { + Assert.ThrowsAsync(() => _client.PostAsJsonAsync((string)null, new object())); + } + + [Fact] + public async Task PostAsJsonAsync_Uri_WhenUriIsNull_ThrowsException() + { + await Assert.ThrowsAsync(() => _client.PostAsJsonAsync((Uri)null, new object())); + } + + [Fact] + public async Task PostAsJsonAsync_String_UsesJsonMediaTypeFormatter() + { + var response = await _client.PostAsJsonAsync("http://example.com", new object()); + + JsonContent content = Assert.IsType(response.RequestMessage.Content); + //Assert.IsType(content.Formatter); + //?? + } + + [Fact] + public async Task PostAsync_String_WhenRequestUriIsSet_CreatesRequestWithAppropriateUri() + { + _client.BaseAddress = new Uri("http://example.com/"); + + var response = await _client.PostAsJsonAsync("myapi/", new object()); + + var request = response.RequestMessage; + Assert.Equal("http://example.com/myapi/", request.RequestUri.ToString()); + } + + [Fact] + public async Task PostAsJsonAsync_Uri_WhenClientIsNull_ThrowsExceptionAsync() + { + HttpClient client = null; + + ArgumentNullException ex = await Assert.ThrowsAsync(() => client.PostAsJsonAsync(new Uri("http://www.example.com"), new object())); + Assert.Equal("client", ex.ParamName); + } + + [Fact] + public async Task PostAsJsonAsync_Uri_UsesJsonMediaTypeFormatter() + { + var response = await _client.PostAsJsonAsync(new Uri("http://example.com"), new object()); + + var content = Assert.IsType(response.RequestMessage.Content); + //Assert.IsType(content.Formatter); + } + } +} diff --git a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpClientJsonExtensionsTests.cs b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpClientJsonExtensionsTests.cs index 18e40917bcc4d3..02017c9eb36b49 100644 --- a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpClientJsonExtensionsTests.cs +++ b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpClientJsonExtensionsTests.cs @@ -1,27 +1,218 @@ -using System.Collections.Generic; +// 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.Text; +using System.IO; namespace System.Net.Http.Json.Functional.Tests { public class HttpClientJsonExtensionsTests { + private void ValidateRequest(HttpRequestData requestData) + { + Assert.NotNull(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 TestGetFromJsonAsync() { + const string json = @"{""Name"":""David"",""Age"":24}"; + + await LoopbackServer.CreateClientAndServerAsync( + async uri => + { + using (HttpClient client = new HttpClient()) + { + Person obj = (Person)await client.GetFromJsonAsync(uri, typeof(Person)); + Assert.NotNull(obj); + obj.Validate(); + } + }, + server => server.HandleRequestAsync(content: json)); + + await LoopbackServer.CreateClientAndServerAsync( + async uri => + { + using (HttpClient client = new HttpClient()) + { + Person obj = await client.GetFromJsonAsync(uri); + Assert.NotNull(obj); + obj.Validate(); + } + }, + server => server.HandleRequestAsync(content: json)); + } + + //[Fact] + //public async Task TestGetFromJsonAsyncNotJsonContent() + //{ + // HttpClient client = new HttpClient(); + // await Assert.ThrowsAsync(() => client.GetFromJsonAsync("http://example.com", typeof(Person))); + // await Assert.ThrowsAsync(() => client.GetFromJsonAsync("http://example.com")); + + // Uri uri = new Uri("http://example.com"); + // await Assert.ThrowsAsync(() => client.GetFromJsonAsync(uri, typeof(Person))); + // await Assert.ThrowsAsync(() => client.GetFromJsonAsync(uri)); + //} + + // add tests with non-json content type. and stress content-type application/json and text/plain. + [Fact] + public async Task TestGetFromJsonAsyncUnsuccessfulResponse() + { + 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 => { + HttpRequestData req = await server.HandleRequestAsync(statusCode: HttpStatusCode.InternalServerError); + }); + } + + [Fact] + public async Task TestGetFromJsonAsyncTextPlainUtf16() + { + const string json = @"{""Name"":""David"",""Age"":24}"; + await LoopbackServer.CreateClientAndServerAsync( + async uri => + { + using (HttpClient client = new HttpClient()) + { + Person per = Assert.IsType(await client.GetFromJsonAsync(uri, typeof(Person))); + per.Validate(); + } + }, + async server => { + byte[] nonUtf8Response = Encoding.Unicode.GetBytes(json); + var buffer = new MemoryStream(); + buffer.Write( + Encoding.ASCII.GetBytes( + $"HTTP/1.1 200 OK\r\nContent-Type: text/plain; charset=utf-16\r\nContent-Length: {nonUtf8Response.Length}\r\nConnection:close\r\n\r\n")); + buffer.Write(nonUtf8Response); + + await server.AcceptConnectionSendCustomResponseAndCloseAsync(buffer.ToArray()); + }); + } + + [Fact] + public async Task TestGetFromJsonAsyncGenericTextPlainUtf16() + { + const string json = @"{""Name"":""David"",""Age"":24}"; + await LoopbackServer.CreateClientAndServerAsync( + async uri => + { + using (HttpClient client = new HttpClient()) + { + Person per = await client.GetFromJsonAsync(uri); + per.Validate(); + } + }, + async server => { + byte[] nonUtf8Response = Encoding.Unicode.GetBytes(json); + var buffer = new MemoryStream(); + buffer.Write( + Encoding.ASCII.GetBytes( + $"HTTP/1.1 200 OK\r\nContent-Type: text/plain; charset=utf-16\r\nContent-Length: {nonUtf8Response.Length}\r\nConnection:close\r\n\r\n")); + buffer.Write(nonUtf8Response); + + await server.AcceptConnectionSendCustomResponseAndCloseAsync(buffer.ToArray()); + }); + } + + [Fact] + public async Task TestPostAsJsonAsync() + { + await LoopbackServer.CreateClientAndServerAsync( + async uri => + { + using (HttpClient client = new HttpClient()) + { + HttpResponseMessage response = await client.PostAsJsonAsync(uri, typeof(Person), Person.Create()); + + Assert.True(response.StatusCode == HttpStatusCode.OK); + } + }, + async server => { + HttpRequestData req = await server.HandleRequestAsync(); + ValidateRequest(req); + + Person obj = JsonSerializer.Deserialize(req.Body); + Assert.NotNull(obj); + obj.Validate(); + }); + await LoopbackServer.CreateClientAndServerAsync( async uri => { using (HttpClient client = new HttpClient()) { - var deserializedObj = await client.GetFromJsonAsync(uri, typeof(object)); - Assert.NotNull(deserializedObj); - Assert.IsType(deserializedObj); + HttpResponseMessage response = await client.PostAsJsonAsync(uri, Person.Create()); + Assert.True(response.StatusCode == HttpStatusCode.OK); } }, - server => server.HandleRequestAsync(content: "{}")); + async server => { + HttpRequestData req = await server.HandleRequestAsync(); + ValidateRequest(req); + + Person obj = JsonSerializer.Deserialize(req.Body); + Assert.NotNull(obj); + obj.Validate(); + }); + } + + [Fact] + public async Task TestPutAsJsonAsync() + { + await LoopbackServer.CreateClientAndServerAsync( + async uri => + { + using (HttpClient client = new HttpClient()) + { + Person obj = Person.Create(); + HttpResponseMessage response = await client.PutAsJsonAsync(uri, typeof(Person), obj); + + Assert.True(response.StatusCode == HttpStatusCode.OK); + } + }, + async server => { + HttpRequestData req = await server.HandleRequestAsync(); + ValidateRequest(req); + + Person obj = JsonSerializer.Deserialize(req.Body); + Assert.NotNull(obj); + obj.Validate(); + }); + + await LoopbackServer.CreateClientAndServerAsync( + async uri => + { + using (HttpClient client = new HttpClient()) + { + Person obj = Person.Create(); + HttpResponseMessage response = await client.PutAsJsonAsync(uri, obj); + Assert.True(response.StatusCode == HttpStatusCode.OK); + } + }, + async server => { + HttpRequestData req = await server.HandleRequestAsync(); + ValidateRequest(req); + + Person obj = JsonSerializer.Deserialize(req.Body); + Assert.NotNull(obj); + obj.Validate(); + }); } } } 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 00000000000000..25608fa6b025cf --- /dev/null +++ b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpContentJsonExtensionsTests.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.Net.Test.Common; +using System.Threading.Tasks; +using Xunit; + +namespace System.Net.Http.Json.Functional.Tests +{ + public class HttpContentJsonExtensionsTests + { + public async Task HttpContentGetPersonAsync() + { + HttpContent content = null; + await LoopbackServer.CreateClientAndServerAsync( + async uri => + { + using (HttpClient client = new HttpClient()) + { + var request = new HttpRequestMessage(HttpMethod.Get, $"getPerson"); + var response = await client.SendAsync(request); + Assert.True(response.StatusCode == HttpStatusCode.OK); + + content = response.Content; + } + }, + async server => { + HttpRequestData req = await server.HandleRequestAsync(content: Person.Create().Serialize()); + }); + + object obj = await content.ReadFromJsonAsync(typeof(Person)); + Person per = Assert.IsType(obj); + per.Validate(); + + per = await content.ReadFromJsonAsync(); + per.Validate(); + } + + public async Task HttpContentTypeIsNull() + { + HttpContent content = null; + await LoopbackServer.CreateClientAndServerAsync( + async uri => + { + using (HttpClient client = new HttpClient()) + { + var request = new HttpRequestMessage(HttpMethod.Get, "getPerson"); + var response = await client.SendAsync(request); + Assert.True(response.StatusCode == HttpStatusCode.OK); + + content = response.Content; + } + }, + async server => { + HttpRequestData req = await server.HandleRequestAsync(content: "null"); + }); + + object obj = await content.ReadFromJsonAsync(typeof(Person)); + Assert.Null(obj); + + Person per = await content.ReadFromJsonAsync(); + Assert.Null(per); + } + } +} 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 00000000000000..27fd497401c3f7 --- /dev/null +++ b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/JsonContentTests.cs @@ -0,0 +1,107 @@ +// 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 Xunit; + +namespace System.Net.Http.Json.Functional.Tests +{ + public class JsonContentTests + { + private const string JsonContentType = "foo/bar; charset=utf-16"; + private readonly MediaTypeHeaderValue s_mediaTypeHeader = MediaTypeHeaderValue.Parse(JsonContentType); + + private class Foo { } + private class Bar { } + + [Fact] + public void JsonContentObjectType() + { + Type fooType = typeof(Foo); + Foo foo = new Foo(); + JsonContent content = new JsonContent(fooType, foo); + Assert.Equal(fooType, content.ObjectType); + + content = JsonContent.Create(foo); + Assert.Equal(fooType, content.ObjectType); + + object fooBoxed = foo; + + // ObjectType is the specified type when using the .ctor. + content = new JsonContent(fooType, fooBoxed); + Assert.Equal(fooType, content.ObjectType); + + // ObjectType is the declared type when using the factory method. + content = JsonContent.Create(fooBoxed); + Assert.Equal(typeof(object), content.ObjectType); + } + + [Fact] + public void JsonContentMediaType() + { + Type fooType = typeof(Foo); + Foo foo = new Foo(); + + JsonContent content = new JsonContent(fooType, foo, mediaType: s_mediaTypeHeader); + Assert.Same(s_mediaTypeHeader, content.Headers.ContentType); + + content = JsonContent.Create(foo, mediaType: s_mediaTypeHeader); + Assert.Same(s_mediaTypeHeader, content.Headers.ContentType); + + string mediaTypeAsString = s_mediaTypeHeader.MediaType; + + content = new JsonContent(fooType, foo, mediaType: mediaTypeAsString); + Assert.Equal(mediaTypeAsString, content.Headers.ContentType.MediaType); + Assert.Equal("utf-8", content.Headers.ContentType.CharSet); + + content = JsonContent.Create(foo, mediaType: mediaTypeAsString); + Assert.Equal(mediaTypeAsString, content.Headers.ContentType.MediaType); + Assert.Equal("utf-8", content.Headers.ContentType.CharSet); + + // JsonContentType define its own charset. + content = new JsonContent(fooType, foo, mediaType: JsonContentType); + Assert.Equal(s_mediaTypeHeader.MediaType, content.Headers.ContentType.MediaType); + Assert.Equal(s_mediaTypeHeader.CharSet, content.Headers.ContentType.CharSet); + + content = JsonContent.Create(foo, mediaType: JsonContentType); + Assert.Equal(s_mediaTypeHeader.MediaType, content.Headers.ContentType.MediaType); + Assert.Equal(s_mediaTypeHeader.CharSet, content.Headers.ContentType.CharSet); + } + + [Fact] + public void JsonContentMediaTypeIsNull() + { + Type fooType = typeof(Foo); + Foo foo = new Foo(); + + ArgumentNullException ex = Assert.Throws(() => new JsonContent(fooType, foo, mediaType: (MediaTypeHeaderValue)null)); + Assert.Equal("mediaType", ex.ParamName); + + ex = Assert.Throws(() => JsonContent.Create(foo, mediaType: (MediaTypeHeaderValue)null)); + Assert.Equal("mediaType", ex.ParamName); + + string mediaTypeAsString = s_mediaTypeHeader.MediaType; + ex = Assert.Throws(() => new JsonContent(fooType, foo, mediaType: (string)null)); + Assert.Equal("mediaType", ex.ParamName); + + ex = Assert.Throws(() => JsonContent.Create(foo, mediaType: (string)null)); + Assert.Equal("mediaType", ex.ParamName); + } + + [Fact] + public void JsonContentTypeIsNull() + { + Assert.Throws(() => new JsonContent(null, null)); + Assert.Throws(() => new JsonContent(null, null, s_mediaTypeHeader)); + } + + [Fact] + public void JsonContentThrowsOnIncompatibleType() + { + var foo = new Foo(); + Assert.Throws(() => new JsonContent(typeof(Bar), foo)); + Assert.Throws(() => new JsonContent(typeof(Bar), foo, s_mediaTypeHeader)); + } + } +} diff --git a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/PErson.cs b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/PErson.cs new file mode 100644 index 00000000000000..a8595866c89c82 --- /dev/null +++ b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/PErson.cs @@ -0,0 +1,33 @@ +// 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 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); + } + } +} 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 index 620c77ddc5e511..86f8a777c238b9 100644 --- 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 @@ -45,5 +45,9 @@ Common\System\Threading\Tasks\TaskTimeoutExtensions.cs + + + + \ No newline at end of file From db0edd580ad020e93740cea5de0008105b52c364 Mon Sep 17 00:00:00 2001 From: David Cantu Date: Tue, 10 Mar 2020 10:26:03 -0700 Subject: [PATCH 03/32] Rename PErson.cs to Person.cs --- .../tests/FunctionalTests/{PErson.cs => Person.cs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/libraries/System.Net.Http.Json/tests/FunctionalTests/{PErson.cs => Person.cs} (100%) diff --git a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/PErson.cs b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/Person.cs similarity index 100% rename from src/libraries/System.Net.Http.Json/tests/FunctionalTests/PErson.cs rename to src/libraries/System.Net.Http.Json/tests/FunctionalTests/Person.cs From 25dfec561dbbaed4c721fd2d524c5b3a495e7223 Mon Sep 17 00:00:00 2001 From: Jose Perez Rodriguez Date: Tue, 10 Mar 2020 12:05:44 -0700 Subject: [PATCH 04/32] Adding package for System.Net.Http.Json library --- .../pkg/System.Net.Http.Json.pkgproj | 10 ++++++++++ .../src/System.Net.Http.Json.csproj | 3 ++- .../System/Net/Http/Json/HttpContentJsonExtensions.cs | 2 +- src/libraries/pkg/baseline/packageIndex.json | 6 ++++++ src/libraries/pkg/descriptions.json | 7 +++++++ 5 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 src/libraries/System.Net.Http.Json/pkg/System.Net.Http.Json.pkgproj 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 00000000000000..eba4cc39e9f90b --- /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/src/System.Net.Http.Json.csproj b/src/libraries/System.Net.Http.Json/src/System.Net.Http.Json.csproj index 6dd887f8b8410e..5d470ea18b6420 100644 --- 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 @@ -1,6 +1,6 @@  - $(NetCoreAppCurrent) + netstandard2.0 enable @@ -16,5 +16,6 @@ + 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 index c8f33f1f12d44a..2c2a85fbb03287 100644 --- 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 @@ -84,7 +84,7 @@ private static async Task GetUtf8JsonBytesFromContentAsync(HttpContent c } } - byte[] contentBytes = await content.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false); + byte[] contentBytes = await content.ReadAsByteArrayAsync().ConfigureAwait(false); // Transcode to UTF-8. if (encoding != null && encoding != Encoding.UTF8) diff --git a/src/libraries/pkg/baseline/packageIndex.json b/src/libraries/pkg/baseline/packageIndex.json index 7114331622978e..6274acb2d8fe06 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 45937674b626f1..6ced05e0aece25 100644 --- a/src/libraries/pkg/descriptions.json +++ b/src/libraries/pkg/descriptions.json @@ -1229,6 +1229,13 @@ "System.Net.HttpListener" ] }, + { + "Name": "System.Net.Http.Json", + "Description": "ToDO: Insert the package description approved by Immo here", + "CommonTypes": [ + "ToDo: Insert Commonly Used types here" + ] + }, { "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.", From 89989711cd14a11dad81fb9ad59a05b3df376235 Mon Sep 17 00:00:00 2001 From: David Cantu Date: Tue, 10 Mar 2020 17:04:42 -0700 Subject: [PATCH 05/32] Improve tests, minor fixes and code clean-up. --- .../ref/System.Net.Http.Json.csproj | 10 - .../Http/Json/HttpClientJsonExtensions.Get.cs | 22 +- .../Http/Json/HttpClientJsonExtensions.Put.cs | 20 ++ .../Http/Json/HttpContentJsonExtensions.cs | 8 +- .../src/System/Net/Http/Json/JsonContent.cs | 14 +- .../HttpClientJsonExtensionsTests.cs | 230 ++++++++---------- .../HttpContentJsonExtensionsTests.cs | 126 +++++++++- .../tests/FunctionalTests/JsonContentTests.cs | 106 ++++++-- ...stem.Net.Http.Json.Functional.Tests.csproj | 12 - 9 files changed, 352 insertions(+), 196 deletions(-) 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 index 3102059dd7a502..f43f195e8ad003 100644 --- 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 @@ -9,14 +9,4 @@ - - 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 index 90a626c92b07ff..a0f77ba51b5486 100644 --- 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 @@ -29,6 +29,11 @@ public static partial class HttpClientJsonExtensions JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) { + if (client == null) + { + throw new ArgumentNullException(nameof(client)); + } + Task taskResponse = client.GetAsync(requestUri, cancellationToken); return GetFromJsonAsyncCore(taskResponse, type, options, cancellationToken); } @@ -49,6 +54,11 @@ public static partial class HttpClientJsonExtensions JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) { + if (client == null) + { + throw new ArgumentNullException(nameof(client)); + } + Task taskResponse = client.GetAsync(requestUri, cancellationToken); return GetFromJsonAsyncCore(taskResponse, type, options, cancellationToken); } @@ -68,7 +78,12 @@ public static Task GetFromJsonAsync( JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) { - Task taskResponse = client.GetAsync(requestUri, cancellationToken); + if (client == null) + { + throw new ArgumentNullException(nameof(client)); + } + + Task taskResponse = client.GetAsync(requestUri, cancellationToken); return GetFromJsonAsyncCore(taskResponse, options, cancellationToken); } @@ -87,6 +102,11 @@ public static Task GetFromJsonAsync( JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) { + if (client == null) + { + throw new ArgumentNullException(nameof(client)); + } + Task taskResponse = client.GetAsync(requestUri, cancellationToken); return GetFromJsonAsyncCore(taskResponse, options, 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 index 0e82bd3ac18731..bfa5f3f4ed62b7 100644 --- 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 @@ -18,6 +18,11 @@ public static Task PutAsJsonAsync( JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) { + if (client == null) + { + throw new ArgumentNullException(nameof(client)); + } + JsonContent content = CreateJsonContent(type, value, options); return client.PutAsync(requestUri, content, cancellationToken); } @@ -30,6 +35,11 @@ public static Task PutAsJsonAsync( JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) { + if (client == null) + { + throw new ArgumentNullException(nameof(client)); + } + JsonContent content = CreateJsonContent(type, value, options); return client.PutAsync(requestUri, content, cancellationToken); } @@ -41,6 +51,11 @@ public static Task PutAsJsonAsync( JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) { + if (client == null) + { + throw new ArgumentNullException(nameof(client)); + } + JsonContent content = CreateJsonContent(value, options); return client.PutAsync(requestUri, content, cancellationToken); } @@ -52,6 +67,11 @@ public static Task PutAsJsonAsync( JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) { + if (client == null) + { + throw new ArgumentNullException(nameof(client)); + } + JsonContent content = CreateJsonContent(value, options); return client.PutAsync(requestUri, content, 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 index 2c2a85fbb03287..527ae391a75c83 100644 --- 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 @@ -2,6 +2,7 @@ // 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.Mime; using System.Text; using System.Text.Json; using System.Threading; @@ -48,10 +49,11 @@ private static async Task GetUtf8JsonBytesFromContentAsync(HttpContent c string? mediaType = content.Headers.ContentType?.MediaType; // if Content-Type == null we assume it as "application/octet-stream" in accordance with section 7.2.1 of the HTTP spec. // This is how Formatting API works - // And at the same time, it is contrary to how HttpContent.ReadAsStringAsync works. + + // And at the same time, it is contrary to how HttpContent.ReadAsStringAsync works (allows null content-type and tries to read content using UTF-8 in that case). // IMO, this should default to application/json - if (mediaType != "application/json" && - mediaType != "text/plain") + if (mediaType != JsonContent.JsonMediaType && + mediaType != MediaTypeNames.Text.Plain) { throw new NotSupportedException("The provided ContentType is not supported; the supported types are 'application/json' and 'text/plain'."); } 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 index a6f98f17331a63..a2e248a4cfd9c9 100644 --- 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 @@ -15,7 +15,7 @@ namespace System.Net.Http.Json /// public class JsonContent : HttpContent { - private const string JsonMediaType = "application/json"; + internal const string JsonMediaType = "application/json"; private readonly byte[] _content; private readonly int _offset; @@ -57,9 +57,9 @@ public JsonContent(Type type, object? value, string mediaType, JsonSerializerOpt // What if someone passes a weird Content-Type? // Should we set mediaType.CharSet = UTF-8? - // Formatting allows it. + // Formatting API allows it. public JsonContent(Type type, object? value, MediaTypeHeaderValue mediaType, JsonSerializerOptions? options = null) - : this(JsonSerializer.SerializeToUtf8Bytes(value, type, options), type, value, mediaType ?? throw new ArgumentNullException(nameof(mediaType))) { } + : this(JsonSerializer.SerializeToUtf8Bytes(value, type, options), type, value, mediaType) { } private JsonContent(byte[] content, Type type, object? value, MediaTypeHeaderValue mediaType) { @@ -84,7 +84,7 @@ public static JsonContent Create(T value, string mediaType, JsonSerializerOpt => Create(value, CreateMediaType(mediaType ?? throw new ArgumentNullException(nameof(mediaType))), options); public static JsonContent Create(T value, MediaTypeHeaderValue mediaType, JsonSerializerOptions? options = null) - => new JsonContent(JsonSerializer.SerializeToUtf8Bytes(value, options), typeof(T), value, mediaType ?? throw new ArgumentNullException(nameof(mediaType))); + => new JsonContent(JsonSerializer.SerializeToUtf8Bytes(value, options), typeof(T), value, mediaType); /// /// Serialize the HTTP content to a stream as an asynchronous operation. @@ -95,12 +95,6 @@ public static JsonContent Create(T value, MediaTypeHeaderValue mediaType, Jso protected override Task SerializeToStreamAsync(Stream stream, TransportContext context) => stream.WriteAsync(_content, _offset, _count); - // Should this method even exist? or we just call WriteAsync from above method without cancellationToken? - // UPDATE: This does not exists on netstandard - // protected override Task SerializeToStreamAsync(Stream stream, TransportContext context, CancellationToken cancellationToken) - // => stream.WriteAsync(_content, _offset, _count, cancellationToken); - - /// /// TODO /// diff --git a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpClientJsonExtensionsTests.cs b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpClientJsonExtensionsTests.cs index 02017c9eb36b49..dd16db2d040a38 100644 --- a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpClientJsonExtensionsTests.cs +++ b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpClientJsonExtensionsTests.cs @@ -9,64 +9,52 @@ using System.Linq; using System.Text; using System.IO; +using System.Collections.Generic; +using System.Net.Http.Headers; namespace System.Net.Http.Json.Functional.Tests { public class HttpClientJsonExtensionsTests { - private void ValidateRequest(HttpRequestData requestData) - { - Assert.NotNull(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 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 obj = (Person)await client.GetFromJsonAsync(uri, typeof(Person)); - Assert.NotNull(obj); - obj.Validate(); + 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(); } }, - server => server.HandleRequestAsync(content: json)); - - await LoopbackServer.CreateClientAndServerAsync( - async uri => + async server => { - using (HttpClient client = new HttpClient()) + for (int i = 0; i < NumRequests; i++) { - Person obj = await client.GetFromJsonAsync(uri); - Assert.NotNull(obj); - obj.Validate(); + await server.HandleRequestAsync(content: json, headers: headers); } - }, - server => server.HandleRequestAsync(content: json)); + }); } - //[Fact] - //public async Task TestGetFromJsonAsyncNotJsonContent() - //{ - // HttpClient client = new HttpClient(); - // await Assert.ThrowsAsync(() => client.GetFromJsonAsync("http://example.com", typeof(Person))); - // await Assert.ThrowsAsync(() => client.GetFromJsonAsync("http://example.com")); - - // Uri uri = new Uri("http://example.com"); - // await Assert.ThrowsAsync(() => client.GetFromJsonAsync(uri, typeof(Person))); - // await Assert.ThrowsAsync(() => client.GetFromJsonAsync(uri)); - //} - - // add tests with non-json content type. and stress content-type application/json and text/plain. [Fact] - public async Task TestGetFromJsonAsyncUnsuccessfulResponse() + public async Task TestGetFromJsonAsyncUnsuccessfulResponseAsync() { + const int NumRequests = 2; await LoopbackServer.CreateClientAndServerAsync( async uri => { @@ -76,143 +64,127 @@ await LoopbackServer.CreateClientAndServerAsync( await Assert.ThrowsAsync(() => client.GetFromJsonAsync(uri)); } }, - async server => { - HttpRequestData req = await server.HandleRequestAsync(statusCode: HttpStatusCode.InternalServerError); - }); - } - - [Fact] - public async Task TestGetFromJsonAsyncTextPlainUtf16() - { - const string json = @"{""Name"":""David"",""Age"":24}"; - await LoopbackServer.CreateClientAndServerAsync( - async uri => + async server => { - using (HttpClient client = new HttpClient()) + for (int i = 0; i < NumRequests; i++) { - Person per = Assert.IsType(await client.GetFromJsonAsync(uri, typeof(Person))); - per.Validate(); + await server.HandleRequestAsync(statusCode: HttpStatusCode.InternalServerError); } - }, - async server => { - byte[] nonUtf8Response = Encoding.Unicode.GetBytes(json); - var buffer = new MemoryStream(); - buffer.Write( - Encoding.ASCII.GetBytes( - $"HTTP/1.1 200 OK\r\nContent-Type: text/plain; charset=utf-16\r\nContent-Length: {nonUtf8Response.Length}\r\nConnection:close\r\n\r\n")); - buffer.Write(nonUtf8Response); - - await server.AcceptConnectionSendCustomResponseAndCloseAsync(buffer.ToArray()); - }); - } - - [Fact] - public async Task TestGetFromJsonAsyncGenericTextPlainUtf16() - { - const string json = @"{""Name"":""David"",""Age"":24}"; - await LoopbackServer.CreateClientAndServerAsync( - async uri => - { - using (HttpClient client = new HttpClient()) - { - Person per = await client.GetFromJsonAsync(uri); - per.Validate(); - } - }, - async server => { - byte[] nonUtf8Response = Encoding.Unicode.GetBytes(json); - var buffer = new MemoryStream(); - buffer.Write( - Encoding.ASCII.GetBytes( - $"HTTP/1.1 200 OK\r\nContent-Type: text/plain; charset=utf-16\r\nContent-Length: {nonUtf8Response.Length}\r\nConnection:close\r\n\r\n")); - buffer.Write(nonUtf8Response); - - await server.AcceptConnectionSendCustomResponseAndCloseAsync(buffer.ToArray()); }); } [Fact] public async Task TestPostAsJsonAsync() { + const int NumRequests = 4; await LoopbackServer.CreateClientAndServerAsync( async uri => { using (HttpClient client = new HttpClient()) { - HttpResponseMessage response = await client.PostAsJsonAsync(uri, typeof(Person), Person.Create()); + Person person = Person.Create(); + Type typePerson = typeof(Person); + HttpResponseMessage response = await client.PostAsJsonAsync(uri.ToString(), typePerson, person); Assert.True(response.StatusCode == HttpStatusCode.OK); - } - }, - async server => { - HttpRequestData req = await server.HandleRequestAsync(); - ValidateRequest(req); - Person obj = JsonSerializer.Deserialize(req.Body); - Assert.NotNull(obj); - obj.Validate(); - }); + response = await client.PostAsJsonAsync(uri, typePerson, person); + Assert.True(response.StatusCode == HttpStatusCode.OK); - await LoopbackServer.CreateClientAndServerAsync( - async uri => - { - using (HttpClient client = new HttpClient()) - { - HttpResponseMessage response = await client.PostAsJsonAsync(uri, Person.Create()); + response = await client.PostAsJsonAsync(uri.ToString(), person); + Assert.True(response.StatusCode == HttpStatusCode.OK); + + response = await client.PostAsJsonAsync(uri, person); Assert.True(response.StatusCode == HttpStatusCode.OK); } }, async server => { - HttpRequestData req = await server.HandleRequestAsync(); - ValidateRequest(req); - - Person obj = JsonSerializer.Deserialize(req.Body); - Assert.NotNull(obj); - obj.Validate(); + for (int i = 0; i < NumRequests; i++) + { + HttpRequestData request = await server.HandleRequestAsync(); + ValidateRequest(request); + Person per = JsonSerializer.Deserialize(request.Body); + per.Validate(); + } }); } [Fact] public async Task TestPutAsJsonAsync() { + const int NumRequests = 4; await LoopbackServer.CreateClientAndServerAsync( async uri => { using (HttpClient client = new HttpClient()) { - Person obj = Person.Create(); - HttpResponseMessage response = await client.PutAsJsonAsync(uri, typeof(Person), obj); + Person person = Person.Create(); + Type typePerson = typeof(Person); + HttpResponseMessage response = await client.PutAsJsonAsync(uri.ToString(), typePerson, person); Assert.True(response.StatusCode == HttpStatusCode.OK); - } - }, - async server => { - HttpRequestData req = await server.HandleRequestAsync(); - ValidateRequest(req); - Person obj = JsonSerializer.Deserialize(req.Body); - Assert.NotNull(obj); - obj.Validate(); - }); + response = await client.PutAsJsonAsync(uri, typePerson, person); + Assert.True(response.StatusCode == HttpStatusCode.OK); - await LoopbackServer.CreateClientAndServerAsync( - async uri => - { - using (HttpClient client = new HttpClient()) - { - Person obj = Person.Create(); - HttpResponseMessage response = await client.PutAsJsonAsync(uri, obj); + response = await client.PutAsJsonAsync(uri.ToString(), person); + Assert.True(response.StatusCode == HttpStatusCode.OK); + + response = await client.PutAsJsonAsync(uri, person); Assert.True(response.StatusCode == HttpStatusCode.OK); } }, async server => { - HttpRequestData req = await server.HandleRequestAsync(); - ValidateRequest(req); - - Person obj = JsonSerializer.Deserialize(req.Body); - Assert.NotNull(obj); - obj.Validate(); + for (int i = 0; i < NumRequests; i++) + { + HttpRequestData request = await server.HandleRequestAsync(); + ValidateRequest(request); + Person obj = JsonSerializer.Deserialize(request.Body); + obj.Validate(); + } }); } + + [Fact] + public async Task TestHttpClientIsNullAsync() + { + HttpClient client = null; + string uriString = "http://example.com"; + Uri uri = new Uri(uriString); + + ArgumentNullException ex; + ex = await Assert.ThrowsAsync(() => client.GetFromJsonAsync(uriString, typeof(Person))); + Assert.Equal("client", ex.ParamName); + ex = await Assert.ThrowsAsync(() => client.GetFromJsonAsync(uri, typeof(Person))); + Assert.Equal("client", ex.ParamName); + ex = await Assert.ThrowsAsync(() => client.GetFromJsonAsync(uriString)); + Assert.Equal("client", ex.ParamName); + ex = await Assert.ThrowsAsync(() => client.GetFromJsonAsync(uri)); + Assert.Equal("client", ex.ParamName); + + ex = await Assert.ThrowsAsync(() => client.PostAsJsonAsync(uriString, typeof(Person), value: null)); + Assert.Equal("client", ex.ParamName); + ex = await Assert.ThrowsAsync(() => client.PostAsJsonAsync(uri, typeof(Person), value: null)); + Assert.Equal("client", ex.ParamName); + ex = await Assert.ThrowsAsync(() => client.PostAsJsonAsync(uriString, null)); + Assert.Equal("client", ex.ParamName); + ex = await Assert.ThrowsAsync(() => client.PostAsJsonAsync(uri, null)); + Assert.Equal("client", ex.ParamName); + + ex = await Assert.ThrowsAsync(() => client.PutAsJsonAsync(uriString, typeof(Person), value: null)); + Assert.Equal("client", ex.ParamName); + ex = await Assert.ThrowsAsync(() => client.PutAsJsonAsync(uri, typeof(Person), value: null)); + Assert.Equal("client", ex.ParamName); + ex = await Assert.ThrowsAsync(() => client.PutAsJsonAsync(uriString, null)); + Assert.Equal("client", ex.ParamName); + ex = await Assert.ThrowsAsync(() => client.PutAsJsonAsync(uri, null)); + Assert.Equal("client", ex.ParamName); + } + + 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); + } } } diff --git a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpContentJsonExtensionsTests.cs b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpContentJsonExtensionsTests.cs index 25608fa6b025cf..737d4259346916 100644 --- a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpContentJsonExtensionsTests.cs +++ b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpContentJsonExtensionsTests.cs @@ -2,7 +2,11 @@ // 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.IO; using System.Net.Test.Common; +using System.Text; +using System.Text.Json; using System.Threading.Tasks; using Xunit; @@ -10,7 +14,10 @@ namespace System.Net.Http.Json.Functional.Tests { public class HttpContentJsonExtensionsTests { - public async Task HttpContentGetPersonAsync() + private readonly List _headers = new List { new HttpHeaderData("Content-Type", "application/json") }; + + [Fact] + public async Task HttpContentGetThenReadFromJsonAsync() { HttpContent content = null; await LoopbackServer.CreateClientAndServerAsync( @@ -18,7 +25,7 @@ await LoopbackServer.CreateClientAndServerAsync( { using (HttpClient client = new HttpClient()) { - var request = new HttpRequestMessage(HttpMethod.Get, $"getPerson"); + var request = new HttpRequestMessage(HttpMethod.Get, uri); var response = await client.SendAsync(request); Assert.True(response.StatusCode == HttpStatusCode.OK); @@ -26,7 +33,7 @@ await LoopbackServer.CreateClientAndServerAsync( } }, async server => { - HttpRequestData req = await server.HandleRequestAsync(content: Person.Create().Serialize()); + HttpRequestData req = await server.HandleRequestAsync(headers: _headers, content: Person.Create().Serialize()); }); object obj = await content.ReadFromJsonAsync(typeof(Person)); @@ -37,7 +44,8 @@ await LoopbackServer.CreateClientAndServerAsync( per.Validate(); } - public async Task HttpContentTypeIsNull() + [Fact] + public async Task HttpContentObjectIsNull() { HttpContent content = null; await LoopbackServer.CreateClientAndServerAsync( @@ -45,7 +53,7 @@ await LoopbackServer.CreateClientAndServerAsync( { using (HttpClient client = new HttpClient()) { - var request = new HttpRequestMessage(HttpMethod.Get, "getPerson"); + var request = new HttpRequestMessage(HttpMethod.Get, uri); var response = await client.SendAsync(request); Assert.True(response.StatusCode == HttpStatusCode.OK); @@ -53,7 +61,7 @@ await LoopbackServer.CreateClientAndServerAsync( } }, async server => { - HttpRequestData req = await server.HandleRequestAsync(content: "null"); + HttpRequestData req = await server.HandleRequestAsync(headers: _headers, content: "null"); }); object obj = await content.ReadFromJsonAsync(typeof(Person)); @@ -62,5 +70,111 @@ await LoopbackServer.CreateClientAndServerAsync( Person per = await content.ReadFromJsonAsync(); Assert.Null(per); } + + [Fact] + public async Task TestGetFromJsonNoMessageBodyAsync() + { + await LoopbackServer.CreateClientAndServerAsync( + async uri => + { + using (HttpClient client = new HttpClient()) + { + JsonException ex = await Assert.ThrowsAsync(() => client.GetFromJsonAsync(uri, typeof(Person))); + Assert.Contains("Path: $ | LineNumber: 0 | BytePositionInLine: 0", ex.Message); + } + }, + + + server => server.HandleRequestAsync(headers: _headers)); + } + + [Fact] + public async Task TestGetFromJsonNoContentTypeAsync() + { + await LoopbackServer.CreateClientAndServerAsync( + async uri => + { + using (HttpClient client = new HttpClient()) + { + await Assert.ThrowsAsync(() => client.GetFromJsonAsync(uri)); + } + }, + server => server.HandleRequestAsync(content: "{}")); + } + + [Fact] + public async Task TestGetFromJsonQuotedCharSetAsync() + { + List customHeaders = new List + { + new HttpHeaderData("Content-Type", "text/plain; charset=\"utf-8\"") + }; + + await LoopbackServer.CreateClientAndServerAsync( + async uri => + { + using (HttpClient client = new HttpClient()) + { + Person person = await client.GetFromJsonAsync(uri); + person.Validate(); + } + }, + server => server.HandleRequestAsync(headers: customHeaders, content: Person.Create().Serialize())); + } + + [Fact] + public async Task TestGetFromJsonThrowOnInvalidCharSetAsync() + { + List customHeaders = new List + { + new HttpHeaderData("Content-Type", "text/plain; charset=\"foo-bar\"") + }; + + await LoopbackServer.CreateClientAndServerAsync( + async uri => + { + using (HttpClient client = new HttpClient()) + { + InvalidOperationException ex = await Assert.ThrowsAsync(() => client.GetFromJsonAsync(uri)); + 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()) + { + Person per = Assert.IsType(await client.GetFromJsonAsync(uri, typeof(Person))); + per.Validate(); + + per = await client.GetFromJsonAsync(uri); + per.Validate(); + } + }, + async server => { + byte[] nonUtf8Response = Encoding.Unicode.GetBytes(json); + var buffer = new MemoryStream(); + buffer.Write( + Encoding.ASCII.GetBytes( + $"HTTP/1.1 200 OK" + + $"\r\nContent-Type: text/plain; charset=utf-16\r\n" + + $"Content-Length: {nonUtf8Response.Length}\r\n" + + $"Connection:close\r\n\r\n")); + buffer.Write(nonUtf8Response); + + for (int i = 0; i < NumRequests; i++) + { + await server.AcceptConnectionSendCustomResponseAndCloseAsync(buffer.ToArray()); + } + }); + } } } diff --git a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/JsonContentTests.cs b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/JsonContentTests.cs index 27fd497401c3f7..08d2d8200987f5 100644 --- a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/JsonContentTests.cs +++ b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/JsonContentTests.cs @@ -3,14 +3,14 @@ // See the LICENSE file in the project root for more information. using System.Net.Http.Headers; +using System.Net.Test.Common; +using System.Threading.Tasks; using Xunit; namespace System.Net.Http.Json.Functional.Tests { public class JsonContentTests { - private const string JsonContentType = "foo/bar; charset=utf-16"; - private readonly MediaTypeHeaderValue s_mediaTypeHeader = MediaTypeHeaderValue.Parse(JsonContentType); private class Foo { } private class Bar { } @@ -20,37 +20,53 @@ public void JsonContentObjectType() { Type fooType = typeof(Foo); Foo foo = new Foo(); + JsonContent content = new JsonContent(fooType, foo); 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 = new JsonContent(fooType, fooBoxed); 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 JsonContentMediaType() { + MediaTypeHeaderValue mediaType = MediaTypeHeaderValue.Parse("foo/bar; charset=utf-16"); Type fooType = typeof(Foo); Foo foo = new Foo(); - JsonContent content = new JsonContent(fooType, foo, mediaType: s_mediaTypeHeader); - Assert.Same(s_mediaTypeHeader, content.Headers.ContentType); + // Use the default content-type if none is provided. + JsonContent content = new JsonContent(fooType, foo); + 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); - content = JsonContent.Create(foo, mediaType: s_mediaTypeHeader); - Assert.Same(s_mediaTypeHeader, content.Headers.ContentType); + // Use the specified MediaTypeHeaderValue if provided. + content = new JsonContent(fooType, foo, mediaType: mediaType); + Assert.Same(mediaType, content.Headers.ContentType); - string mediaTypeAsString = s_mediaTypeHeader.MediaType; + content = JsonContent.Create(foo, mediaType: mediaType); + Assert.Same(mediaType, content.Headers.ContentType); + // Use the specified mediaType string but use the default charset if not provided. + string mediaTypeAsString = "foo/bar"; content = new JsonContent(fooType, foo, mediaType: mediaTypeAsString); Assert.Equal(mediaTypeAsString, content.Headers.ContentType.MediaType); Assert.Equal("utf-8", content.Headers.ContentType.CharSet); @@ -59,41 +75,81 @@ public void JsonContentMediaType() Assert.Equal(mediaTypeAsString, content.Headers.ContentType.MediaType); Assert.Equal("utf-8", content.Headers.ContentType.CharSet); - // JsonContentType define its own charset. - content = new JsonContent(fooType, foo, mediaType: JsonContentType); - Assert.Equal(s_mediaTypeHeader.MediaType, content.Headers.ContentType.MediaType); - Assert.Equal(s_mediaTypeHeader.CharSet, content.Headers.ContentType.CharSet); + // Use the specifed mediaType and charset. + string mediaTypeAndCharSetAsString = "foo/bar; charset=utf-16"; + content = new JsonContent(fooType, foo, mediaType: mediaTypeAndCharSetAsString); + Assert.Equal("foo/bar", content.Headers.ContentType.MediaType); + Assert.Equal("utf-16", content.Headers.ContentType.CharSet); + + content = JsonContent.Create(foo, mediaType: mediaTypeAndCharSetAsString); + Assert.Equal("foo/bar", content.Headers.ContentType.MediaType); + Assert.Equal("utf-16", content.Headers.ContentType.CharSet); + } - content = JsonContent.Create(foo, mediaType: JsonContentType); - Assert.Equal(s_mediaTypeHeader.MediaType, content.Headers.ContentType.MediaType); - Assert.Equal(s_mediaTypeHeader.CharSet, content.Headers.ContentType.CharSet); + [Fact] + public async Task SendJsonContentMediaTypeValidateOnServerAsync() + { + await LoopbackServer.CreateClientAndServerAsync( + async uri => + { + using (HttpClient client = new HttpClient()) + { + var request = new HttpRequestMessage(HttpMethod.Post, uri); + request.Content = JsonContent.Create(Person.Create(), mediaType: "foo/bar"); + await client.SendAsync(request); + + request = new HttpRequestMessage(HttpMethod.Post, uri); + request.Content = JsonContent.Create(Person.Create(), mediaType: "foo/bar; charset=utf-16"); + await client.SendAsync(request); + + request = new HttpRequestMessage(HttpMethod.Post, uri); + MediaTypeHeaderValue mediaType = MediaTypeHeaderValue.Parse("foo/bar"); + request.Content = JsonContent.Create(Person.Create(), mediaType: mediaType); + await client.SendAsync(request); + + request = new HttpRequestMessage(HttpMethod.Post, uri); + mediaType = MediaTypeHeaderValue.Parse("foo/bar; charset=baz"); + 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")); + + req = await server.HandleRequestAsync(); + Assert.Equal("foo/bar; charset=utf-16", req.GetSingleHeaderValue("Content-Type")); + + req = await server.HandleRequestAsync(); + Assert.Equal("foo/bar", req.GetSingleHeaderValue("Content-Type")); + + req = await server.HandleRequestAsync(); + Assert.Equal("foo/bar; charset=baz", req.GetSingleHeaderValue("Content-Type")); + }); } [Fact] public void JsonContentMediaTypeIsNull() { Type fooType = typeof(Foo); - Foo foo = new Foo(); - - ArgumentNullException ex = Assert.Throws(() => new JsonContent(fooType, foo, mediaType: (MediaTypeHeaderValue)null)); - Assert.Equal("mediaType", ex.ParamName); - - ex = Assert.Throws(() => JsonContent.Create(foo, mediaType: (MediaTypeHeaderValue)null)); - Assert.Equal("mediaType", ex.ParamName); + Foo foo = null; - string mediaTypeAsString = s_mediaTypeHeader.MediaType; + ArgumentNullException ex; ex = Assert.Throws(() => new JsonContent(fooType, foo, mediaType: (string)null)); Assert.Equal("mediaType", ex.ParamName); - + ex = Assert.Throws(() => new JsonContent(fooType, foo, mediaType: (MediaTypeHeaderValue)null)); + Assert.Equal("mediaType", ex.ParamName); ex = Assert.Throws(() => JsonContent.Create(foo, mediaType: (string)null)); Assert.Equal("mediaType", ex.ParamName); + ex = Assert.Throws(() => JsonContent.Create(foo, mediaType: (MediaTypeHeaderValue)null)); + Assert.Equal("mediaType", ex.ParamName); } [Fact] public void JsonContentTypeIsNull() { Assert.Throws(() => new JsonContent(null, null)); - Assert.Throws(() => new JsonContent(null, null, s_mediaTypeHeader)); + Assert.Throws(() => new JsonContent(null, null, MediaTypeHeaderValue.Parse("foo/bar; charset=utf-16"))); } [Fact] @@ -101,7 +157,7 @@ public void JsonContentThrowsOnIncompatibleType() { var foo = new Foo(); Assert.Throws(() => new JsonContent(typeof(Bar), foo)); - Assert.Throws(() => new JsonContent(typeof(Bar), foo, s_mediaTypeHeader)); + Assert.Throws(() => new JsonContent(typeof(Bar), foo, MediaTypeHeaderValue.Parse("foo/bar; charset=utf-16"))); } } } 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 index 86f8a777c238b9..d5170f386f67bd 100644 --- 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 @@ -21,15 +21,6 @@ Common\System\Net\Configuration.Security.cs - - Common\System\Net\Http\LoopbackServer.cs @@ -39,9 +30,6 @@ Common\System\Net\Http\GenericLoopbackServer.cs - Common\System\Threading\Tasks\TaskTimeoutExtensions.cs From c1eb128f5dc53f0290c44b1d7512a6d5cdbec4b0 Mon Sep 17 00:00:00 2001 From: David Cantu Date: Tue, 10 Mar 2020 17:51:31 -0700 Subject: [PATCH 06/32] Remove annotations and code clean-up. --- .../src/System.Net.Http.Json.csproj | 1 - .../Http/Json/HttpClientJsonExtensions.Get.cs | 63 +------------ .../Json/HttpClientJsonExtensions.Helpers.cs | 25 ----- .../Json/HttpClientJsonExtensions.Post.cs | 3 - .../Http/Json/HttpClientJsonExtensions.Put.cs | 40 +++----- .../Http/Json/HttpContentJsonExtensions.cs | 24 +---- .../src/System/Net/Http/Json/JsonContent.cs | 28 ------ ...HttpClientJsonExtensionsTests.Formatter.cs | 91 ------------------- .../HttpClientJsonExtensionsTests.cs | 3 - .../HttpContentJsonExtensionsTests.cs | 3 +- ...stem.Net.Http.Json.Functional.Tests.csproj | 1 - 11 files changed, 25 insertions(+), 257 deletions(-) delete mode 100644 src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Helpers.cs delete mode 100644 src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpClientJsonExtensionsTests.Formatter.cs 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 index 5d470ea18b6420..006281f618e493 100644 --- 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 @@ -5,7 +5,6 @@ - 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 index a0f77ba51b5486..b602f48292a857 100644 --- 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 @@ -13,21 +13,7 @@ namespace System.Net.Http.Json /// public static partial class HttpClientJsonExtensions { - /// - /// TODO - /// - /// - /// - /// - /// - /// - /// - public static Task GetFromJsonAsync( - this HttpClient client, - string requestUri, - Type type, - JsonSerializerOptions? options = null, - CancellationToken cancellationToken = default) + public static Task GetFromJsonAsync(this HttpClient client, string requestUri, Type type, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) { if (client == null) { @@ -38,21 +24,7 @@ public static partial class HttpClientJsonExtensions return GetFromJsonAsyncCore(taskResponse, type, options, cancellationToken); } - /// - /// TODO - /// - /// - /// - /// - /// - /// - /// - public static Task GetFromJsonAsync( - this HttpClient client, - Uri requestUri, - Type type, - JsonSerializerOptions? options = null, - CancellationToken cancellationToken = default) + public static Task GetFromJsonAsync(this HttpClient client, Uri requestUri, Type type, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) { if (client == null) { @@ -63,20 +35,7 @@ public static partial class HttpClientJsonExtensions return GetFromJsonAsyncCore(taskResponse, type, options, cancellationToken); } - /// - /// TODO - /// - /// - /// - /// - /// - /// - /// - public static Task GetFromJsonAsync( - this HttpClient client, - string requestUri, - JsonSerializerOptions? options = null, - CancellationToken cancellationToken = default) + public static Task GetFromJsonAsync(this HttpClient client, string requestUri, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) { if (client == null) { @@ -87,20 +46,7 @@ public static Task GetFromJsonAsync( return GetFromJsonAsyncCore(taskResponse, options, cancellationToken); } - /// - /// TODO - /// - /// - /// - /// - /// - /// - /// - public static Task GetFromJsonAsync( - this HttpClient client, - Uri requestUri, - JsonSerializerOptions? options = null, - CancellationToken cancellationToken = default) + public static Task GetFromJsonAsync(this HttpClient client, Uri requestUri, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) { if (client == null) { @@ -118,6 +64,7 @@ public static Task GetFromJsonAsync( return await response.Content.ReadFromJsonAsync(type, options, cancellationToken).ConfigureAwait(false); } + private static async Task GetFromJsonAsyncCore(Task taskResponse, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) { HttpResponseMessage response = await taskResponse.ConfigureAwait(false); diff --git a/src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Helpers.cs b/src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Helpers.cs deleted file mode 100644 index 5701e5f5644e1b..00000000000000 --- a/src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Helpers.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; - -namespace System.Net.Http.Json -{ - public static partial class HttpClientJsonExtensions - { - #region Post/Put helpers - private static JsonContent CreateJsonContent(Type type, object? value, JsonSerializerOptions? options) - { - return new JsonContent(type, value, options); - } - - private static JsonContent CreateJsonContent(T value, JsonSerializerOptions? options) - { - return JsonContent.Create(value, options); - } - #endregion - } -} 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 index e12829f73e551d..895464f396de16 100644 --- 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 @@ -10,9 +10,6 @@ namespace System.Net.Http.Json { public static partial class HttpClientJsonExtensions { - // It would be nice to funnel object methods to the generic methods, but since I choose to make JsonContent.ObjectType = type arg on object signatures; we can't funnel - // and therefore validations need to be duplicated. - public static Task PostAsJsonAsync(this HttpClient client, string requestUri, Type type, object? value, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) { if (client == null) 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 index bfa5f3f4ed62b7..7c9268b030d54a 100644 --- 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 @@ -10,13 +10,7 @@ namespace System.Net.Http.Json { public static partial class HttpClientJsonExtensions { - public static Task PutAsJsonAsync( - this HttpClient client, - string requestUri, - Type type, - object? value, - JsonSerializerOptions? options = null, - CancellationToken cancellationToken = default) + public static Task PutAsJsonAsync(this HttpClient client, string requestUri, Type type, object? value, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) { if (client == null) { @@ -27,13 +21,7 @@ public static Task PutAsJsonAsync( return client.PutAsync(requestUri, content, cancellationToken); } - public static Task PutAsJsonAsync( - this HttpClient client, - Uri requestUri, - Type type, - object? value, - JsonSerializerOptions? options = null, - CancellationToken cancellationToken = default) + public static Task PutAsJsonAsync(this HttpClient client, Uri requestUri, Type type, object? value, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) { if (client == null) { @@ -44,12 +32,7 @@ public static Task PutAsJsonAsync( return client.PutAsync(requestUri, content, cancellationToken); } - public static Task PutAsJsonAsync( - this HttpClient client, - string requestUri, - T value, - JsonSerializerOptions? options = null, - CancellationToken cancellationToken = default) + public static Task PutAsJsonAsync(this HttpClient client, string requestUri, T value, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) { if (client == null) { @@ -60,12 +43,7 @@ public static Task PutAsJsonAsync( return client.PutAsync(requestUri, content, cancellationToken); } - public static Task PutAsJsonAsync( - this HttpClient client, - Uri requestUri, - T value, - JsonSerializerOptions? options = null, - CancellationToken cancellationToken = default) + public static Task PutAsJsonAsync(this HttpClient client, Uri requestUri, T value, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) { if (client == null) { @@ -75,5 +53,15 @@ public static Task PutAsJsonAsync( JsonContent content = CreateJsonContent(value, options); return client.PutAsync(requestUri, content, cancellationToken); } + + private static JsonContent CreateJsonContent(Type type, object? value, JsonSerializerOptions? options) + { + return new JsonContent(type, value, options); + } + + private static JsonContent CreateJsonContent(T value, JsonSerializerOptions? options) + { + return JsonContent.Create(value, options); + } } } 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 index 527ae391a75c83..2c7f746797c551 100644 --- 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 @@ -12,19 +12,12 @@ namespace System.Net.Http.Json { public static class HttpContentJsonExtensions { - public static Task ReadFromJsonAsync( - this HttpContent content, - Type type, - JsonSerializerOptions? options = null, - CancellationToken cancellationToken = default) + public static Task ReadFromJsonAsync(this HttpContent content, Type type, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) { return ReadFromJsonAsyncCore(content, type, options, cancellationToken); } - public static Task ReadFromJsonAsync( - this HttpContent content, - JsonSerializerOptions? options = null, - CancellationToken cancellationToken = default) + public static Task ReadFromJsonAsync(this HttpContent content, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) { return ReadFromJsonAsyncCore(content, options, cancellationToken); } @@ -32,34 +25,27 @@ public static Task ReadFromJsonAsync( private static async Task ReadFromJsonAsyncCore(HttpContent content, Type type, JsonSerializerOptions? options, CancellationToken cancellationToken) { byte[] contentBytes = await GetUtf8JsonBytesFromContentAsync(content, cancellationToken).ConfigureAwait(false); - // NOTE: I wanted to use DeserializeAsync and ReadAsStreamAsync, but if we need to transcode, we need the whole content, so it makes more sense to use ReadAsByteArrayAsync. - //Stream utf8Stream = await ReadStreamFromContent(content).ConfigureAwait(false); - //return await JsonSerializer.DeserializeAsync(utf8Stream, type, options, cancellationToken); return JsonSerializer.Deserialize(contentBytes, type, options); } private static async Task ReadFromJsonAsyncCore(HttpContent content, JsonSerializerOptions? options, CancellationToken cancellationToken) { byte[] contentBytes = await GetUtf8JsonBytesFromContentAsync(content, cancellationToken).ConfigureAwait(false); - return JsonSerializer.Deserialize(contentBytes, options)!; + return JsonSerializer.Deserialize(contentBytes, options); } private static async Task GetUtf8JsonBytesFromContentAsync(HttpContent content, CancellationToken cancellationToken) { string? mediaType = content.Headers.ContentType?.MediaType; - // if Content-Type == null we assume it as "application/octet-stream" in accordance with section 7.2.1 of the HTTP spec. - // This is how Formatting API works - // And at the same time, it is contrary to how HttpContent.ReadAsStringAsync works (allows null content-type and tries to read content using UTF-8 in that case). - // IMO, this should default to application/json if (mediaType != JsonContent.JsonMediaType && mediaType != MediaTypeNames.Text.Plain) { throw new NotSupportedException("The provided ContentType is not supported; the supported types are 'application/json' and 'text/plain'."); } - // https://source.dot.net/#System.Net.Http/System/Net/Http/HttpContent.cs,047409be2a4d70a8 - string? charset = content.Headers.ContentType!.CharSet; + // Code taken from https://source.dot.net/#System.Net.Http/System/Net/Http/HttpContent.cs,047409be2a4d70a8 + string? charset = content.Headers.ContentType.CharSet; Encoding? encoding = null; if (charset != null) { 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 index a2e248a4cfd9c9..8b1404ad5ffd1e 100644 --- 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 @@ -10,9 +10,6 @@ namespace System.Net.Http.Json { - /// - /// TODO - /// public class JsonContent : HttpContent { internal const string JsonMediaType = "application/json"; @@ -23,7 +20,6 @@ public class JsonContent : HttpContent private static MediaTypeHeaderValue CreateMediaType(string mediaTypeAsString) { - //MediaTypeHeaderValue mediaType = new MediaTypeHeaderValue(mediaTypeAsString); // this one is used by the Formatting API. MediaTypeHeaderValue mediaType = MediaTypeHeaderValue.Parse(mediaTypeAsString); // If the instantiated mediaType does not specify its CharSet, set UTF-8 by default. @@ -35,9 +31,6 @@ private static MediaTypeHeaderValue CreateMediaType(string mediaTypeAsString) return mediaType; } - // When Create is callled, this is the typeof(T). - // When .ctor is called, this is the specified type argument. - // As per Formatting, this is always the declared type. public Type ObjectType { get; } public object? Value { get; } @@ -45,19 +38,9 @@ private static MediaTypeHeaderValue CreateMediaType(string mediaTypeAsString) public JsonContent(Type type, object? value, JsonSerializerOptions? options = null) : this(type, value, CreateMediaType(JsonMediaType), options) { } - /// - /// TODO - /// - /// - /// - /// The authoritative value of the request's content's Content-Type header. Can be null in which case the default content type will be used. - /// public JsonContent(Type type, object? value, string mediaType, JsonSerializerOptions? options = null) : this(type, value, CreateMediaType(mediaType?? throw new ArgumentNullException(nameof(mediaType))), options) { } - // What if someone passes a weird Content-Type? - // Should we set mediaType.CharSet = UTF-8? - // Formatting API allows it. public JsonContent(Type type, object? value, MediaTypeHeaderValue mediaType, JsonSerializerOptions? options = null) : this(JsonSerializer.SerializeToUtf8Bytes(value, type, options), type, value, mediaType) { } @@ -86,20 +69,9 @@ public static JsonContent Create(T value, string mediaType, JsonSerializerOpt public static JsonContent Create(T value, MediaTypeHeaderValue mediaType, JsonSerializerOptions? options = null) => new JsonContent(JsonSerializer.SerializeToUtf8Bytes(value, options), typeof(T), value, mediaType); - /// - /// Serialize the HTTP content to a stream as an asynchronous operation. - /// - /// - /// - /// protected override Task SerializeToStreamAsync(Stream stream, TransportContext context) => stream.WriteAsync(_content, _offset, _count); - /// - /// TODO - /// - /// - /// protected override bool TryComputeLength(out long length) { length = _count; diff --git a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpClientJsonExtensionsTests.Formatter.cs b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpClientJsonExtensionsTests.Formatter.cs deleted file mode 100644 index 231b3a1913092c..00000000000000 --- a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpClientJsonExtensionsTests.Formatter.cs +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -// 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; - -namespace System.Net.Http.Json.Functional.Tests -{ - // Tests taken from https://github.com/aspnet/AspNetWebStack/blob/master/test/System.Net.Http.Formatting.Test/HttpClientExtensionsTest.cs - public class HttpClientExtensionsTest - { - //private readonly MediaTypeFormatter _formatter = new MockMediaTypeFormatter { CallBase = true }; - private readonly HttpClient _client = new HttpClient(); - // TODO: Use this for JsonContent unit tests - //private readonly MediaTypeHeaderValue _mediaTypeHeader = MediaTypeHeaderValue.Parse("foo/bar; charset=utf-16"); - - //public HttpClientExtensionsTest() - //{ - // Mock handlerMock = new Mock { CallBase = true }; - // handlerMock - // .Setup(h => h.SendAsyncPublic(It.IsAny(), It.IsAny())) - // .Returns((HttpRequestMessage request, CancellationToken _) => Task.FromResult(new HttpResponseMessage() { RequestMessage = request })); - - // _client = new HttpClient(handlerMock.Object); - //} - - [Fact] - public async Task PostAsJsonAsync_String_WhenClientIsNull_ThrowsException() - { - HttpClient client = null; - - ArgumentNullException ex = await Assert.ThrowsAsync(() => client.PostAsJsonAsync("http://www.example.com", new object())); - Assert.Equal("client", ex.ParamName); - } - - [Fact] - public void PostAsJsonAsync_String_WhenUriIsNull_ThrowsExceptionAsync() - { - Assert.ThrowsAsync(() => _client.PostAsJsonAsync((string)null, new object())); - } - - [Fact] - public async Task PostAsJsonAsync_Uri_WhenUriIsNull_ThrowsException() - { - await Assert.ThrowsAsync(() => _client.PostAsJsonAsync((Uri)null, new object())); - } - - [Fact] - public async Task PostAsJsonAsync_String_UsesJsonMediaTypeFormatter() - { - var response = await _client.PostAsJsonAsync("http://example.com", new object()); - - JsonContent content = Assert.IsType(response.RequestMessage.Content); - //Assert.IsType(content.Formatter); - //?? - } - - [Fact] - public async Task PostAsync_String_WhenRequestUriIsSet_CreatesRequestWithAppropriateUri() - { - _client.BaseAddress = new Uri("http://example.com/"); - - var response = await _client.PostAsJsonAsync("myapi/", new object()); - - var request = response.RequestMessage; - Assert.Equal("http://example.com/myapi/", request.RequestUri.ToString()); - } - - [Fact] - public async Task PostAsJsonAsync_Uri_WhenClientIsNull_ThrowsExceptionAsync() - { - HttpClient client = null; - - ArgumentNullException ex = await Assert.ThrowsAsync(() => client.PostAsJsonAsync(new Uri("http://www.example.com"), new object())); - Assert.Equal("client", ex.ParamName); - } - - [Fact] - public async Task PostAsJsonAsync_Uri_UsesJsonMediaTypeFormatter() - { - var response = await _client.PostAsJsonAsync(new Uri("http://example.com"), new object()); - - var content = Assert.IsType(response.RequestMessage.Content); - //Assert.IsType(content.Formatter); - } - } -} diff --git a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpClientJsonExtensionsTests.cs b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpClientJsonExtensionsTests.cs index dd16db2d040a38..4933afa362dfa2 100644 --- a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpClientJsonExtensionsTests.cs +++ b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpClientJsonExtensionsTests.cs @@ -7,10 +7,7 @@ using System.Net.Test.Common; using System.Text.Json; using System.Linq; -using System.Text; -using System.IO; using System.Collections.Generic; -using System.Net.Http.Headers; namespace System.Net.Http.Json.Functional.Tests { diff --git a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpContentJsonExtensionsTests.cs b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpContentJsonExtensionsTests.cs index 737d4259346916..a8f39bc9d6d483 100644 --- a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpContentJsonExtensionsTests.cs +++ b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpContentJsonExtensionsTests.cs @@ -79,12 +79,11 @@ await LoopbackServer.CreateClientAndServerAsync( { using (HttpClient client = new HttpClient()) { + // 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(() => client.GetFromJsonAsync(uri, typeof(Person))); Assert.Contains("Path: $ | LineNumber: 0 | BytePositionInLine: 0", ex.Message); } }, - - server => server.HandleRequestAsync(headers: _headers)); } 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 index d5170f386f67bd..4b40853bd6b1f3 100644 --- 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 @@ -33,7 +33,6 @@ Common\System\Threading\Tasks\TaskTimeoutExtensions.cs - From 06294dcb5d463328db6afcb97467b6acd3806d50 Mon Sep 17 00:00:00 2001 From: David Cantu Date: Tue, 10 Mar 2020 18:12:32 -0700 Subject: [PATCH 07/32] Add draft description and common types to NuGet pkg. --- src/libraries/pkg/descriptions.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/libraries/pkg/descriptions.json b/src/libraries/pkg/descriptions.json index 6ced05e0aece25..c87d4149744057 100644 --- a/src/libraries/pkg/descriptions.json +++ b/src/libraries/pkg/descriptions.json @@ -1231,9 +1231,11 @@ }, { "Name": "System.Net.Http.Json", - "Description": "ToDO: Insert the package description approved by Immo here", + "Description": "Provides extension methods for System.Net.Http.HttpClient and System.Net.Http.HttpContent that focus on ease of use of JSON as the data interchange format for HTTP clients (pending approval).", "CommonTypes": [ - "ToDo: Insert Commonly Used types here" + "System.Net.Http.Json.HttpClientJsonExtensions", + "System.Net.Http.Json.HttpContentJsonExtensions", + "System.Net.Http.Json.JsonContent" ] }, { From 73d66f6f33f746d6e948416ad49450a7759b8b69 Mon Sep 17 00:00:00 2001 From: David Cantu Date: Wed, 11 Mar 2020 14:55:15 -0700 Subject: [PATCH 08/32] Addres nullability issues on API surface area. --- .../ref/System.Net.Http.Json.cs | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) 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 index c6139e321fe752..56a7ddb0427a3b 100644 --- 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 @@ -13,30 +13,30 @@ public static partial class HttpClientJsonExtensions public static System.Threading.Tasks.Task GetFromJsonAsync(this System.Net.Http.HttpClient client, System.Uri requestUri, 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 GetFromJsonAsync(this System.Net.Http.HttpClient client, string requestUri, System.Text.Json.JsonSerializerOptions? options = null, 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 = 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, System.Type type, object 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, System.Type type, object? 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, T 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, T 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, System.Type type, object? 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, System.Type type, object? 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, T 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, T 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, System.Type type, object? 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, System.Type type, object? 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, T 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, T 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, System.Type type, object? 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, System.Type type, object? 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, T 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, T value, System.Text.Json.JsonSerializerOptions? options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.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 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 partial class JsonContent : System.Net.Http.HttpContent { - public JsonContent(System.Type type, object? value, System.Net.Http.Headers.MediaTypeHeaderValue mediaType, System.Text.Json.JsonSerializerOptions options = null) { } - public JsonContent(System.Type type, object? value, string mediaType, System.Text.Json.JsonSerializerOptions options = null) { } - public JsonContent(System.Type type, object? value, System.Text.Json.JsonSerializerOptions options = null) { } + public JsonContent(System.Type type, object? value, System.Net.Http.Headers.MediaTypeHeaderValue mediaType, System.Text.Json.JsonSerializerOptions? options = null) { } + public JsonContent(System.Type type, object? value, string mediaType, System.Text.Json.JsonSerializerOptions? options = null) { } + public JsonContent(System.Type type, object? value, System.Text.Json.JsonSerializerOptions? options = null) { } public System.Type ObjectType { get { throw null; } } public object? Value { get { throw null; } } - public static System.Net.Http.Json.JsonContent Create(T value, System.Net.Http.Headers.MediaTypeHeaderValue mediaType, System.Text.Json.JsonSerializerOptions options = null) { throw null; } - public static System.Net.Http.Json.JsonContent Create(T value, string mediaType, System.Text.Json.JsonSerializerOptions options = null) { throw null; } - public static System.Net.Http.Json.JsonContent Create(T value, System.Text.Json.JsonSerializerOptions options = null) { throw null; } + public static System.Net.Http.Json.JsonContent Create(T value, System.Net.Http.Headers.MediaTypeHeaderValue mediaType, System.Text.Json.JsonSerializerOptions? options = null) { throw null; } + public static System.Net.Http.Json.JsonContent Create(T value, string mediaType, System.Text.Json.JsonSerializerOptions? options = null) { throw null; } + public static System.Net.Http.Json.JsonContent Create(T value, 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; } } From ada5036368b2110a783b8575ae011c63f3b24b45 Mon Sep 17 00:00:00 2001 From: Jose Perez Rodriguez Date: Wed, 11 Mar 2020 15:22:49 -0700 Subject: [PATCH 09/32] Add missing configurations to projects --- .../System.Net.Http.Json/ref/System.Net.Http.Json.csproj | 9 ++++++++- .../System.Net.Http.Json/src/System.Net.Http.Json.csproj | 7 ++++++- .../System.Net.Http.Json.Functional.Tests.csproj | 2 +- 3 files changed, 15 insertions(+), 3 deletions(-) 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 index f43f195e8ad003..0f452349719655 100644 --- 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 @@ -1,11 +1,18 @@  - netstandard2.0 + netstandard2.0;$(NetCoreAppCurrent) enable + + + + + + + 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 index 006281f618e493..db2760067dbdcf 100644 --- 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 @@ -1,6 +1,6 @@  - netstandard2.0 + netstandard2.0;$(NetCoreAppCurrent) enable @@ -10,6 +10,11 @@ + + + + + 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 index 4b40853bd6b1f3..da8a09e33d4f89 100644 --- 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 @@ -1,6 +1,6 @@  - $(NetCoreAppCurrent) + $(NetCoreAppCurrent);$(NetFrameworkCurrent) From 44c69ae546444d9378a11c56490a063b914c4ace Mon Sep 17 00:00:00 2001 From: David Cantu Date: Thu, 12 Mar 2020 10:56:48 -0700 Subject: [PATCH 10/32] Addressing most of the feedback. TODO: Implement transcoding for non-UTF-8 content. --- .../ref/System.Net.Http.Json.cs | 3 + .../ref/System.Net.Http.Json.csproj | 2 - .../src/Resources/Strings.resx | 129 ++++++++++++++++++ .../src/System.Net.Http.Json.csproj | 12 +- .../Http/Json/HttpClientJsonExtensions.Get.cs | 28 ++-- .../Json/HttpClientJsonExtensions.Post.cs | 8 +- .../Http/Json/HttpClientJsonExtensions.Put.cs | 18 +-- .../Http/Json/HttpContentJsonExtensions.cs | 36 +++-- .../src/System/Net/Http/Json/JsonContent.cs | 54 ++++---- .../Net/Http/Json/JsonContent.netcoreapp.cs | 17 +++ .../HttpClientJsonExtensionsTests.cs | 29 ++++ .../HttpContentJsonExtensionsTests.cs | 46 ++++--- .../tests/FunctionalTests/JsonContentTests.cs | 63 ++++----- src/libraries/pkg/descriptions.json | 2 +- 14 files changed, 309 insertions(+), 138 deletions(-) create mode 100644 src/libraries/System.Net.Http.Json/src/Resources/Strings.resx create mode 100644 src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/JsonContent.netcoreapp.cs 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 index 56a7ddb0427a3b..7dc1f7f4de2857 100644 --- 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 @@ -38,6 +38,9 @@ public JsonContent(System.Type type, object? value, System.Text.Json.JsonSeriali public static System.Net.Http.Json.JsonContent Create(T value, string mediaType, System.Text.Json.JsonSerializerOptions? options = null) { throw null; } public static System.Net.Http.Json.JsonContent Create(T value, 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; } +#if !NETSTANDARD2_0 + protected override System.Threading.Tasks.Task SerializeToStreamAsync(System.IO.Stream stream, System.Net.TransportContext context, System.Threading.CancellationToken cancellationToken) { throw null; } +#endif 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 index 0f452349719655..e0ad7b0d43b1b3 100644 --- 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 @@ -7,8 +7,6 @@ - - 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 00000000000000..5f592a90a4f2b2 --- /dev/null +++ b/src/libraries/System.Net.Http.Json/src/Resources/Strings.resx @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + 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 'text/plain'. + + 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 index db2760067dbdcf..4261f0266b82ad 100644 --- 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 @@ -11,15 +11,15 @@ - - + - - - - + + + + + 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 index b602f48292a857..50457640532d38 100644 --- 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 @@ -20,7 +20,7 @@ public static partial class HttpClientJsonExtensions throw new ArgumentNullException(nameof(client)); } - Task taskResponse = client.GetAsync(requestUri, cancellationToken); + Task taskResponse = client.GetAsync(requestUri, HttpCompletionOption.ResponseHeadersRead, cancellationToken); return GetFromJsonAsyncCore(taskResponse, type, options, cancellationToken); } @@ -31,7 +31,7 @@ public static partial class HttpClientJsonExtensions throw new ArgumentNullException(nameof(client)); } - Task taskResponse = client.GetAsync(requestUri, cancellationToken); + Task taskResponse = client.GetAsync(requestUri, HttpCompletionOption.ResponseHeadersRead, cancellationToken); return GetFromJsonAsyncCore(taskResponse, type, options, cancellationToken); } @@ -42,7 +42,7 @@ public static Task GetFromJsonAsync(this HttpClient client, string request throw new ArgumentNullException(nameof(client)); } - Task taskResponse = client.GetAsync(requestUri, cancellationToken); + Task taskResponse = client.GetAsync(requestUri, HttpCompletionOption.ResponseHeadersRead, cancellationToken); return GetFromJsonAsyncCore(taskResponse, options, cancellationToken); } @@ -53,24 +53,28 @@ public static Task GetFromJsonAsync(this HttpClient client, Uri requestUri throw new ArgumentNullException(nameof(client)); } - Task taskResponse = client.GetAsync(requestUri, cancellationToken); + Task taskResponse = client.GetAsync(requestUri, HttpCompletionOption.ResponseHeadersRead, cancellationToken); return GetFromJsonAsyncCore(taskResponse, options, cancellationToken); } - private static async Task GetFromJsonAsyncCore(Task taskResponse, Type type, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) + private static async Task GetFromJsonAsyncCore(Task taskResponse, Type type, JsonSerializerOptions? options, CancellationToken cancellationToken) { - HttpResponseMessage response = await taskResponse.ConfigureAwait(false); - response.EnsureSuccessStatusCode(); + using (HttpResponseMessage response = await taskResponse.ConfigureAwait(false)) + { + response.EnsureSuccessStatusCode(); - return await response.Content.ReadFromJsonAsync(type, options, cancellationToken).ConfigureAwait(false); + return await response.Content.ReadFromJsonAsync(type, options, cancellationToken).ConfigureAwait(false); + } } - private static async Task GetFromJsonAsyncCore(Task taskResponse, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) + private static async Task GetFromJsonAsyncCore(Task taskResponse, JsonSerializerOptions? options, CancellationToken cancellationToken) { - HttpResponseMessage response = await taskResponse.ConfigureAwait(false); - response.EnsureSuccessStatusCode(); + using (HttpResponseMessage response = await taskResponse.ConfigureAwait(false)) + { + response.EnsureSuccessStatusCode(); - return await response.Content.ReadFromJsonAsync(options, cancellationToken).ConfigureAwait(false); + 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 index 895464f396de16..5600565123f66e 100644 --- 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 @@ -17,7 +17,7 @@ public static Task PostAsJsonAsync(this HttpClient client, throw new ArgumentNullException(nameof(client)); } - JsonContent content = CreateJsonContent(type, value, options); + JsonContent content = new JsonContent(type, value, options); return client.PostAsync(requestUri, content, cancellationToken); } @@ -28,7 +28,7 @@ public static Task PostAsJsonAsync(this HttpClient client, throw new ArgumentNullException(nameof(client)); } - JsonContent content = CreateJsonContent(type, value, options); + JsonContent content = new JsonContent(type, value, options); return client.PostAsync(requestUri, content, cancellationToken); } @@ -39,7 +39,7 @@ public static Task PostAsJsonAsync(this HttpClient clien throw new ArgumentNullException(nameof(client)); } - JsonContent content = CreateJsonContent(value, options); + JsonContent content = JsonContent.Create(value, options); return client.PostAsync(requestUri, content, cancellationToken); } @@ -50,7 +50,7 @@ public static Task PostAsJsonAsync(this HttpClient clien throw new ArgumentNullException(nameof(client)); } - JsonContent content = CreateJsonContent(value, options); + JsonContent content = JsonContent.Create(value, options); return client.PostAsync(requestUri, content, 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 index 7c9268b030d54a..55c226c3793141 100644 --- 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 @@ -17,7 +17,7 @@ public static Task PutAsJsonAsync(this HttpClient client, s throw new ArgumentNullException(nameof(client)); } - JsonContent content = CreateJsonContent(type, value, options); + JsonContent content = new JsonContent(type, value, options); return client.PutAsync(requestUri, content, cancellationToken); } @@ -28,7 +28,7 @@ public static Task PutAsJsonAsync(this HttpClient client, U throw new ArgumentNullException(nameof(client)); } - JsonContent content = CreateJsonContent(type, value, options); + JsonContent content = new JsonContent(type, value, options); return client.PutAsync(requestUri, content, cancellationToken); } @@ -39,7 +39,7 @@ public static Task PutAsJsonAsync(this HttpClient client throw new ArgumentNullException(nameof(client)); } - JsonContent content = CreateJsonContent(value, options); + JsonContent content = JsonContent.Create(value, options); return client.PutAsync(requestUri, content, cancellationToken); } @@ -50,18 +50,8 @@ public static Task PutAsJsonAsync(this HttpClient client throw new ArgumentNullException(nameof(client)); } - JsonContent content = CreateJsonContent(value, options); + JsonContent content = JsonContent.Create(value, options); return client.PutAsync(requestUri, content, cancellationToken); } - - private static JsonContent CreateJsonContent(Type type, object? value, JsonSerializerOptions? options) - { - return new JsonContent(type, value, options); - } - - private static JsonContent CreateJsonContent(T value, JsonSerializerOptions? options) - { - return JsonContent.Create(value, options); - } } } 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 index 2c7f746797c551..2fa6e50052639e 100644 --- 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 @@ -2,6 +2,8 @@ // 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.Mime; using System.Text; using System.Text.Json; @@ -24,37 +26,37 @@ public static Task ReadFromJsonAsync(this HttpContent content, JsonSeriali private static async Task ReadFromJsonAsyncCore(HttpContent content, Type type, JsonSerializerOptions? options, CancellationToken cancellationToken) { - byte[] contentBytes = await GetUtf8JsonBytesFromContentAsync(content, cancellationToken).ConfigureAwait(false); - return JsonSerializer.Deserialize(contentBytes, type, options); + Stream contentStream = await GetJsonStreamFromContentAsync(content, cancellationToken).ConfigureAwait(false); + return await JsonSerializer.DeserializeAsync(contentStream, type, options, cancellationToken); } private static async Task ReadFromJsonAsyncCore(HttpContent content, JsonSerializerOptions? options, CancellationToken cancellationToken) { - byte[] contentBytes = await GetUtf8JsonBytesFromContentAsync(content, cancellationToken).ConfigureAwait(false); - return JsonSerializer.Deserialize(contentBytes, options); + Stream contentStream = await GetJsonStreamFromContentAsync(content, cancellationToken).ConfigureAwait(false); + return await JsonSerializer.DeserializeAsync(contentStream, options, cancellationToken); } - private static async Task GetUtf8JsonBytesFromContentAsync(HttpContent content, CancellationToken cancellationToken) + private static async Task GetJsonStreamFromContentAsync(HttpContent content, CancellationToken cancellationToken) { string? mediaType = content.Headers.ContentType?.MediaType; if (mediaType != JsonContent.JsonMediaType && mediaType != MediaTypeNames.Text.Plain) { - throw new NotSupportedException("The provided ContentType is not supported; the supported types are 'application/json' and 'text/plain'."); + throw new NotSupportedException(SR.ContentTypeNotSupported); } - // Code taken from https://source.dot.net/#System.Net.Http/System/Net/Http/HttpContent.cs,047409be2a4d70a8 + Debug.Assert(content.Headers.ContentType != null); + string? charset = content.Headers.ContentType.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] == '\"') + if (charset.Length > 2 && charset[0] == '\"' && charset[charset.Length - 1] == '\"') { encoding = Encoding.GetEncoding(charset.Substring(1, charset.Length - 2)); } @@ -62,25 +64,21 @@ private static async Task GetUtf8JsonBytesFromContentAsync(HttpContent c { encoding = Encoding.GetEncoding(charset); } - - // Byte-order-mark (BOM) characters may be present even if a charset was specified. - // bomLength = GetPreambleLength(buffer, encoding); } catch (ArgumentException e) { - throw new InvalidOperationException("The character set provided in ContentType is invalid.", e); + throw new InvalidOperationException(SR.CharSetInvalid, e); } } - byte[] contentBytes = await content.ReadAsByteArrayAsync().ConfigureAwait(false); - - // Transcode to UTF-8. + //TODO: We should allow encodings other than UTF-8 and we should transcode to UTF-8 if we get one. + // This would be easier to achieve once https://github.com/dotnet/runtime/issues/30260 is done. if (encoding != null && encoding != Encoding.UTF8) { - contentBytes = Encoding.Convert(encoding, Encoding.UTF8, contentBytes); + throw new NotSupportedException(SR.CharSetNotSupported); } - return contentBytes; + return await content.ReadAsStreamAsync().ConfigureAwait(false); } } } 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 index 8b1404ad5ffd1e..20924d76e53200 100644 --- 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 @@ -2,31 +2,26 @@ // 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 class JsonContent : HttpContent + public partial class JsonContent : HttpContent { internal const string JsonMediaType = "application/json"; + private readonly JsonSerializerOptions? _jsonSerializerOptions; - private readonly byte[] _content; - private readonly int _offset; - private readonly int _count; - - private static MediaTypeHeaderValue CreateMediaType(string mediaTypeAsString) + private static MediaTypeHeaderValue CreateMediaTypeFromString(string mediaTypeAsString) { - MediaTypeHeaderValue mediaType = MediaTypeHeaderValue.Parse(mediaTypeAsString); - - // If the instantiated mediaType does not specify its CharSet, set UTF-8 by default. - if (mediaType.CharSet == null) - { - mediaType.CharSet = Encoding.UTF8.WebName; - } + MediaTypeHeaderValue mediaType = new MediaTypeHeaderValue(mediaTypeAsString); + Debug.Assert(mediaType.CharSet == null); + mediaType.CharSet = Encoding.UTF8.WebName; return mediaType; } @@ -36,46 +31,51 @@ private static MediaTypeHeaderValue CreateMediaType(string mediaTypeAsString) public object? Value { get; } public JsonContent(Type type, object? value, JsonSerializerOptions? options = null) - : this(type, value, CreateMediaType(JsonMediaType), options) { } + : this(type, value, CreateMediaTypeFromString(JsonMediaType), options) { } public JsonContent(Type type, object? value, string mediaType, JsonSerializerOptions? options = null) - : this(type, value, CreateMediaType(mediaType?? throw new ArgumentNullException(nameof(mediaType))), options) { } + : this(type, value, CreateMediaTypeFromString(mediaType ?? throw new ArgumentNullException(nameof(mediaType))), options) { } public JsonContent(Type type, object? value, MediaTypeHeaderValue mediaType, JsonSerializerOptions? options = null) - : this(JsonSerializer.SerializeToUtf8Bytes(value, type, options), type, value, mediaType) { } - - private JsonContent(byte[] content, Type type, object? value, MediaTypeHeaderValue mediaType) { if (mediaType == null) { throw new ArgumentNullException(nameof(mediaType)); } - _content = content; - _offset = 0; - _count = content.Length; + // TODO: Support other charsets once https://github.com/dotnet/runtime/issues/30260 is done. + if (mediaType.CharSet != Encoding.UTF8.WebName) + { + throw new NotSupportedException(SR.CharSetInvalid); + } Value = value; ObjectType = type; Headers.ContentType = mediaType; + // TODO: Set DefaultWebOptions if no options were provided. + _jsonSerializerOptions = options; } public static JsonContent Create(T value, JsonSerializerOptions? options = null) - => Create(value, CreateMediaType(JsonMediaType), options); + => Create(value, CreateMediaTypeFromString(JsonMediaType), options); public static JsonContent Create(T value, string mediaType, JsonSerializerOptions? options = null) - => Create(value, CreateMediaType(mediaType ?? throw new ArgumentNullException(nameof(mediaType))), options); + => Create(value, CreateMediaTypeFromString(mediaType ?? throw new ArgumentNullException(nameof(mediaType))), options); public static JsonContent Create(T value, MediaTypeHeaderValue mediaType, JsonSerializerOptions? options = null) - => new JsonContent(JsonSerializer.SerializeToUtf8Bytes(value, options), typeof(T), value, mediaType); + => new JsonContent(typeof(T), value, mediaType); protected override Task SerializeToStreamAsync(Stream stream, TransportContext context) - => stream.WriteAsync(_content, _offset, _count); +#if !NETSTANDARD2_0 + => SerializeToStreamAsync(stream, context, CancellationToken.None); +#else + => JsonSerializer.SerializeAsync(stream, Value, ObjectType, _jsonSerializerOptions); +#endif protected override bool TryComputeLength(out long length) { - length = _count; - return true; + length = 0; + return false; } } } 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 00000000000000..48b6bc6ce662e4 --- /dev/null +++ b/src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/JsonContent.netcoreapp.cs @@ -0,0 +1,17 @@ +// 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.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace System.Net.Http.Json +{ + public partial class JsonContent + { + protected override Task SerializeToStreamAsync(Stream stream, TransportContext context, CancellationToken cancellationToken) + => JsonSerializer.SerializeAsync(stream, Value, ObjectType, _jsonSerializerOptions, cancellationToken); + } +} diff --git a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpClientJsonExtensionsTests.cs b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpClientJsonExtensionsTests.cs index 4933afa362dfa2..2e17003a738f45 100644 --- a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpClientJsonExtensionsTests.cs +++ b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpClientJsonExtensionsTests.cs @@ -183,5 +183,34 @@ 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 index a8f39bc9d6d483..001796abaae2c1 100644 --- a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpContentJsonExtensionsTests.cs +++ b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpContentJsonExtensionsTests.cs @@ -19,7 +19,7 @@ public class HttpContentJsonExtensionsTests [Fact] public async Task HttpContentGetThenReadFromJsonAsync() { - HttpContent content = null; + const int NumRequests = 2; await LoopbackServer.CreateClientAndServerAsync( async uri => { @@ -27,27 +27,28 @@ await LoopbackServer.CreateClientAndServerAsync( { var request = new HttpRequestMessage(HttpMethod.Get, uri); var response = await client.SendAsync(request); - Assert.True(response.StatusCode == HttpStatusCode.OK); + object obj = await response.Content.ReadFromJsonAsync(typeof(Person)); + Person per = Assert.IsType(obj); + per.Validate(); - content = response.Content; + request = new HttpRequestMessage(HttpMethod.Get, uri); + response = await client.SendAsync(request); + per = await response.Content.ReadFromJsonAsync(); + per.Validate(); } }, async server => { - HttpRequestData req = await server.HandleRequestAsync(headers: _headers, content: Person.Create().Serialize()); + for (int i = 0; i < NumRequests; i++) + { + HttpRequestData req = await server.HandleRequestAsync(headers: _headers, content: Person.Create().Serialize()); + } }); - - object obj = await content.ReadFromJsonAsync(typeof(Person)); - Person per = Assert.IsType(obj); - per.Validate(); - - per = await content.ReadFromJsonAsync(); - per.Validate(); } [Fact] public async Task HttpContentObjectIsNull() { - HttpContent content = null; + const int NumRequests = 2; await LoopbackServer.CreateClientAndServerAsync( async uri => { @@ -55,20 +56,21 @@ await LoopbackServer.CreateClientAndServerAsync( { var request = new HttpRequestMessage(HttpMethod.Get, uri); var response = await client.SendAsync(request); - Assert.True(response.StatusCode == HttpStatusCode.OK); + object obj = await response.Content.ReadFromJsonAsync(typeof(Person)); + Assert.Null(obj); - content = response.Content; + request = new HttpRequestMessage(HttpMethod.Get, uri); + response = await client.SendAsync(request); + Person per = await response.Content.ReadFromJsonAsync(); + Assert.Null(per); } }, async server => { - HttpRequestData req = await server.HandleRequestAsync(headers: _headers, content: "null"); + for (int i = 0; i < NumRequests; i++) + { + HttpRequestData req = await server.HandleRequestAsync(headers: _headers, content: "null"); + } }); - - object obj = await content.ReadFromJsonAsync(typeof(Person)); - Assert.Null(obj); - - Person per = await content.ReadFromJsonAsync(); - Assert.Null(per); } [Fact] @@ -141,7 +143,7 @@ await LoopbackServer.CreateClientAndServerAsync( server => server.HandleRequestAsync(headers: customHeaders, content: Person.Create().Serialize())); } - [Fact] + [Fact(Skip ="Disable temporarily until transcode support is added.")] public async Task TestGetFromJsonAsyncTextPlainUtf16Async() { const string json = @"{""Name"":""David"",""Age"":24}"; diff --git a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/JsonContentTests.cs b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/JsonContentTests.cs index 08d2d8200987f5..b87c53641eb7b6 100644 --- a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/JsonContentTests.cs +++ b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/JsonContentTests.cs @@ -45,7 +45,6 @@ public void JsonContentObjectType() [Fact] public void JsonContentMediaType() { - MediaTypeHeaderValue mediaType = MediaTypeHeaderValue.Parse("foo/bar; charset=utf-16"); Type fooType = typeof(Foo); Foo foo = new Foo(); @@ -59,6 +58,7 @@ public void JsonContentMediaType() Assert.Equal("utf-8", content.Headers.ContentType.CharSet); // Use the specified MediaTypeHeaderValue if provided. + MediaTypeHeaderValue mediaType = MediaTypeHeaderValue.Parse("foo/bar; charset=utf-8"); content = new JsonContent(fooType, foo, mediaType: mediaType); Assert.Same(mediaType, content.Headers.ContentType); @@ -75,15 +75,15 @@ public void JsonContentMediaType() Assert.Equal(mediaTypeAsString, content.Headers.ContentType.MediaType); Assert.Equal("utf-8", content.Headers.ContentType.CharSet); - // Use the specifed mediaType and charset. + // Specifying a charset is not supported by the string overload. string mediaTypeAndCharSetAsString = "foo/bar; charset=utf-16"; - content = new JsonContent(fooType, foo, mediaType: mediaTypeAndCharSetAsString); - Assert.Equal("foo/bar", content.Headers.ContentType.MediaType); - Assert.Equal("utf-16", content.Headers.ContentType.CharSet); + Assert.Throws(() => new JsonContent(fooType, foo, mediaType: mediaTypeAndCharSetAsString)); + Assert.Throws(() => JsonContent.Create(foo, mediaType: mediaTypeAndCharSetAsString)); - content = JsonContent.Create(foo, mediaType: mediaTypeAndCharSetAsString); - Assert.Equal("foo/bar", content.Headers.ContentType.MediaType); - Assert.Equal("utf-16", content.Headers.ContentType.CharSet); + // Charsets other than UTF-8 are not supported. + mediaType = MediaTypeHeaderValue.Parse("foo/bar; charset=utf-16"); + Assert.Throws(() => new JsonContent(fooType, foo, mediaType: mediaType)); + Assert.Throws(() => JsonContent.Create(foo, mediaType: mediaType)); } [Fact] @@ -99,16 +99,7 @@ await LoopbackServer.CreateClientAndServerAsync( await client.SendAsync(request); request = new HttpRequestMessage(HttpMethod.Post, uri); - request.Content = JsonContent.Create(Person.Create(), mediaType: "foo/bar; charset=utf-16"); - await client.SendAsync(request); - - request = new HttpRequestMessage(HttpMethod.Post, uri); - MediaTypeHeaderValue mediaType = MediaTypeHeaderValue.Parse("foo/bar"); - request.Content = JsonContent.Create(Person.Create(), mediaType: mediaType); - await client.SendAsync(request); - - request = new HttpRequestMessage(HttpMethod.Post, uri); - mediaType = MediaTypeHeaderValue.Parse("foo/bar; charset=baz"); + MediaTypeHeaderValue mediaType = MediaTypeHeaderValue.Parse("foo/bar; charset=utf-8"); request.Content = JsonContent.Create(Person.Create(), mediaType: mediaType); await client.SendAsync(request); } @@ -118,13 +109,7 @@ await LoopbackServer.CreateClientAndServerAsync( Assert.Equal("foo/bar; charset=utf-8", req.GetSingleHeaderValue("Content-Type")); req = await server.HandleRequestAsync(); - Assert.Equal("foo/bar; charset=utf-16", req.GetSingleHeaderValue("Content-Type")); - - req = await server.HandleRequestAsync(); - Assert.Equal("foo/bar", req.GetSingleHeaderValue("Content-Type")); - - req = await server.HandleRequestAsync(); - Assert.Equal("foo/bar; charset=baz", req.GetSingleHeaderValue("Content-Type")); + Assert.Equal("foo/bar; charset=utf-8", req.GetSingleHeaderValue("Content-Type")); }); } @@ -146,18 +131,34 @@ public void JsonContentMediaTypeIsNull() } [Fact] - public void JsonContentTypeIsNull() + public async Task JsonContentTypeIsNull() { - Assert.Throws(() => new JsonContent(null, null)); - Assert.Throws(() => new JsonContent(null, null, MediaTypeHeaderValue.Parse("foo/bar; charset=utf-16"))); + HttpClient client = new HttpClient(); + string foo = "test"; + + var request = new HttpRequestMessage(HttpMethod.Post, "http://example.com"); + request.Content = new JsonContent(null, foo); + await Assert.ThrowsAsync(() => client.SendAsync(request)); + + request = new HttpRequestMessage(HttpMethod.Post, "http://example.com"); + request.Content = new JsonContent(null, foo, MediaTypeHeaderValue.Parse("application/json; charset=utf-8")); + await Assert.ThrowsAsync(() => client.SendAsync(request)); } [Fact] - public void JsonContentThrowsOnIncompatibleType() + public async Task JsonContentThrowsOnIncompatibleTypeAsync() { + HttpClient client = new HttpClient(); var foo = new Foo(); - Assert.Throws(() => new JsonContent(typeof(Bar), foo)); - Assert.Throws(() => new JsonContent(typeof(Bar), foo, MediaTypeHeaderValue.Parse("foo/bar; charset=utf-16"))); + Type typeOfBar = typeof(Bar); + + var request = new HttpRequestMessage(HttpMethod.Post, "http://example.com"); + request.Content = new JsonContent(typeOfBar, foo); + await Assert.ThrowsAsync(() => client.SendAsync(request)); + + request = new HttpRequestMessage(HttpMethod.Post, "http://example.com"); + request.Content = new JsonContent(typeOfBar, foo, MediaTypeHeaderValue.Parse("application/json; charset=utf-8")); + await Assert.ThrowsAsync(() => client.SendAsync(request)); } } } diff --git a/src/libraries/pkg/descriptions.json b/src/libraries/pkg/descriptions.json index c87d4149744057..88ca15427cc5ff 100644 --- a/src/libraries/pkg/descriptions.json +++ b/src/libraries/pkg/descriptions.json @@ -1231,7 +1231,7 @@ }, { "Name": "System.Net.Http.Json", - "Description": "Provides extension methods for System.Net.Http.HttpClient and System.Net.Http.HttpContent that focus on ease of use of JSON as the data interchange format for HTTP clients (pending approval).", + "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", From 9dfd8693826e772d207e928c379de2387ed4aff3 Mon Sep 17 00:00:00 2001 From: David Cantu Date: Fri, 13 Mar 2020 11:23:30 -0700 Subject: [PATCH 11/32] Fix csproj formatting and use open StrongNameKeyId --- .../System.Net.Http.Json/Directory.Build.props | 4 ++-- .../System.Net.Http.Json/ref/System.Net.Http.Json.cs | 3 --- .../ref/System.Net.Http.Json.csproj | 1 + .../ref/System.Net.Http.Json.netcoreapp.cs | 11 +++++++++++ .../System.Net.Http.Json.Functional.Tests.csproj | 8 ++++---- 5 files changed, 18 insertions(+), 9 deletions(-) create mode 100644 src/libraries/System.Net.Http.Json/ref/System.Net.Http.Json.netcoreapp.cs diff --git a/src/libraries/System.Net.Http.Json/Directory.Build.props b/src/libraries/System.Net.Http.Json/Directory.Build.props index 465e1110d6b07f..749d7fc1c6b56d 100644 --- a/src/libraries/System.Net.Http.Json/Directory.Build.props +++ b/src/libraries/System.Net.Http.Json/Directory.Build.props @@ -1,7 +1,7 @@  - Microsoft + Open true - \ 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 index 7dc1f7f4de2857..56a7ddb0427a3b 100644 --- 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 @@ -38,9 +38,6 @@ public JsonContent(System.Type type, object? value, System.Text.Json.JsonSeriali public static System.Net.Http.Json.JsonContent Create(T value, string mediaType, System.Text.Json.JsonSerializerOptions? options = null) { throw null; } public static System.Net.Http.Json.JsonContent Create(T value, 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; } -#if !NETSTANDARD2_0 - protected override System.Threading.Tasks.Task SerializeToStreamAsync(System.IO.Stream stream, System.Net.TransportContext context, System.Threading.CancellationToken cancellationToken) { throw null; } -#endif 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 index e0ad7b0d43b1b3..b59dd09ae596cb 100644 --- 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 @@ -7,6 +7,7 @@ + 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 00000000000000..0410a1c3f4476c --- /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 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/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 index da8a09e33d4f89..07d31fabec7c05 100644 --- 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 @@ -4,6 +4,9 @@ + + + @@ -33,8 +36,5 @@ Common\System\Threading\Tasks\TaskTimeoutExtensions.cs - - - - \ No newline at end of file + From 708316d9a18fafbbe020c626d53237e2eb06f48f Mon Sep 17 00:00:00 2001 From: David Cantu Date: Sun, 15 Mar 2020 19:31:51 -0700 Subject: [PATCH 12/32] Address API review comments. --- .../ref/System.Net.Http.Json.cs | 35 +++++------ .../Http/Json/HttpClientJsonExtensions.Get.cs | 24 ++++++-- .../Json/HttpClientJsonExtensions.Post.cs | 32 +++------- .../Http/Json/HttpClientJsonExtensions.Put.cs | 32 +++------- .../Http/Json/HttpContentJsonExtensions.cs | 26 +++++---- .../src/System/Net/Http/Json/JsonContent.cs | 58 +++++++++---------- .../HttpContentJsonExtensionsTests.cs | 15 +++-- .../tests/FunctionalTests/JsonContentTests.cs | 20 +++++++ 8 files changed, 123 insertions(+), 119 deletions(-) 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 index 56a7ddb0427a3b..fd69ed09cb6054 100644 --- 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 @@ -9,18 +9,22 @@ 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 = null, 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 = null, 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 = null, 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 = 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, System.Type type, object? 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, System.Type type, object? 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, T 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, T 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, System.Type type, object? 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, System.Type type, object? 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, T 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, T value, System.Text.Json.JsonSerializerOptions? options = null, 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.Text.Json.JsonSerializerOptions? options = null, 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 = null, 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 = null, 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 = null, 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 { @@ -29,14 +33,11 @@ public static partial class HttpContentJsonExtensions } public partial class JsonContent : System.Net.Http.HttpContent { - public JsonContent(System.Type type, object? value, System.Net.Http.Headers.MediaTypeHeaderValue mediaType, System.Text.Json.JsonSerializerOptions? options = null) { } - public JsonContent(System.Type type, object? value, string mediaType, System.Text.Json.JsonSerializerOptions? options = null) { } - public JsonContent(System.Type type, object? value, System.Text.Json.JsonSerializerOptions? options = null) { } + 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, System.Text.Json.JsonSerializerOptions? options = null) { throw null; } public static System.Net.Http.Json.JsonContent Create(T value, System.Net.Http.Headers.MediaTypeHeaderValue mediaType, System.Text.Json.JsonSerializerOptions? options = null) { throw null; } - public static System.Net.Http.Json.JsonContent Create(T value, string mediaType, System.Text.Json.JsonSerializerOptions? options = null) { throw null; } - public static System.Net.Http.Json.JsonContent Create(T value, 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/src/System/Net/Http/Json/HttpClientJsonExtensions.Get.cs b/src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Get.cs index 50457640532d38..289fd5c7347901 100644 --- 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 @@ -13,7 +13,7 @@ namespace System.Net.Http.Json /// public static partial class HttpClientJsonExtensions { - public static Task GetFromJsonAsync(this HttpClient client, string requestUri, Type type, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) + public static Task GetFromJsonAsync(this HttpClient client, string? requestUri, Type type, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) { if (client == null) { @@ -24,7 +24,7 @@ public static partial class HttpClientJsonExtensions return GetFromJsonAsyncCore(taskResponse, type, options, cancellationToken); } - public static Task GetFromJsonAsync(this HttpClient client, Uri requestUri, Type type, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) + public static Task GetFromJsonAsync(this HttpClient client, Uri? requestUri, Type type, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) { if (client == null) { @@ -35,7 +35,7 @@ public static partial class HttpClientJsonExtensions return GetFromJsonAsyncCore(taskResponse, type, options, cancellationToken); } - public static Task GetFromJsonAsync(this HttpClient client, string requestUri, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) + public static Task GetFromJsonAsync(this HttpClient client, string? requestUri, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) { if (client == null) { @@ -43,10 +43,10 @@ public static Task GetFromJsonAsync(this HttpClient client, string request } Task taskResponse = client.GetAsync(requestUri, HttpCompletionOption.ResponseHeadersRead, cancellationToken); - return GetFromJsonAsyncCore(taskResponse, options, cancellationToken); + return GetFromJsonAsyncCore(taskResponse, options, cancellationToken); } - public static Task GetFromJsonAsync(this HttpClient client, Uri requestUri, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) + public static Task GetFromJsonAsync(this HttpClient client, Uri? requestUri, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) { if (client == null) { @@ -54,9 +54,21 @@ public static Task GetFromJsonAsync(this HttpClient client, Uri requestUri } Task taskResponse = client.GetAsync(requestUri, HttpCompletionOption.ResponseHeadersRead, cancellationToken); - return GetFromJsonAsyncCore(taskResponse, options, cancellationToken); + return GetFromJsonAsyncCore(taskResponse, options, cancellationToken); } + public static Task GetFromJsonAsync(this HttpClient client, string? requestUri, Type type, CancellationToken cancellationToken = default) + => client.GetFromJsonAsync(requestUri, type, null, cancellationToken); + + public static Task GetFromJsonAsync(this HttpClient client, Uri? requestUri, Type type, CancellationToken cancellationToken = default) + => client.GetFromJsonAsync(requestUri, type, null, cancellationToken); + + public static Task GetFromJsonAsync(this HttpClient client, string? requestUri, CancellationToken cancellationToken = default) + => client.GetFromJsonAsync(requestUri, null, cancellationToken); + + public static Task GetFromJsonAsync(this HttpClient client, Uri? requestUri, CancellationToken cancellationToken = default) + => client.GetFromJsonAsync(requestUri, null, cancellationToken); + private static async Task GetFromJsonAsyncCore(Task taskResponse, Type type, JsonSerializerOptions? options, CancellationToken cancellationToken) { using (HttpResponseMessage response = await taskResponse.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 index 5600565123f66e..5b188baae4fa1e 100644 --- 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 @@ -10,48 +10,32 @@ namespace System.Net.Http.Json { public static partial class HttpClientJsonExtensions { - public static Task PostAsJsonAsync(this HttpClient client, string requestUri, Type type, object? value, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) + 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 = new JsonContent(type, value, options); + JsonContent content = JsonContent.Create(value, JsonContent.DefaultMediaType, options); return client.PostAsync(requestUri, content, cancellationToken); } - public static Task PostAsJsonAsync(this HttpClient client, Uri requestUri, Type type, object? value, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) + 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 = new JsonContent(type, value, options); + JsonContent content = JsonContent.Create(value, JsonContent.DefaultMediaType, options); return client.PostAsync(requestUri, content, cancellationToken); } - public static Task PostAsJsonAsync(this HttpClient client, string requestUri, T value, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) - { - if (client == null) - { - throw new ArgumentNullException(nameof(client)); - } - - JsonContent content = JsonContent.Create(value, options); - return client.PostAsync(requestUri, content, cancellationToken); - } + public static Task PostAsJsonAsync(this HttpClient client, string? requestUri, TValue value, CancellationToken cancellationToken) + => client.PostAsJsonAsync(requestUri, value, null, cancellationToken); - public static Task PostAsJsonAsync(this HttpClient client, Uri requestUri, T value, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) - { - if (client == null) - { - throw new ArgumentNullException(nameof(client)); - } - - JsonContent content = JsonContent.Create(value, options); - return client.PostAsync(requestUri, content, cancellationToken); - } + public static Task PostAsJsonAsync(this HttpClient client, Uri? requestUri, TValue value, CancellationToken cancellationToken) + => client.PostAsJsonAsync(requestUri, value, 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 index 55c226c3793141..a7e59a66d4d1ad 100644 --- 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 @@ -10,48 +10,32 @@ namespace System.Net.Http.Json { public static partial class HttpClientJsonExtensions { - public static Task PutAsJsonAsync(this HttpClient client, string requestUri, Type type, object? value, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) + 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 = new JsonContent(type, value, options); + JsonContent content = JsonContent.Create(value, JsonContent.DefaultMediaType, options); return client.PutAsync(requestUri, content, cancellationToken); } - public static Task PutAsJsonAsync(this HttpClient client, Uri requestUri, Type type, object? value, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) + 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 = new JsonContent(type, value, options); + JsonContent content = JsonContent.Create(value, JsonContent.DefaultMediaType, options); return client.PutAsync(requestUri, content, cancellationToken); } - public static Task PutAsJsonAsync(this HttpClient client, string requestUri, T value, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) - { - if (client == null) - { - throw new ArgumentNullException(nameof(client)); - } - - JsonContent content = JsonContent.Create(value, options); - return client.PutAsync(requestUri, content, cancellationToken); - } + public static Task PutAsJsonAsync(this HttpClient client, string? requestUri, TValue value, CancellationToken cancellationToken) + => client.PutAsJsonAsync(requestUri, value, null, cancellationToken); - public static Task PutAsJsonAsync(this HttpClient client, Uri requestUri, T value, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) - { - if (client == null) - { - throw new ArgumentNullException(nameof(client)); - } - - JsonContent content = JsonContent.Create(value, options); - return client.PutAsync(requestUri, content, cancellationToken); - } + public static Task PutAsJsonAsync(this HttpClient client, Uri? requestUri, TValue value, CancellationToken cancellationToken) + => client.PutAsJsonAsync(requestUri, value, 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 index 2fa6e50052639e..d101f8c838761f 100644 --- 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 @@ -26,26 +26,19 @@ public static Task ReadFromJsonAsync(this HttpContent content, JsonSeriali private static async Task ReadFromJsonAsyncCore(HttpContent content, Type type, JsonSerializerOptions? options, CancellationToken cancellationToken) { - Stream contentStream = await GetJsonStreamFromContentAsync(content, cancellationToken).ConfigureAwait(false); + Stream contentStream = await GetJsonStreamFromContentAsync(content).ConfigureAwait(false); return await JsonSerializer.DeserializeAsync(contentStream, type, options, cancellationToken); } private static async Task ReadFromJsonAsyncCore(HttpContent content, JsonSerializerOptions? options, CancellationToken cancellationToken) { - Stream contentStream = await GetJsonStreamFromContentAsync(content, cancellationToken).ConfigureAwait(false); + Stream contentStream = await GetJsonStreamFromContentAsync(content).ConfigureAwait(false); return await JsonSerializer.DeserializeAsync(contentStream, options, cancellationToken); } - private static async Task GetJsonStreamFromContentAsync(HttpContent content, CancellationToken cancellationToken) + private static Task GetJsonStreamFromContentAsync(HttpContent content) { - string? mediaType = content.Headers.ContentType?.MediaType; - - if (mediaType != JsonContent.JsonMediaType && - mediaType != MediaTypeNames.Text.Plain) - { - throw new NotSupportedException(SR.ContentTypeNotSupported); - } - + ValidateMediaType(content.Headers.ContentType?.MediaType); Debug.Assert(content.Headers.ContentType != null); string? charset = content.Headers.ContentType.CharSet; @@ -78,7 +71,16 @@ private static async Task GetJsonStreamFromContentAsync(HttpContent cont throw new NotSupportedException(SR.CharSetNotSupported); } - return await content.ReadAsStreamAsync().ConfigureAwait(false); + return content.ReadAsStreamAsync(); + } + + private static void ValidateMediaType(string? mediaType) + { + if (mediaType != JsonContent.JsonMediaType && + mediaType != MediaTypeNames.Text.Plain) + { + throw new NotSupportedException(SR.ContentTypeNotSupported); + } } } } 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 index 20924d76e53200..1a36cbe5029ce7 100644 --- 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 @@ -2,7 +2,6 @@ // 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; @@ -15,55 +14,54 @@ namespace System.Net.Http.Json public partial class JsonContent : HttpContent { internal const string JsonMediaType = "application/json"; - private readonly JsonSerializerOptions? _jsonSerializerOptions; - - private static MediaTypeHeaderValue CreateMediaTypeFromString(string mediaTypeAsString) - { - MediaTypeHeaderValue mediaType = new MediaTypeHeaderValue(mediaTypeAsString); - Debug.Assert(mediaType.CharSet == null); - mediaType.CharSet = Encoding.UTF8.WebName; - - return mediaType; - } + internal static readonly MediaTypeHeaderValue DefaultMediaType = MediaTypeHeaderValue.Parse(string.Format("{0} {1}", JsonMediaType, Encoding.UTF8.WebName)); + private readonly JsonSerializerOptions? _jsonSerializerOptions; public Type ObjectType { get; } - public object? Value { get; } - public JsonContent(Type type, object? value, JsonSerializerOptions? options = null) - : this(type, value, CreateMediaTypeFromString(JsonMediaType), options) { } - - public JsonContent(Type type, object? value, string mediaType, JsonSerializerOptions? options = null) - : this(type, value, CreateMediaTypeFromString(mediaType ?? throw new ArgumentNullException(nameof(mediaType))), options) { } - - public JsonContent(Type type, object? value, MediaTypeHeaderValue mediaType, JsonSerializerOptions? options = null) + private JsonContent(object? value, Type inputType, MediaTypeHeaderValue mediaType, JsonSerializerOptions? options) { if (mediaType == null) { throw new ArgumentNullException(nameof(mediaType)); } - // TODO: Support other charsets once https://github.com/dotnet/runtime/issues/30260 is done. - if (mediaType.CharSet != Encoding.UTF8.WebName) + if (inputType == null) { - throw new NotSupportedException(SR.CharSetInvalid); + throw new ArgumentNullException(nameof(inputType)); } Value = value; - ObjectType = type; + ObjectType = inputType; Headers.ContentType = mediaType; - // TODO: Set DefaultWebOptions if no options were provided. + + // // TODO: Support other charsets once https://github.com/dotnet/runtime/issues/30260 is done. + // string charset = mediaType.CharSet; + // if (charset != null && charset != Encoding.UTF8.WebName) + // { + // // Add validations for uppercase, quoted and invalid charsets. + // _encoding = Encoding.GetEncoding(charset); + // //throw new NotSupportedException(SR.CharSetInvalid); + // } + _jsonSerializerOptions = options; } - public static JsonContent Create(T value, JsonSerializerOptions? options = null) - => Create(value, CreateMediaTypeFromString(JsonMediaType), options); + public static JsonContent Create(T value, MediaTypeHeaderValue mediaType, JsonSerializerOptions? options = null) + { + return Create(value, typeof(T), mediaType, options); + } - public static JsonContent Create(T value, string mediaType, JsonSerializerOptions? options = null) - => Create(value, CreateMediaTypeFromString(mediaType ?? throw new ArgumentNullException(nameof(mediaType))), options); + public static JsonContent Create(object? inputValue, Type inputType, MediaTypeHeaderValue mediaType, JsonSerializerOptions? options = null) + { + if (mediaType == null) + { + throw new ArgumentNullException(nameof(mediaType)); + } - public static JsonContent Create(T value, MediaTypeHeaderValue mediaType, JsonSerializerOptions? options = null) - => new JsonContent(typeof(T), value, mediaType); + return new JsonContent(inputValue, inputType, mediaType, options); + } protected override Task SerializeToStreamAsync(Stream stream, TransportContext context) #if !NETSTANDARD2_0 diff --git a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpContentJsonExtensionsTests.cs b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpContentJsonExtensionsTests.cs index 001796abaae2c1..47fa5991a7c8cf 100644 --- a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpContentJsonExtensionsTests.cs +++ b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpContentJsonExtensionsTests.cs @@ -161,15 +161,18 @@ await LoopbackServer.CreateClientAndServerAsync( } }, async server => { - byte[] nonUtf8Response = Encoding.Unicode.GetBytes(json); - var buffer = new MemoryStream(); - buffer.Write( + byte[] utf16Content = Encoding.Unicode.GetBytes(json); + byte[] bytes = Encoding.ASCII.GetBytes( $"HTTP/1.1 200 OK" + $"\r\nContent-Type: text/plain; charset=utf-16\r\n" + - $"Content-Length: {nonUtf8Response.Length}\r\n" + - $"Connection:close\r\n\r\n")); - buffer.Write(nonUtf8Response); + $"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, bytes.Length - 1, utf16Content.Length); for (int i = 0; i < NumRequests; i++) { diff --git a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/JsonContentTests.cs b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/JsonContentTests.cs index b87c53641eb7b6..2ba52806583cfe 100644 --- a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/JsonContentTests.cs +++ b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/JsonContentTests.cs @@ -160,5 +160,25 @@ public async Task JsonContentThrowsOnIncompatibleTypeAsync() request.Content = new JsonContent(typeOfBar, foo, MediaTypeHeaderValue.Parse("application/json; charset=utf-8")); await Assert.ThrowsAsync(() => client.SendAsync(request)); } + + [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"); + request.Content = JsonContent.Create(Person.Create(), mediaType: mediaType); + await client.SendAsync(request); + } + }, + async server => { + HttpRequestData req = await server.HandleRequestAsync(); + Assert.Equal("application/json; charset=utf-16", req.GetSingleHeaderValue("Content-Type")); + }); + } } } From 1a63b2c17a4353ae4fd7443054f05159384387ac Mon Sep 17 00:00:00 2001 From: David Cantu Date: Mon, 16 Mar 2020 00:53:42 -0700 Subject: [PATCH 13/32] Add transcoding support for .NET 5 --- .../src/System.Net.Http.Json.csproj | 2 + .../Http/Json/HttpContentJsonExtensions.cs | 47 ++-- .../src/System/Net/Http/Json/JsonContent.cs | 56 ++++- .../Net/Http/Json/JsonContent.netcoreapp.cs | 3 +- .../Net/Http/Json/TranscodingReadStream.cs | 231 ++++++++++++++++++ .../Net/Http/Json/TranscodingWriteStream.cs | 183 ++++++++++++++ 6 files changed, 491 insertions(+), 31 deletions(-) create mode 100644 src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/TranscodingReadStream.cs create mode 100644 src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/TranscodingWriteStream.cs 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 index 4261f0266b82ad..94e434258a04b6 100644 --- 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 @@ -9,6 +9,8 @@ + + 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 index d101f8c838761f..03a19d74d12287 100644 --- 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 @@ -33,15 +33,38 @@ public static Task ReadFromJsonAsync(this HttpContent content, JsonSeriali private static async Task ReadFromJsonAsyncCore(HttpContent content, JsonSerializerOptions? options, CancellationToken cancellationToken) { Stream contentStream = await GetJsonStreamFromContentAsync(content).ConfigureAwait(false); - return await JsonSerializer.DeserializeAsync(contentStream, options, cancellationToken); + return await JsonSerializer.DeserializeAsync(contentStream, options, cancellationToken).ConfigureAwait(false); } - private static Task GetJsonStreamFromContentAsync(HttpContent content) + private static async Task GetJsonStreamFromContentAsync(HttpContent content) { ValidateMediaType(content.Headers.ContentType?.MediaType); Debug.Assert(content.Headers.ContentType != null); - string? charset = content.Headers.ContentType.CharSet; + Encoding? sourceEncoding = GetEncoding(content.Headers.ContentType.CharSet); + + Stream jsonStream = 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) + { + jsonStream = new TranscodingReadStream(jsonStream, sourceEncoding); + } + + return jsonStream; + } + + private static void ValidateMediaType(string? mediaType) + { + if (mediaType != JsonContent.JsonMediaType && + mediaType != MediaTypeNames.Text.Plain) + { + throw new NotSupportedException(SR.ContentTypeNotSupported); + } + } + + private static Encoding? GetEncoding(string charset) + { Encoding? encoding = null; if (charset != null) @@ -62,25 +85,11 @@ private static Task GetJsonStreamFromContentAsync(HttpContent content) { throw new InvalidOperationException(SR.CharSetInvalid, e); } - } - //TODO: We should allow encodings other than UTF-8 and we should transcode to UTF-8 if we get one. - // This would be easier to achieve once https://github.com/dotnet/runtime/issues/30260 is done. - if (encoding != null && encoding != Encoding.UTF8) - { - throw new NotSupportedException(SR.CharSetNotSupported); + Debug.Assert(encoding != null); } - return content.ReadAsStreamAsync(); - } - - private static void ValidateMediaType(string? mediaType) - { - if (mediaType != JsonContent.JsonMediaType && - mediaType != MediaTypeNames.Text.Plain) - { - throw new NotSupportedException(SR.ContentTypeNotSupported); - } + return encoding; } } } 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 index 1a36cbe5029ce7..c167cbf93e1f96 100644 --- 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 @@ -2,6 +2,7 @@ // 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; @@ -35,16 +36,6 @@ private JsonContent(object? value, Type inputType, MediaTypeHeaderValue mediaTyp Value = value; ObjectType = inputType; Headers.ContentType = mediaType; - - // // TODO: Support other charsets once https://github.com/dotnet/runtime/issues/30260 is done. - // string charset = mediaType.CharSet; - // if (charset != null && charset != Encoding.UTF8.WebName) - // { - // // Add validations for uppercase, quoted and invalid charsets. - // _encoding = Encoding.GetEncoding(charset); - // //throw new NotSupportedException(SR.CharSetInvalid); - // } - _jsonSerializerOptions = options; } @@ -67,7 +58,7 @@ protected override Task SerializeToStreamAsync(Stream stream, TransportContext c #if !NETSTANDARD2_0 => SerializeToStreamAsync(stream, context, CancellationToken.None); #else - => JsonSerializer.SerializeAsync(stream, Value, ObjectType, _jsonSerializerOptions); + => JsonSerializer.SerializeAsync(GetStreamToWriteTo(stream), Value, ObjectType, _jsonSerializerOptions); #endif protected override bool TryComputeLength(out long length) @@ -75,5 +66,48 @@ protected override bool TryComputeLength(out long length) length = 0; return false; } + + private Stream GetStreamToWriteTo(Stream targetStream) + { + Stream jsonStream = targetStream; + 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) + { + jsonStream = new TranscodingWriteStream(jsonStream, targetEncoding); + } + + return jsonStream; + } + + private 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 index 48b6bc6ce662e4..f5979724188d40 100644 --- 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 @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System.IO; +using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -12,6 +13,6 @@ namespace System.Net.Http.Json public partial class JsonContent { protected override Task SerializeToStreamAsync(Stream stream, TransportContext context, CancellationToken cancellationToken) - => JsonSerializer.SerializeAsync(stream, Value, ObjectType, _jsonSerializerOptions, cancellationToken); + => JsonSerializer.SerializeAsync(GetStreamToWriteTo(stream), Value, ObjectType, _jsonSerializerOptions, 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 00000000000000..a0d44e253a1549 --- /dev/null +++ b/src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/TranscodingReadStream.cs @@ -0,0 +1,231 @@ +// 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.Buffers; +using System.Diagnostics; +using System.IO; +using System.Text; +using System.Text.Unicode; +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 + + internal const int MaxByteBufferSize = 4096; + internal const int MaxCharBufferSize = 3 * MaxByteBufferSize; + + private readonly Stream _stream; + private readonly Decoder _decoder; + + 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. + // However limit the buffer size to prevent an encoding that has a very poor worst-case scenario. + // The conversion process is tolerant of char buffer that is not large enough to convert all the bytes at once. + int maxCharBufferSize = Math.Min(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(); + } + + 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 void Flush() + => throw new NotSupportedException(); + + public override int Read(byte[] buffer, int offset, int count) + => throw new NotSupportedException(); + + public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + ThrowArgumentOutOfRangeException(buffer, offset, count); + + if (count == 0) + { + return 0; + } + + ArraySegment readBuffer = new ArraySegment(buffer, offset, count); + + if (_overflowBuffer.Count > 0) + { + int bytesToCopy = Math.Min(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; + } + + if (_charBuffer.Count == 0) + { + // Only read more content from the input stream if we have exhausted all the buffered chars. + await ReadInputChars(cancellationToken).ConfigureAwait(false); + } + + OperationStatus operationStatus = Utf8.FromUtf16(_charBuffer, readBuffer, out int charsRead, out int bytesWritten, isFinalBlock: false); + _charBuffer = _charBuffer.Slice(charsRead); + + switch (operationStatus) + { + case OperationStatus.Done: + return bytesWritten; + + case OperationStatus.DestinationTooSmall: + if (bytesWritten != 0) + { + return bytesWritten; + } + + // Overflow buffer is always empty when we get here and we can use it's full length to write contents to. + Utf8.FromUtf16(_charBuffer, _overflowBuffer.Array, out int overFlowChars, out int overflowBytes, isFinalBlock: false); + + 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; + + default: + Debug.Fail("We should never see this"); + throw new InvalidOperationException(); + } + } + + private async Task ReadInputChars(CancellationToken cancellationToken) + { + Debug.Assert(_byteBuffer.Array != null); + // If we had left-over bytes from a previous read, move it to the start of the buffer and read content in to + // the segment that follows. + Buffer.BlockCopy( + _byteBuffer.Array, + _byteBuffer.Offset, + _byteBuffer.Array, + 0, + _byteBuffer.Count); + + int readBytes = await _stream.ReadAsync(_byteBuffer.Array.AsMemory(_byteBuffer.Count), cancellationToken).ConfigureAwait(false); + _byteBuffer = new ArraySegment(_byteBuffer.Array, 0, _byteBuffer.Count + readBytes); + + Debug.Assert(_charBuffer.Count == 0, "We should only expect to read more input chars once all buffered content is read"); + + _decoder.Convert( + _byteBuffer.AsSpan(), + _charBuffer.Array, + flush: readBytes == 0, + out int bytesUsed, + out int charsUsed, + out _); + + Debug.Assert(_charBuffer.Array != null); + + _byteBuffer = _byteBuffer.Slice(bytesUsed); + _charBuffer = new ArraySegment(_charBuffer.Array, 0, charsUsed); + } + + private static void ThrowArgumentOutOfRangeException(byte[] buffer, int offset, int count) + { + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + + if (offset < 0 || offset >= buffer.Length) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + + if (buffer.Length - offset < count) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + } + + 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) + { + Debug.Assert(_charBuffer.Array != null); + Debug.Assert(_byteBuffer.Array != null); + Debug.Assert(_overflowBuffer.Array != null); + _disposed = true; + ArrayPool.Shared.Return(_charBuffer.Array); + ArrayPool.Shared.Return(_byteBuffer.Array); + ArrayPool.Shared.Return(_overflowBuffer.Array); + } + } + } +} 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 00000000000000..7a81e4821cb525 --- /dev/null +++ b/src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/TranscodingWriteStream.cs @@ -0,0 +1,183 @@ +// 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; +using System.Buffers; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace System.Net.Http.Json +{ + internal sealed class TranscodingWriteStream : Stream + { + internal const int MaxCharBufferSize = 4096; + internal const int MaxByteBufferSize = 4 * MaxCharBufferSize; + private readonly int _maxByteBufferSize; + + private readonly Stream _stream; + private readonly Decoder _decoder; + private readonly Encoder _encoder; + private readonly 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 async Task FlushAsync(CancellationToken cancellationToken) + { + await _stream.FlushAsync(cancellationToken).ConfigureAwait(false); + } + + 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) + { + ThrowArgumentException(buffer, offset, count); + ArraySegment bufferSegment = new ArraySegment(buffer, offset, count); + return WriteAsync(bufferSegment, cancellationToken); + } + + private async Task WriteAsync( + ArraySegment bufferSegment, + CancellationToken cancellationToken) + { + bool decoderCompleted = false; + while (!decoderCompleted) + { + _decoder.Convert( + bufferSegment, + _charBuffer.AsSpan(_charsDecoded), + flush: false, + out int bytesDecoded, + out int charsDecoded, + out decoderCompleted); + + _charsDecoded += charsDecoded; + bufferSegment = bufferSegment.Slice(bytesDecoded); + + if (!decoderCompleted) + { + 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.AsSpan(charsWritten, _charsDecoded - charsWritten), + byteBuffer, + flush: false, + out int charsEncoded, + out int bytesUsed, + out encoderCompleted); + + await _stream.WriteAsync(byteBuffer.AsMemory(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; + } + + private static void ThrowArgumentException(byte[] buffer, int offset, int count) + { + if (count <= 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + + if (offset < 0 || offset >= buffer.Length) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + + if (buffer.Length - offset < count) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + } + + protected override void Dispose(bool disposing) + { + if (!_disposed) + { + _disposed = true; + ArrayPool.Shared.Return(_charBuffer); + } + } + + public async Task FinalWriteAsync(CancellationToken cancellationToken) + { + // First write any buffered content + await WriteBufferAsync(cancellationToken).ConfigureAwait(false); + + // Now flush the encoder. + byte[] byteBuffer = ArrayPool.Shared.Rent(_maxByteBufferSize); + bool encoderCompleted = false; + + while (!encoderCompleted) + { + _encoder.Convert( + Array.Empty(), + byteBuffer, + flush: true, + out _, + out int bytesUsed, + out encoderCompleted); + + await _stream.WriteAsync(byteBuffer.AsMemory(0, bytesUsed), cancellationToken).ConfigureAwait(false); + } + + ArrayPool.Shared.Return(byteBuffer); + } + } +} From 8a8df04a8aff658ed451725ab5eb551c817e3ae7 Mon Sep 17 00:00:00 2001 From: David Cantu Date: Mon, 16 Mar 2020 16:46:22 -0700 Subject: [PATCH 14/32] Port TranscodingRead/WriteStream to netstandard2.0 --- .../src/Properties/InternalsVisibleTo.cs | 7 + .../src/System.Net.Http.Json.csproj | 5 + .../ArraySegmentExtensions.netstandard.cs | 24 ++ .../Net/Http/Json/TranscodingReadStream.cs | 52 +++- .../Net/Http/Json/TranscodingWriteStream.cs | 21 ++ ...stem.Net.Http.Json.Functional.Tests.csproj | 15 +- .../TranscodingReadStreamTests.cs | 260 ++++++++++++++++++ .../TranscodingWriteStreamTests.cs | 93 +++++++ 8 files changed, 468 insertions(+), 9 deletions(-) create mode 100644 src/libraries/System.Net.Http.Json/src/Properties/InternalsVisibleTo.cs create mode 100644 src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/ArraySegmentExtensions.netstandard.cs create mode 100644 src/libraries/System.Net.Http.Json/tests/FunctionalTests/TranscodingReadStreamTests.cs create mode 100644 src/libraries/System.Net.Http.Json/tests/FunctionalTests/TranscodingWriteStreamTests.cs diff --git a/src/libraries/System.Net.Http.Json/src/Properties/InternalsVisibleTo.cs b/src/libraries/System.Net.Http.Json/src/Properties/InternalsVisibleTo.cs new file mode 100644 index 00000000000000..ebfde5cba53130 --- /dev/null +++ b/src/libraries/System.Net.Http.Json/src/Properties/InternalsVisibleTo.cs @@ -0,0 +1,7 @@ +// 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.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("System.Net.Http.Json.Functional.Tests, PublicKey=00240000048000009400000006020000002400005253413100040000010001004b86c4cb78549b34bab61a3b1800e23bfeb5b3ec390074041536a7e3cbd97f5f04cf0f857155a8928eaa29ebfd11cfbbad3ba70efea7bda3226c6a8d370a4cd303f714486b6ebc225985a638471e6ef571cc92a4613c00b8fa65d61ccee0cbe5f36330c9a01f4183559f1bef24cc2917c6d913e3a541333a1d05d9bed22b38cb")] 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 index 94e434258a04b6..d9ecaa89a3fe67 100644 --- 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 @@ -4,6 +4,7 @@ enable + @@ -19,6 +20,10 @@ + + + + diff --git a/src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/ArraySegmentExtensions.netstandard.cs b/src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/ArraySegmentExtensions.netstandard.cs new file mode 100644 index 00000000000000..887281e5a01372 --- /dev/null +++ b/src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/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.Net.Http.Json +{ + 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/TranscodingReadStream.cs b/src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/TranscodingReadStream.cs index a0d44e253a1549..cd4082180ba488 100644 --- 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 @@ -6,7 +6,9 @@ using System.Diagnostics; using System.IO; using System.Text; +#if !NETSTANDARD2_0 using System.Text.Unicode; +#endif using System.Threading; using System.Threading.Tasks; @@ -22,6 +24,10 @@ internal sealed class TranscodingReadStream : Stream private readonly Stream _stream; private readonly Decoder _decoder; +#if NETSTANDARD2_0 + private readonly Encoder _encoder; +#endif + private ArraySegment _byteBuffer; private ArraySegment _charBuffer; private ArraySegment _overflowBuffer; @@ -54,6 +60,10 @@ public TranscodingReadStream(Stream input, Encoding sourceEncoding) count: 0); _decoder = sourceEncoding.GetDecoder(); + +#if NETSTANDARD2_0 + _encoder = Encoding.UTF8.GetEncoder(); +#endif } public override bool CanRead => true; @@ -106,7 +116,22 @@ public override async Task ReadAsync(byte[] buffer, int offset, int count, await ReadInputChars(cancellationToken).ConfigureAwait(false); } +#if NETSTANDARD2_0 + OperationStatus operationStatus; + int charsRead = 0, bytesWritten = 0; + if (_encoder.GetByteCount(_charBuffer.Array, _charBuffer.Offset, _charBuffer.Count, false) > readBuffer.Count) + { + operationStatus = OperationStatus.DestinationTooSmall; + } + else + { + _encoder.Convert(_charBuffer.Array, _charBuffer.Offset, _charBuffer.Count, readBuffer.Array, readBuffer.Offset, readBuffer.Count, + false, out charsRead, out bytesWritten, out bool _); + operationStatus = OperationStatus.Done; + } +#else OperationStatus operationStatus = Utf8.FromUtf16(_charBuffer, readBuffer, out int charsRead, out int bytesWritten, isFinalBlock: false); +#endif _charBuffer = _charBuffer.Slice(charsRead); switch (operationStatus) @@ -121,7 +146,12 @@ public override async Task ReadAsync(byte[] buffer, int offset, int count, } // Overflow buffer is always empty when we get here and we can use it's full length to write contents to. +#if NETSTANDARD2_0 + _encoder.Convert(_charBuffer.Array, _charBuffer.Offset, _charBuffer.Count, _overflowBuffer.Array, _overflowBuffer.Offset, _overflowBuffer.Count, + false, out int overFlowChars, out int overflowBytes, out bool _); +#else Utf8.FromUtf16(_charBuffer, _overflowBuffer.Array, out int overFlowChars, out int overflowBytes, isFinalBlock: false); +#endif 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."); @@ -161,12 +191,20 @@ private async Task ReadInputChars(CancellationToken cancellationToken) _byteBuffer.Array, 0, _byteBuffer.Count); - - int readBytes = await _stream.ReadAsync(_byteBuffer.Array.AsMemory(_byteBuffer.Count), cancellationToken).ConfigureAwait(false); + int readBytes = +#if NETSTANDARD2_0 + await _stream.ReadAsync(_byteBuffer.Array, _byteBuffer.Count, _byteBuffer.Array.Length, cancellationToken).ConfigureAwait(false); +#else + await _stream.ReadAsync(_byteBuffer.Array.AsMemory(_byteBuffer.Count), cancellationToken).ConfigureAwait(false); +#endif _byteBuffer = new ArraySegment(_byteBuffer.Array, 0, _byteBuffer.Count + readBytes); Debug.Assert(_charBuffer.Count == 0, "We should only expect to read more input chars once all buffered content is read"); +#if NETSTANDARD2_0 + _decoder.Convert(_byteBuffer.Array, _byteBuffer.Offset, _byteBuffer.Count, _charBuffer.Array, 0, _charBuffer.Array.Length, + flush: readBytes == 0, out int bytesUsed, out int charsUsed, out _); +#else _decoder.Convert( _byteBuffer.AsSpan(), _charBuffer.Array, @@ -174,6 +212,7 @@ private async Task ReadInputChars(CancellationToken cancellationToken) out int bytesUsed, out int charsUsed, out _); +#endif Debug.Assert(_charBuffer.Array != null); @@ -218,12 +257,15 @@ protected override void Dispose(bool disposing) { if (!_disposed) { - Debug.Assert(_charBuffer.Array != null); - Debug.Assert(_byteBuffer.Array != null); - Debug.Assert(_overflowBuffer.Array != null); _disposed = true; + + Debug.Assert(_charBuffer.Array != null); ArrayPool.Shared.Return(_charBuffer.Array); + + Debug.Assert(_byteBuffer.Array != null); ArrayPool.Shared.Return(_byteBuffer.Array); + + Debug.Assert(_overflowBuffer.Array != null); ArrayPool.Shared.Return(_overflowBuffer.Array); } } 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 index 7a81e4821cb525..58ecaa03c613e6 100644 --- 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 @@ -82,8 +82,13 @@ private async Task WriteAsync( CancellationToken cancellationToken) { bool decoderCompleted = false; + while (!decoderCompleted) { +#if NETSTANDARD2_0 + _decoder.Convert(bufferSegment.Array, bufferSegment.Offset, bufferSegment.Count, _charBuffer, _charsDecoded, _charBuffer.Length - _charsDecoded, + flush: false, out int bytesDecoded, out int charsDecoded, out decoderCompleted); +#else _decoder.Convert( bufferSegment, _charBuffer.AsSpan(_charsDecoded), @@ -91,6 +96,7 @@ private async Task WriteAsync( out int bytesDecoded, out int charsDecoded, out decoderCompleted); +#endif _charsDecoded += charsDecoded; bufferSegment = bufferSegment.Slice(bytesDecoded); @@ -110,6 +116,12 @@ private async Task WriteBufferAsync(CancellationToken cancellationToken) while (!encoderCompleted && charsWritten < _charsDecoded) { +#if NETSTANDARD2_0 + _encoder.Convert(_charBuffer, charsWritten, _charsDecoded - charsWritten, byteBuffer, 0, byteBuffer.Length, + flush: false, out int charsEncoded, out int bytesUsed, out encoderCompleted); + + await _stream.WriteAsync(byteBuffer, 0, bytesUsed, cancellationToken).ConfigureAwait(false); +#else _encoder.Convert( _charBuffer.AsSpan(charsWritten, _charsDecoded - charsWritten), byteBuffer, @@ -119,6 +131,8 @@ private async Task WriteBufferAsync(CancellationToken cancellationToken) out encoderCompleted); await _stream.WriteAsync(byteBuffer.AsMemory(0, bytesUsed), cancellationToken).ConfigureAwait(false); +#endif + charsWritten += charsEncoded; } @@ -166,6 +180,12 @@ public async Task FinalWriteAsync(CancellationToken cancellationToken) while (!encoderCompleted) { +#if NETSTANDARD2_0 + _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); +#else _encoder.Convert( Array.Empty(), byteBuffer, @@ -175,6 +195,7 @@ public async Task FinalWriteAsync(CancellationToken cancellationToken) out encoderCompleted); await _stream.WriteAsync(byteBuffer.AsMemory(0, bytesUsed), cancellationToken).ConfigureAwait(false); +#endif } ArrayPool.Shared.Return(byteBuffer); 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 index 07d31fabec7c05..587a2e409d8852 100644 --- 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 @@ -3,12 +3,14 @@ $(NetCoreAppCurrent);$(NetFrameworkCurrent) - - - + + + + + - + Common\System\Net\Capability.Security.cs @@ -37,4 +39,9 @@ Common\System\Threading\Tasks\TaskTimeoutExtensions.cs + + + + + diff --git a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/TranscodingReadStreamTests.cs b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/TranscodingReadStreamTests.cs new file mode 100644 index 00000000000000..19ca65039a5c4d --- /dev/null +++ b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/TranscodingReadStreamTests.cs @@ -0,0 +1,260 @@ +// 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.Collections.Generic; +using System.Diagnostics; +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 + var input = "Hello world"; + var encoding = Encoding.Unicode; + using var stream = new TranscodingReadStream(new MemoryStream(encoding.GetBytes(input)), encoding); + var bytes = new byte[4]; + + // Act + var 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(10, stream.CharBufferCount); + Assert.Equal(0, stream.OverflowCount); + } + + [Fact] + public async Task ReadAsync_FillsBuffer() + { + Debugger.Launch(); + // Arrange + var input = "Hello world"; + var encoding = Encoding.Unicode; + using var stream = new TranscodingReadStream(new MemoryStream(encoding.GetBytes(input)), encoding); + var bytes = new byte[3]; + var expected = Encoding.UTF8.GetBytes(input.Substring(0, bytes.Length)); + + // Act + var readBytes = await stream.ReadAsync(bytes, 0, bytes.Length); + + // Assert + Assert.Equal(3, readBytes); + Assert.Equal(expected, bytes); + Assert.Equal(0, stream.ByteBufferCount); + Assert.Equal(8, stream.CharBufferCount); + Assert.Equal(0, stream.OverflowCount); + } + + [Fact] + public async Task ReadAsync_CompletedInSecondIteration() + { + // Arrange + var input = new string('A', 1024 + 10); + var encoding = Encoding.Unicode; + using var stream = new TranscodingReadStream(new MemoryStream(encoding.GetBytes(input)), encoding); + var bytes = new byte[1024]; + var expected = Encoding.UTF8.GetBytes(input.Substring(0, bytes.Length)); + + // Act + var 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 + var input = "☀"; + var encoding = Encoding.Unicode; + using var stream = new TranscodingReadStream(new MemoryStream(encoding.GetBytes(input)), encoding); + var bytes = new byte[1]; + var expected = Encoding.UTF8.GetBytes(input); + + // Act + var 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 => new TheoryData + { + new string('a', TranscodingReadStream.MaxCharBufferSize - 1) + "☀", + new string('a', TranscodingReadStream.MaxCharBufferSize - 2) + "☀", + new string('a', TranscodingReadStream.MaxCharBufferSize) + "☀", + }; + + [Theory] + [MemberData(nameof(ReadAsync_WithOverflowBuffer_AtBoundariesData))] + public Task ReadAsync_WithOverflowBuffer_WithBufferSize1(string input) => ReadAsync_WithOverflowBufferAtCharBufferBoundaries(input, bufferSize: 1); + + [Theory] + [MemberData(nameof(ReadAsync_WithOverflowBuffer_AtBoundariesData))] + public Task ReadAsync_WithOverflowBuffer_WithBufferSize2(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 + var encoding = Encoding.Unicode; + var stream = new TranscodingReadStream(new MemoryStream(encoding.GetBytes(input)), encoding); + var bytes = new byte[1]; + var expected = Encoding.UTF8.GetBytes(input); + + // Act + int read; + var buffer = new byte[bufferSize]; + var actual = new List(); + + while ((read = await stream.ReadAsync(buffer, 0, bufferSize)) != 0) + { + actual.AddRange(buffer); + } + + Assert.Equal(expected, actual); + return stream; + } + + public static TheoryData ReadAsyncInputLatin => + GetLatinTextInput(TranscodingReadStream.MaxCharBufferSize, TranscodingReadStream.MaxByteBufferSize); + + public static TheoryData ReadAsyncInputUnicode => + GetUnicodeText(TranscodingReadStream.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('Æ', count: 7), + new string('A', count: maxCharBufferSize - 1) + 'Æ', + "AbĀāĂ㥹ĆŊŋŌōŎŏŐőŒœŔŕŖŗŘřŚşŠšŢţŤťŦŧŨũŪūŬŭŮůŰűŲųŴŵŶŷŸŹźŻżŽžſAbc", + "Abcஐஒஓஔகஙசஜஞடணதநனபமயரறலளழவஷஸஹ", + "☀☁☂☃☄★☆☇☈☉☊☋☌☍☎☏☐☑☒☓☚☛☜☝☞☟☠☡☢☣☤☥☦☧☨☩☪☫☬☭☮☯☰☱☲☳☴☵☶☷☸", + new string('Æ', count: 64 * 1024), + new string('Æ', count: 64 * 1024 + 1), + "pingüino", + new string('ऄ', count: maxCharBufferSize + 1), // This uses 3 bytes to represent in UTF8 + }; + } + + [Theory] + [MemberData(nameof(ReadAsyncInputLatin))] + [MemberData(nameof(ReadAsyncInputUnicode))] + public Task ReadAsync_Works_WhenInputIs_UTF32(string message) + { + var sourceEncoding = Encoding.UTF32; + return ReadAsyncTest(sourceEncoding, message); + } + + [Theory] + [MemberData(nameof(ReadAsyncInputLatin))] + [MemberData(nameof(ReadAsyncInputUnicode))] + public Task ReadAsync_Works_WhenInputIs_Unicode(string message) + { + var sourceEncoding = Encoding.Unicode; + return ReadAsyncTest(sourceEncoding, message); + } + + [Theory] + [MemberData(nameof(ReadAsyncInputLatin))] + [MemberData(nameof(ReadAsyncInputUnicode))] + public Task ReadAsync_Works_WhenInputIs_UTF7(string message) + { + var sourceEncoding = Encoding.UTF7; + return ReadAsyncTest(sourceEncoding, message); + } + + [Theory] + [MemberData(nameof(ReadAsyncInputLatin))] + public Task ReadAsync_Works_WhenInputIs_WesternEuropeanEncoding(string message) + { + // Arrange + var sourceEncoding = Encoding.GetEncoding(28591); + return ReadAsyncTest(sourceEncoding, message); + } + + [Theory] + [MemberData(nameof(ReadAsyncInputLatin))] + public Task ReadAsync_Works_WhenInputIs_ASCII(string message) + { + // Arrange + var sourceEncoding = Encoding.ASCII; + return ReadAsyncTest(sourceEncoding, message); + } + + private static async Task ReadAsyncTest(Encoding sourceEncoding, string message) + { + var input = $"{{ \"Message\": \"{message}\" }}"; + var stream = new MemoryStream(sourceEncoding.GetBytes(input)); + + var transcodingStream = new TranscodingReadStream(stream, sourceEncoding); + + var model = await JsonSerializer.DeserializeAsync(transcodingStream, typeof(TestModel)); + var testModel = Assert.IsType(model); + + Assert.Equal(message, testModel.Message); + } + + public class TestModel + { + public string Message { get; set; } + } + + } +} diff --git a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/TranscodingWriteStreamTests.cs b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/TranscodingWriteStreamTests.cs new file mode 100644 index 00000000000000..e8f9057febd609 --- /dev/null +++ b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/TranscodingWriteStreamTests.cs @@ -0,0 +1,93 @@ +// 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) + { + var targetEncoding = Encoding.UTF32; + return WriteAsyncTest(targetEncoding, message); + } + + [Theory] + [MemberData(nameof(WriteAsyncInputLatin))] + [MemberData(nameof(WriteAsyncInputUnicode))] + public Task WriteAsync_Works_WhenOutputIs_Unicode(string message) + { + var targetEncoding = Encoding.Unicode; + return WriteAsyncTest(targetEncoding, message); + } + + [Theory] + [MemberData(nameof(WriteAsyncInputLatin))] + public Task WriteAsync_Works_WhenOutputIs_UTF7(string message) + { + var targetEncoding = Encoding.UTF7; + return WriteAsyncTest(targetEncoding, message); + } + + [Theory] + [MemberData(nameof(WriteAsyncInputLatin))] + public Task WriteAsync_Works_WhenOutputIs_WesternEuropeanEncoding(string message) + { + // Arrange + var targetEncoding = Encoding.GetEncoding(28591); + return WriteAsyncTest(targetEncoding, message); + } + + + [Theory] + [MemberData(nameof(WriteAsyncInputLatin))] + public Task WriteAsync_Works_WhenOutputIs_ASCII(string message) + { + // Arrange + var 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()); + await transcodingStream.FinalWriteAsync(default); + await transcodingStream.FlushAsync(); + + var actual = targetEncoding.GetString(stream.ToArray()); + Assert.Equal(expected, actual, StringComparer.OrdinalIgnoreCase); + } + + private class TestModel + { + public string Message { get; set; } + } + } +} From 304aab6daa9d885004be0c9c390b17b196dc5ad6 Mon Sep 17 00:00:00 2001 From: David Cantu Date: Wed, 18 Mar 2020 22:24:30 -0700 Subject: [PATCH 15/32] Remove netcoreapp dependencies on TranscodingStream classes. --- .../ref/System.Net.Http.Json.cs | 4 +- .../src/Properties/InternalsVisibleTo.cs | 7 - .../src/System.Net.Http.Json.csproj | 1 - .../Http/Json/HttpContentJsonExtensions.cs | 22 +- .../src/System/Net/Http/Json/JsonContent.cs | 49 ++-- .../Net/Http/Json/JsonContent.netcoreapp.cs | 2 +- .../Net/Http/Json/TranscodingReadStream.cs | 215 ++++++++---------- .../Net/Http/Json/TranscodingWriteStream.cs | 89 +++----- ...stem.Net.Http.Json.Functional.Tests.csproj | 8 +- .../TranscodingReadStreamTests.cs | 47 ++-- 10 files changed, 187 insertions(+), 257 deletions(-) delete mode 100644 src/libraries/System.Net.Http.Json/src/Properties/InternalsVisibleTo.cs 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 index fd69ed09cb6054..c44ee208adb88e 100644 --- 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 @@ -36,8 +36,8 @@ public 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, System.Text.Json.JsonSerializerOptions? options = null) { throw null; } - public static System.Net.Http.Json.JsonContent Create(T value, System.Net.Http.Headers.MediaTypeHeaderValue mediaType, System.Text.Json.JsonSerializerOptions? options = null) { throw null; } + public static System.Net.Http.Json.JsonContent Create(object? inputValue, System.Type inputType, System.Net.Http.Headers.MediaTypeHeaderValue? mediaType, System.Text.Json.JsonSerializerOptions? options = null) { throw null; } + public static System.Net.Http.Json.JsonContent Create(T inputValue, System.Net.Http.Headers.MediaTypeHeaderValue? mediaType, 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/src/Properties/InternalsVisibleTo.cs b/src/libraries/System.Net.Http.Json/src/Properties/InternalsVisibleTo.cs deleted file mode 100644 index ebfde5cba53130..00000000000000 --- a/src/libraries/System.Net.Http.Json/src/Properties/InternalsVisibleTo.cs +++ /dev/null @@ -1,7 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System.Runtime.CompilerServices; - -[assembly: InternalsVisibleTo("System.Net.Http.Json.Functional.Tests, PublicKey=00240000048000009400000006020000002400005253413100040000010001004b86c4cb78549b34bab61a3b1800e23bfeb5b3ec390074041536a7e3cbd97f5f04cf0f857155a8928eaa29ebfd11cfbbad3ba70efea7bda3226c6a8d370a4cd303f714486b6ebc225985a638471e6ef571cc92a4613c00b8fa65d61ccee0cbe5f36330c9a01f4183559f1bef24cc2917c6d913e3a541333a1d05d9bed22b38cb")] 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 index d9ecaa89a3fe67..57d3b40bc3ea87 100644 --- 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 @@ -4,7 +4,6 @@ enable - 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 index 03a19d74d12287..3b687f873a9366 100644 --- 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 @@ -15,25 +15,25 @@ namespace System.Net.Http.Json public static class HttpContentJsonExtensions { public static Task ReadFromJsonAsync(this HttpContent content, Type type, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) - { - return ReadFromJsonAsyncCore(content, type, options, cancellationToken); - } + => ReadFromJsonAsyncCore(content, type, options, cancellationToken); public static Task ReadFromJsonAsync(this HttpContent content, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) - { - return ReadFromJsonAsyncCore(content, options, cancellationToken); - } + => ReadFromJsonAsyncCore(content, options, cancellationToken); private static async Task ReadFromJsonAsyncCore(HttpContent content, Type type, JsonSerializerOptions? options, CancellationToken cancellationToken) { - Stream contentStream = await GetJsonStreamFromContentAsync(content).ConfigureAwait(false); - return await JsonSerializer.DeserializeAsync(contentStream, type, options, cancellationToken); + using (Stream contentStream = await GetJsonStreamFromContentAsync(content).ConfigureAwait(false)) + { + return await JsonSerializer.DeserializeAsync(contentStream, type, options, cancellationToken).ConfigureAwait(false); + } } private static async Task ReadFromJsonAsyncCore(HttpContent content, JsonSerializerOptions? options, CancellationToken cancellationToken) { - Stream contentStream = await GetJsonStreamFromContentAsync(content).ConfigureAwait(false); - return await JsonSerializer.DeserializeAsync(contentStream, options, cancellationToken).ConfigureAwait(false); + using (Stream contentStream = await GetJsonStreamFromContentAsync(content).ConfigureAwait(false)) + { + return await JsonSerializer.DeserializeAsync(contentStream, options, cancellationToken).ConfigureAwait(false); + } } private static async Task GetJsonStreamFromContentAsync(HttpContent content) @@ -57,7 +57,7 @@ private static async Task GetJsonStreamFromContentAsync(HttpContent cont private static void ValidateMediaType(string? mediaType) { if (mediaType != JsonContent.JsonMediaType && - mediaType != MediaTypeNames.Text.Plain) + mediaType != MediaTypeNames.Text.Plain) { throw new NotSupportedException(SR.ContentTypeNotSupported); } 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 index c167cbf93e1f96..78a1cdd6e64c25 100644 --- 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 @@ -21,45 +21,27 @@ public partial class JsonContent : HttpContent public Type ObjectType { get; } public object? Value { get; } - private JsonContent(object? value, Type inputType, MediaTypeHeaderValue mediaType, JsonSerializerOptions? options) + private JsonContent(object? inputValue, Type inputType, MediaTypeHeaderValue? mediaType, JsonSerializerOptions? options) { - if (mediaType == null) - { - throw new ArgumentNullException(nameof(mediaType)); - } - if (inputType == null) { throw new ArgumentNullException(nameof(inputType)); } - Value = value; + Value = inputValue; ObjectType = inputType; - Headers.ContentType = mediaType; + Headers.ContentType = mediaType ?? DefaultMediaType; _jsonSerializerOptions = options; } - public static JsonContent Create(T value, MediaTypeHeaderValue mediaType, JsonSerializerOptions? options = null) - { - return Create(value, typeof(T), mediaType, options); - } + public static JsonContent Create(T inputValue, MediaTypeHeaderValue? mediaType, JsonSerializerOptions? options = null) + => Create(inputValue, typeof(T), mediaType, options); - public static JsonContent Create(object? inputValue, Type inputType, MediaTypeHeaderValue mediaType, JsonSerializerOptions? options = null) - { - if (mediaType == null) - { - throw new ArgumentNullException(nameof(mediaType)); - } - - return new JsonContent(inputValue, inputType, mediaType, options); - } + public static JsonContent Create(object? inputValue, Type inputType, MediaTypeHeaderValue? mediaType, JsonSerializerOptions? options = null) + => new JsonContent(inputValue, inputType, mediaType, options); protected override Task SerializeToStreamAsync(Stream stream, TransportContext context) -#if !NETSTANDARD2_0 - => SerializeToStreamAsync(stream, context, CancellationToken.None); -#else - => JsonSerializer.SerializeAsync(GetStreamToWriteTo(stream), Value, ObjectType, _jsonSerializerOptions); -#endif + => SerializeToStreamAsyncCore(stream, CancellationToken.None); protected override bool TryComputeLength(out long length) { @@ -67,18 +49,23 @@ protected override bool TryComputeLength(out long length) return false; } - private Stream GetStreamToWriteTo(Stream targetStream) + private async Task SerializeToStreamAsyncCore(Stream targetStream, CancellationToken cancellationToken) { - Stream jsonStream = targetStream; 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) { - jsonStream = new TranscodingWriteStream(jsonStream, targetEncoding); + using (TranscodingWriteStream transcodingStream = new TranscodingWriteStream(targetStream, targetEncoding)) + { + await JsonSerializer.SerializeAsync(transcodingStream, Value, ObjectType, _jsonSerializerOptions, cancellationToken).ConfigureAwait(false); + await transcodingStream.FinalWriteAsync(cancellationToken).ConfigureAwait(false); + } + } + else + { + await JsonSerializer.SerializeAsync(targetStream, Value, ObjectType, _jsonSerializerOptions, cancellationToken).ConfigureAwait(false); } - - return jsonStream; } private static Encoding? GetEncoding(string charset) 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 index f5979724188d40..203d0c9c8e4d16 100644 --- 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 @@ -13,6 +13,6 @@ namespace System.Net.Http.Json public partial class JsonContent { protected override Task SerializeToStreamAsync(Stream stream, TransportContext context, CancellationToken cancellationToken) - => JsonSerializer.SerializeAsync(GetStreamToWriteTo(stream), Value, ObjectType, _jsonSerializerOptions, 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 index cd4082180ba488..51efb040cc87ee 100644 --- 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 @@ -2,13 +2,12 @@ // 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; -#if !NETSTANDARD2_0 -using System.Text.Unicode; -#endif using System.Threading; using System.Threading.Tasks; @@ -23,10 +22,7 @@ internal sealed class TranscodingReadStream : Stream private readonly Stream _stream; private readonly Decoder _decoder; - -#if NETSTANDARD2_0 private readonly Encoder _encoder; -#endif private ArraySegment _byteBuffer; private ArraySegment _charBuffer; @@ -39,31 +35,19 @@ public TranscodingReadStream(Stream input, Encoding sourceEncoding) // 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); + _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. // However limit the buffer size to prevent an encoding that has a very poor worst-case scenario. // The conversion process is tolerant of char buffer that is not large enough to convert all the bytes at once. int maxCharBufferSize = Math.Min(MaxCharBufferSize, sourceEncoding.GetMaxCharCount(MaxByteBufferSize)); - _charBuffer = new ArraySegment( - ArrayPool.Shared.Rent(maxCharBufferSize), - 0, - count: 0); + _charBuffer = new ArraySegment(ArrayPool.Shared.Rent(maxCharBufferSize), 0, count: 0); - _overflowBuffer = new ArraySegment( - ArrayPool.Shared.Rent(OverflowBufferSize), - 0, - count: 0); + _overflowBuffer = new ArraySegment(ArrayPool.Shared.Rent(OverflowBufferSize), 0, count: 0); _decoder = sourceEncoding.GetDecoder(); - -#if NETSTANDARD2_0 _encoder = Encoding.UTF8.GetEncoder(); -#endif } public override bool CanRead => true; @@ -81,26 +65,47 @@ public override long Position internal int CharBufferCount => _charBuffer.Count; internal int OverflowCount => _overflowBuffer.Count; - public override void Flush() - => throw new NotSupportedException(); + public override void Flush() { } public override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException(); - public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { - ThrowArgumentOutOfRangeException(buffer, offset, count); + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } - if (count == 0) + if (offset < 0) { - return 0; + throw new ArgumentOutOfRangeException(nameof(offset)); + } + + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + + if (buffer.Length - offset < count) + { + throw new ArgumentOutOfRangeException(); } ArraySegment 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(count, _overflowBuffer.Count); + int bytesToCopy = Math.Min(readBuffer.Count, _overflowBuffer.Count); _overflowBuffer.Slice(0, bytesToCopy).CopyTo(readBuffer); _overflowBuffer = _overflowBuffer.Slice(bytesToCopy); @@ -110,132 +115,93 @@ public override async Task ReadAsync(byte[] buffer, int offset, int count, return bytesToCopy; } + // Only read more content from the input stream if we have exhausted all the buffered chars. if (_charBuffer.Count == 0) { - // Only read more content from the input stream if we have exhausted all the buffered chars. - await ReadInputChars(cancellationToken).ConfigureAwait(false); + int bytesRead = await ReadInputChars(cancellationToken).ConfigureAwait(false); + + if (bytesRead == 0) + { + // We are done, flush encoder. + _encoder.Convert(_charBuffer.Array!, 0, 0, _byteBuffer.Array!, 0, 0, flush: true, out _, out _, out _); + return 0; + } } -#if NETSTANDARD2_0 - OperationStatus operationStatus; - int charsRead = 0, bytesWritten = 0; - if (_encoder.GetByteCount(_charBuffer.Array, _charBuffer.Offset, _charBuffer.Count, false) > readBuffer.Count) - { - operationStatus = OperationStatus.DestinationTooSmall; - } - else + bool completed = false; + int charsRead = default; + int bytesWritten = default; + // 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, - false, out charsRead, out bytesWritten, out bool _); - operationStatus = OperationStatus.Done; + _encoder.Convert(_charBuffer.Array!, _charBuffer.Offset, _charBuffer.Count, readBuffer.Array!, readBuffer.Offset, readBuffer.Count, + flush: false, out charsRead, out bytesWritten, out completed); } -#else - OperationStatus operationStatus = Utf8.FromUtf16(_charBuffer, readBuffer, out int charsRead, out int bytesWritten, isFinalBlock: false); -#endif + _charBuffer = _charBuffer.Slice(charsRead); - switch (operationStatus) + if (completed) { - case OperationStatus.Done: + return bytesWritten; + } + else + { + if (bytesWritten > 0) + { return bytesWritten; + } - case OperationStatus.DestinationTooSmall: - if (bytesWritten != 0) - { - return bytesWritten; - } - - // Overflow buffer is always empty when we get here and we can use it's full length to write contents to. -#if NETSTANDARD2_0 - _encoder.Convert(_charBuffer.Array, _charBuffer.Offset, _charBuffer.Count, _overflowBuffer.Array, _overflowBuffer.Offset, _overflowBuffer.Count, - false, out int overFlowChars, out int overflowBytes, out bool _); -#else - Utf8.FromUtf16(_charBuffer, _overflowBuffer.Array, out int overFlowChars, out int overflowBytes, isFinalBlock: false); -#endif + _encoder.Convert(_charBuffer.Array!, _charBuffer.Offset, _charBuffer.Count, _overflowBuffer.Array!, 0, _overflowBuffer.Array!.Length, + flush: false, 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."); + 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); + _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); + // 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.Array != null); - Debug.Assert(_overflowBuffer.Count != 0); + _overflowBuffer = new ArraySegment(_overflowBuffer.Array, readBuffer.Count, overflowBytes - readBuffer.Count); - return readBuffer.Count; + Debug.Assert(_overflowBuffer.Count > 0); - default: - Debug.Fail("We should never see this"); - throw new InvalidOperationException(); + return readBuffer.Count; } } - private async Task ReadInputChars(CancellationToken cancellationToken) + private async Task ReadInputChars(CancellationToken cancellationToken) { - Debug.Assert(_byteBuffer.Array != null); - // If we had left-over bytes from a previous read, move it to the start of the buffer and read content in to + // 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. - Buffer.BlockCopy( - _byteBuffer.Array, - _byteBuffer.Offset, - _byteBuffer.Array, - 0, - _byteBuffer.Count); - int readBytes = -#if NETSTANDARD2_0 - await _stream.ReadAsync(_byteBuffer.Array, _byteBuffer.Count, _byteBuffer.Array.Length, cancellationToken).ConfigureAwait(false); -#else - await _stream.ReadAsync(_byteBuffer.Array.AsMemory(_byteBuffer.Count), cancellationToken).ConfigureAwait(false); -#endif - _byteBuffer = new ArraySegment(_byteBuffer.Array, 0, _byteBuffer.Count + readBytes); + 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"); -#if NETSTANDARD2_0 _decoder.Convert(_byteBuffer.Array, _byteBuffer.Offset, _byteBuffer.Count, _charBuffer.Array, 0, _charBuffer.Array.Length, - flush: readBytes == 0, out int bytesUsed, out int charsUsed, out _); -#else - _decoder.Convert( - _byteBuffer.AsSpan(), - _charBuffer.Array, - flush: readBytes == 0, - out int bytesUsed, - out int charsUsed, - out _); -#endif + flush: bytesRead == 0, out int bytesUsed, out int charsUsed, out _); - Debug.Assert(_charBuffer.Array != null); + // We flush only when the stream is exhausted and there are no pending bytes in the buffer. + Debug.Assert(bytesRead != 0 || _byteBuffer.Count == 0); _byteBuffer = _byteBuffer.Slice(bytesUsed); _charBuffer = new ArraySegment(_charBuffer.Array, 0, charsUsed); - } - - private static void ThrowArgumentOutOfRangeException(byte[] buffer, int offset, int count) - { - if (count < 0) - { - throw new ArgumentOutOfRangeException(nameof(count)); - } - - if (offset < 0 || offset >= buffer.Length) - { - throw new ArgumentOutOfRangeException(nameof(offset)); - } - if (buffer.Length - offset < count) - { - throw new ArgumentOutOfRangeException(nameof(count)); - } + return bytesRead; } public override long Seek(long offset, SeekOrigin origin) @@ -261,12 +227,17 @@ protected override void Dispose(bool disposing) 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 index 58ecaa03c613e6..0eb6d5f9791ce8 100644 --- 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 @@ -2,9 +2,9 @@ // 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; +// Taken from https://github.com/dotnet/aspnetcore/blob/master/src/Mvc/Mvc.Core/src/Formatters/TranscodingWriteStream.cs + using System.Buffers; -using System.Collections.Generic; using System.IO; using System.Text; using System.Threading; @@ -21,7 +21,7 @@ internal sealed class TranscodingWriteStream : Stream private readonly Stream _stream; private readonly Decoder _decoder; private readonly Encoder _encoder; - private readonly char[] _charBuffer; + private char[] _charBuffer; private int _charsDecoded; private bool _disposed; @@ -72,31 +72,38 @@ public override void Write(byte[] buffer, int offset, int count) public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { - ThrowArgumentException(buffer, offset, count); + 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 ArgumentOutOfRangeException(); + } + ArraySegment bufferSegment = new ArraySegment(buffer, offset, count); - return WriteAsync(bufferSegment, cancellationToken); + return WriteAsyncCore(bufferSegment, cancellationToken); } - private async Task WriteAsync( - ArraySegment bufferSegment, - CancellationToken cancellationToken) + private async Task WriteAsyncCore(ArraySegment bufferSegment, CancellationToken cancellationToken) { bool decoderCompleted = false; while (!decoderCompleted) { -#if NETSTANDARD2_0 - _decoder.Convert(bufferSegment.Array, bufferSegment.Offset, bufferSegment.Count, _charBuffer, _charsDecoded, _charBuffer.Length - _charsDecoded, + _decoder.Convert(bufferSegment.Array!, bufferSegment.Offset, bufferSegment.Count, _charBuffer, _charsDecoded, _charBuffer.Length - _charsDecoded, flush: false, out int bytesDecoded, out int charsDecoded, out decoderCompleted); -#else - _decoder.Convert( - bufferSegment, - _charBuffer.AsSpan(_charsDecoded), - flush: false, - out int bytesDecoded, - out int charsDecoded, - out decoderCompleted); -#endif _charsDecoded += charsDecoded; bufferSegment = bufferSegment.Slice(bytesDecoded); @@ -116,23 +123,10 @@ private async Task WriteBufferAsync(CancellationToken cancellationToken) while (!encoderCompleted && charsWritten < _charsDecoded) { -#if NETSTANDARD2_0 _encoder.Convert(_charBuffer, charsWritten, _charsDecoded - charsWritten, byteBuffer, 0, byteBuffer.Length, flush: false, out int charsEncoded, out int bytesUsed, out encoderCompleted); await _stream.WriteAsync(byteBuffer, 0, bytesUsed, cancellationToken).ConfigureAwait(false); -#else - _encoder.Convert( - _charBuffer.AsSpan(charsWritten, _charsDecoded - charsWritten), - byteBuffer, - flush: false, - out int charsEncoded, - out int bytesUsed, - out encoderCompleted); - - await _stream.WriteAsync(byteBuffer.AsMemory(0, bytesUsed), cancellationToken).ConfigureAwait(false); -#endif - charsWritten += charsEncoded; } @@ -142,30 +136,13 @@ private async Task WriteBufferAsync(CancellationToken cancellationToken) _charsDecoded = 0; } - private static void ThrowArgumentException(byte[] buffer, int offset, int count) - { - if (count <= 0) - { - throw new ArgumentOutOfRangeException(nameof(count)); - } - - if (offset < 0 || offset >= buffer.Length) - { - throw new ArgumentOutOfRangeException(nameof(offset)); - } - - if (buffer.Length - offset < count) - { - throw new ArgumentOutOfRangeException(nameof(count)); - } - } - protected override void Dispose(bool disposing) { if (!_disposed) { _disposed = true; ArrayPool.Shared.Return(_charBuffer); + _charBuffer = null!; } } @@ -180,22 +157,10 @@ public async Task FinalWriteAsync(CancellationToken cancellationToken) while (!encoderCompleted) { -#if NETSTANDARD2_0 _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); -#else - _encoder.Convert( - Array.Empty(), - byteBuffer, - flush: true, - out _, - out int bytesUsed, - out encoderCompleted); - - await _stream.WriteAsync(byteBuffer.AsMemory(0, bytesUsed), cancellationToken).ConfigureAwait(false); -#endif } ArrayPool.Shared.Return(byteBuffer); 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 index 587a2e409d8852..36fb8a201a25cf 100644 --- 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 @@ -39,9 +39,11 @@ Common\System\Threading\Tasks\TaskTimeoutExtensions.cs + + + - - - + + diff --git a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/TranscodingReadStreamTests.cs b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/TranscodingReadStreamTests.cs index 19ca65039a5c4d..3b879412f20a50 100644 --- a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/TranscodingReadStreamTests.cs +++ b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/TranscodingReadStreamTests.cs @@ -5,7 +5,6 @@ // Taken from https://github.com/dotnet/aspnetcore/blob/master/src/Mvc/Mvc.Core/test/Formatters/TranscodingReadStreamTest.cs using System.Collections.Generic; -using System.Diagnostics; using System.IO; using System.Linq; using System.Text; @@ -27,7 +26,7 @@ public async Task ReadAsync_SingleByte() var bytes = new byte[4]; // Act - var readBytes = await stream.ReadAsync(bytes, 0, 1); + int readBytes = await stream.ReadAsync(bytes, 0, 1); // Assert Assert.Equal(1, readBytes); @@ -35,30 +34,29 @@ public async Task ReadAsync_SingleByte() Assert.Equal(0, bytes[1]); Assert.Equal(0, stream.ByteBufferCount); - Assert.Equal(10, stream.CharBufferCount); - Assert.Equal(0, stream.OverflowCount); + Assert.Equal(0, stream.CharBufferCount); + Assert.Equal(10, stream.OverflowCount); } [Fact] public async Task ReadAsync_FillsBuffer() { - Debugger.Launch(); // Arrange - var input = "Hello world"; - var encoding = Encoding.Unicode; - using var stream = new TranscodingReadStream(new MemoryStream(encoding.GetBytes(input)), encoding); - var bytes = new byte[3]; - var expected = Encoding.UTF8.GetBytes(input.Substring(0, bytes.Length)); + 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 - var readBytes = await stream.ReadAsync(bytes, 0, bytes.Length); + 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(8, stream.CharBufferCount); - Assert.Equal(0, stream.OverflowCount); + Assert.Equal(0, stream.CharBufferCount); + Assert.Equal(8, stream.OverflowCount); } [Fact] @@ -243,12 +241,14 @@ private static async Task ReadAsyncTest(Encoding sourceEncoding, string message) var input = $"{{ \"Message\": \"{message}\" }}"; var stream = new MemoryStream(sourceEncoding.GetBytes(input)); - var transcodingStream = new TranscodingReadStream(stream, sourceEncoding); + using (var transcodingStream = new TranscodingReadStream(stream, sourceEncoding)) + { - var model = await JsonSerializer.DeserializeAsync(transcodingStream, typeof(TestModel)); - var testModel = Assert.IsType(model); + var model = await JsonSerializer.DeserializeAsync(transcodingStream, typeof(TestModel)); + var testModel = Assert.IsType(model); - Assert.Equal(message, testModel.Message); + Assert.Equal(message, testModel.Message); + } } public class TestModel @@ -256,5 +256,18 @@ 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 (Stream transcodingStream = new TranscodingReadStream(stream, sourceEncoding)) + { + string deserializedMessage = await JsonSerializer.DeserializeAsync(transcodingStream); + Assert.Equal(message.Trim('"'), deserializedMessage); + } + } } } From b6a5143f7c5e0d91dd4259c3a769fdfa8d457d73 Mon Sep 17 00:00:00 2001 From: David Cantu Date: Thu, 19 Mar 2020 10:25:56 -0700 Subject: [PATCH 16/32] Mark TransportContext as nullable to fix CI issues. --- src/libraries/System.Net.Http.Json/ref/System.Net.Http.Json.cs | 2 +- .../System.Net.Http.Json/ref/System.Net.Http.Json.netcoreapp.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 index c44ee208adb88e..6831447bab314b 100644 --- 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 @@ -38,7 +38,7 @@ internal JsonContent() { } 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, System.Text.Json.JsonSerializerOptions? options = null) { throw null; } public static System.Net.Http.Json.JsonContent Create(T inputValue, System.Net.Http.Headers.MediaTypeHeaderValue? mediaType, 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 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.netcoreapp.cs b/src/libraries/System.Net.Http.Json/ref/System.Net.Http.Json.netcoreapp.cs index 0410a1c3f4476c..489e402268fc90 100644 --- 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 @@ -6,6 +6,6 @@ namespace System.Net.Http.Json { public 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; } + protected override System.Threading.Tasks.Task SerializeToStreamAsync(System.IO.Stream stream, System.Net.TransportContext? context, System.Threading.CancellationToken cancellationToken) { throw null; } } } From d40e26a3534e1d63e3d362d875bb9ad401ab0ddd Mon Sep 17 00:00:00 2001 From: David Cantu Date: Thu, 19 Mar 2020 11:52:04 -0700 Subject: [PATCH 17/32] Addres issues raised by new nullablity awareness code in System.Net.Http --- .../System/Net/Http/Json/HttpClientJsonExtensions.Get.cs | 3 +++ .../System/Net/Http/Json/HttpClientJsonExtensions.Post.cs | 4 ++-- .../src/System/Net/Http/Json/HttpContentJsonExtensions.cs | 2 +- .../src/System/Net/Http/Json/JsonContent.cs | 6 +++--- .../src/System/Net/Http/Json/JsonContent.netcoreapp.cs | 2 +- 5 files changed, 10 insertions(+), 7 deletions(-) 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 index 289fd5c7347901..240040e2e3f41e 100644 --- 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 @@ -2,6 +2,7 @@ // 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.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -74,6 +75,7 @@ public static Task GetFromJsonAsync(this HttpClient client, Uri? using (HttpResponseMessage response = await taskResponse.ConfigureAwait(false)) { response.EnsureSuccessStatusCode(); + Debug.Assert(response.Content != null); return await response.Content.ReadFromJsonAsync(type, options, cancellationToken).ConfigureAwait(false); } @@ -84,6 +86,7 @@ private static async Task GetFromJsonAsyncCore(Task t using (HttpResponseMessage response = await taskResponse.ConfigureAwait(false)) { response.EnsureSuccessStatusCode(); + Debug.Assert(response.Content != null); 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 index 5b188baae4fa1e..6a651c648d30d9 100644 --- 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 @@ -17,7 +17,7 @@ public static Task PostAsJsonAsync(this HttpClient throw new ArgumentNullException(nameof(client)); } - JsonContent content = JsonContent.Create(value, JsonContent.DefaultMediaType, options); + JsonContent content = JsonContent.Create(value, null, options); return client.PostAsync(requestUri, content, cancellationToken); } @@ -28,7 +28,7 @@ public static Task PostAsJsonAsync(this HttpClient throw new ArgumentNullException(nameof(client)); } - JsonContent content = JsonContent.Create(value, JsonContent.DefaultMediaType, options); + JsonContent content = JsonContent.Create(value, null, options); return client.PostAsync(requestUri, content, 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 index 3b687f873a9366..8895013dbbbfe3 100644 --- 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 @@ -63,7 +63,7 @@ private static void ValidateMediaType(string? mediaType) } } - private static Encoding? GetEncoding(string charset) + private static Encoding? GetEncoding(string? charset) { Encoding? encoding = null; 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 index 78a1cdd6e64c25..c9cf0f0117d77b 100644 --- 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 @@ -40,7 +40,7 @@ public static JsonContent Create(T inputValue, MediaTypeHeaderValue? mediaTyp public static JsonContent Create(object? inputValue, Type inputType, MediaTypeHeaderValue? mediaType, JsonSerializerOptions? options = null) => new JsonContent(inputValue, inputType, mediaType, options); - protected override Task SerializeToStreamAsync(Stream stream, TransportContext context) + protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context) => SerializeToStreamAsyncCore(stream, CancellationToken.None); protected override bool TryComputeLength(out long length) @@ -51,7 +51,7 @@ protected override bool TryComputeLength(out long length) private async Task SerializeToStreamAsyncCore(Stream targetStream, CancellationToken cancellationToken) { - Encoding? targetEncoding = GetEncoding(Headers.ContentType.CharSet); + 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) @@ -68,7 +68,7 @@ private async Task SerializeToStreamAsyncCore(Stream targetStream, CancellationT } } - private static Encoding? GetEncoding(string charset) + private static Encoding? GetEncoding(string? charset) { Encoding? encoding = null; 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 index 203d0c9c8e4d16..ff559b196cf0c5 100644 --- 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 @@ -12,7 +12,7 @@ namespace System.Net.Http.Json { public partial class JsonContent { - protected override Task SerializeToStreamAsync(Stream stream, TransportContext context, CancellationToken cancellationToken) + protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context, CancellationToken cancellationToken) => SerializeToStreamAsyncCore(stream, cancellationToken); } } From d7d13bba597f0888ab045c5048f9928bfe335845 Mon Sep 17 00:00:00 2001 From: David Cantu Date: Fri, 20 Mar 2020 03:29:55 -0700 Subject: [PATCH 18/32] Re-enable LoopBackServer tests, address suggestions and code clean-up. --- .../ref/System.Net.Http.Json.cs | 8 +- .../Http/Json/HttpClientJsonExtensions.Get.cs | 22 +- .../Http/Json/HttpClientJsonExtensions.Put.cs | 4 +- .../Http/Json/HttpContentJsonExtensions.cs | 72 +++++-- .../src/System/Net/Http/Json/JsonContent.cs | 2 +- .../Net/Http/Json/TranscodingWriteStream.cs | 6 +- .../HttpClientJsonExtensionsTests.cs | 26 +-- .../HttpContentJsonExtensionsTests.cs | 52 +++-- .../tests/FunctionalTests/JsonContentTests.cs | 99 ++++----- ...stem.Net.Http.Json.Functional.Tests.csproj | 8 +- .../TranscodingReadStreamTests.cs | 197 ++++++++++-------- 11 files changed, 277 insertions(+), 219 deletions(-) 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 index 6831447bab314b..687092ae2fab1d 100644 --- 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 @@ -9,13 +9,13 @@ 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 = null, 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.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 = null, 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 = null, 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 = null, 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; } 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 index 240040e2e3f41e..88b33be3413077 100644 --- 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 @@ -14,7 +14,7 @@ namespace System.Net.Http.Json /// public static partial class HttpClientJsonExtensions { - public static Task GetFromJsonAsync(this HttpClient client, string? requestUri, Type type, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) + public static Task GetFromJsonAsync(this HttpClient client, string? requestUri, Type type, JsonSerializerOptions? options, CancellationToken cancellationToken = default) { if (client == null) { @@ -25,7 +25,7 @@ public static partial class HttpClientJsonExtensions return GetFromJsonAsyncCore(taskResponse, type, options, cancellationToken); } - public static Task GetFromJsonAsync(this HttpClient client, Uri? requestUri, Type type, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) + public static Task GetFromJsonAsync(this HttpClient client, Uri? requestUri, Type type, JsonSerializerOptions? options, CancellationToken cancellationToken = default) { if (client == null) { @@ -36,7 +36,7 @@ public static partial class HttpClientJsonExtensions return GetFromJsonAsyncCore(taskResponse, type, options, cancellationToken); } - public static Task GetFromJsonAsync(this HttpClient client, string? requestUri, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) + public static Task GetFromJsonAsync(this HttpClient client, string? requestUri, JsonSerializerOptions? options, CancellationToken cancellationToken = default) { if (client == null) { @@ -47,7 +47,7 @@ public static Task GetFromJsonAsync(this HttpClient client, stri return GetFromJsonAsyncCore(taskResponse, options, cancellationToken); } - public static Task GetFromJsonAsync(this HttpClient client, Uri? requestUri, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) + public static Task GetFromJsonAsync(this HttpClient client, Uri? requestUri, JsonSerializerOptions? options, CancellationToken cancellationToken = default) { if (client == null) { @@ -75,9 +75,10 @@ public static Task GetFromJsonAsync(this HttpClient client, Uri? using (HttpResponseMessage response = await taskResponse.ConfigureAwait(false)) { response.EnsureSuccessStatusCode(); - Debug.Assert(response.Content != null); - - return await response.Content.ReadFromJsonAsync(type, options, cancellationToken).ConfigureAwait(false); + // 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); } } @@ -86,9 +87,10 @@ private static async Task GetFromJsonAsyncCore(Task t using (HttpResponseMessage response = await taskResponse.ConfigureAwait(false)) { response.EnsureSuccessStatusCode(); - Debug.Assert(response.Content != null); - - return await response.Content.ReadFromJsonAsync(options, cancellationToken).ConfigureAwait(false); + // 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.Put.cs b/src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Put.cs index a7e59a66d4d1ad..ed35819adee86c 100644 --- 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 @@ -17,7 +17,7 @@ public static Task PutAsJsonAsync(this HttpClient c throw new ArgumentNullException(nameof(client)); } - JsonContent content = JsonContent.Create(value, JsonContent.DefaultMediaType, options); + JsonContent content = JsonContent.Create(value, null, options); return client.PutAsync(requestUri, content, cancellationToken); } @@ -28,7 +28,7 @@ public static Task PutAsJsonAsync(this HttpClient c throw new ArgumentNullException(nameof(client)); } - JsonContent content = JsonContent.Create(value, JsonContent.DefaultMediaType, options); + JsonContent content = JsonContent.Create(value, null, options); return client.PutAsync(requestUri, content, 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 index 8895013dbbbfe3..05bbe8009c12e4 100644 --- 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 @@ -15,52 +15,80 @@ namespace System.Net.Http.Json public static class HttpContentJsonExtensions { public static Task ReadFromJsonAsync(this HttpContent content, Type type, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) - => ReadFromJsonAsyncCore(content, type, options, cancellationToken); + { + ValidateContent(content); + Debug.Assert(content.Headers.ContentType != null); + Encoding? sourceEncoding = GetEncoding(content.Headers.ContentType.CharSet); - public static Task ReadFromJsonAsync(this HttpContent content, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) - => ReadFromJsonAsyncCore(content, options, cancellationToken); + return ReadFromJsonAsyncCore(content, type, sourceEncoding, options, cancellationToken); + } - private static async Task ReadFromJsonAsyncCore(HttpContent content, Type type, JsonSerializerOptions? options, CancellationToken cancellationToken) + public static Task ReadFromJsonAsync(this HttpContent content, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) { - using (Stream contentStream = await GetJsonStreamFromContentAsync(content).ConfigureAwait(false)) - { - return await JsonSerializer.DeserializeAsync(contentStream, type, options, cancellationToken).ConfigureAwait(false); - } + ValidateContent(content); + Debug.Assert(content.Headers.ContentType != null); + Encoding? sourceEncoding = GetEncoding(content.Headers.ContentType.CharSet); + + return ReadFromJsonAsyncCore(content, sourceEncoding, options, cancellationToken); } - private static async Task ReadFromJsonAsyncCore(HttpContent content, JsonSerializerOptions? options, CancellationToken cancellationToken) + private static async Task ReadFromJsonAsyncCore(HttpContent content, Type type, Encoding? sourceEncoding, JsonSerializerOptions? options, CancellationToken cancellationToken) { - using (Stream contentStream = await GetJsonStreamFromContentAsync(content).ConfigureAwait(false)) + 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) + { + using (Stream transcodingStream = new TranscodingReadStream(contentStream, sourceEncoding)) + { + return await JsonSerializer.DeserializeAsync(transcodingStream, type, options, cancellationToken).ConfigureAwait(false); + } + } + else { - return await JsonSerializer.DeserializeAsync(contentStream, options, cancellationToken).ConfigureAwait(false); + using (contentStream) + { + return await JsonSerializer.DeserializeAsync(contentStream, type, options, cancellationToken).ConfigureAwait(false); + } } } - private static async Task GetJsonStreamFromContentAsync(HttpContent content) + private static async Task ReadFromJsonAsyncCore(HttpContent content, Encoding? sourceEncoding, JsonSerializerOptions? options, CancellationToken cancellationToken) { - ValidateMediaType(content.Headers.ContentType?.MediaType); - Debug.Assert(content.Headers.ContentType != null); - - Encoding? sourceEncoding = GetEncoding(content.Headers.ContentType.CharSet); - - Stream jsonStream = await content.ReadAsStreamAsync().ConfigureAwait(false); + 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) { - jsonStream = new TranscodingReadStream(jsonStream, sourceEncoding); + using (Stream transcodingStream = new TranscodingReadStream(contentStream, sourceEncoding)) + { + return await JsonSerializer.DeserializeAsync(transcodingStream, options, cancellationToken).ConfigureAwait(false); + } + } + else + { + using (contentStream) + { + return await JsonSerializer.DeserializeAsync(contentStream, options, cancellationToken).ConfigureAwait(false); + } } - - return jsonStream; } - private static void ValidateMediaType(string? mediaType) + private static void ValidateContent(HttpContent content) { + if (content == null) + { + throw new ArgumentNullException(nameof(content)); + } + + string? mediaType = content.Headers.ContentType?.MediaType; + if (mediaType != JsonContent.JsonMediaType && mediaType != MediaTypeNames.Text.Plain) { throw new NotSupportedException(SR.ContentTypeNotSupported); } + } private static Encoding? GetEncoding(string? charset) 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 index c9cf0f0117d77b..70c2bf62ba390b 100644 --- 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 @@ -15,7 +15,7 @@ namespace System.Net.Http.Json public partial class JsonContent : HttpContent { internal const string JsonMediaType = "application/json"; - internal static readonly MediaTypeHeaderValue DefaultMediaType = MediaTypeHeaderValue.Parse(string.Format("{0} {1}", JsonMediaType, Encoding.UTF8.WebName)); + private static MediaTypeHeaderValue DefaultMediaType => MediaTypeHeaderValue.Parse(string.Format("{0}; charset={1}", JsonMediaType, Encoding.UTF8.WebName)); private readonly JsonSerializerOptions? _jsonSerializerOptions; public Type ObjectType { get; } 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 index 0eb6d5f9791ce8..aed04bacebaa72 100644 --- 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 @@ -49,10 +49,8 @@ public TranscodingWriteStream(Stream stream, Encoding targetEncoding) public override void Flush() => throw new NotSupportedException(); - public override async Task FlushAsync(CancellationToken cancellationToken) - { - await _stream.FlushAsync(cancellationToken).ConfigureAwait(false); - } + public override Task FlushAsync(CancellationToken cancellationToken) + => _stream.FlushAsync(cancellationToken); public override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException(); diff --git a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpClientJsonExtensionsTests.cs b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpClientJsonExtensionsTests.cs index 2e17003a738f45..0884505c33acaa 100644 --- a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpClientJsonExtensionsTests.cs +++ b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpClientJsonExtensionsTests.cs @@ -8,6 +8,7 @@ using System.Text.Json; using System.Linq; using System.Collections.Generic; +using System.Threading; namespace System.Net.Http.Json.Functional.Tests { @@ -80,18 +81,17 @@ await LoopbackServer.CreateClientAndServerAsync( using (HttpClient client = new HttpClient()) { Person person = Person.Create(); - Type typePerson = typeof(Person); - HttpResponseMessage response = await client.PostAsJsonAsync(uri.ToString(), typePerson, person); + HttpResponseMessage response = await client.PostAsJsonAsync(uri.ToString(), person); Assert.True(response.StatusCode == HttpStatusCode.OK); - response = await client.PostAsJsonAsync(uri, typePerson, person); + response = await client.PostAsJsonAsync(uri, person); Assert.True(response.StatusCode == HttpStatusCode.OK); - response = await client.PostAsJsonAsync(uri.ToString(), person); + response = await client.PostAsJsonAsync(uri.ToString(), person, CancellationToken.None); Assert.True(response.StatusCode == HttpStatusCode.OK); - response = await client.PostAsJsonAsync(uri, person); + response = await client.PostAsJsonAsync(uri, person, CancellationToken.None); Assert.True(response.StatusCode == HttpStatusCode.OK); } }, @@ -118,16 +118,16 @@ await LoopbackServer.CreateClientAndServerAsync( Person person = Person.Create(); Type typePerson = typeof(Person); - HttpResponseMessage response = await client.PutAsJsonAsync(uri.ToString(), typePerson, person); + HttpResponseMessage response = await client.PutAsJsonAsync(uri.ToString(), person); Assert.True(response.StatusCode == HttpStatusCode.OK); - response = await client.PutAsJsonAsync(uri, typePerson, person); + response = await client.PutAsJsonAsync(uri, person); Assert.True(response.StatusCode == HttpStatusCode.OK); - response = await client.PutAsJsonAsync(uri.ToString(), person); + response = await client.PutAsJsonAsync(uri.ToString(), person, CancellationToken.None); Assert.True(response.StatusCode == HttpStatusCode.OK); - response = await client.PutAsJsonAsync(uri, person); + response = await client.PutAsJsonAsync(uri, person, CancellationToken.None); Assert.True(response.StatusCode == HttpStatusCode.OK); } }, @@ -159,19 +159,11 @@ public async Task TestHttpClientIsNullAsync() ex = await Assert.ThrowsAsync(() => client.GetFromJsonAsync(uri)); Assert.Equal("client", ex.ParamName); - ex = await Assert.ThrowsAsync(() => client.PostAsJsonAsync(uriString, typeof(Person), value: null)); - Assert.Equal("client", ex.ParamName); - ex = await Assert.ThrowsAsync(() => client.PostAsJsonAsync(uri, typeof(Person), value: null)); - Assert.Equal("client", ex.ParamName); ex = await Assert.ThrowsAsync(() => client.PostAsJsonAsync(uriString, null)); Assert.Equal("client", ex.ParamName); ex = await Assert.ThrowsAsync(() => client.PostAsJsonAsync(uri, null)); Assert.Equal("client", ex.ParamName); - ex = await Assert.ThrowsAsync(() => client.PutAsJsonAsync(uriString, typeof(Person), value: null)); - Assert.Equal("client", ex.ParamName); - ex = await Assert.ThrowsAsync(() => client.PutAsJsonAsync(uri, typeof(Person), value: null)); - Assert.Equal("client", ex.ParamName); ex = await Assert.ThrowsAsync(() => client.PutAsJsonAsync(uriString, null)); Assert.Equal("client", ex.ParamName); ex = await Assert.ThrowsAsync(() => client.PutAsJsonAsync(uri, null)); diff --git a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpContentJsonExtensionsTests.cs b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpContentJsonExtensionsTests.cs index 47fa5991a7c8cf..e94da9d2038fbf 100644 --- a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpContentJsonExtensionsTests.cs +++ b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpContentJsonExtensionsTests.cs @@ -3,6 +3,7 @@ // 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; @@ -16,6 +17,14 @@ public class HttpContentJsonExtensionsTests { private readonly List _headers = new List { new HttpHeaderData("Content-Type", "application/json") }; + [Fact] + public async Task ThrowOnNull() + { + HttpContent content = null; + await Assert.ThrowsAsync(() => content.ReadFromJsonAsync()); + await Assert.ThrowsAsync(() => content.ReadFromJsonAsync(typeof(Person))); + } + [Fact] public async Task HttpContentGetThenReadFromJsonAsync() { @@ -46,7 +55,7 @@ await LoopbackServer.CreateClientAndServerAsync( } [Fact] - public async Task HttpContentObjectIsNull() + public async Task HttpContentReturnValueIsNull() { const int NumRequests = 2; await LoopbackServer.CreateClientAndServerAsync( @@ -74,15 +83,18 @@ await LoopbackServer.CreateClientAndServerAsync( } [Fact] - public async Task TestGetFromJsonNoMessageBodyAsync() + public async Task TestReadFromJsonNoMessageBodyAsync() { await LoopbackServer.CreateClientAndServerAsync( async uri => { using (HttpClient client = new HttpClient()) { + var request = new HttpRequestMessage(HttpMethod.Get, uri); + var 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(() => client.GetFromJsonAsync(uri, typeof(Person))); + JsonException ex = await Assert.ThrowsAsync(() => response.Content.ReadFromJsonAsync(typeof(Person))); Assert.Contains("Path: $ | LineNumber: 0 | BytePositionInLine: 0", ex.Message); } }, @@ -90,14 +102,17 @@ await LoopbackServer.CreateClientAndServerAsync( } [Fact] - public async Task TestGetFromJsonNoContentTypeAsync() + public async Task TestReadFromJsonNoContentTypeAsync() { await LoopbackServer.CreateClientAndServerAsync( async uri => { using (HttpClient client = new HttpClient()) { - await Assert.ThrowsAsync(() => client.GetFromJsonAsync(uri)); + var request = new HttpRequestMessage(HttpMethod.Get, uri); + var response = await client.SendAsync(request); + + await Assert.ThrowsAsync(() => response.Content.ReadFromJsonAsync()); } }, server => server.HandleRequestAsync(content: "{}")); @@ -116,7 +131,10 @@ await LoopbackServer.CreateClientAndServerAsync( { using (HttpClient client = new HttpClient()) { - Person person = await client.GetFromJsonAsync(uri); + var request = new HttpRequestMessage(HttpMethod.Get, uri); + var response = await client.SendAsync(request); + + Person person = await response.Content.ReadFromJsonAsync(); person.Validate(); } }, @@ -136,14 +154,17 @@ await LoopbackServer.CreateClientAndServerAsync( { using (HttpClient client = new HttpClient()) { - InvalidOperationException ex = await Assert.ThrowsAsync(() => client.GetFromJsonAsync(uri)); + var request = new HttpRequestMessage(HttpMethod.Get, uri); + var 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(Skip ="Disable temporarily until transcode support is added.")] + [Fact] public async Task TestGetFromJsonAsyncTextPlainUtf16Async() { const string json = @"{""Name"":""David"",""Age"":24}"; @@ -153,14 +174,21 @@ await LoopbackServer.CreateClientAndServerAsync( { using (HttpClient client = new HttpClient()) { - Person per = Assert.IsType(await client.GetFromJsonAsync(uri, typeof(Person))); + var request = new HttpRequestMessage(HttpMethod.Get, uri); + var response = await client.SendAsync(request); + + Person per = Assert.IsType(await response.Content.ReadFromJsonAsync(typeof(Person))); per.Validate(); - per = await client.GetFromJsonAsync(uri); + request = new HttpRequestMessage(HttpMethod.Get, uri); + response = await client.SendAsync(request); + + per = await response.Content.ReadFromJsonAsync(); per.Validate(); } }, - async server => { + async server => + { byte[] utf16Content = Encoding.Unicode.GetBytes(json); byte[] bytes = Encoding.ASCII.GetBytes( @@ -172,7 +200,7 @@ await LoopbackServer.CreateClientAndServerAsync( var buffer = new MemoryStream(); buffer.Write(bytes, 0, bytes.Length); - buffer.Write(utf16Content, bytes.Length - 1, utf16Content.Length); + buffer.Write(utf16Content, 0, utf16Content.Length); for (int i = 0; i < NumRequests; i++) { diff --git a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/JsonContentTests.cs b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/JsonContentTests.cs index 2ba52806583cfe..7ed830777de05f 100644 --- a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/JsonContentTests.cs +++ b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/JsonContentTests.cs @@ -2,6 +2,7 @@ // 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.Net.Http.Headers; using System.Net.Test.Common; using System.Threading.Tasks; @@ -21,73 +22,77 @@ public void JsonContentObjectType() Type fooType = typeof(Foo); Foo foo = new Foo(); - JsonContent content = new JsonContent(fooType, foo); + JsonContent content = JsonContent.Create(foo, fooType, null); Assert.Equal(fooType, content.ObjectType); Assert.Same(foo, content.Value); - content = JsonContent.Create(foo); + content = JsonContent.Create(foo, null); Assert.Equal(fooType, content.ObjectType); Assert.Same(foo, content.Value); object fooBoxed = foo; // ObjectType is the specified type when using the .ctor. - content = new JsonContent(fooType, fooBoxed); + content = JsonContent.Create(fooBoxed, fooType, null); Assert.Equal(fooType, content.ObjectType); Assert.Same(fooBoxed, content.Value); // ObjectType is the declared type when using the factory method. - content = JsonContent.Create(fooBoxed); + content = JsonContent.Create(fooBoxed, null); Assert.Equal(typeof(object), content.ObjectType); Assert.Same(fooBoxed, content.Value); } [Fact] - public void JsonContentMediaType() + public void TestJsonContentMediaType() { Type fooType = typeof(Foo); Foo foo = new Foo(); // Use the default content-type if none is provided. - JsonContent content = new JsonContent(fooType, foo); + JsonContent content = JsonContent.Create(foo, fooType, null); Assert.Equal("application/json", content.Headers.ContentType.MediaType); Assert.Equal("utf-8", content.Headers.ContentType.CharSet); - content = JsonContent.Create(foo); + content = JsonContent.Create(foo, null); 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 = new JsonContent(fooType, foo, mediaType: mediaType); + content = JsonContent.Create(foo, fooType, mediaType); Assert.Same(mediaType, content.Headers.ContentType); content = JsonContent.Create(foo, mediaType: mediaType); Assert.Same(mediaType, content.Headers.ContentType); + } - // Use the specified mediaType string but use the default charset if not provided. - string mediaTypeAsString = "foo/bar"; - content = new JsonContent(fooType, foo, mediaType: mediaTypeAsString); - Assert.Equal(mediaTypeAsString, content.Headers.ContentType.MediaType); - Assert.Equal("utf-8", content.Headers.ContentType.CharSet); + [Fact] + public async Task SendQuotedCharsetAsync() + { + JsonContent content = JsonContent.Create(null, null); + content.Headers.ContentType.CharSet = "\"utf-8\""; - content = JsonContent.Create(foo, mediaType: mediaTypeAsString); - Assert.Equal(mediaTypeAsString, content.Headers.ContentType.MediaType); - Assert.Equal("utf-8", content.Headers.ContentType.CharSet); + HttpClient client = new HttpClient(); + var request = new HttpRequestMessage(HttpMethod.Post, "http://example.com"); + request.Content = content; + await client.SendAsync(request); + } + + [Fact] + public void TestJsonContentContentTypeIsNotTheSameOnMultipleInstances() + { + JsonContent jsonContent1 = JsonContent.Create(null, null); + JsonContent jsonContent2 = JsonContent.Create(null, null); - // Specifying a charset is not supported by the string overload. - string mediaTypeAndCharSetAsString = "foo/bar; charset=utf-16"; - Assert.Throws(() => new JsonContent(fooType, foo, mediaType: mediaTypeAndCharSetAsString)); - Assert.Throws(() => JsonContent.Create(foo, mediaType: mediaTypeAndCharSetAsString)); + jsonContent1.Headers.ContentType.CharSet = "foo-bar"; - // Charsets other than UTF-8 are not supported. - mediaType = MediaTypeHeaderValue.Parse("foo/bar; charset=utf-16"); - Assert.Throws(() => new JsonContent(fooType, foo, mediaType: mediaType)); - Assert.Throws(() => JsonContent.Create(foo, mediaType: mediaType)); + Assert.NotEqual(jsonContent1.Headers.ContentType.CharSet, jsonContent2.Headers.ContentType.CharSet); + Assert.NotSame(jsonContent1.Headers.ContentType, jsonContent2.Headers.ContentType); } [Fact] - public async Task SendJsonContentMediaTypeValidateOnServerAsync() + public async Task JsonContentMediaTypeValidateOnServerAsync() { await LoopbackServer.CreateClientAndServerAsync( async uri => @@ -95,10 +100,6 @@ await LoopbackServer.CreateClientAndServerAsync( using (HttpClient client = new HttpClient()) { var request = new HttpRequestMessage(HttpMethod.Post, uri); - request.Content = JsonContent.Create(Person.Create(), mediaType: "foo/bar"); - await client.SendAsync(request); - - 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); @@ -107,45 +108,37 @@ await LoopbackServer.CreateClientAndServerAsync( async server => { HttpRequestData req = await server.HandleRequestAsync(); Assert.Equal("foo/bar; charset=utf-8", req.GetSingleHeaderValue("Content-Type")); - - req = await server.HandleRequestAsync(); - Assert.Equal("foo/bar; charset=utf-8", req.GetSingleHeaderValue("Content-Type")); }); } [Fact] - public void JsonContentMediaTypeIsNull() + public void JsonContentMediaTypeDefaultIfNull() { Type fooType = typeof(Foo); Foo foo = null; - ArgumentNullException ex; - ex = Assert.Throws(() => new JsonContent(fooType, foo, mediaType: (string)null)); - Assert.Equal("mediaType", ex.ParamName); - ex = Assert.Throws(() => new JsonContent(fooType, foo, mediaType: (MediaTypeHeaderValue)null)); - Assert.Equal("mediaType", ex.ParamName); - ex = Assert.Throws(() => JsonContent.Create(foo, mediaType: (string)null)); - Assert.Equal("mediaType", ex.ParamName); - ex = Assert.Throws(() => JsonContent.Create(foo, mediaType: (MediaTypeHeaderValue)null)); - Assert.Equal("mediaType", ex.ParamName); + 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 async Task JsonContentTypeIsNull() + public void JsonContentInputTypeIsNull() { - HttpClient client = new HttpClient(); string foo = "test"; - var request = new HttpRequestMessage(HttpMethod.Post, "http://example.com"); - request.Content = new JsonContent(null, foo); - await Assert.ThrowsAsync(() => client.SendAsync(request)); + ArgumentNullException ex = Assert.Throws(() => JsonContent.Create(foo, inputType: null, mediaType: null)); + Assert.Equal("inputType", ex.ParamName); - request = new HttpRequestMessage(HttpMethod.Post, "http://example.com"); - request.Content = new JsonContent(null, foo, MediaTypeHeaderValue.Parse("application/json; charset=utf-8")); - await Assert.ThrowsAsync(() => client.SendAsync(request)); + ex = Assert.Throws(() => JsonContent.Create(foo, inputType: null, mediaType: null)); + Assert.Equal("inputType", ex.ParamName); } - [Fact] + [Fact(Skip = "Should we throw when !inputType,IsAssignableFrom(inputValue.GetType()) on instantiation or let the JsonSerializer throw later?")] public async Task JsonContentThrowsOnIncompatibleTypeAsync() { HttpClient client = new HttpClient(); @@ -153,11 +146,11 @@ public async Task JsonContentThrowsOnIncompatibleTypeAsync() Type typeOfBar = typeof(Bar); var request = new HttpRequestMessage(HttpMethod.Post, "http://example.com"); - request.Content = new JsonContent(typeOfBar, foo); + request.Content = JsonContent.Create(foo, typeOfBar, null); await Assert.ThrowsAsync(() => client.SendAsync(request)); request = new HttpRequestMessage(HttpMethod.Post, "http://example.com"); - request.Content = new JsonContent(typeOfBar, foo, MediaTypeHeaderValue.Parse("application/json; charset=utf-8")); + request.Content = JsonContent.Create(foo, typeOfBar, MediaTypeHeaderValue.Parse("application/json; charset=utf-8")); await Assert.ThrowsAsync(() => client.SendAsync(request)); } 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 index 36fb8a201a25cf..4925a27d942bce 100644 --- 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 @@ -3,14 +3,14 @@ $(NetCoreAppCurrent);$(NetFrameworkCurrent) - - - + + + - + Common\System\Net\Capability.Security.cs diff --git a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/TranscodingReadStreamTests.cs b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/TranscodingReadStreamTests.cs index 3b879412f20a50..74748d01a609b4 100644 --- a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/TranscodingReadStreamTests.cs +++ b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/TranscodingReadStreamTests.cs @@ -22,20 +22,22 @@ public async Task ReadAsync_SingleByte() // Arrange var input = "Hello world"; var encoding = Encoding.Unicode; - using var stream = new TranscodingReadStream(new MemoryStream(encoding.GetBytes(input)), encoding); - var bytes = new byte[4]; + 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); + // 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 + 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); + Assert.Equal(0, stream.ByteBufferCount); + Assert.Equal(0, stream.CharBufferCount); + Assert.Equal(10, stream.OverflowCount); + } } [Fact] @@ -44,19 +46,21 @@ 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); + 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] @@ -65,25 +69,27 @@ public async Task ReadAsync_CompletedInSecondIteration() // Arrange var input = new string('A', 1024 + 10); var encoding = Encoding.Unicode; - using var stream = new TranscodingReadStream(new MemoryStream(encoding.GetBytes(input)), encoding); - var bytes = new byte[1024]; - var expected = Encoding.UTF8.GetBytes(input.Substring(0, bytes.Length)); - - // Act - var 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); + using (TranscodingReadStream stream = new TranscodingReadStream(new MemoryStream(encoding.GetBytes(input)), encoding)) + { + var bytes = new byte[1024]; + var expected = Encoding.UTF8.GetBytes(input.Substring(0, bytes.Length)); + + // Act + var 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] @@ -93,29 +99,31 @@ public async Task ReadAsync_WithOverflowBuffer() // Test ensures that the overflow buffer works correctly var input = "☀"; var encoding = Encoding.Unicode; - using var stream = new TranscodingReadStream(new MemoryStream(encoding.GetBytes(input)), encoding); - var bytes = new byte[1]; - var expected = Encoding.UTF8.GetBytes(input); - - // Act - var 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); + using (TranscodingReadStream stream = new TranscodingReadStream(new MemoryStream(encoding.GetBytes(input)), encoding)) + { + var bytes = new byte[1]; + var expected = Encoding.UTF8.GetBytes(input); + + // Act + var 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 => new TheoryData @@ -133,27 +141,28 @@ public async Task ReadAsync_WithOverflowBuffer() [MemberData(nameof(ReadAsync_WithOverflowBuffer_AtBoundariesData))] public Task ReadAsync_WithOverflowBuffer_WithBufferSize2(string input) => ReadAsync_WithOverflowBufferAtCharBufferBoundaries(input, bufferSize: 1); - private static async Task ReadAsync_WithOverflowBufferAtCharBufferBoundaries(string input, int bufferSize) + private static async Task ReadAsync_WithOverflowBufferAtCharBufferBoundaries(string input, int bufferSize) { // Arrange // Test ensures that the overflow buffer works correctly var encoding = Encoding.Unicode; - var stream = new TranscodingReadStream(new MemoryStream(encoding.GetBytes(input)), encoding); - var bytes = new byte[1]; - var expected = Encoding.UTF8.GetBytes(input); + using (TranscodingReadStream stream = new TranscodingReadStream(new MemoryStream(encoding.GetBytes(input)), encoding)) + { + var bytes = new byte[1]; + var expected = Encoding.UTF8.GetBytes(input); - // Act - int read; - var buffer = new byte[bufferSize]; - var actual = new List(); + // Act + int read; + var buffer = new byte[bufferSize]; + var actual = new List(); - while ((read = await stream.ReadAsync(buffer, 0, bufferSize)) != 0) - { - actual.AddRange(buffer); - } + while ((read = await stream.ReadAsync(buffer, 0, bufferSize)) != 0) + { + actual.AddRange(buffer); + } - Assert.Equal(expected, actual); - return stream; + Assert.Equal(expected, actual); + } } public static TheoryData ReadAsyncInputLatin => @@ -179,15 +188,23 @@ internal static TheoryData GetUnicodeText(int maxCharBufferSize) { return new TheoryData { - new string('Æ', count: 7), - new string('A', count: maxCharBufferSize - 1) + 'Æ', - "AbĀāĂ㥹ĆŊŋŌōŎŏŐőŒœŔŕŖŗŘřŚşŠšŢţŤťŦŧŨũŪūŬŭŮůŰűŲųŴŵŶŷŸŹźŻżŽžſAbc", - "Abcஐஒஓஔகஙசஜஞடணதநனபமயரறலளழவஷஸஹ", - "☀☁☂☃☄★☆☇☈☉☊☋☌☍☎☏☐☑☒☓☚☛☜☝☞☟☠☡☢☣☤☥☦☧☨☩☪☫☬☭☮☯☰☱☲☳☴☵☶☷☸", - new string('Æ', count: 64 * 1024), - new string('Æ', count: 64 * 1024 + 1), - "pingüino", - new string('ऄ', count: maxCharBufferSize + 1), // This uses 3 bytes to represent in UTF8 + 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 }; } @@ -263,7 +280,7 @@ public async Task TestOneToOneTranscodingAsync() string message = '"' + new string('A', TranscodingReadStream.MaxByteBufferSize - 2 + 1) + '"'; Stream stream = new MemoryStream(sourceEncoding.GetBytes(message)); - using (Stream transcodingStream = new TranscodingReadStream(stream, sourceEncoding)) + using (TranscodingReadStream transcodingStream = new TranscodingReadStream(stream, sourceEncoding)) { string deserializedMessage = await JsonSerializer.DeserializeAsync(transcodingStream); Assert.Equal(message.Trim('"'), deserializedMessage); From c85075e8cd9fdd9a68f3cda1466733e036239966 Mon Sep 17 00:00:00 2001 From: David Cantu Date: Fri, 20 Mar 2020 11:01:02 -0700 Subject: [PATCH 19/32] Use default JsonSerializerOptions when passed-in is null --- .../Http/Json/HttpContentJsonExtensions.cs | 4 +- .../src/System/Net/Http/Json/JsonContent.cs | 7 +- .../HttpClientJsonExtensionsTests.cs | 6 +- .../HttpContentJsonExtensionsTests.cs | 16 +++++ .../tests/FunctionalTests/JsonContentTests.cs | 12 +++- .../tests/FunctionalTests/Person.cs | 33 ---------- ...stem.Net.Http.Json.Functional.Tests.csproj | 2 +- .../tests/FunctionalTests/TestClasses.cs | 66 +++++++++++++++++++ 8 files changed, 104 insertions(+), 42 deletions(-) delete mode 100644 src/libraries/System.Net.Http.Json/tests/FunctionalTests/Person.cs create mode 100644 src/libraries/System.Net.Http.Json/tests/FunctionalTests/TestClasses.cs 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 index 05bbe8009c12e4..3e45cd8eef2b2c 100644 --- 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 @@ -20,7 +20,7 @@ public static class HttpContentJsonExtensions Debug.Assert(content.Headers.ContentType != null); Encoding? sourceEncoding = GetEncoding(content.Headers.ContentType.CharSet); - return ReadFromJsonAsyncCore(content, type, sourceEncoding, options, cancellationToken); + return ReadFromJsonAsyncCore(content, type, sourceEncoding, options ?? JsonContent.s_defaultSerializerOptions, cancellationToken); } public static Task ReadFromJsonAsync(this HttpContent content, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) @@ -29,7 +29,7 @@ public static Task ReadFromJsonAsync(this HttpContent content, JsonSeriali Debug.Assert(content.Headers.ContentType != null); Encoding? sourceEncoding = GetEncoding(content.Headers.ContentType.CharSet); - return ReadFromJsonAsyncCore(content, sourceEncoding, options, cancellationToken); + return ReadFromJsonAsyncCore(content, sourceEncoding, options ?? JsonContent.s_defaultSerializerOptions, cancellationToken); } private static async Task ReadFromJsonAsyncCore(HttpContent content, Type type, Encoding? sourceEncoding, JsonSerializerOptions? options, CancellationToken cancellationToken) 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 index 70c2bf62ba390b..7cce2770b0c16f 100644 --- 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 @@ -15,7 +15,8 @@ namespace System.Net.Http.Json public partial class JsonContent : HttpContent { internal const string JsonMediaType = "application/json"; - private static MediaTypeHeaderValue DefaultMediaType => MediaTypeHeaderValue.Parse(string.Format("{0}; charset={1}", JsonMediaType, Encoding.UTF8.WebName)); + private static MediaTypeHeaderValue s_defaultMediaType => MediaTypeHeaderValue.Parse(string.Format("{0}; charset={1}", JsonMediaType, Encoding.UTF8.WebName)); + internal static JsonSerializerOptions s_defaultSerializerOptions => new JsonSerializerOptions { PropertyNameCaseInsensitive = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; private readonly JsonSerializerOptions? _jsonSerializerOptions; public Type ObjectType { get; } @@ -30,8 +31,8 @@ private JsonContent(object? inputValue, Type inputType, MediaTypeHeaderValue? me Value = inputValue; ObjectType = inputType; - Headers.ContentType = mediaType ?? DefaultMediaType; - _jsonSerializerOptions = options; + Headers.ContentType = mediaType ?? s_defaultMediaType; + _jsonSerializerOptions = options ?? s_defaultSerializerOptions; } public static JsonContent Create(T inputValue, MediaTypeHeaderValue? mediaType, JsonSerializerOptions? options = null) diff --git a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpClientJsonExtensionsTests.cs b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpClientJsonExtensionsTests.cs index 0884505c33acaa..6b5aa80452922f 100644 --- a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpClientJsonExtensionsTests.cs +++ b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpClientJsonExtensionsTests.cs @@ -14,6 +14,8 @@ 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() { @@ -100,7 +102,7 @@ await LoopbackServer.CreateClientAndServerAsync( { HttpRequestData request = await server.HandleRequestAsync(); ValidateRequest(request); - Person per = JsonSerializer.Deserialize(request.Body); + Person per = JsonSerializer.Deserialize(request.Body, s_DefaultSerializerOptions); per.Validate(); } }); @@ -136,7 +138,7 @@ await LoopbackServer.CreateClientAndServerAsync( { HttpRequestData request = await server.HandleRequestAsync(); ValidateRequest(request); - Person obj = JsonSerializer.Deserialize(request.Body); + Person obj = JsonSerializer.Deserialize(request.Body, s_DefaultSerializerOptions); obj.Validate(); } }); diff --git a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpContentJsonExtensionsTests.cs b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpContentJsonExtensionsTests.cs index e94da9d2038fbf..e61a9a3ed5f895 100644 --- a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpContentJsonExtensionsTests.cs +++ b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpContentJsonExtensionsTests.cs @@ -208,5 +208,21 @@ await LoopbackServer.CreateClientAndServerAsync( } }); } + + [Fact] + public async Task EnsureDefaultJsonSerializerOptionsAsync() + { + await LoopbackServer.CreateClientAndServerAsync( + async uri => + { + using (HttpClient client = new HttpClient()) + { + var request = new HttpRequestMessage(HttpMethod.Get, uri); + var response = await client.SendAsync(request); + await response.Content.ReadFromJsonAsync(typeof(EnsureDefaultOptions)); + } + }, + server => server.HandleRequestAsync(headers: _headers, content: "{}")); + } } } diff --git a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/JsonContentTests.cs b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/JsonContentTests.cs index 7ed830777de05f..abe641e2a37f92 100644 --- a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/JsonContentTests.cs +++ b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/JsonContentTests.cs @@ -2,7 +2,6 @@ // 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.Net.Http.Headers; using System.Net.Test.Common; using System.Threading.Tasks; @@ -173,5 +172,16 @@ await LoopbackServer.CreateClientAndServerAsync( Assert.Equal("application/json; charset=utf-16", req.GetSingleHeaderValue("Content-Type")); }); } + + [Fact] + public void EnsureDefaultJsonSerializerOptions() + { + HttpClient client = new HttpClient(); + EnsureDefaultOptions obj = new EnsureDefaultOptions(); + + var request = new HttpRequestMessage(HttpMethod.Post, "http://example.com"); + request.Content = JsonContent.Create(obj, mediaType: null); + client.SendAsync(request); + } } } diff --git a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/Person.cs b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/Person.cs deleted file mode 100644 index a8595866c89c82..00000000000000 --- a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/Person.cs +++ /dev/null @@ -1,33 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System.Text.Json; -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); - } - } -} 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 index 4925a27d942bce..33dd6c8f8f0124 100644 --- 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 @@ -6,7 +6,7 @@ - + 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 00000000000000..8dc962bae91559 --- /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 { } +} From 3612a9ae456cd6fbacc942dfeed8ad698a9c807d Mon Sep 17 00:00:00 2001 From: David Cantu Date: Fri, 20 Mar 2020 11:32:48 -0700 Subject: [PATCH 20/32] Escape a few remaining Unicode characters in test files. --- .../tests/FunctionalTests/TranscodingReadStreamTests.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/TranscodingReadStreamTests.cs b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/TranscodingReadStreamTests.cs index 74748d01a609b4..aa576b38a693a2 100644 --- a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/TranscodingReadStreamTests.cs +++ b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/TranscodingReadStreamTests.cs @@ -97,7 +97,7 @@ public async Task ReadAsync_WithOverflowBuffer() { // Arrange // Test ensures that the overflow buffer works correctly - var input = "☀"; + var input = "\u2600"; var encoding = Encoding.Unicode; using (TranscodingReadStream stream = new TranscodingReadStream(new MemoryStream(encoding.GetBytes(input)), encoding)) { @@ -128,9 +128,9 @@ public async Task ReadAsync_WithOverflowBuffer() public static TheoryData ReadAsync_WithOverflowBuffer_AtBoundariesData => new TheoryData { - new string('a', TranscodingReadStream.MaxCharBufferSize - 1) + "☀", - new string('a', TranscodingReadStream.MaxCharBufferSize - 2) + "☀", - new string('a', TranscodingReadStream.MaxCharBufferSize) + "☀", + new string('a', TranscodingReadStream.MaxCharBufferSize - 1) + '\u2600', + new string('a', TranscodingReadStream.MaxCharBufferSize - 2) + '\u2600', + new string('a', TranscodingReadStream.MaxCharBufferSize) + '\u2600', }; [Theory] From 9242af77de81799d6507cce3bdc79a4c19411111 Mon Sep 17 00:00:00 2001 From: David Cantu Date: Fri, 20 Mar 2020 12:30:20 -0700 Subject: [PATCH 21/32] Address nits. --- .../Net/Http/Json/HttpContentJsonExtensions.cs | 12 ++++-------- .../System/Net/Http/Json/TranscodingReadStream.cs | 2 +- .../System/Net/Http/Json/TranscodingWriteStream.cs | 2 +- 3 files changed, 6 insertions(+), 10 deletions(-) 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 index 3e45cd8eef2b2c..31ca33bff5cea0 100644 --- 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 @@ -39,10 +39,8 @@ public static Task ReadFromJsonAsync(this HttpContent content, JsonSeriali // Wrap content stream into a transcoding stream that buffers the data transcoded from the sourceEncoding to utf-8. if (sourceEncoding != null && sourceEncoding != Encoding.UTF8) { - using (Stream transcodingStream = new TranscodingReadStream(contentStream, sourceEncoding)) - { - return await JsonSerializer.DeserializeAsync(transcodingStream, type, options, cancellationToken).ConfigureAwait(false); - } + using Stream transcodingStream = new TranscodingReadStream(contentStream, sourceEncoding); + return await JsonSerializer.DeserializeAsync(transcodingStream, type, options, cancellationToken).ConfigureAwait(false); } else { @@ -60,10 +58,8 @@ private static async Task ReadFromJsonAsyncCore(HttpContent content, Encod // Wrap content stream into a transcoding stream that buffers the data transcoded from the sourceEncoding to utf-8. if (sourceEncoding != null && sourceEncoding != Encoding.UTF8) { - using (Stream transcodingStream = new TranscodingReadStream(contentStream, sourceEncoding)) - { - return await JsonSerializer.DeserializeAsync(transcodingStream, options, cancellationToken).ConfigureAwait(false); - } + using Stream transcodingStream = new TranscodingReadStream(contentStream, sourceEncoding); + return await JsonSerializer.DeserializeAsync(transcodingStream, options, cancellationToken).ConfigureAwait(false); } else { 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 index 51efb040cc87ee..554fc3c4932c56 100644 --- 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 @@ -92,7 +92,7 @@ public override Task ReadAsync(byte[] buffer, int offset, int count, Cancel throw new ArgumentOutOfRangeException(); } - ArraySegment readBuffer = new ArraySegment(buffer, offset, count); + var readBuffer = new ArraySegment(buffer, offset, count); return ReadAsyncCore(readBuffer, cancellationToken); } 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 index aed04bacebaa72..5cad1b9c856673 100644 --- 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 @@ -90,7 +90,7 @@ public override Task WriteAsync(byte[] buffer, int offset, int count, Cancellati throw new ArgumentOutOfRangeException(); } - ArraySegment bufferSegment = new ArraySegment(buffer, offset, count); + var bufferSegment = new ArraySegment(buffer, offset, count); return WriteAsyncCore(bufferSegment, cancellationToken); } From 16e73dcb4f8a1c0d04017f0aadc972778c1745dc Mon Sep 17 00:00:00 2001 From: David Cantu Date: Fri, 20 Mar 2020 12:56:40 -0700 Subject: [PATCH 22/32] Validate inputType.IsAssignableFrom(inputValue.GetType()) on JsonContent ctor. --- .../src/Resources/Strings.resx | 3 +++ .../src/System/Net/Http/Json/JsonContent.cs | 5 +++++ .../tests/FunctionalTests/JsonContentTests.cs | 16 ++++++++-------- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/libraries/System.Net.Http.Json/src/Resources/Strings.resx b/src/libraries/System.Net.Http.Json/src/Resources/Strings.resx index 5f592a90a4f2b2..60bb3ba55e5cfd 100644 --- a/src/libraries/System.Net.Http.Json/src/Resources/Strings.resx +++ b/src/libraries/System.Net.Http.Json/src/Resources/Strings.resx @@ -126,4 +126,7 @@ The provided ContentType is not supported; the supported types are 'application/json' and 'text/plain'. + + 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/JsonContent.cs b/src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/JsonContent.cs index 7cce2770b0c16f..c4919533a8b7c2 100644 --- 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 @@ -29,6 +29,11 @@ private JsonContent(object? inputValue, Type inputType, MediaTypeHeaderValue? me 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 ?? s_defaultMediaType; diff --git a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/JsonContentTests.cs b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/JsonContentTests.cs index abe641e2a37f92..a69597ac4a2e74 100644 --- a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/JsonContentTests.cs +++ b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/JsonContentTests.cs @@ -137,20 +137,20 @@ public void JsonContentInputTypeIsNull() Assert.Equal("inputType", ex.ParamName); } - [Fact(Skip = "Should we throw when !inputType,IsAssignableFrom(inputValue.GetType()) on instantiation or let the JsonSerializer throw later?")] - public async Task JsonContentThrowsOnIncompatibleTypeAsync() + [Fact] + public void JsonContentThrowsOnIncompatibleTypeAsync() { HttpClient client = new HttpClient(); var foo = new Foo(); Type typeOfBar = typeof(Bar); - var request = new HttpRequestMessage(HttpMethod.Post, "http://example.com"); - request.Content = JsonContent.Create(foo, typeOfBar, null); - await Assert.ThrowsAsync(() => client.SendAsync(request)); + Exception ex = Assert.Throws(() => JsonContent.Create(foo, typeOfBar, null)); + + string strTypeOfBar = typeOfBar.ToString(); + Assert.Contains(strTypeOfBar, ex.Message); - request = new HttpRequestMessage(HttpMethod.Post, "http://example.com"); - request.Content = JsonContent.Create(foo, typeOfBar, MediaTypeHeaderValue.Parse("application/json; charset=utf-8")); - await Assert.ThrowsAsync(() => client.SendAsync(request)); + string afterInputTypeMessage = ex.Message.Split(strTypeOfBar.ToCharArray())[1]; + Assert.Contains(afterInputTypeMessage, ex.Message); } [Fact] From 37cd8fc00bf8df5d8f1ea0db9cdf39b0de52f8b7 Mon Sep 17 00:00:00 2001 From: David Cantu Date: Mon, 23 Mar 2020 15:32:55 -0700 Subject: [PATCH 23/32] Allow only ContentTypes application/json and application/+json. --- .../src/Resources/Strings.resx | 2 +- .../src/System.Net.Http.Json.csproj | 2 +- .../ArraySegmentExtensions.netstandard.cs | 2 +- .../Http/Json/HttpContentJsonExtensions.cs | 57 ++++++++++-------- .../src/System/Net/Http/Json/JsonContent.cs | 11 +++- .../HttpContentJsonExtensionsTests.cs | 60 ++++++++++++++++++- ...stem.Net.Http.Json.Functional.Tests.csproj | 2 +- 7 files changed, 101 insertions(+), 35 deletions(-) rename src/libraries/System.Net.Http.Json/src/System/{Net/Http/Json => }/ArraySegmentExtensions.netstandard.cs (96%) diff --git a/src/libraries/System.Net.Http.Json/src/Resources/Strings.resx b/src/libraries/System.Net.Http.Json/src/Resources/Strings.resx index 60bb3ba55e5cfd..8a89c544c40d15 100644 --- a/src/libraries/System.Net.Http.Json/src/Resources/Strings.resx +++ b/src/libraries/System.Net.Http.Json/src/Resources/Strings.resx @@ -124,7 +124,7 @@ The character set provided in ContentType is not supported. - The provided ContentType is not supported; the supported types are 'application/json' and 'text/plain'. + 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 index 57d3b40bc3ea87..04dd510bb1c79f 100644 --- 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 @@ -20,7 +20,7 @@ - + diff --git a/src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/ArraySegmentExtensions.netstandard.cs b/src/libraries/System.Net.Http.Json/src/System/ArraySegmentExtensions.netstandard.cs similarity index 96% rename from src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/ArraySegmentExtensions.netstandard.cs rename to src/libraries/System.Net.Http.Json/src/System/ArraySegmentExtensions.netstandard.cs index 887281e5a01372..e5f89eeff710cc 100644 --- a/src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/ArraySegmentExtensions.netstandard.cs +++ b/src/libraries/System.Net.Http.Json/src/System/ArraySegmentExtensions.netstandard.cs @@ -2,7 +2,7 @@ // 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.Net.Http.Json +namespace System { internal static class ArraySegmentExtensions { 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 index 31ca33bff5cea0..b61afab900548a 100644 --- 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 @@ -18,7 +18,7 @@ public static class HttpContentJsonExtensions { ValidateContent(content); Debug.Assert(content.Headers.ContentType != null); - Encoding? sourceEncoding = GetEncoding(content.Headers.ContentType.CharSet); + Encoding? sourceEncoding = JsonContent.GetEncoding(content.Headers.ContentType.CharSet); return ReadFromJsonAsyncCore(content, type, sourceEncoding, options ?? JsonContent.s_defaultSerializerOptions, cancellationToken); } @@ -27,7 +27,7 @@ public static Task ReadFromJsonAsync(this HttpContent content, JsonSeriali { ValidateContent(content); Debug.Assert(content.Headers.ContentType != null); - Encoding? sourceEncoding = GetEncoding(content.Headers.ContentType.CharSet); + Encoding? sourceEncoding = JsonContent.GetEncoding(content.Headers.ContentType.CharSet); return ReadFromJsonAsyncCore(content, sourceEncoding, options ?? JsonContent.s_defaultSerializerOptions, cancellationToken); } @@ -79,41 +79,48 @@ private static void ValidateContent(HttpContent content) string? mediaType = content.Headers.ContentType?.MediaType; - if (mediaType != JsonContent.JsonMediaType && - mediaType != MediaTypeNames.Text.Plain) + if (mediaType == null) { throw new NotSupportedException(SR.ContentTypeNotSupported); } + mediaType = mediaType.ToLower(); + + if (mediaType != JsonContent.JsonMediaType && + !IsValidStructuredSyntaxJsonSuffix(mediaType.AsSpan())) + { + throw new NotSupportedException(SR.ContentTypeNotSupported); + } } - private static Encoding? GetEncoding(string? charset) + private static bool IsValidStructuredSyntaxJsonSuffix(ReadOnlySpan mediaType) { - Encoding? encoding = null; + int index = 0; + int typeLength = mediaType.IndexOf('/'); - if (charset != null) + if (typeLength < 0 || + !mediaType.Slice(index, typeLength).SequenceEqual(JsonContent.JsonType.AsSpan())) { - 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); - } + return false; + } + + index += typeLength + 1; + int suffixStart = mediaType.Slice(index).IndexOf('+'); + + // Empty prefix subtype ("application/+json") not allowed. + if (suffixStart <= 0) + { + return false; + } - Debug.Assert(encoding != null); + index += suffixStart + 1; + + if (!mediaType.Slice(index).SequenceEqual(JsonContent.JsonSubtype.AsSpan())) + { + return false; } - return encoding; + 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 index c4919533a8b7c2..49dce3c44179f2 100644 --- 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 @@ -15,8 +15,13 @@ namespace System.Net.Http.Json public partial class JsonContent : HttpContent { internal const string JsonMediaType = "application/json"; - private static MediaTypeHeaderValue s_defaultMediaType => MediaTypeHeaderValue.Parse(string.Format("{0}; charset={1}", JsonMediaType, Encoding.UTF8.WebName)); - internal static JsonSerializerOptions s_defaultSerializerOptions => new JsonSerializerOptions { PropertyNameCaseInsensitive = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; + internal const string JsonType = "application"; + internal const string JsonSubtype = "json"; + private static MediaTypeHeaderValue s_defaultMediaType + => MediaTypeHeaderValue.Parse(string.Format("{0}; charset={1}", JsonMediaType, Encoding.UTF8.WebName)); + + internal static JsonSerializerOptions s_defaultSerializerOptions + => new JsonSerializerOptions { PropertyNameCaseInsensitive = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; private readonly JsonSerializerOptions? _jsonSerializerOptions; public Type ObjectType { get; } @@ -74,7 +79,7 @@ private async Task SerializeToStreamAsyncCore(Stream targetStream, CancellationT } } - private static Encoding? GetEncoding(string? charset) + internal static Encoding? GetEncoding(string? charset) { Encoding? encoding = null; diff --git a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpContentJsonExtensionsTests.cs b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpContentJsonExtensionsTests.cs index e61a9a3ed5f895..1a9a75bde2e13e 100644 --- a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpContentJsonExtensionsTests.cs +++ b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpContentJsonExtensionsTests.cs @@ -123,7 +123,7 @@ public async Task TestGetFromJsonQuotedCharSetAsync() { List customHeaders = new List { - new HttpHeaderData("Content-Type", "text/plain; charset=\"utf-8\"") + new HttpHeaderData("Content-Type", "application/json; charset=\"utf-8\"") }; await LoopbackServer.CreateClientAndServerAsync( @@ -146,7 +146,7 @@ public async Task TestGetFromJsonThrowOnInvalidCharSetAsync() { List customHeaders = new List { - new HttpHeaderData("Content-Type", "text/plain; charset=\"foo-bar\"") + new HttpHeaderData("Content-Type", "application/json; charset=\"foo-bar\"") }; await LoopbackServer.CreateClientAndServerAsync( @@ -193,7 +193,7 @@ await LoopbackServer.CreateClientAndServerAsync( byte[] bytes = Encoding.ASCII.GetBytes( $"HTTP/1.1 200 OK" + - $"\r\nContent-Type: text/plain; charset=utf-16\r\n" + + $"\r\nContent-Type: application/json; charset=utf-16\r\n" + $"Content-Length: {utf16Content.Length}\r\n" + $"Connection:close\r\n\r\n"); @@ -224,5 +224,59 @@ await LoopbackServer.CreateClientAndServerAsync( }, server => server.HandleRequestAsync(headers: _headers, content: "{}")); } + + [Fact] + public async Task TestStructuredSyntaxJsonSuffix() + { + List customHeaders = new List + { + new HttpHeaderData("Content-Type", "application/problem+json; charset=\"utf-8\"") + }; + + await LoopbackServer.CreateClientAndServerAsync( + async uri => + { + using (HttpClient client = new HttpClient()) + { + var request = new HttpRequestMessage(HttpMethod.Get, uri); + var 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); + var 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/System.Net.Http.Json.Functional.Tests.csproj b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/System.Net.Http.Json.Functional.Tests.csproj index 33dd6c8f8f0124..3614052e6c2e20 100644 --- 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 @@ -40,7 +40,7 @@ - + From edb2dde4f5324146579f9c2d4f59843e0db32611 Mon Sep 17 00:00:00 2001 From: David Cantu Date: Tue, 24 Mar 2020 01:02:42 -0700 Subject: [PATCH 24/32] Address comments. --- .../src/Resources/Strings.resx | 3 + .../src/System/Net/Http/Json/JsonContent.cs | 2 +- .../Net/Http/Json/TranscodingReadStream.cs | 67 ++++------ .../Net/Http/Json/TranscodingWriteStream.cs | 10 +- .../HttpClientJsonExtensionsTests.cs | 11 +- .../HttpContentJsonExtensionsTests.cs | 8 +- .../tests/FunctionalTests/JsonContentTests.cs | 27 +++- .../FunctionalTests/Resources/Strings.resx | 123 ++++++++++++++++++ .../TranscodingReadStreamTests.cs | 29 +++++ 9 files changed, 222 insertions(+), 58 deletions(-) create mode 100644 src/libraries/System.Net.Http.Json/tests/FunctionalTests/Resources/Strings.resx diff --git a/src/libraries/System.Net.Http.Json/src/Resources/Strings.resx b/src/libraries/System.Net.Http.Json/src/Resources/Strings.resx index 8a89c544c40d15..1722e93d5009d7 100644 --- a/src/libraries/System.Net.Http.Json/src/Resources/Strings.resx +++ b/src/libraries/System.Net.Http.Json/src/Resources/Strings.resx @@ -117,6 +117,9 @@ 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. 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 index 49dce3c44179f2..012863c64dae7f 100644 --- 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 @@ -62,7 +62,7 @@ protected override bool TryComputeLength(out long length) private async Task SerializeToStreamAsyncCore(Stream targetStream, CancellationToken cancellationToken) { - Encoding? targetEncoding = GetEncoding(Headers.ContentType!.CharSet); + 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) 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 index 554fc3c4932c56..c096e4fce2a8a6 100644 --- 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 @@ -65,8 +65,6 @@ public override long Position internal int CharBufferCount => _charBuffer.Count; internal int OverflowCount => _overflowBuffer.Count; - public override void Flush() { } - public override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException(); @@ -89,7 +87,7 @@ public override Task ReadAsync(byte[] buffer, int offset, int count, Cancel if (buffer.Length - offset < count) { - throw new ArgumentOutOfRangeException(); + throw new ArgumentException(SR.Argument_InvalidOffLen); } var readBuffer = new ArraySegment(buffer, offset, count); @@ -115,17 +113,12 @@ private async Task ReadAsyncCore(ArraySegment readBuffer, Cancellatio 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); - - if (bytesRead == 0) - { - // We are done, flush encoder. - _encoder.Convert(_charBuffer.Array!, 0, 0, _byteBuffer.Array!, 0, 0, flush: true, out _, out _, out _); - return 0; - } + shouldFlushEncoder = bytesRead == 0; } bool completed = false; @@ -135,43 +128,36 @@ private async Task ReadAsyncCore(ArraySegment readBuffer, Cancellatio if (readBuffer.Count > OverflowBufferSize || _charBuffer.Count == 0) { _encoder.Convert(_charBuffer.Array!, _charBuffer.Offset, _charBuffer.Count, readBuffer.Array!, readBuffer.Offset, readBuffer.Count, - flush: false, out charsRead, out bytesWritten, out completed); + flush: shouldFlushEncoder, out charsRead, out bytesWritten, out completed); } _charBuffer = _charBuffer.Slice(charsRead); - if (completed) + if (completed || bytesWritten > 0) { return bytesWritten; } - else - { - if (bytesWritten > 0) - { - return bytesWritten; - } - _encoder.Convert(_charBuffer.Array!, _charBuffer.Offset, _charBuffer.Count, _overflowBuffer.Array!, 0, _overflowBuffer.Array!.Length, - flush: false, out int overFlowChars, out int overflowBytes, out completed); + _encoder.Convert(_charBuffer.Array!, _charBuffer.Offset, _charBuffer.Count, _overflowBuffer.Array!, 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."); + 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); + _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); + // 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); + Debug.Assert(_overflowBuffer.Array != null); - _overflowBuffer = new ArraySegment(_overflowBuffer.Array, readBuffer.Count, overflowBytes - readBuffer.Count); + _overflowBuffer = new ArraySegment(_overflowBuffer.Array, readBuffer.Count, overflowBytes - readBuffer.Count); - Debug.Assert(_overflowBuffer.Count > 0); + Debug.Assert(_overflowBuffer.Count > 0); - return readBuffer.Count; - } + return readBuffer.Count; } private async Task ReadInputChars(CancellationToken cancellationToken) @@ -196,7 +182,7 @@ private async Task ReadInputChars(CancellationToken cancellationToken) 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 == 0); + Debug.Assert(bytesRead != 0 || _byteBuffer.Count - bytesUsed == 0); _byteBuffer = _byteBuffer.Slice(bytesUsed); _charBuffer = new ArraySegment(_charBuffer.Array, 0, charsUsed); @@ -204,20 +190,17 @@ private async Task ReadInputChars(CancellationToken cancellationToken) return bytesRead; } + public override void Flush() + => throw new NotSupportedException(); + public override long Seek(long offset, SeekOrigin origin) - { - throw new NotSupportedException(); - } + => throw new NotSupportedException(); public override void SetLength(long value) - { - throw new NotSupportedException(); - } + => throw new NotSupportedException(); public override void Write(byte[] buffer, int offset, int count) - { - throw new NotSupportedException(); - } + => throw new NotSupportedException(); protected override void Dispose(bool disposing) { 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 index 5cad1b9c856673..f4ab289bd55f9c 100644 --- 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 @@ -56,14 +56,10 @@ public override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException(); public override long Seek(long offset, SeekOrigin origin) - { - throw new NotSupportedException(); - } + => throw new NotSupportedException(); public override void SetLength(long value) - { - throw new NotSupportedException(); - } + => throw new NotSupportedException(); public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); @@ -87,7 +83,7 @@ public override Task WriteAsync(byte[] buffer, int offset, int count, Cancellati if (buffer.Length - offset < count) { - throw new ArgumentOutOfRangeException(); + throw new ArgumentException(SR.Argument_InvalidOffLen); } var bufferSegment = new ArraySegment(buffer, offset, count); diff --git a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpClientJsonExtensionsTests.cs b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpClientJsonExtensionsTests.cs index 6b5aa80452922f..6699631d740e74 100644 --- a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpClientJsonExtensionsTests.cs +++ b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpClientJsonExtensionsTests.cs @@ -14,7 +14,12 @@ namespace System.Net.Http.Json.Functional.Tests { public class HttpClientJsonExtensionsTests { - private static readonly JsonSerializerOptions s_DefaultSerializerOptions = new JsonSerializerOptions { PropertyNameCaseInsensitive = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; + private static readonly JsonSerializerOptions s_defaultSerializerOptions + = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; [Fact] public async Task TestGetFromJsonAsync() @@ -102,7 +107,7 @@ await LoopbackServer.CreateClientAndServerAsync( { HttpRequestData request = await server.HandleRequestAsync(); ValidateRequest(request); - Person per = JsonSerializer.Deserialize(request.Body, s_DefaultSerializerOptions); + Person per = JsonSerializer.Deserialize(request.Body, s_defaultSerializerOptions); per.Validate(); } }); @@ -138,7 +143,7 @@ await LoopbackServer.CreateClientAndServerAsync( { HttpRequestData request = await server.HandleRequestAsync(); ValidateRequest(request); - Person obj = JsonSerializer.Deserialize(request.Body, s_DefaultSerializerOptions); + Person obj = JsonSerializer.Deserialize(request.Body, s_defaultSerializerOptions); obj.Validate(); } }); diff --git a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpContentJsonExtensionsTests.cs b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpContentJsonExtensionsTests.cs index 1a9a75bde2e13e..60d1320cca79b4 100644 --- a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpContentJsonExtensionsTests.cs +++ b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpContentJsonExtensionsTests.cs @@ -225,12 +225,14 @@ await LoopbackServer.CreateClientAndServerAsync( server => server.HandleRequestAsync(headers: _headers, content: "{}")); } - [Fact] - public async Task TestStructuredSyntaxJsonSuffix() + [Theory] + [InlineData("application/json")] + [InlineData("application/foo+json")] // Structured Syntax Json Suffix + public async Task TestValidMediaTypes(string mediaType) { List customHeaders = new List { - new HttpHeaderData("Content-Type", "application/problem+json; charset=\"utf-8\"") + new HttpHeaderData("Content-Type", $"{mediaType}; charset=\"utf-8\"") }; await LoopbackServer.CreateClientAndServerAsync( diff --git a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/JsonContentTests.cs b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/JsonContentTests.cs index a69597ac4a2e74..df7f208796cf0b 100644 --- a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/JsonContentTests.cs +++ b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/JsonContentTests.cs @@ -174,14 +174,37 @@ await LoopbackServer.CreateClientAndServerAsync( } [Fact] - public void EnsureDefaultJsonSerializerOptions() + public async Task EnsureDefaultJsonSerializerOptionsAsync() { HttpClient client = new HttpClient(); EnsureDefaultOptions obj = new EnsureDefaultOptions(); var request = new HttpRequestMessage(HttpMethod.Post, "http://example.com"); request.Content = JsonContent.Create(obj, mediaType: null); - client.SendAsync(request); + await client.SendAsync(request); + } + + [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/Resources/Strings.resx b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/Resources/Strings.resx new file mode 100644 index 00000000000000..fcc6751c6d1278 --- /dev/null +++ b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/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/FunctionalTests/TranscodingReadStreamTests.cs b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/TranscodingReadStreamTests.cs index aa576b38a693a2..2055db59cfb8bf 100644 --- a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/TranscodingReadStreamTests.cs +++ b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/TranscodingReadStreamTests.cs @@ -4,6 +4,7 @@ // 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; @@ -286,5 +287,33 @@ public async Task TestOneToOneTranscodingAsync() Assert.Equal(message.Trim('"'), deserializedMessage); } } + + [Fact(Skip ="TODO: Find out how to simulate exhausting the ArrayPool's bucket.")] + public async Task ByteBufferIsLargerThanCharBuffer() + { + List rentedArrays = new List(); + + while (true) + { + byte[] rented = ArrayPool.Shared.Rent(TranscodingReadStream.MaxByteBufferSize); + + rentedArrays.Add(rented); + if (rented.Length > TranscodingReadStream.MaxByteBufferSize) + { + // Rented array is larger than expected... proceeding with the test. + break; + } + } + + 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); + } + } } } From 86edaf005802d506f2aec4838862ce408bcb8fa5 Mon Sep 17 00:00:00 2001 From: David Cantu Date: Tue, 24 Mar 2020 01:12:19 -0700 Subject: [PATCH 25/32] Add Unit.Tests project --- .../System.Net.Http.Json.sln | 7 +++++++ ...stem.Net.Http.Json.Functional.Tests.csproj | 9 --------- .../Resources/Strings.resx | 0 .../System.Net.Http.Json.Unit.Tests.csproj | 20 +++++++++++++++++++ .../TranscodingReadStreamTests.cs | 0 .../TranscodingWriteStreamTests.cs | 0 6 files changed, 27 insertions(+), 9 deletions(-) rename src/libraries/System.Net.Http.Json/tests/{FunctionalTests => UnitTests}/Resources/Strings.resx (100%) create mode 100644 src/libraries/System.Net.Http.Json/tests/UnitTests/System.Net.Http.Json.Unit.Tests.csproj rename src/libraries/System.Net.Http.Json/tests/{FunctionalTests => UnitTests}/TranscodingReadStreamTests.cs (100%) rename src/libraries/System.Net.Http.Json/tests/{FunctionalTests => UnitTests}/TranscodingWriteStreamTests.cs (100%) 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 index cb5094d589ea5d..9efc47bab27603 100644 --- a/src/libraries/System.Net.Http.Json/System.Net.Http.Json.sln +++ b/src/libraries/System.Net.Http.Json/System.Net.Http.Json.sln @@ -20,6 +20,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Net.Http.Json.Functi {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 @@ -38,6 +40,10 @@ Global {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 @@ -46,6 +52,7 @@ Global {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} 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 index 3614052e6c2e20..ad6333b1aa6e54 100644 --- 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 @@ -7,8 +7,6 @@ - - @@ -39,11 +37,4 @@ Common\System\Threading\Tasks\TaskTimeoutExtensions.cs - - - - - - - diff --git a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/Resources/Strings.resx b/src/libraries/System.Net.Http.Json/tests/UnitTests/Resources/Strings.resx similarity index 100% rename from src/libraries/System.Net.Http.Json/tests/FunctionalTests/Resources/Strings.resx rename to src/libraries/System.Net.Http.Json/tests/UnitTests/Resources/Strings.resx 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 00000000000000..211288bbea6f77 --- /dev/null +++ b/src/libraries/System.Net.Http.Json/tests/UnitTests/System.Net.Http.Json.Unit.Tests.csproj @@ -0,0 +1,20 @@ + + + $(NetCoreAppCurrent);$(NetFrameworkCurrent) + + + + + + + + + + + ProductionCode\System\Net\Http\Json\TranscodingReadStream.cs + + + ProductionCode\System\Net\Http\Json\TranscodingWriteStream.cs + + + diff --git a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/TranscodingReadStreamTests.cs b/src/libraries/System.Net.Http.Json/tests/UnitTests/TranscodingReadStreamTests.cs similarity index 100% rename from src/libraries/System.Net.Http.Json/tests/FunctionalTests/TranscodingReadStreamTests.cs rename to src/libraries/System.Net.Http.Json/tests/UnitTests/TranscodingReadStreamTests.cs diff --git a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/TranscodingWriteStreamTests.cs b/src/libraries/System.Net.Http.Json/tests/UnitTests/TranscodingWriteStreamTests.cs similarity index 100% rename from src/libraries/System.Net.Http.Json/tests/FunctionalTests/TranscodingWriteStreamTests.cs rename to src/libraries/System.Net.Http.Json/tests/UnitTests/TranscodingWriteStreamTests.cs From 3a8c7a320fa3f9668e046ce67894aabae4f86cb6 Mon Sep 17 00:00:00 2001 From: David Cantu Date: Tue, 24 Mar 2020 01:30:27 -0700 Subject: [PATCH 26/32] Make mediaType argument optional on JsonContent.Create --- .../ref/System.Net.Http.Json.cs | 4 +-- .../src/System/Net/Http/Json/JsonContent.cs | 4 +-- .../tests/FunctionalTests/JsonContentTests.cs | 25 ++++++++----------- 3 files changed, 15 insertions(+), 18 deletions(-) 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 index 687092ae2fab1d..12ac0c0eabe21f 100644 --- 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 @@ -36,8 +36,8 @@ public 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, System.Text.Json.JsonSerializerOptions? options = null) { throw null; } - public static System.Net.Http.Json.JsonContent Create(T inputValue, System.Net.Http.Headers.MediaTypeHeaderValue? mediaType, System.Text.Json.JsonSerializerOptions? options = null) { 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/src/System/Net/Http/Json/JsonContent.cs b/src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/JsonContent.cs index 012863c64dae7f..ddd6915cbb8251 100644 --- 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 @@ -45,10 +45,10 @@ private JsonContent(object? inputValue, Type inputType, MediaTypeHeaderValue? me _jsonSerializerOptions = options ?? s_defaultSerializerOptions; } - public static JsonContent Create(T inputValue, MediaTypeHeaderValue? mediaType, JsonSerializerOptions? options = null) + 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, JsonSerializerOptions? options = null) + 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) diff --git a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/JsonContentTests.cs b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/JsonContentTests.cs index df7f208796cf0b..caddae7a9d7f15 100644 --- a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/JsonContentTests.cs +++ b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/JsonContentTests.cs @@ -21,23 +21,23 @@ public void JsonContentObjectType() Type fooType = typeof(Foo); Foo foo = new Foo(); - JsonContent content = JsonContent.Create(foo, fooType, null); + JsonContent content = JsonContent.Create(foo, fooType); Assert.Equal(fooType, content.ObjectType); Assert.Same(foo, content.Value); - content = JsonContent.Create(foo, null); + 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, null); + 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, null); + content = JsonContent.Create(fooBoxed); Assert.Equal(typeof(object), content.ObjectType); Assert.Same(fooBoxed, content.Value); } @@ -49,11 +49,11 @@ public void TestJsonContentMediaType() Foo foo = new Foo(); // Use the default content-type if none is provided. - JsonContent content = JsonContent.Create(foo, fooType, null); + 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, null); + content = JsonContent.Create(foo); Assert.Equal("application/json", content.Headers.ContentType.MediaType); Assert.Equal("utf-8", content.Headers.ContentType.CharSet); @@ -69,7 +69,7 @@ public void TestJsonContentMediaType() [Fact] public async Task SendQuotedCharsetAsync() { - JsonContent content = JsonContent.Create(null, null); + JsonContent content = JsonContent.Create(null); content.Headers.ContentType.CharSet = "\"utf-8\""; HttpClient client = new HttpClient(); @@ -81,8 +81,8 @@ public async Task SendQuotedCharsetAsync() [Fact] public void TestJsonContentContentTypeIsNotTheSameOnMultipleInstances() { - JsonContent jsonContent1 = JsonContent.Create(null, null); - JsonContent jsonContent2 = JsonContent.Create(null, null); + JsonContent jsonContent1 = JsonContent.Create(null); + JsonContent jsonContent2 = JsonContent.Create(null); jsonContent1.Headers.ContentType.CharSet = "foo-bar"; @@ -132,9 +132,6 @@ public void JsonContentInputTypeIsNull() ArgumentNullException ex = Assert.Throws(() => JsonContent.Create(foo, inputType: null, mediaType: null)); Assert.Equal("inputType", ex.ParamName); - - ex = Assert.Throws(() => JsonContent.Create(foo, inputType: null, mediaType: null)); - Assert.Equal("inputType", ex.ParamName); } [Fact] @@ -144,7 +141,7 @@ public void JsonContentThrowsOnIncompatibleTypeAsync() var foo = new Foo(); Type typeOfBar = typeof(Bar); - Exception ex = Assert.Throws(() => JsonContent.Create(foo, typeOfBar, null)); + Exception ex = Assert.Throws(() => JsonContent.Create(foo, typeOfBar)); string strTypeOfBar = typeOfBar.ToString(); Assert.Contains(strTypeOfBar, ex.Message); @@ -180,7 +177,7 @@ public async Task EnsureDefaultJsonSerializerOptionsAsync() EnsureDefaultOptions obj = new EnsureDefaultOptions(); var request = new HttpRequestMessage(HttpMethod.Post, "http://example.com"); - request.Content = JsonContent.Create(obj, mediaType: null); + request.Content = JsonContent.Create(obj); await client.SendAsync(request); } From 8190ae6fa720a31c7b68ad8a61c925368c911025 Mon Sep 17 00:00:00 2001 From: David Cantu Date: Tue, 24 Mar 2020 13:24:19 -0700 Subject: [PATCH 27/32] Address latest comments. --- .../Http/Json/HttpClientJsonExtensions.Get.cs | 8 +++--- .../Json/HttpClientJsonExtensions.Post.cs | 8 +++--- .../Http/Json/HttpClientJsonExtensions.Put.cs | 8 +++--- .../Http/Json/HttpContentJsonExtensions.cs | 28 ++++++++----------- .../src/System/Net/Http/Json/JsonContent.cs | 8 +++--- .../Net/Http/Json/TranscodingReadStream.cs | 4 +-- .../Net/Http/Json/TranscodingWriteStream.cs | 2 +- .../System.Net.Http.Json.Unit.Tests.csproj | 4 ++- 8 files changed, 33 insertions(+), 37 deletions(-) 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 index 88b33be3413077..b1c8fb6855939b 100644 --- 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 @@ -59,16 +59,16 @@ public static Task GetFromJsonAsync(this HttpClient client, Uri? } public static Task GetFromJsonAsync(this HttpClient client, string? requestUri, Type type, CancellationToken cancellationToken = default) - => client.GetFromJsonAsync(requestUri, type, null, cancellationToken); + => 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, null, cancellationToken); + => client.GetFromJsonAsync(requestUri, type, options: null, cancellationToken); public static Task GetFromJsonAsync(this HttpClient client, string? requestUri, CancellationToken cancellationToken = default) - => client.GetFromJsonAsync(requestUri, null, cancellationToken); + => client.GetFromJsonAsync(requestUri, options: null, cancellationToken); public static Task GetFromJsonAsync(this HttpClient client, Uri? requestUri, CancellationToken cancellationToken = default) - => client.GetFromJsonAsync(requestUri, null, cancellationToken); + => client.GetFromJsonAsync(requestUri, options: null, cancellationToken); private static async Task GetFromJsonAsyncCore(Task taskResponse, Type type, JsonSerializerOptions? options, CancellationToken cancellationToken) { 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 index 6a651c648d30d9..1ae586e864e325 100644 --- 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 @@ -17,7 +17,7 @@ public static Task PostAsJsonAsync(this HttpClient throw new ArgumentNullException(nameof(client)); } - JsonContent content = JsonContent.Create(value, null, options); + JsonContent content = JsonContent.Create(value, mediaType: null, options); return client.PostAsync(requestUri, content, cancellationToken); } @@ -28,14 +28,14 @@ public static Task PostAsJsonAsync(this HttpClient throw new ArgumentNullException(nameof(client)); } - JsonContent content = JsonContent.Create(value, null, options); + 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, null, 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, null, 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 index ed35819adee86c..cddd46443367ca 100644 --- 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 @@ -17,7 +17,7 @@ public static Task PutAsJsonAsync(this HttpClient c throw new ArgumentNullException(nameof(client)); } - JsonContent content = JsonContent.Create(value, null, options); + JsonContent content = JsonContent.Create(value, mediaType: null, options); return client.PutAsync(requestUri, content, cancellationToken); } @@ -28,14 +28,14 @@ public static Task PutAsJsonAsync(this HttpClient c throw new ArgumentNullException(nameof(client)); } - JsonContent content = JsonContent.Create(value, null, options); + 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, null, 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, null, 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 index b61afab900548a..d4d1af642e93ab 100644 --- 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 @@ -20,7 +20,7 @@ public static class HttpContentJsonExtensions Debug.Assert(content.Headers.ContentType != null); Encoding? sourceEncoding = JsonContent.GetEncoding(content.Headers.ContentType.CharSet); - return ReadFromJsonAsyncCore(content, type, sourceEncoding, options ?? JsonContent.s_defaultSerializerOptions, cancellationToken); + return ReadFromJsonAsyncCore(content, type, sourceEncoding, options ?? JsonContent.DefaultSerializerOptions, cancellationToken); } public static Task ReadFromJsonAsync(this HttpContent content, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) @@ -29,7 +29,7 @@ public static Task ReadFromJsonAsync(this HttpContent content, JsonSeriali Debug.Assert(content.Headers.ContentType != null); Encoding? sourceEncoding = JsonContent.GetEncoding(content.Headers.ContentType.CharSet); - return ReadFromJsonAsyncCore(content, sourceEncoding, options ?? JsonContent.s_defaultSerializerOptions, cancellationToken); + return ReadFromJsonAsyncCore(content, sourceEncoding, options ?? JsonContent.DefaultSerializerOptions, cancellationToken); } private static async Task ReadFromJsonAsyncCore(HttpContent content, Type type, Encoding? sourceEncoding, JsonSerializerOptions? options, CancellationToken cancellationToken) @@ -39,15 +39,12 @@ public static Task ReadFromJsonAsync(this HttpContent content, JsonSeriali // Wrap content stream into a transcoding stream that buffers the data transcoded from the sourceEncoding to utf-8. if (sourceEncoding != null && sourceEncoding != Encoding.UTF8) { - using Stream transcodingStream = new TranscodingReadStream(contentStream, sourceEncoding); - return await JsonSerializer.DeserializeAsync(transcodingStream, type, options, cancellationToken).ConfigureAwait(false); + contentStream = new TranscodingReadStream(contentStream, sourceEncoding); } - else + + using (contentStream) { - using (contentStream) - { - return await JsonSerializer.DeserializeAsync(contentStream, type, options, cancellationToken).ConfigureAwait(false); - } + return await JsonSerializer.DeserializeAsync(contentStream, type, options, cancellationToken).ConfigureAwait(false); } } @@ -58,15 +55,12 @@ private static async Task ReadFromJsonAsyncCore(HttpContent content, Encod // Wrap content stream into a transcoding stream that buffers the data transcoded from the sourceEncoding to utf-8. if (sourceEncoding != null && sourceEncoding != Encoding.UTF8) { - using Stream transcodingStream = new TranscodingReadStream(contentStream, sourceEncoding); - return await JsonSerializer.DeserializeAsync(transcodingStream, options, cancellationToken).ConfigureAwait(false); + contentStream = new TranscodingReadStream(contentStream, sourceEncoding); } - else + + using (contentStream) { - using (contentStream) - { - return await JsonSerializer.DeserializeAsync(contentStream, options, cancellationToken).ConfigureAwait(false); - } + return await JsonSerializer.DeserializeAsync(contentStream, options, cancellationToken).ConfigureAwait(false); } } @@ -84,7 +78,7 @@ private static void ValidateContent(HttpContent content) throw new NotSupportedException(SR.ContentTypeNotSupported); } - mediaType = mediaType.ToLower(); + mediaType = mediaType.ToLowerInvariant(); if (mediaType != JsonContent.JsonMediaType && !IsValidStructuredSyntaxJsonSuffix(mediaType.AsSpan())) 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 index ddd6915cbb8251..7e1d22edccbf05 100644 --- 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 @@ -17,10 +17,10 @@ public partial class JsonContent : HttpContent internal const string JsonMediaType = "application/json"; internal const string JsonType = "application"; internal const string JsonSubtype = "json"; - private static MediaTypeHeaderValue s_defaultMediaType + private static MediaTypeHeaderValue DefaultMediaType => MediaTypeHeaderValue.Parse(string.Format("{0}; charset={1}", JsonMediaType, Encoding.UTF8.WebName)); - internal static JsonSerializerOptions s_defaultSerializerOptions + internal static JsonSerializerOptions DefaultSerializerOptions => new JsonSerializerOptions { PropertyNameCaseInsensitive = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; private readonly JsonSerializerOptions? _jsonSerializerOptions; @@ -41,8 +41,8 @@ private JsonContent(object? inputValue, Type inputType, MediaTypeHeaderValue? me Value = inputValue; ObjectType = inputType; - Headers.ContentType = mediaType ?? s_defaultMediaType; - _jsonSerializerOptions = options ?? s_defaultSerializerOptions; + Headers.ContentType = mediaType ?? DefaultMediaType; + _jsonSerializerOptions = options ?? DefaultSerializerOptions; } public static JsonContent Create(T inputValue, MediaTypeHeaderValue? mediaType = null, JsonSerializerOptions? options = null) 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 index c096e4fce2a8a6..3e0e009f779bbd 100644 --- 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 @@ -138,7 +138,7 @@ private async Task ReadAsyncCore(ArraySegment readBuffer, Cancellatio return bytesWritten; } - _encoder.Convert(_charBuffer.Array!, _charBuffer.Offset, _charBuffer.Count, _overflowBuffer.Array!, 0, _overflowBuffer.Array!.Length, + _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."); @@ -178,7 +178,7 @@ private async Task ReadInputChars(CancellationToken cancellationToken) 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, 0, _charBuffer.Array.Length, + _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. 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 index f4ab289bd55f9c..3ac32a7ae5c9bf 100644 --- 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 @@ -117,7 +117,7 @@ private async Task WriteBufferAsync(CancellationToken cancellationToken) while (!encoderCompleted && charsWritten < _charsDecoded) { - _encoder.Convert(_charBuffer, charsWritten, _charsDecoded - charsWritten, byteBuffer, 0, byteBuffer.Length, + _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); 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 index 211288bbea6f77..4b7b36a6be21f5 100644 --- 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 @@ -7,7 +7,9 @@ - + + ProductionCode\System\ArraySegmentExtensions.netstandard.cs + From 231085d589fbe012989b3000ff36bc2e4a868806 Mon Sep 17 00:00:00 2001 From: David Cantu Date: Tue, 24 Mar 2020 14:11:15 -0700 Subject: [PATCH 28/32] Perform case-insensitive comparisons on MediaType instead of ToLower. --- .../Http/Json/HttpClientJsonExtensions.Get.cs | 1 - .../Http/Json/HttpContentJsonExtensions.cs | 17 ++++------- .../Net/Http/Json/JsonContent.netcoreapp.cs | 2 -- .../HttpContentJsonExtensionsTests.cs | 3 ++ .../UnitTests/TranscodingReadStreamTests.cs | 28 ------------------- 5 files changed, 8 insertions(+), 43 deletions(-) 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 index b1c8fb6855939b..1f4d2494e7703f 100644 --- 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 @@ -2,7 +2,6 @@ // 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.Text.Json; using System.Threading; using System.Threading.Tasks; 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 index d4d1af642e93ab..acb2fc0e57abb2 100644 --- 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 @@ -4,7 +4,6 @@ using System.Diagnostics; using System.IO; -using System.Net.Mime; using System.Text; using System.Text.Json; using System.Threading; @@ -73,14 +72,8 @@ private static void ValidateContent(HttpContent content) string? mediaType = content.Headers.ContentType?.MediaType; - if (mediaType == null) - { - throw new NotSupportedException(SR.ContentTypeNotSupported); - } - - mediaType = mediaType.ToLowerInvariant(); - - if (mediaType != JsonContent.JsonMediaType && + if (mediaType == null || + !mediaType.Equals(JsonContent.JsonMediaType, StringComparison.OrdinalIgnoreCase) && !IsValidStructuredSyntaxJsonSuffix(mediaType.AsSpan())) { throw new NotSupportedException(SR.ContentTypeNotSupported); @@ -93,7 +86,7 @@ private static bool IsValidStructuredSyntaxJsonSuffix(ReadOnlySpan mediaTy int typeLength = mediaType.IndexOf('/'); if (typeLength < 0 || - !mediaType.Slice(index, typeLength).SequenceEqual(JsonContent.JsonType.AsSpan())) + mediaType.Slice(index, typeLength).CompareTo(JsonContent.JsonType.AsSpan(), StringComparison.OrdinalIgnoreCase) != 0) { return false; } @@ -108,8 +101,8 @@ private static bool IsValidStructuredSyntaxJsonSuffix(ReadOnlySpan mediaTy } index += suffixStart + 1; - - if (!mediaType.Slice(index).SequenceEqual(JsonContent.JsonSubtype.AsSpan())) + ReadOnlySpan mediaTypeSuffix = mediaType.Slice(index); + if (mediaTypeSuffix.CompareTo(JsonContent.JsonSubtype.AsSpan(), StringComparison.OrdinalIgnoreCase) != 0) { return false; } 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 index ff559b196cf0c5..cdd46968b12c7d 100644 --- 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 @@ -3,8 +3,6 @@ // See the LICENSE file in the project root for more information. using System.IO; -using System.Text; -using System.Text.Json; using System.Threading; using System.Threading.Tasks; diff --git a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpContentJsonExtensionsTests.cs b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpContentJsonExtensionsTests.cs index 60d1320cca79b4..377a43df52fdc4 100644 --- a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpContentJsonExtensionsTests.cs +++ b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpContentJsonExtensionsTests.cs @@ -227,7 +227,10 @@ await LoopbackServer.CreateClientAndServerAsync( [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 diff --git a/src/libraries/System.Net.Http.Json/tests/UnitTests/TranscodingReadStreamTests.cs b/src/libraries/System.Net.Http.Json/tests/UnitTests/TranscodingReadStreamTests.cs index 2055db59cfb8bf..341d771141451c 100644 --- a/src/libraries/System.Net.Http.Json/tests/UnitTests/TranscodingReadStreamTests.cs +++ b/src/libraries/System.Net.Http.Json/tests/UnitTests/TranscodingReadStreamTests.cs @@ -287,33 +287,5 @@ public async Task TestOneToOneTranscodingAsync() Assert.Equal(message.Trim('"'), deserializedMessage); } } - - [Fact(Skip ="TODO: Find out how to simulate exhausting the ArrayPool's bucket.")] - public async Task ByteBufferIsLargerThanCharBuffer() - { - List rentedArrays = new List(); - - while (true) - { - byte[] rented = ArrayPool.Shared.Rent(TranscodingReadStream.MaxByteBufferSize); - - rentedArrays.Add(rented); - if (rented.Length > TranscodingReadStream.MaxByteBufferSize) - { - // Rented array is larger than expected... proceeding with the test. - break; - } - } - - 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); - } - } } } From 4970a538111515c734bec677d2bc4cf8e8c88a75 Mon Sep 17 00:00:00 2001 From: David Cantu Date: Tue, 24 Mar 2020 19:10:22 -0700 Subject: [PATCH 29/32] Address comments related to Transcoding classes. --- .../System.Net.Http.Json/ref/System.Net.Http.Json.cs | 2 +- .../ref/System.Net.Http.Json.netcoreapp.cs | 2 +- .../src/System/Net/Http/Json/JsonContent.cs | 5 ++++- .../System/Net/Http/Json/JsonContent.netcoreapp.cs | 2 +- .../src/System/Net/Http/Json/TranscodingReadStream.cs | 4 ++-- .../System/Net/Http/Json/TranscodingWriteStream.cs | 11 ++--------- .../tests/FunctionalTests/JsonContentTests.cs | 7 ++++++- .../tests/UnitTests/TranscodingWriteStreamTests.cs | 3 +++ 8 files changed, 20 insertions(+), 16 deletions(-) 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 index 12ac0c0eabe21f..bdb7399a2f6dc5 100644 --- 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 @@ -31,7 +31,7 @@ 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 partial class JsonContent : System.Net.Http.HttpContent + public sealed partial class JsonContent : System.Net.Http.HttpContent { internal JsonContent() { } public System.Type ObjectType { get { throw null; } } 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 index 489e402268fc90..49131cd7c3e384 100644 --- 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 @@ -4,7 +4,7 @@ namespace System.Net.Http.Json { - public partial class JsonContent : System.Net.Http.HttpContent + 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/System/Net/Http/Json/JsonContent.cs b/src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/JsonContent.cs index 7e1d22edccbf05..21b18581b83e2a 100644 --- 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 @@ -12,7 +12,7 @@ namespace System.Net.Http.Json { - public partial class JsonContent : HttpContent + public sealed partial class JsonContent : HttpContent { internal const string JsonMediaType = "application/json"; internal const string JsonType = "application"; @@ -70,6 +70,9 @@ private async Task SerializeToStreamAsyncCore(Stream targetStream, CancellationT 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); } } 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 index cdd46968b12c7d..a669b915a0aa83 100644 --- 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 @@ -8,7 +8,7 @@ namespace System.Net.Http.Json { - public partial class JsonContent + 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 index 3e0e009f779bbd..b75c17883f7095 100644 --- 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 @@ -18,7 +18,7 @@ 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 internal const int MaxByteBufferSize = 4096; - internal const int MaxCharBufferSize = 3 * MaxByteBufferSize; + internal const int MaxCharBufferSize = 4096; private readonly Stream _stream; private readonly Decoder _decoder; @@ -118,7 +118,7 @@ private async Task ReadAsyncCore(ArraySegment readBuffer, Cancellatio if (_charBuffer.Count == 0) { int bytesRead = await ReadInputChars(cancellationToken).ConfigureAwait(false); - shouldFlushEncoder = bytesRead == 0; + shouldFlushEncoder = bytesRead == 0 && _byteBuffer.Count == 0; } bool completed = false; 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 index 3ac32a7ae5c9bf..28099594ba6bf3 100644 --- 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 @@ -101,11 +101,7 @@ private async Task WriteAsyncCore(ArraySegment bufferSegment, Cancellation _charsDecoded += charsDecoded; bufferSegment = bufferSegment.Slice(bytesDecoded); - - if (!decoderCompleted) - { - await WriteBufferAsync(cancellationToken).ConfigureAwait(false); - } + await WriteBufferAsync(cancellationToken).ConfigureAwait(false); } } @@ -142,10 +138,7 @@ protected override void Dispose(bool disposing) public async Task FinalWriteAsync(CancellationToken cancellationToken) { - // First write any buffered content - await WriteBufferAsync(cancellationToken).ConfigureAwait(false); - - // Now flush the encoder. + // Flush the encoder. byte[] byteBuffer = ArrayPool.Shared.Rent(_maxByteBufferSize); bool encoderCompleted = false; diff --git a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/JsonContentTests.cs b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/JsonContentTests.cs index caddae7a9d7f15..a05645cc342441 100644 --- a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/JsonContentTests.cs +++ b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/JsonContentTests.cs @@ -4,6 +4,8 @@ using System.Net.Http.Headers; using System.Net.Test.Common; +using System.Text; +using System.Text.Json; using System.Threading.Tasks; using Xunit; @@ -160,13 +162,16 @@ await LoopbackServer.CreateClientAndServerAsync( { var request = new HttpRequestMessage(HttpMethod.Post, uri); MediaTypeHeaderValue mediaType = MediaTypeHeaderValue.Parse("application/json; charset=utf-16"); - request.Content = JsonContent.Create(Person.Create(), mediaType: mediaType); + // 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(); }); } diff --git a/src/libraries/System.Net.Http.Json/tests/UnitTests/TranscodingWriteStreamTests.cs b/src/libraries/System.Net.Http.Json/tests/UnitTests/TranscodingWriteStreamTests.cs index e8f9057febd609..788dadba5c6480 100644 --- a/src/libraries/System.Net.Http.Json/tests/UnitTests/TranscodingWriteStreamTests.cs +++ b/src/libraries/System.Net.Http.Json/tests/UnitTests/TranscodingWriteStreamTests.cs @@ -78,6 +78,9 @@ private static async Task WriteAsyncTest(Encoding targetEncoding, string message 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(); From c4282f2e7e20c2a866db33b12e887e91d75b3e4e Mon Sep 17 00:00:00 2001 From: David Cantu Date: Wed, 25 Mar 2020 19:40:26 -0700 Subject: [PATCH 30/32] Address feedback --- .../Http/Json/HttpContentJsonExtensions.cs | 7 +- .../Net/Http/Json/TranscodingReadStream.cs | 7 +- .../Net/Http/Json/TranscodingWriteStream.cs | 2 + .../HttpClientJsonExtensionsTests.cs | 59 +++++------- .../HttpContentJsonExtensionsTests.cs | 26 ++--- .../tests/FunctionalTests/JsonContentTests.cs | 67 ++++++++----- .../UnitTests/TranscodingReadStreamTests.cs | 95 ++++++++++--------- .../UnitTests/TranscodingWriteStreamTests.cs | 12 +-- 8 files changed, 145 insertions(+), 130 deletions(-) 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 index acb2fc0e57abb2..6d1d309760f3a9 100644 --- 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 @@ -85,8 +85,9 @@ private static bool IsValidStructuredSyntaxJsonSuffix(ReadOnlySpan mediaTy int index = 0; int typeLength = mediaType.IndexOf('/'); + ReadOnlySpan type = mediaType.Slice(index, typeLength); if (typeLength < 0 || - mediaType.Slice(index, typeLength).CompareTo(JsonContent.JsonType.AsSpan(), StringComparison.OrdinalIgnoreCase) != 0) + type.CompareTo(JsonContent.JsonType.AsSpan(), StringComparison.OrdinalIgnoreCase) != 0) { return false; } @@ -101,8 +102,8 @@ private static bool IsValidStructuredSyntaxJsonSuffix(ReadOnlySpan mediaTy } index += suffixStart + 1; - ReadOnlySpan mediaTypeSuffix = mediaType.Slice(index); - if (mediaTypeSuffix.CompareTo(JsonContent.JsonSubtype.AsSpan(), StringComparison.OrdinalIgnoreCase) != 0) + ReadOnlySpan suffix = mediaType.Slice(index); + if (suffix.CompareTo(JsonContent.JsonSubtype.AsSpan(), StringComparison.OrdinalIgnoreCase) != 0) { return false; } 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 index b75c17883f7095..e78546ad852f2e 100644 --- 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 @@ -17,8 +17,9 @@ 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; - internal const int MaxCharBufferSize = 4096; private readonly Stream _stream; private readonly Decoder _decoder; @@ -39,9 +40,8 @@ public TranscodingReadStream(Stream input, Encoding sourceEncoding) // 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. - // However limit the buffer size to prevent an encoding that has a very poor worst-case scenario. // The conversion process is tolerant of char buffer that is not large enough to convert all the bytes at once. - int maxCharBufferSize = Math.Min(MaxCharBufferSize, sourceEncoding.GetMaxCharCount(MaxByteBufferSize)); + 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); @@ -124,6 +124,7 @@ private async Task ReadAsyncCore(ArraySegment readBuffer, Cancellatio 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) { 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 index 28099594ba6bf3..dc0140b05a2118 100644 --- 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 @@ -14,6 +14,8 @@ 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; internal const int MaxByteBufferSize = 4 * MaxCharBufferSize; private readonly int _maxByteBufferSize; diff --git a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpClientJsonExtensionsTests.cs b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpClientJsonExtensionsTests.cs index 6699631d740e74..4186235c864be3 100644 --- a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpClientJsonExtensionsTests.cs +++ b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpClientJsonExtensionsTests.cs @@ -89,17 +89,17 @@ await LoopbackServer.CreateClientAndServerAsync( { Person person = Person.Create(); - HttpResponseMessage response = await client.PostAsJsonAsync(uri.ToString(), person); + using HttpResponseMessage response = await client.PostAsJsonAsync(uri.ToString(), person); Assert.True(response.StatusCode == HttpStatusCode.OK); - response = await client.PostAsJsonAsync(uri, person); - Assert.True(response.StatusCode == HttpStatusCode.OK); + using HttpResponseMessage response2 = await client.PostAsJsonAsync(uri, person); + Assert.True(response2.StatusCode == HttpStatusCode.OK); - response = await client.PostAsJsonAsync(uri.ToString(), person, CancellationToken.None); - Assert.True(response.StatusCode == HttpStatusCode.OK); + using HttpResponseMessage response3 = await client.PostAsJsonAsync(uri.ToString(), person, CancellationToken.None); + Assert.True(response3.StatusCode == HttpStatusCode.OK); - response = await client.PostAsJsonAsync(uri, person, CancellationToken.None); - Assert.True(response.StatusCode == HttpStatusCode.OK); + using HttpResponseMessage response4 = await client.PostAsJsonAsync(uri, person, CancellationToken.None); + Assert.True(response4.StatusCode == HttpStatusCode.OK); } }, async server => { @@ -125,17 +125,17 @@ await LoopbackServer.CreateClientAndServerAsync( Person person = Person.Create(); Type typePerson = typeof(Person); - HttpResponseMessage response = await client.PutAsJsonAsync(uri.ToString(), person); + using HttpResponseMessage response = await client.PutAsJsonAsync(uri.ToString(), person); Assert.True(response.StatusCode == HttpStatusCode.OK); - response = await client.PutAsJsonAsync(uri, person); - Assert.True(response.StatusCode == HttpStatusCode.OK); + using HttpResponseMessage response2 = await client.PutAsJsonAsync(uri, person); + Assert.True(response2.StatusCode == HttpStatusCode.OK); - response = await client.PutAsJsonAsync(uri.ToString(), person, CancellationToken.None); - Assert.True(response.StatusCode == HttpStatusCode.OK); + using HttpResponseMessage response3 = await client.PutAsJsonAsync(uri.ToString(), person, CancellationToken.None); + Assert.True(response3.StatusCode == HttpStatusCode.OK); - response = await client.PutAsJsonAsync(uri, person, CancellationToken.None); - Assert.True(response.StatusCode == HttpStatusCode.OK); + using HttpResponseMessage response4 = await client.PutAsJsonAsync(uri, person, CancellationToken.None); + Assert.True(response4.StatusCode == HttpStatusCode.OK); } }, async server => { @@ -150,31 +150,22 @@ await LoopbackServer.CreateClientAndServerAsync( } [Fact] - public async Task TestHttpClientIsNullAsync() + public void TestHttpClientIsNullAsync() { HttpClient client = null; string uriString = "http://example.com"; Uri uri = new Uri(uriString); - ArgumentNullException ex; - ex = await Assert.ThrowsAsync(() => client.GetFromJsonAsync(uriString, typeof(Person))); - Assert.Equal("client", ex.ParamName); - ex = await Assert.ThrowsAsync(() => client.GetFromJsonAsync(uri, typeof(Person))); - Assert.Equal("client", ex.ParamName); - ex = await Assert.ThrowsAsync(() => client.GetFromJsonAsync(uriString)); - Assert.Equal("client", ex.ParamName); - ex = await Assert.ThrowsAsync(() => client.GetFromJsonAsync(uri)); - Assert.Equal("client", ex.ParamName); - - ex = await Assert.ThrowsAsync(() => client.PostAsJsonAsync(uriString, null)); - Assert.Equal("client", ex.ParamName); - ex = await Assert.ThrowsAsync(() => client.PostAsJsonAsync(uri, null)); - Assert.Equal("client", ex.ParamName); - - ex = await Assert.ThrowsAsync(() => client.PutAsJsonAsync(uriString, null)); - Assert.Equal("client", ex.ParamName); - ex = await Assert.ThrowsAsync(() => client.PutAsJsonAsync(uri, null)); - Assert.Equal("client", ex.ParamName); + 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) diff --git a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpContentJsonExtensionsTests.cs b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpContentJsonExtensionsTests.cs index 377a43df52fdc4..d381247878f2dd 100644 --- a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpContentJsonExtensionsTests.cs +++ b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpContentJsonExtensionsTests.cs @@ -18,11 +18,11 @@ public class HttpContentJsonExtensionsTests private readonly List _headers = new List { new HttpHeaderData("Content-Type", "application/json") }; [Fact] - public async Task ThrowOnNull() + public void ThrowOnNull() { HttpContent content = null; - await Assert.ThrowsAsync(() => content.ReadFromJsonAsync()); - await Assert.ThrowsAsync(() => content.ReadFromJsonAsync(typeof(Person))); + AssertExtensions.Throws("content", () => content.ReadFromJsonAsync()); + AssertExtensions.Throws("content", () => content.ReadFromJsonAsync(typeof(Person))); } [Fact] @@ -35,7 +35,7 @@ await LoopbackServer.CreateClientAndServerAsync( using (HttpClient client = new HttpClient()) { var request = new HttpRequestMessage(HttpMethod.Get, uri); - var response = await client.SendAsync(request); + HttpResponseMessage response = await client.SendAsync(request); object obj = await response.Content.ReadFromJsonAsync(typeof(Person)); Person per = Assert.IsType(obj); per.Validate(); @@ -64,7 +64,7 @@ await LoopbackServer.CreateClientAndServerAsync( using (HttpClient client = new HttpClient()) { var request = new HttpRequestMessage(HttpMethod.Get, uri); - var response = await client.SendAsync(request); + HttpResponseMessage response = await client.SendAsync(request); object obj = await response.Content.ReadFromJsonAsync(typeof(Person)); Assert.Null(obj); @@ -91,7 +91,7 @@ await LoopbackServer.CreateClientAndServerAsync( using (HttpClient client = new HttpClient()) { var request = new HttpRequestMessage(HttpMethod.Get, uri); - var response = await client.SendAsync(request); + 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))); @@ -110,7 +110,7 @@ await LoopbackServer.CreateClientAndServerAsync( using (HttpClient client = new HttpClient()) { var request = new HttpRequestMessage(HttpMethod.Get, uri); - var response = await client.SendAsync(request); + HttpResponseMessage response = await client.SendAsync(request); await Assert.ThrowsAsync(() => response.Content.ReadFromJsonAsync()); } @@ -132,7 +132,7 @@ await LoopbackServer.CreateClientAndServerAsync( using (HttpClient client = new HttpClient()) { var request = new HttpRequestMessage(HttpMethod.Get, uri); - var response = await client.SendAsync(request); + HttpResponseMessage response = await client.SendAsync(request); Person person = await response.Content.ReadFromJsonAsync(); person.Validate(); @@ -155,7 +155,7 @@ await LoopbackServer.CreateClientAndServerAsync( using (HttpClient client = new HttpClient()) { var request = new HttpRequestMessage(HttpMethod.Get, uri); - var response = await client.SendAsync(request); + HttpResponseMessage response = await client.SendAsync(request); InvalidOperationException ex = await Assert.ThrowsAsync(() => response.Content.ReadFromJsonAsync()); Assert.IsType(ex.InnerException); @@ -175,7 +175,7 @@ await LoopbackServer.CreateClientAndServerAsync( using (HttpClient client = new HttpClient()) { var request = new HttpRequestMessage(HttpMethod.Get, uri); - var response = await client.SendAsync(request); + HttpResponseMessage response = await client.SendAsync(request); Person per = Assert.IsType(await response.Content.ReadFromJsonAsync(typeof(Person))); per.Validate(); @@ -218,7 +218,7 @@ await LoopbackServer.CreateClientAndServerAsync( using (HttpClient client = new HttpClient()) { var request = new HttpRequestMessage(HttpMethod.Get, uri); - var response = await client.SendAsync(request); + HttpResponseMessage response = await client.SendAsync(request); await response.Content.ReadFromJsonAsync(typeof(EnsureDefaultOptions)); } }, @@ -244,7 +244,7 @@ await LoopbackServer.CreateClientAndServerAsync( using (HttpClient client = new HttpClient()) { var request = new HttpRequestMessage(HttpMethod.Get, uri); - var response = await client.SendAsync(request); + HttpResponseMessage response = await client.SendAsync(request); Person person = await response.Content.ReadFromJsonAsync(); person.Validate(); @@ -274,7 +274,7 @@ await LoopbackServer.CreateClientAndServerAsync( using (HttpClient client = new HttpClient()) { var request = new HttpRequestMessage(HttpMethod.Get, uri); - var response = await client.SendAsync(request); + HttpResponseMessage response = await client.SendAsync(request); Exception ex = await Assert.ThrowsAsync(() => response.Content.ReadFromJsonAsync()); Assert.Contains("application/json", ex.Message); diff --git a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/JsonContentTests.cs b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/JsonContentTests.cs index a05645cc342441..243f303f49cf57 100644 --- a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/JsonContentTests.cs +++ b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/JsonContentTests.cs @@ -71,13 +71,24 @@ public void TestJsonContentMediaType() [Fact] public async Task SendQuotedCharsetAsync() { - JsonContent content = JsonContent.Create(null); - content.Headers.ContentType.CharSet = "\"utf-8\""; - HttpClient client = new HttpClient(); - var request = new HttpRequestMessage(HttpMethod.Post, "http://example.com"); - request.Content = content; - await client.SendAsync(request); + 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] @@ -129,27 +140,24 @@ public void JsonContentMediaTypeDefaultIfNull() [Fact] public void JsonContentInputTypeIsNull() - { - string foo = "test"; - - ArgumentNullException ex = Assert.Throws(() => JsonContent.Create(foo, inputType: null, mediaType: null)); - Assert.Equal("inputType", ex.ParamName); - } + => AssertExtensions.Throws("inputType", () => JsonContent.Create(null, inputType: null, mediaType: null)); [Fact] public void JsonContentThrowsOnIncompatibleTypeAsync() { - HttpClient client = new HttpClient(); - var foo = new Foo(); - Type typeOfBar = typeof(Bar); + using (HttpClient client = new HttpClient()) + { + var foo = new Foo(); + Type typeOfBar = typeof(Bar); - Exception ex = Assert.Throws(() => JsonContent.Create(foo, typeOfBar)); + Exception ex = Assert.Throws(() => JsonContent.Create(foo, typeOfBar)); - string strTypeOfBar = typeOfBar.ToString(); - Assert.Contains(strTypeOfBar, ex.Message); + string strTypeOfBar = typeOfBar.ToString(); + Assert.Contains(strTypeOfBar, ex.Message); - string afterInputTypeMessage = ex.Message.Split(strTypeOfBar.ToCharArray())[1]; - Assert.Contains(afterInputTypeMessage, ex.Message); + string afterInputTypeMessage = ex.Message.Split(strTypeOfBar.ToCharArray())[1]; + Assert.Contains(afterInputTypeMessage, ex.Message); + } } [Fact] @@ -178,12 +186,19 @@ await LoopbackServer.CreateClientAndServerAsync( [Fact] public async Task EnsureDefaultJsonSerializerOptionsAsync() { - HttpClient client = new HttpClient(); - EnsureDefaultOptions obj = new EnsureDefaultOptions(); - - var request = new HttpRequestMessage(HttpMethod.Post, "http://example.com"); - request.Content = JsonContent.Create(obj); - await client.SendAsync(request); + 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] diff --git a/src/libraries/System.Net.Http.Json/tests/UnitTests/TranscodingReadStreamTests.cs b/src/libraries/System.Net.Http.Json/tests/UnitTests/TranscodingReadStreamTests.cs index 341d771141451c..3a8ee5840f7dfc 100644 --- a/src/libraries/System.Net.Http.Json/tests/UnitTests/TranscodingReadStreamTests.cs +++ b/src/libraries/System.Net.Http.Json/tests/UnitTests/TranscodingReadStreamTests.cs @@ -21,8 +21,8 @@ public class TranscodingReadStreamTest public async Task ReadAsync_SingleByte() { // Arrange - var input = "Hello world"; - var encoding = Encoding.Unicode; + string input = "Hello world"; + Encoding encoding = Encoding.Unicode; using (TranscodingReadStream stream = new TranscodingReadStream(new MemoryStream(encoding.GetBytes(input)), encoding)) { var bytes = new byte[4]; @@ -68,15 +68,15 @@ public async Task ReadAsync_FillsBuffer() public async Task ReadAsync_CompletedInSecondIteration() { // Arrange - var input = new string('A', 1024 + 10); - var encoding = Encoding.Unicode; + 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]; - var expected = Encoding.UTF8.GetBytes(input.Substring(0, bytes.Length)); + byte[] expected = Encoding.UTF8.GetBytes(input.Substring(0, bytes.Length)); // Act - var readBytes = await stream.ReadAsync(bytes, 0, bytes.Length); + int readBytes = await stream.ReadAsync(bytes, 0, bytes.Length); // Assert Assert.Equal(bytes.Length, readBytes); @@ -98,15 +98,15 @@ public async Task ReadAsync_WithOverflowBuffer() { // Arrange // Test ensures that the overflow buffer works correctly - var input = "\u2600"; - var encoding = Encoding.Unicode; + string input = "\u2600"; + Encoding encoding = Encoding.Unicode; using (TranscodingReadStream stream = new TranscodingReadStream(new MemoryStream(encoding.GetBytes(input)), encoding)) { var bytes = new byte[1]; - var expected = Encoding.UTF8.GetBytes(input); + byte[] expected = Encoding.UTF8.GetBytes(input); // Act - var readBytes = await stream.ReadAsync(bytes, 0, bytes.Length); + int readBytes = await stream.ReadAsync(bytes, 0, bytes.Length); // Assert Assert.Equal(1, readBytes); @@ -127,37 +127,36 @@ public async Task ReadAsync_WithOverflowBuffer() } } - public static TheoryData ReadAsync_WithOverflowBuffer_AtBoundariesData => new TheoryData + public static TheoryData ReadAsync_WithOverflowBuffer_AtBoundariesData(string encoding) { - new string('a', TranscodingReadStream.MaxCharBufferSize - 1) + '\u2600', - new string('a', TranscodingReadStream.MaxCharBufferSize - 2) + '\u2600', - new string('a', TranscodingReadStream.MaxCharBufferSize) + '\u2600', - }; + int maxCharBufferSize = Encoding.GetEncoding(encoding).GetMaxCharCount(TranscodingReadStream.MaxByteBufferSize); - [Theory] - [MemberData(nameof(ReadAsync_WithOverflowBuffer_AtBoundariesData))] - public Task ReadAsync_WithOverflowBuffer_WithBufferSize1(string input) => ReadAsync_WithOverflowBufferAtCharBufferBoundaries(input, bufferSize: 1); + 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))] - public Task ReadAsync_WithOverflowBuffer_WithBufferSize2(string input) => ReadAsync_WithOverflowBufferAtCharBufferBoundaries(input, bufferSize: 1); + [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 - var encoding = Encoding.Unicode; + Encoding encoding = Encoding.Unicode; using (TranscodingReadStream stream = new TranscodingReadStream(new MemoryStream(encoding.GetBytes(input)), encoding)) { - var bytes = new byte[1]; - var expected = Encoding.UTF8.GetBytes(input); + byte[] expected = Encoding.UTF8.GetBytes(input); // Act - int read; var buffer = new byte[bufferSize]; var actual = new List(); - while ((read = await stream.ReadAsync(buffer, 0, bufferSize)) != 0) + while (await stream.ReadAsync(buffer, 0, bufferSize) != 0) { actual.AddRange(buffer); } @@ -166,11 +165,17 @@ private static async Task ReadAsync_WithOverflowBufferAtCharBufferBoundaries(str } } - public static TheoryData ReadAsyncInputLatin => - GetLatinTextInput(TranscodingReadStream.MaxCharBufferSize, TranscodingReadStream.MaxByteBufferSize); + public static TheoryData ReadAsyncInputLatin(string encoding) + { + int maxCharBufferSize = Encoding.GetEncoding(encoding).GetMaxCharCount(TranscodingReadStream.MaxByteBufferSize); + return GetLatinTextInput(maxCharBufferSize, TranscodingReadStream.MaxByteBufferSize); + } - public static TheoryData ReadAsyncInputUnicode => - GetUnicodeText(TranscodingReadStream.MaxCharBufferSize); + 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) { @@ -210,60 +215,60 @@ internal static TheoryData GetUnicodeText(int maxCharBufferSize) } [Theory] - [MemberData(nameof(ReadAsyncInputLatin))] - [MemberData(nameof(ReadAsyncInputUnicode))] + [MemberData(nameof(ReadAsyncInputLatin), "utf-32")] + [MemberData(nameof(ReadAsyncInputUnicode), "utf-32")] public Task ReadAsync_Works_WhenInputIs_UTF32(string message) { - var sourceEncoding = Encoding.UTF32; + Encoding sourceEncoding = Encoding.UTF32; return ReadAsyncTest(sourceEncoding, message); } [Theory] - [MemberData(nameof(ReadAsyncInputLatin))] - [MemberData(nameof(ReadAsyncInputUnicode))] + [MemberData(nameof(ReadAsyncInputLatin), "utf-16")] + [MemberData(nameof(ReadAsyncInputUnicode), "utf-16")] public Task ReadAsync_Works_WhenInputIs_Unicode(string message) { - var sourceEncoding = Encoding.Unicode; + Encoding sourceEncoding = Encoding.Unicode; return ReadAsyncTest(sourceEncoding, message); } [Theory] - [MemberData(nameof(ReadAsyncInputLatin))] - [MemberData(nameof(ReadAsyncInputUnicode))] + [MemberData(nameof(ReadAsyncInputLatin), "utf-7")] + [MemberData(nameof(ReadAsyncInputUnicode), "utf-7")] public Task ReadAsync_Works_WhenInputIs_UTF7(string message) { - var sourceEncoding = Encoding.UTF7; + Encoding sourceEncoding = Encoding.UTF7; return ReadAsyncTest(sourceEncoding, message); } [Theory] - [MemberData(nameof(ReadAsyncInputLatin))] + [MemberData(nameof(ReadAsyncInputLatin), "iso-8859-1")] public Task ReadAsync_Works_WhenInputIs_WesternEuropeanEncoding(string message) { // Arrange - var sourceEncoding = Encoding.GetEncoding(28591); + Encoding sourceEncoding = Encoding.GetEncoding(28591); return ReadAsyncTest(sourceEncoding, message); } [Theory] - [MemberData(nameof(ReadAsyncInputLatin))] + [MemberData(nameof(ReadAsyncInputLatin), "us-ascii")] public Task ReadAsync_Works_WhenInputIs_ASCII(string message) { // Arrange - var sourceEncoding = Encoding.ASCII; + Encoding sourceEncoding = Encoding.ASCII; return ReadAsyncTest(sourceEncoding, message); } private static async Task ReadAsyncTest(Encoding sourceEncoding, string message) { - var input = $"{{ \"Message\": \"{message}\" }}"; + string input = $"{{ \"Message\": \"{message}\" }}"; var stream = new MemoryStream(sourceEncoding.GetBytes(input)); using (var transcodingStream = new TranscodingReadStream(stream, sourceEncoding)) { - var model = await JsonSerializer.DeserializeAsync(transcodingStream, typeof(TestModel)); - var testModel = Assert.IsType(model); + object model = await JsonSerializer.DeserializeAsync(transcodingStream, typeof(TestModel)); + TestModel testModel = Assert.IsType(model); Assert.Equal(message, testModel.Message); } diff --git a/src/libraries/System.Net.Http.Json/tests/UnitTests/TranscodingWriteStreamTests.cs b/src/libraries/System.Net.Http.Json/tests/UnitTests/TranscodingWriteStreamTests.cs index 788dadba5c6480..546f666c7c1133 100644 --- a/src/libraries/System.Net.Http.Json/tests/UnitTests/TranscodingWriteStreamTests.cs +++ b/src/libraries/System.Net.Http.Json/tests/UnitTests/TranscodingWriteStreamTests.cs @@ -29,7 +29,7 @@ public class TranscodingWriteStreamTest [MemberData(nameof(WriteAsyncInputUnicode))] public Task WriteAsync_Works_WhenOutputIs_UTF32(string message) { - var targetEncoding = Encoding.UTF32; + Encoding targetEncoding = Encoding.UTF32; return WriteAsyncTest(targetEncoding, message); } @@ -38,7 +38,7 @@ public Task WriteAsync_Works_WhenOutputIs_UTF32(string message) [MemberData(nameof(WriteAsyncInputUnicode))] public Task WriteAsync_Works_WhenOutputIs_Unicode(string message) { - var targetEncoding = Encoding.Unicode; + Encoding targetEncoding = Encoding.Unicode; return WriteAsyncTest(targetEncoding, message); } @@ -46,7 +46,7 @@ public Task WriteAsync_Works_WhenOutputIs_Unicode(string message) [MemberData(nameof(WriteAsyncInputLatin))] public Task WriteAsync_Works_WhenOutputIs_UTF7(string message) { - var targetEncoding = Encoding.UTF7; + Encoding targetEncoding = Encoding.UTF7; return WriteAsyncTest(targetEncoding, message); } @@ -55,7 +55,7 @@ public Task WriteAsync_Works_WhenOutputIs_UTF7(string message) public Task WriteAsync_Works_WhenOutputIs_WesternEuropeanEncoding(string message) { // Arrange - var targetEncoding = Encoding.GetEncoding(28591); + Encoding targetEncoding = Encoding.GetEncoding(28591); return WriteAsyncTest(targetEncoding, message); } @@ -65,7 +65,7 @@ public Task WriteAsync_Works_WhenOutputIs_WesternEuropeanEncoding(string message public Task WriteAsync_Works_WhenOutputIs_ASCII(string message) { // Arrange - var targetEncoding = Encoding.ASCII; + Encoding targetEncoding = Encoding.ASCII; return WriteAsyncTest(targetEncoding, message); } @@ -84,7 +84,7 @@ private static async Task WriteAsyncTest(Encoding targetEncoding, string message await transcodingStream.FinalWriteAsync(default); await transcodingStream.FlushAsync(); - var actual = targetEncoding.GetString(stream.ToArray()); + string actual = targetEncoding.GetString(stream.ToArray()); Assert.Equal(expected, actual, StringComparer.OrdinalIgnoreCase); } From 8e16505a3f87bbad7708f272334708037db6ca48 Mon Sep 17 00:00:00 2001 From: David Cantu Date: Wed, 25 Mar 2020 19:54:36 -0700 Subject: [PATCH 31/32] Document TranscodingWriteStream.MaxByteBufferSize --- .../src/System/Net/Http/Json/TranscodingWriteStream.cs | 1 + 1 file changed, 1 insertion(+) 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 index dc0140b05a2118..1135ad4bc1142b 100644 --- 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 @@ -17,6 +17,7 @@ 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; From afd6d59b4b17ee1b1dc100615b7e9293f0cc1ecf Mon Sep 17 00:00:00 2001 From: David Cantu Date: Wed, 25 Mar 2020 21:06:32 -0700 Subject: [PATCH 32/32] Replace MediaTypeHeaderValue.Parse with .ctor since it performs better. --- .../src/System/Net/Http/Json/JsonContent.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 21b18581b83e2a..a84d375b137c1f 100644 --- 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 @@ -18,7 +18,7 @@ public sealed partial class JsonContent : HttpContent internal const string JsonType = "application"; internal const string JsonSubtype = "json"; private static MediaTypeHeaderValue DefaultMediaType - => MediaTypeHeaderValue.Parse(string.Format("{0}; charset={1}", JsonMediaType, Encoding.UTF8.WebName)); + => new MediaTypeHeaderValue(JsonMediaType) { CharSet = "utf-8" }; internal static JsonSerializerOptions DefaultSerializerOptions => new JsonSerializerOptions { PropertyNameCaseInsensitive = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase };