From da6d3b895c5e33165732ba6a7f3153742a4deed6 Mon Sep 17 00:00:00 2001 From: David Cantu Date: Wed, 11 Mar 2020 12:44:01 -0700 Subject: [PATCH 01/25] Add System.Net.Http.Json related projects --- .../Directory.Build.props | 8 ++ .../System.Net.Http.Json.sln | 53 +++++++ .../ref/Configurations.props | 7 + .../ref/System.Net.Http.Json.cs | 91 ++++++++++++ .../ref/System.Net.Http.Json.csproj | 22 +++ .../src/Configurations.props | 7 + .../src/System.Net.Http.Json.csproj | 19 +++ .../Http/Json/HttpClientJsonExtensions.Get.cs | 132 ++++++++++++++++++ .../Json/HttpClientJsonExtensions.Post.cs | 24 ++++ .../Http/Json/HttpContentJsonExtensions.cs | 37 +++++ .../src/System/Net/Http/Json/JsonContent.cs | 87 ++++++++++++ .../FunctionalTests/Configurations.props | 7 + .../HttpClientJsonExtensionsTests.cs | 27 ++++ ...stem.Net.Http.Json.Functional.Tests.csproj | 49 +++++++ 14 files changed, 570 insertions(+) create mode 100644 src/System.Net.Http.Json/Directory.Build.props create mode 100644 src/System.Net.Http.Json/System.Net.Http.Json.sln create mode 100644 src/System.Net.Http.Json/ref/Configurations.props create mode 100644 src/System.Net.Http.Json/ref/System.Net.Http.Json.cs create mode 100644 src/System.Net.Http.Json/ref/System.Net.Http.Json.csproj create mode 100644 src/System.Net.Http.Json/src/Configurations.props create mode 100644 src/System.Net.Http.Json/src/System.Net.Http.Json.csproj create mode 100644 src/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Get.cs create mode 100644 src/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Post.cs create mode 100644 src/System.Net.Http.Json/src/System/Net/Http/Json/HttpContentJsonExtensions.cs create mode 100644 src/System.Net.Http.Json/src/System/Net/Http/Json/JsonContent.cs create mode 100644 src/System.Net.Http.Json/tests/FunctionalTests/Configurations.props create mode 100644 src/System.Net.Http.Json/tests/FunctionalTests/HttpClientJsonExtensionsTests.cs create mode 100644 src/System.Net.Http.Json/tests/FunctionalTests/System.Net.Http.Json.Functional.Tests.csproj diff --git a/src/System.Net.Http.Json/Directory.Build.props b/src/System.Net.Http.Json/Directory.Build.props new file mode 100644 index 000000000000..39a7a3fef456 --- /dev/null +++ b/src/System.Net.Http.Json/Directory.Build.props @@ -0,0 +1,8 @@ + + + + Microsoft + 3.2.0.0 + 3.2.0 + + \ No newline at end of file diff --git a/src/System.Net.Http.Json/System.Net.Http.Json.sln b/src/System.Net.Http.Json/System.Net.Http.Json.sln new file mode 100644 index 000000000000..cb5094d589ea --- /dev/null +++ b/src/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/System.Net.Http.Json/ref/Configurations.props b/src/System.Net.Http.Json/ref/Configurations.props new file mode 100644 index 000000000000..665a8d43a6cc --- /dev/null +++ b/src/System.Net.Http.Json/ref/Configurations.props @@ -0,0 +1,7 @@ + + + + netstandard; + + + \ No newline at end of file diff --git a/src/System.Net.Http.Json/ref/System.Net.Http.Json.cs b/src/System.Net.Http.Json/ref/System.Net.Http.Json.cs new file mode 100644 index 000000000000..3d88de9f49cf --- /dev/null +++ b/src/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/System.Net.Http.Json/ref/System.Net.Http.Json.csproj b/src/System.Net.Http.Json/ref/System.Net.Http.Json.csproj new file mode 100644 index 000000000000..52675147e37e --- /dev/null +++ b/src/System.Net.Http.Json/ref/System.Net.Http.Json.csproj @@ -0,0 +1,22 @@ + + + netstandard-Debug;netstandard-Release + enable + + + + + + + + + + diff --git a/src/System.Net.Http.Json/src/Configurations.props b/src/System.Net.Http.Json/src/Configurations.props new file mode 100644 index 000000000000..beb53a974e68 --- /dev/null +++ b/src/System.Net.Http.Json/src/Configurations.props @@ -0,0 +1,7 @@ + + + + netcoreapp; + + + \ No newline at end of file diff --git a/src/System.Net.Http.Json/src/System.Net.Http.Json.csproj b/src/System.Net.Http.Json/src/System.Net.Http.Json.csproj new file mode 100644 index 000000000000..98e225f2e302 --- /dev/null +++ b/src/System.Net.Http.Json/src/System.Net.Http.Json.csproj @@ -0,0 +1,19 @@ + + + netcoreapp-Debug;netcoreapp-Release + enable + + + + + + + + + + + + + + + diff --git a/src/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Get.cs b/src/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Get.cs new file mode 100644 index 000000000000..af7ac63733c5 --- /dev/null +++ b/src/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/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Post.cs b/src/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Post.cs new file mode 100644 index 000000000000..cb790e83de41 --- /dev/null +++ b/src/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/System.Net.Http.Json/src/System/Net/Http/Json/HttpContentJsonExtensions.cs b/src/System.Net.Http.Json/src/System/Net/Http/Json/HttpContentJsonExtensions.cs new file mode 100644 index 000000000000..081bba344129 --- /dev/null +++ b/src/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/System.Net.Http.Json/src/System/Net/Http/Json/JsonContent.cs b/src/System.Net.Http.Json/src/System/Net/Http/Json/JsonContent.cs new file mode 100644 index 000000000000..376dfb986af8 --- /dev/null +++ b/src/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/System.Net.Http.Json/tests/FunctionalTests/Configurations.props b/src/System.Net.Http.Json/tests/FunctionalTests/Configurations.props new file mode 100644 index 000000000000..beb53a974e68 --- /dev/null +++ b/src/System.Net.Http.Json/tests/FunctionalTests/Configurations.props @@ -0,0 +1,7 @@ + + + + netcoreapp; + + + \ No newline at end of file diff --git a/src/System.Net.Http.Json/tests/FunctionalTests/HttpClientJsonExtensionsTests.cs b/src/System.Net.Http.Json/tests/FunctionalTests/HttpClientJsonExtensionsTests.cs new file mode 100644 index 000000000000..18e40917bcc4 --- /dev/null +++ b/src/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/System.Net.Http.Json/tests/FunctionalTests/System.Net.Http.Json.Functional.Tests.csproj b/src/System.Net.Http.Json/tests/FunctionalTests/System.Net.Http.Json.Functional.Tests.csproj new file mode 100644 index 000000000000..cb610a40e62c --- /dev/null +++ b/src/System.Net.Http.Json/tests/FunctionalTests/System.Net.Http.Json.Functional.Tests.csproj @@ -0,0 +1,49 @@ + + + netcoreapp-Debug;netcoreapp-Release + + + + + + + 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 d1e22bf1af2ef375b04e0c0f394a47c799b3f0d0 Mon Sep 17 00:00:00 2001 From: David Cantu Date: Wed, 11 Mar 2020 13:04:30 -0700 Subject: [PATCH 02/25] 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/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Helpers.cs create mode 100644 src/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Put.cs create mode 100644 src/System.Net.Http.Json/tests/FunctionalTests/HttpClientJsonExtensionsTests.Formatter.cs create mode 100644 src/System.Net.Http.Json/tests/FunctionalTests/HttpContentJsonExtensionsTests.cs create mode 100644 src/System.Net.Http.Json/tests/FunctionalTests/JsonContentTests.cs create mode 100644 src/System.Net.Http.Json/tests/FunctionalTests/PErson.cs diff --git a/src/System.Net.Http.Json/ref/System.Net.Http.Json.cs b/src/System.Net.Http.Json/ref/System.Net.Http.Json.cs index 3d88de9f49cf..c6139e321fe7 100644 --- a/src/System.Net.Http.Json/ref/System.Net.Http.Json.cs +++ b/src/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/System.Net.Http.Json/src/System.Net.Http.Json.csproj b/src/System.Net.Http.Json/src/System.Net.Http.Json.csproj index 98e225f2e302..dee557785d74 100644 --- a/src/System.Net.Http.Json/src/System.Net.Http.Json.csproj +++ b/src/System.Net.Http.Json/src/System.Net.Http.Json.csproj @@ -5,7 +5,9 @@ + + diff --git a/src/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Get.cs b/src/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Get.cs index af7ac63733c5..90a626c92b07 100644 --- a/src/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Get.cs +++ b/src/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/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Helpers.cs b/src/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Helpers.cs new file mode 100644 index 000000000000..5701e5f5644e --- /dev/null +++ b/src/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/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Post.cs b/src/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Post.cs index cb790e83de41..e12829f73e55 100644 --- a/src/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Post.cs +++ b/src/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/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Put.cs b/src/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Put.cs new file mode 100644 index 000000000000..0e82bd3ac187 --- /dev/null +++ b/src/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/System.Net.Http.Json/src/System/Net/Http/Json/HttpContentJsonExtensions.cs b/src/System.Net.Http.Json/src/System/Net/Http/Json/HttpContentJsonExtensions.cs index 081bba344129..c8f33f1f12d4 100644 --- a/src/System.Net.Http.Json/src/System/Net/Http/Json/HttpContentJsonExtensions.cs +++ b/src/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/System.Net.Http.Json/src/System/Net/Http/Json/JsonContent.cs b/src/System.Net.Http.Json/src/System/Net/Http/Json/JsonContent.cs index 376dfb986af8..a6f98f17331a 100644 --- a/src/System.Net.Http.Json/src/System/Net/Http/Json/JsonContent.cs +++ b/src/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/System.Net.Http.Json/tests/FunctionalTests/HttpClientJsonExtensionsTests.Formatter.cs b/src/System.Net.Http.Json/tests/FunctionalTests/HttpClientJsonExtensionsTests.Formatter.cs new file mode 100644 index 000000000000..231b3a191309 --- /dev/null +++ b/src/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/System.Net.Http.Json/tests/FunctionalTests/HttpClientJsonExtensionsTests.cs b/src/System.Net.Http.Json/tests/FunctionalTests/HttpClientJsonExtensionsTests.cs index 18e40917bcc4..02017c9eb36b 100644 --- a/src/System.Net.Http.Json/tests/FunctionalTests/HttpClientJsonExtensionsTests.cs +++ b/src/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/System.Net.Http.Json/tests/FunctionalTests/HttpContentJsonExtensionsTests.cs b/src/System.Net.Http.Json/tests/FunctionalTests/HttpContentJsonExtensionsTests.cs new file mode 100644 index 000000000000..25608fa6b025 --- /dev/null +++ b/src/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/System.Net.Http.Json/tests/FunctionalTests/JsonContentTests.cs b/src/System.Net.Http.Json/tests/FunctionalTests/JsonContentTests.cs new file mode 100644 index 000000000000..27fd497401c3 --- /dev/null +++ b/src/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/System.Net.Http.Json/tests/FunctionalTests/PErson.cs b/src/System.Net.Http.Json/tests/FunctionalTests/PErson.cs new file mode 100644 index 000000000000..a8595866c89c --- /dev/null +++ b/src/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/System.Net.Http.Json/tests/FunctionalTests/System.Net.Http.Json.Functional.Tests.csproj b/src/System.Net.Http.Json/tests/FunctionalTests/System.Net.Http.Json.Functional.Tests.csproj index cb610a40e62c..6afeb24dad42 100644 --- a/src/System.Net.Http.Json/tests/FunctionalTests/System.Net.Http.Json.Functional.Tests.csproj +++ b/src/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 58f2ac01fb54cb2691266d5cd896c383a83f22a0 Mon Sep 17 00:00:00 2001 From: David Cantu Date: Wed, 11 Mar 2020 13:09:27 -0700 Subject: [PATCH 03/25] Rename PErson.cs to Person.cs --- .../tests/FunctionalTests/{PErson.cs => Person.cs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/System.Net.Http.Json/tests/FunctionalTests/{PErson.cs => Person.cs} (100%) diff --git a/src/System.Net.Http.Json/tests/FunctionalTests/PErson.cs b/src/System.Net.Http.Json/tests/FunctionalTests/Person.cs similarity index 100% rename from src/System.Net.Http.Json/tests/FunctionalTests/PErson.cs rename to src/System.Net.Http.Json/tests/FunctionalTests/Person.cs From 45a84bbf80b28c15f633390dd723aef2e7007a65 Mon Sep 17 00:00:00 2001 From: Jose Perez Rodriguez Date: Wed, 11 Mar 2020 13:23:15 -0700 Subject: [PATCH 04/25] Adding package for System.Net.Http.Json library --- .../packageIndex.json | 8 +++++++- pkg/descriptions.json | 7 +++++++ .../pkg/System.Net.Http.Json.pkgproj | 10 ++++++++++ src/System.Net.Http.Json/src/Configurations.props | 2 +- .../src/System.Net.Http.Json.csproj | 3 ++- .../System/Net/Http/Json/HttpContentJsonExtensions.cs | 2 +- 6 files changed, 28 insertions(+), 4 deletions(-) create mode 100644 src/System.Net.Http.Json/pkg/System.Net.Http.Json.pkgproj diff --git a/pkg/Microsoft.Private.PackageBaseline/packageIndex.json b/pkg/Microsoft.Private.PackageBaseline/packageIndex.json index 8db87521ca7e..f3e2850bd93c 100644 --- a/pkg/Microsoft.Private.PackageBaseline/packageIndex.json +++ b/pkg/Microsoft.Private.PackageBaseline/packageIndex.json @@ -2992,6 +2992,12 @@ "4.2.0.0": "4.4.0" } }, + "System.Net.Http.Json": { + "InboxOn": {}, + "AssemblyVersionInPackageVersion": { + "3.2.0.0": "3.2.0" + } + }, "System.Net.Http.Rtc": { "StableVersions": [ "4.0.0", @@ -6591,4 +6597,4 @@ "System.Xml.XDocument" ] } -} +} \ No newline at end of file diff --git a/pkg/descriptions.json b/pkg/descriptions.json index bb20251c2803..8583eefead19 100644 --- a/pkg/descriptions.json +++ b/pkg/descriptions.json @@ -1080,6 +1080,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.Http.Rtc", "Description": "Provides the System.Net.Http.RtcRequestFactory class, which creates HTTP requests for use with the Real-Time-Communications (RTC) background notification infrastructure.", diff --git a/src/System.Net.Http.Json/pkg/System.Net.Http.Json.pkgproj b/src/System.Net.Http.Json/pkg/System.Net.Http.Json.pkgproj new file mode 100644 index 000000000000..eba4cc39e9f9 --- /dev/null +++ b/src/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/System.Net.Http.Json/src/Configurations.props b/src/System.Net.Http.Json/src/Configurations.props index beb53a974e68..665a8d43a6cc 100644 --- a/src/System.Net.Http.Json/src/Configurations.props +++ b/src/System.Net.Http.Json/src/Configurations.props @@ -1,7 +1,7 @@ - netcoreapp; + netstandard; \ No newline at end of file diff --git a/src/System.Net.Http.Json/src/System.Net.Http.Json.csproj b/src/System.Net.Http.Json/src/System.Net.Http.Json.csproj index dee557785d74..c5529615afa9 100644 --- a/src/System.Net.Http.Json/src/System.Net.Http.Json.csproj +++ b/src/System.Net.Http.Json/src/System.Net.Http.Json.csproj @@ -1,6 +1,6 @@  - netcoreapp-Debug;netcoreapp-Release + netstandard-Debug;netstandard-Release enable @@ -17,5 +17,6 @@ + diff --git a/src/System.Net.Http.Json/src/System/Net/Http/Json/HttpContentJsonExtensions.cs b/src/System.Net.Http.Json/src/System/Net/Http/Json/HttpContentJsonExtensions.cs index c8f33f1f12d4..2c2a85fbb032 100644 --- a/src/System.Net.Http.Json/src/System/Net/Http/Json/HttpContentJsonExtensions.cs +++ b/src/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) From d3be353e6869863e61e2802bbdcfacaceebf4f72 Mon Sep 17 00:00:00 2001 From: David Cantu Date: Wed, 11 Mar 2020 13:55:50 -0700 Subject: [PATCH 05/25] 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/System.Net.Http.Json/ref/System.Net.Http.Json.csproj b/src/System.Net.Http.Json/ref/System.Net.Http.Json.csproj index 52675147e37e..7ccd4a399a31 100644 --- a/src/System.Net.Http.Json/ref/System.Net.Http.Json.csproj +++ b/src/System.Net.Http.Json/ref/System.Net.Http.Json.csproj @@ -9,14 +9,4 @@ - - diff --git a/src/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Get.cs b/src/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Get.cs index 90a626c92b07..a0f77ba51b54 100644 --- a/src/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Get.cs +++ b/src/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/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Put.cs b/src/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Put.cs index 0e82bd3ac187..bfa5f3f4ed62 100644 --- a/src/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Put.cs +++ b/src/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/System.Net.Http.Json/src/System/Net/Http/Json/HttpContentJsonExtensions.cs b/src/System.Net.Http.Json/src/System/Net/Http/Json/HttpContentJsonExtensions.cs index 2c2a85fbb032..527ae391a75c 100644 --- a/src/System.Net.Http.Json/src/System/Net/Http/Json/HttpContentJsonExtensions.cs +++ b/src/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/System.Net.Http.Json/src/System/Net/Http/Json/JsonContent.cs b/src/System.Net.Http.Json/src/System/Net/Http/Json/JsonContent.cs index a6f98f17331a..a2e248a4cfd9 100644 --- a/src/System.Net.Http.Json/src/System/Net/Http/Json/JsonContent.cs +++ b/src/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/System.Net.Http.Json/tests/FunctionalTests/HttpClientJsonExtensionsTests.cs b/src/System.Net.Http.Json/tests/FunctionalTests/HttpClientJsonExtensionsTests.cs index 02017c9eb36b..dd16db2d040a 100644 --- a/src/System.Net.Http.Json/tests/FunctionalTests/HttpClientJsonExtensionsTests.cs +++ b/src/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/System.Net.Http.Json/tests/FunctionalTests/HttpContentJsonExtensionsTests.cs b/src/System.Net.Http.Json/tests/FunctionalTests/HttpContentJsonExtensionsTests.cs index 25608fa6b025..737d42593469 100644 --- a/src/System.Net.Http.Json/tests/FunctionalTests/HttpContentJsonExtensionsTests.cs +++ b/src/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/System.Net.Http.Json/tests/FunctionalTests/JsonContentTests.cs b/src/System.Net.Http.Json/tests/FunctionalTests/JsonContentTests.cs index 27fd497401c3..08d2d8200987 100644 --- a/src/System.Net.Http.Json/tests/FunctionalTests/JsonContentTests.cs +++ b/src/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/System.Net.Http.Json/tests/FunctionalTests/System.Net.Http.Json.Functional.Tests.csproj b/src/System.Net.Http.Json/tests/FunctionalTests/System.Net.Http.Json.Functional.Tests.csproj index 6afeb24dad42..1dc0cc8176f2 100644 --- a/src/System.Net.Http.Json/tests/FunctionalTests/System.Net.Http.Json.Functional.Tests.csproj +++ b/src/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 384c0bf5557bf649b8b8b46ae121b4b188ef85fe Mon Sep 17 00:00:00 2001 From: David Cantu Date: Wed, 11 Mar 2020 14:00:20 -0700 Subject: [PATCH 06/25] 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/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Helpers.cs delete mode 100644 src/System.Net.Http.Json/tests/FunctionalTests/HttpClientJsonExtensionsTests.Formatter.cs diff --git a/src/System.Net.Http.Json/src/System.Net.Http.Json.csproj b/src/System.Net.Http.Json/src/System.Net.Http.Json.csproj index c5529615afa9..99a3e7340885 100644 --- a/src/System.Net.Http.Json/src/System.Net.Http.Json.csproj +++ b/src/System.Net.Http.Json/src/System.Net.Http.Json.csproj @@ -5,7 +5,6 @@ - diff --git a/src/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Get.cs b/src/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Get.cs index a0f77ba51b54..b602f48292a8 100644 --- a/src/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Get.cs +++ b/src/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/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Helpers.cs b/src/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Helpers.cs deleted file mode 100644 index 5701e5f5644e..000000000000 --- a/src/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/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Post.cs b/src/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Post.cs index e12829f73e55..895464f396de 100644 --- a/src/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Post.cs +++ b/src/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/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Put.cs b/src/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Put.cs index bfa5f3f4ed62..7c9268b030d5 100644 --- a/src/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Put.cs +++ b/src/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/System.Net.Http.Json/src/System/Net/Http/Json/HttpContentJsonExtensions.cs b/src/System.Net.Http.Json/src/System/Net/Http/Json/HttpContentJsonExtensions.cs index 527ae391a75c..2c7f746797c5 100644 --- a/src/System.Net.Http.Json/src/System/Net/Http/Json/HttpContentJsonExtensions.cs +++ b/src/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/System.Net.Http.Json/src/System/Net/Http/Json/JsonContent.cs b/src/System.Net.Http.Json/src/System/Net/Http/Json/JsonContent.cs index a2e248a4cfd9..8b1404ad5ffd 100644 --- a/src/System.Net.Http.Json/src/System/Net/Http/Json/JsonContent.cs +++ b/src/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/System.Net.Http.Json/tests/FunctionalTests/HttpClientJsonExtensionsTests.Formatter.cs b/src/System.Net.Http.Json/tests/FunctionalTests/HttpClientJsonExtensionsTests.Formatter.cs deleted file mode 100644 index 231b3a191309..000000000000 --- a/src/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/System.Net.Http.Json/tests/FunctionalTests/HttpClientJsonExtensionsTests.cs b/src/System.Net.Http.Json/tests/FunctionalTests/HttpClientJsonExtensionsTests.cs index dd16db2d040a..4933afa362df 100644 --- a/src/System.Net.Http.Json/tests/FunctionalTests/HttpClientJsonExtensionsTests.cs +++ b/src/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/System.Net.Http.Json/tests/FunctionalTests/HttpContentJsonExtensionsTests.cs b/src/System.Net.Http.Json/tests/FunctionalTests/HttpContentJsonExtensionsTests.cs index 737d42593469..a8f39bc9d6d4 100644 --- a/src/System.Net.Http.Json/tests/FunctionalTests/HttpContentJsonExtensionsTests.cs +++ b/src/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/System.Net.Http.Json/tests/FunctionalTests/System.Net.Http.Json.Functional.Tests.csproj b/src/System.Net.Http.Json/tests/FunctionalTests/System.Net.Http.Json.Functional.Tests.csproj index 1dc0cc8176f2..576863342406 100644 --- a/src/System.Net.Http.Json/tests/FunctionalTests/System.Net.Http.Json.Functional.Tests.csproj +++ b/src/System.Net.Http.Json/tests/FunctionalTests/System.Net.Http.Json.Functional.Tests.csproj @@ -33,7 +33,6 @@ Common\System\Threading\Tasks\TaskTimeoutExtensions.cs - From 08f9910c94b029bde67784e38b91fd522b928162 Mon Sep 17 00:00:00 2001 From: David Cantu Date: Wed, 11 Mar 2020 14:03:14 -0700 Subject: [PATCH 07/25] Add draft description and common types to NuGet pkg. --- pkg/descriptions.json | 6 ++++-- src/packages.builds | 3 +++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/pkg/descriptions.json b/pkg/descriptions.json index 8583eefead19..22dc8be2bfd4 100644 --- a/pkg/descriptions.json +++ b/pkg/descriptions.json @@ -1082,9 +1082,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" ] }, { diff --git a/src/packages.builds b/src/packages.builds index ed26da8a8786..2fc395adfe9d 100644 --- a/src/packages.builds +++ b/src/packages.builds @@ -32,6 +32,9 @@ $(AdditionalProperties) + + $(AdditionalProperties) + From d6d75879305df9c1c2836b66d0d4f198f6dc8c74 Mon Sep 17 00:00:00 2001 From: David Cantu Date: Wed, 11 Mar 2020 15:50:22 -0700 Subject: [PATCH 08/25] 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/System.Net.Http.Json/ref/System.Net.Http.Json.cs b/src/System.Net.Http.Json/ref/System.Net.Http.Json.cs index c6139e321fe7..56a7ddb0427a 100644 --- a/src/System.Net.Http.Json/ref/System.Net.Http.Json.cs +++ b/src/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 c19f2322f68aa52978d869757aae4a5e8e6e5084 Mon Sep 17 00:00:00 2001 From: Jose Perez Rodriguez Date: Thu, 12 Mar 2020 11:06:44 -0700 Subject: [PATCH 09/25] Add missing configuration to test project --- .../tests/FunctionalTests/Configurations.props | 1 + .../System.Net.Http.Json.Functional.Tests.csproj | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/System.Net.Http.Json/tests/FunctionalTests/Configurations.props b/src/System.Net.Http.Json/tests/FunctionalTests/Configurations.props index beb53a974e68..e2b34f602200 100644 --- a/src/System.Net.Http.Json/tests/FunctionalTests/Configurations.props +++ b/src/System.Net.Http.Json/tests/FunctionalTests/Configurations.props @@ -2,6 +2,7 @@ netcoreapp; + netfx; \ No newline at end of file diff --git a/src/System.Net.Http.Json/tests/FunctionalTests/System.Net.Http.Json.Functional.Tests.csproj b/src/System.Net.Http.Json/tests/FunctionalTests/System.Net.Http.Json.Functional.Tests.csproj index 576863342406..9f40d34cfcec 100644 --- a/src/System.Net.Http.Json/tests/FunctionalTests/System.Net.Http.Json.Functional.Tests.csproj +++ b/src/System.Net.Http.Json/tests/FunctionalTests/System.Net.Http.Json.Functional.Tests.csproj @@ -1,6 +1,6 @@  - netcoreapp-Debug;netcoreapp-Release + netcoreapp-Debug;netcoreapp-Release;netfx-Debug;netfx-Release From 14aa391d6ec2b1c50e5f4b52719c1c40e4a007c8 Mon Sep 17 00:00:00 2001 From: David Cantu Date: Thu, 12 Mar 2020 12:03:04 -0700 Subject: [PATCH 10/25] Addressing most of the feedback. TODO: Implement transcoding for non-UTF-8 content. --- pkg/descriptions.json | 2 +- .../src/Resources/Strings.resx | 129 ++++++++++++++++++ .../src/System.Net.Http.Json.csproj | 1 + .../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 | 50 ++++--- .../HttpClientJsonExtensionsTests.cs | 29 ++++ .../HttpContentJsonExtensionsTests.cs | 46 ++++--- .../tests/FunctionalTests/JsonContentTests.cs | 63 ++++----- 11 files changed, 280 insertions(+), 130 deletions(-) create mode 100644 src/System.Net.Http.Json/src/Resources/Strings.resx diff --git a/pkg/descriptions.json b/pkg/descriptions.json index 22dc8be2bfd4..20c2475e0ab2 100644 --- a/pkg/descriptions.json +++ b/pkg/descriptions.json @@ -1082,7 +1082,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", diff --git a/src/System.Net.Http.Json/src/Resources/Strings.resx b/src/System.Net.Http.Json/src/Resources/Strings.resx new file mode 100644 index 000000000000..5f592a90a4f2 --- /dev/null +++ b/src/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/System.Net.Http.Json/src/System.Net.Http.Json.csproj b/src/System.Net.Http.Json/src/System.Net.Http.Json.csproj index 99a3e7340885..98d048c04c20 100644 --- a/src/System.Net.Http.Json/src/System.Net.Http.Json.csproj +++ b/src/System.Net.Http.Json/src/System.Net.Http.Json.csproj @@ -16,6 +16,7 @@ + diff --git a/src/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Get.cs b/src/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Get.cs index b602f48292a8..50457640532d 100644 --- a/src/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Get.cs +++ b/src/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/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Post.cs b/src/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Post.cs index 895464f396de..5600565123f6 100644 --- a/src/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Post.cs +++ b/src/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/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Put.cs b/src/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Put.cs index 7c9268b030d5..55c226c37931 100644 --- a/src/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Put.cs +++ b/src/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/System.Net.Http.Json/src/System/Net/Http/Json/HttpContentJsonExtensions.cs b/src/System.Net.Http.Json/src/System/Net/Http/Json/HttpContentJsonExtensions.cs index 2c7f746797c5..2fa6e5005263 100644 --- a/src/System.Net.Http.Json/src/System/Net/Http/Json/HttpContentJsonExtensions.cs +++ b/src/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/System.Net.Http.Json/src/System/Net/Http/Json/JsonContent.cs b/src/System.Net.Http.Json/src/System/Net/Http/Json/JsonContent.cs index 8b1404ad5ffd..89fb9a9b83bb 100644 --- a/src/System.Net.Http.Json/src/System/Net/Http/Json/JsonContent.cs +++ b/src/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,47 @@ 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); + => JsonSerializer.SerializeAsync(stream, Value, ObjectType, _jsonSerializerOptions); protected override bool TryComputeLength(out long length) { - length = _count; - return true; + length = 0; + return false; } } } diff --git a/src/System.Net.Http.Json/tests/FunctionalTests/HttpClientJsonExtensionsTests.cs b/src/System.Net.Http.Json/tests/FunctionalTests/HttpClientJsonExtensionsTests.cs index 4933afa362df..2e17003a738f 100644 --- a/src/System.Net.Http.Json/tests/FunctionalTests/HttpClientJsonExtensionsTests.cs +++ b/src/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/System.Net.Http.Json/tests/FunctionalTests/HttpContentJsonExtensionsTests.cs b/src/System.Net.Http.Json/tests/FunctionalTests/HttpContentJsonExtensionsTests.cs index a8f39bc9d6d4..001796abaae2 100644 --- a/src/System.Net.Http.Json/tests/FunctionalTests/HttpContentJsonExtensionsTests.cs +++ b/src/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/System.Net.Http.Json/tests/FunctionalTests/JsonContentTests.cs b/src/System.Net.Http.Json/tests/FunctionalTests/JsonContentTests.cs index 08d2d8200987..b87c53641eb7 100644 --- a/src/System.Net.Http.Json/tests/FunctionalTests/JsonContentTests.cs +++ b/src/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)); } } } From a8c59d29f91007ee4a8cb02375dc4af883a35e93 Mon Sep 17 00:00:00 2001 From: David Cantu Date: Fri, 13 Mar 2020 11:42:27 -0700 Subject: [PATCH 11/25] Fix csproj formatting and use open StrongNameKeyId --- src/System.Net.Http.Json/Directory.Build.props | 2 +- .../System.Net.Http.Json.Functional.Tests.csproj | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/System.Net.Http.Json/Directory.Build.props b/src/System.Net.Http.Json/Directory.Build.props index 39a7a3fef456..1b98b3bb8e91 100644 --- a/src/System.Net.Http.Json/Directory.Build.props +++ b/src/System.Net.Http.Json/Directory.Build.props @@ -1,7 +1,7 @@  - Microsoft + Open 3.2.0.0 3.2.0 diff --git a/src/System.Net.Http.Json/tests/FunctionalTests/System.Net.Http.Json.Functional.Tests.csproj b/src/System.Net.Http.Json/tests/FunctionalTests/System.Net.Http.Json.Functional.Tests.csproj index 9f40d34cfcec..c30e6fa82b48 100644 --- a/src/System.Net.Http.Json/tests/FunctionalTests/System.Net.Http.Json.Functional.Tests.csproj +++ b/src/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 d2f858cf05f5807bea22154a6adeb3fcbb64e813 Mon Sep 17 00:00:00 2001 From: Jose Perez Rodriguez Date: Mon, 16 Mar 2020 15:43:40 -0700 Subject: [PATCH 12/25] Changing prereleaselabel and ensuring the package won't go stable in the next release --- src/System.Net.Http.Json/Directory.Build.props | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/System.Net.Http.Json/Directory.Build.props b/src/System.Net.Http.Json/Directory.Build.props index 1b98b3bb8e91..28b80d429b62 100644 --- a/src/System.Net.Http.Json/Directory.Build.props +++ b/src/System.Net.Http.Json/Directory.Build.props @@ -4,5 +4,11 @@ Open 3.2.0.0 3.2.0 + + + preview + true + false \ No newline at end of file From 686e18ed20045b91ca647f68ab8403728980caf3 Mon Sep 17 00:00:00 2001 From: David Cantu Date: Mon, 16 Mar 2020 16:53:36 -0700 Subject: [PATCH 13/25] 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/System.Net.Http.Json/ref/System.Net.Http.Json.cs b/src/System.Net.Http.Json/ref/System.Net.Http.Json.cs index 56a7ddb0427a..fd69ed09cb60 100644 --- a/src/System.Net.Http.Json/ref/System.Net.Http.Json.cs +++ b/src/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/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Get.cs b/src/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Get.cs index 50457640532d..289fd5c73479 100644 --- a/src/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Get.cs +++ b/src/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/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Post.cs b/src/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Post.cs index 5600565123f6..5b188baae4fa 100644 --- a/src/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Post.cs +++ b/src/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/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Put.cs b/src/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Put.cs index 55c226c37931..a7e59a66d4d1 100644 --- a/src/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Put.cs +++ b/src/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/System.Net.Http.Json/src/System/Net/Http/Json/HttpContentJsonExtensions.cs b/src/System.Net.Http.Json/src/System/Net/Http/Json/HttpContentJsonExtensions.cs index 2fa6e5005263..d101f8c83876 100644 --- a/src/System.Net.Http.Json/src/System/Net/Http/Json/HttpContentJsonExtensions.cs +++ b/src/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/System.Net.Http.Json/src/System/Net/Http/Json/JsonContent.cs b/src/System.Net.Http.Json/src/System/Net/Http/Json/JsonContent.cs index 89fb9a9b83bb..3235835e2e39 100644 --- a/src/System.Net.Http.Json/src/System/Net/Http/Json/JsonContent.cs +++ b/src/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) => JsonSerializer.SerializeAsync(stream, Value, ObjectType, _jsonSerializerOptions); diff --git a/src/System.Net.Http.Json/tests/FunctionalTests/HttpContentJsonExtensionsTests.cs b/src/System.Net.Http.Json/tests/FunctionalTests/HttpContentJsonExtensionsTests.cs index 001796abaae2..47fa5991a7c8 100644 --- a/src/System.Net.Http.Json/tests/FunctionalTests/HttpContentJsonExtensionsTests.cs +++ b/src/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/System.Net.Http.Json/tests/FunctionalTests/JsonContentTests.cs b/src/System.Net.Http.Json/tests/FunctionalTests/JsonContentTests.cs index b87c53641eb7..2ba52806583c 100644 --- a/src/System.Net.Http.Json/tests/FunctionalTests/JsonContentTests.cs +++ b/src/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 283818d8dd29e276ef2765c7db53a900ba41152a Mon Sep 17 00:00:00 2001 From: David Cantu Date: Mon, 16 Mar 2020 17:00:54 -0700 Subject: [PATCH 14/25] 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/TranscodingReadStream.cs | 231 ++++++++++++++++++ .../Net/Http/Json/TranscodingWriteStream.cs | 183 ++++++++++++++ 5 files changed, 489 insertions(+), 30 deletions(-) create mode 100644 src/System.Net.Http.Json/src/System/Net/Http/Json/TranscodingReadStream.cs create mode 100644 src/System.Net.Http.Json/src/System/Net/Http/Json/TranscodingWriteStream.cs diff --git a/src/System.Net.Http.Json/src/System.Net.Http.Json.csproj b/src/System.Net.Http.Json/src/System.Net.Http.Json.csproj index 98d048c04c20..4f6700bbdc7c 100644 --- a/src/System.Net.Http.Json/src/System.Net.Http.Json.csproj +++ b/src/System.Net.Http.Json/src/System.Net.Http.Json.csproj @@ -9,6 +9,8 @@ + + diff --git a/src/System.Net.Http.Json/src/System/Net/Http/Json/HttpContentJsonExtensions.cs b/src/System.Net.Http.Json/src/System/Net/Http/Json/HttpContentJsonExtensions.cs index d101f8c83876..03a19d74d122 100644 --- a/src/System.Net.Http.Json/src/System/Net/Http/Json/HttpContentJsonExtensions.cs +++ b/src/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/System.Net.Http.Json/src/System/Net/Http/Json/JsonContent.cs b/src/System.Net.Http.Json/src/System/Net/Http/Json/JsonContent.cs index 3235835e2e39..4481390ec4b9 100644 --- a/src/System.Net.Http.Json/src/System/Net/Http/Json/JsonContent.cs +++ b/src/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; } @@ -64,12 +55,55 @@ public static JsonContent Create(object? inputValue, Type inputType, MediaTypeHe } protected override Task SerializeToStreamAsync(Stream stream, TransportContext context) - => JsonSerializer.SerializeAsync(stream, Value, ObjectType, _jsonSerializerOptions); + => JsonSerializer.SerializeAsync(GetStreamToWriteTo(stream), Value, ObjectType, _jsonSerializerOptions); 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/System.Net.Http.Json/src/System/Net/Http/Json/TranscodingReadStream.cs b/src/System.Net.Http.Json/src/System/Net/Http/Json/TranscodingReadStream.cs new file mode 100644 index 000000000000..a0d44e253a15 --- /dev/null +++ b/src/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/System.Net.Http.Json/src/System/Net/Http/Json/TranscodingWriteStream.cs b/src/System.Net.Http.Json/src/System/Net/Http/Json/TranscodingWriteStream.cs new file mode 100644 index 000000000000..7a81e4821cb5 --- /dev/null +++ b/src/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 e97f7a2218312d98e0e1597ad0fc653bc6a619fa Mon Sep 17 00:00:00 2001 From: David Cantu Date: Mon, 16 Mar 2020 17:27:34 -0700 Subject: [PATCH 15/25] Port TranscodingReadWriteStream to netstandard2.0 --- .../src/Properties/InternalsVisibleTo.cs | 7 + .../src/System.Net.Http.Json.csproj | 3 + .../ArraySegmentExtensions.netstandard.cs | 24 ++ .../Net/Http/Json/TranscodingReadStream.cs | 43 ++- .../Net/Http/Json/TranscodingWriteStream.cs | 37 +-- ...stem.Net.Http.Json.Functional.Tests.csproj | 15 +- .../TranscodingReadStreamTests.cs | 260 ++++++++++++++++++ .../TranscodingWriteStreamTests.cs | 93 +++++++ 8 files changed, 438 insertions(+), 44 deletions(-) create mode 100644 src/System.Net.Http.Json/src/Properties/InternalsVisibleTo.cs create mode 100644 src/System.Net.Http.Json/src/System/Net/Http/Json/ArraySegmentExtensions.netstandard.cs create mode 100644 src/System.Net.Http.Json/tests/FunctionalTests/TranscodingReadStreamTests.cs create mode 100644 src/System.Net.Http.Json/tests/FunctionalTests/TranscodingWriteStreamTests.cs diff --git a/src/System.Net.Http.Json/src/Properties/InternalsVisibleTo.cs b/src/System.Net.Http.Json/src/Properties/InternalsVisibleTo.cs new file mode 100644 index 000000000000..ebfde5cba531 --- /dev/null +++ b/src/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/System.Net.Http.Json/src/System.Net.Http.Json.csproj b/src/System.Net.Http.Json/src/System.Net.Http.Json.csproj index 4f6700bbdc7c..a5bc901957ad 100644 --- a/src/System.Net.Http.Json/src/System.Net.Http.Json.csproj +++ b/src/System.Net.Http.Json/src/System.Net.Http.Json.csproj @@ -4,6 +4,7 @@ enable + @@ -11,6 +12,7 @@ + @@ -20,5 +22,6 @@ + diff --git a/src/System.Net.Http.Json/src/System/Net/Http/Json/ArraySegmentExtensions.netstandard.cs b/src/System.Net.Http.Json/src/System/Net/Http/Json/ArraySegmentExtensions.netstandard.cs new file mode 100644 index 000000000000..887281e5a013 --- /dev/null +++ b/src/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/System.Net.Http.Json/src/System/Net/Http/Json/TranscodingReadStream.cs b/src/System.Net.Http.Json/src/System/Net/Http/Json/TranscodingReadStream.cs index a0d44e253a15..63333319858d 100644 --- a/src/System.Net.Http.Json/src/System/Net/Http/Json/TranscodingReadStream.cs +++ b/src/System.Net.Http.Json/src/System/Net/Http/Json/TranscodingReadStream.cs @@ -6,7 +6,6 @@ using System.Diagnostics; using System.IO; using System.Text; -using System.Text.Unicode; using System.Threading; using System.Threading.Tasks; @@ -22,6 +21,8 @@ internal sealed class TranscodingReadStream : Stream private readonly Stream _stream; private readonly Decoder _decoder; + private readonly Encoder _encoder; + private ArraySegment _byteBuffer; private ArraySegment _charBuffer; private ArraySegment _overflowBuffer; @@ -54,6 +55,8 @@ public TranscodingReadStream(Stream input, Encoding sourceEncoding) count: 0); _decoder = sourceEncoding.GetDecoder(); + + _encoder = Encoding.UTF8.GetEncoder(); } public override bool CanRead => true; @@ -106,7 +109,18 @@ public override async Task ReadAsync(byte[] buffer, int offset, int count, await ReadInputChars(cancellationToken).ConfigureAwait(false); } - OperationStatus operationStatus = Utf8.FromUtf16(_charBuffer, readBuffer, out int charsRead, out int bytesWritten, isFinalBlock: false); + 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; + } _charBuffer = _charBuffer.Slice(charsRead); switch (operationStatus) @@ -121,7 +135,8 @@ 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. - Utf8.FromUtf16(_charBuffer, _overflowBuffer.Array, out int overFlowChars, out int overflowBytes, isFinalBlock: false); + _encoder.Convert(_charBuffer.Array, _charBuffer.Offset, _charBuffer.Count, _overflowBuffer.Array, _overflowBuffer.Offset, _overflowBuffer.Count, + false, out int overFlowChars, out int overflowBytes, out bool _); 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,19 +176,14 @@ 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 = + await _stream.ReadAsync(_byteBuffer.Array, _byteBuffer.Count, _byteBuffer.Array.Length, 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 _); + _decoder.Convert(_byteBuffer.Array, _byteBuffer.Offset, _byteBuffer.Count, _charBuffer.Array, 0, _charBuffer.Array.Length, + flush: readBytes == 0, out int bytesUsed, out int charsUsed, out _); Debug.Assert(_charBuffer.Array != null); @@ -218,12 +228,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/System.Net.Http.Json/src/System/Net/Http/Json/TranscodingWriteStream.cs b/src/System.Net.Http.Json/src/System/Net/Http/Json/TranscodingWriteStream.cs index 7a81e4821cb5..ac03a88dc954 100644 --- a/src/System.Net.Http.Json/src/System/Net/Http/Json/TranscodingWriteStream.cs +++ b/src/System.Net.Http.Json/src/System/Net/Http/Json/TranscodingWriteStream.cs @@ -82,15 +82,11 @@ private async Task WriteAsync( CancellationToken cancellationToken) { bool decoderCompleted = false; + while (!decoderCompleted) { - _decoder.Convert( - bufferSegment, - _charBuffer.AsSpan(_charsDecoded), - flush: false, - out int bytesDecoded, - out int charsDecoded, - out decoderCompleted); + _decoder.Convert(bufferSegment.Array, bufferSegment.Offset, bufferSegment.Count, _charBuffer, _charsDecoded, _charBuffer.Length - _charsDecoded, + flush: false, out int bytesDecoded, out int charsDecoded, out decoderCompleted); _charsDecoded += charsDecoded; bufferSegment = bufferSegment.Slice(bytesDecoded); @@ -110,15 +106,11 @@ private async Task WriteBufferAsync(CancellationToken cancellationToken) 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); + _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); + charsWritten += charsEncoded; } @@ -166,15 +158,10 @@ public async Task FinalWriteAsync(CancellationToken cancellationToken) 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); + _encoder.Convert(Array.Empty(), 0, 0, byteBuffer, 0, byteBuffer.Length, + flush: true, out _, out int bytesUsed, out encoderCompleted); + + await _stream.WriteAsync(byteBuffer, 0, bytesUsed, cancellationToken).ConfigureAwait(false); } ArrayPool.Shared.Return(byteBuffer); diff --git a/src/System.Net.Http.Json/tests/FunctionalTests/System.Net.Http.Json.Functional.Tests.csproj b/src/System.Net.Http.Json/tests/FunctionalTests/System.Net.Http.Json.Functional.Tests.csproj index c30e6fa82b48..aca738836a5f 100644 --- a/src/System.Net.Http.Json/tests/FunctionalTests/System.Net.Http.Json.Functional.Tests.csproj +++ b/src/System.Net.Http.Json/tests/FunctionalTests/System.Net.Http.Json.Functional.Tests.csproj @@ -3,12 +3,14 @@ netcoreapp-Debug;netcoreapp-Release;netfx-Debug;netfx-Release - - - + + + + + - + Common\System\Net\Capability.Security.cs @@ -37,4 +39,9 @@ Common\System\Threading\Tasks\TaskTimeoutExtensions.cs + + + + + diff --git a/src/System.Net.Http.Json/tests/FunctionalTests/TranscodingReadStreamTests.cs b/src/System.Net.Http.Json/tests/FunctionalTests/TranscodingReadStreamTests.cs new file mode 100644 index 000000000000..19ca65039a5c --- /dev/null +++ b/src/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/System.Net.Http.Json/tests/FunctionalTests/TranscodingWriteStreamTests.cs b/src/System.Net.Http.Json/tests/FunctionalTests/TranscodingWriteStreamTests.cs new file mode 100644 index 000000000000..e8f9057febd6 --- /dev/null +++ b/src/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 aab639f1e8b0b54a96e2a5e53d2a20788243fb0f Mon Sep 17 00:00:00 2001 From: Jose Perez Rodriguez Date: Tue, 17 Mar 2020 10:35:56 -0700 Subject: [PATCH 16/25] Fix package testing issue by reverting the inbox version of System.Text.Json in netcoreapp3.1 --- .../Microsoft.Private.CoreFx.NETCoreApp.pkgproj | 8 ++++++++ pkg/Microsoft.Private.PackageBaseline/packageIndex.json | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/pkg/Microsoft.Private.CoreFx.NETCoreApp/Microsoft.Private.CoreFx.NETCoreApp.pkgproj b/pkg/Microsoft.Private.CoreFx.NETCoreApp/Microsoft.Private.CoreFx.NETCoreApp.pkgproj index 0ce075873592..b2fc6cb4be1a 100644 --- a/pkg/Microsoft.Private.CoreFx.NETCoreApp/Microsoft.Private.CoreFx.NETCoreApp.pkgproj +++ b/pkg/Microsoft.Private.CoreFx.NETCoreApp/Microsoft.Private.CoreFx.NETCoreApp.pkgproj @@ -60,6 +60,14 @@ System.ComponentModel.Composition; + + + + System.Text.Json; + + Date: Tue, 17 Mar 2020 11:48:27 -0700 Subject: [PATCH 17/25] Adding BlockStable attribute to make sure the package won't be marked as stable on the official build --- src/System.Net.Http.Json/Directory.Build.props | 1 + 1 file changed, 1 insertion(+) diff --git a/src/System.Net.Http.Json/Directory.Build.props b/src/System.Net.Http.Json/Directory.Build.props index 28b80d429b62..de7b036f7bcd 100644 --- a/src/System.Net.Http.Json/Directory.Build.props +++ b/src/System.Net.Http.Json/Directory.Build.props @@ -8,6 +8,7 @@ preview + true true false From 1d24f388ff508ac942b9d3e5bfe2a0d19975b6eb Mon Sep 17 00:00:00 2001 From: David Cantu Date: Thu, 19 Mar 2020 09:44:00 -0700 Subject: [PATCH 18/25] 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 | 45 ++--- .../Net/Http/Json/TranscodingReadStream.cs | 186 +++++++++--------- .../Net/Http/Json/TranscodingWriteStream.cs | 55 +++--- ...stem.Net.Http.Json.Functional.Tests.csproj | 10 +- .../TranscodingReadStreamTests.cs | 47 +++-- 9 files changed, 187 insertions(+), 190 deletions(-) delete mode 100644 src/System.Net.Http.Json/src/Properties/InternalsVisibleTo.cs diff --git a/src/System.Net.Http.Json/ref/System.Net.Http.Json.cs b/src/System.Net.Http.Json/ref/System.Net.Http.Json.cs index fd69ed09cb60..c44ee208adb8 100644 --- a/src/System.Net.Http.Json/ref/System.Net.Http.Json.cs +++ b/src/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/System.Net.Http.Json/src/Properties/InternalsVisibleTo.cs b/src/System.Net.Http.Json/src/Properties/InternalsVisibleTo.cs deleted file mode 100644 index ebfde5cba531..000000000000 --- a/src/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/System.Net.Http.Json/src/System.Net.Http.Json.csproj b/src/System.Net.Http.Json/src/System.Net.Http.Json.csproj index a5bc901957ad..cbc4c57d697e 100644 --- a/src/System.Net.Http.Json/src/System.Net.Http.Json.csproj +++ b/src/System.Net.Http.Json/src/System.Net.Http.Json.csproj @@ -4,7 +4,6 @@ enable - diff --git a/src/System.Net.Http.Json/src/System/Net/Http/Json/HttpContentJsonExtensions.cs b/src/System.Net.Http.Json/src/System/Net/Http/Json/HttpContentJsonExtensions.cs index 03a19d74d122..3b687f873a93 100644 --- a/src/System.Net.Http.Json/src/System/Net/Http/Json/HttpContentJsonExtensions.cs +++ b/src/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/System.Net.Http.Json/src/System/Net/Http/Json/JsonContent.cs b/src/System.Net.Http.Json/src/System/Net/Http/Json/JsonContent.cs index 4481390ec4b9..78a1cdd6e64c 100644 --- a/src/System.Net.Http.Json/src/System/Net/Http/Json/JsonContent.cs +++ b/src/System.Net.Http.Json/src/System/Net/Http/Json/JsonContent.cs @@ -21,41 +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) - => JsonSerializer.SerializeAsync(GetStreamToWriteTo(stream), Value, ObjectType, _jsonSerializerOptions); + => SerializeToStreamAsyncCore(stream, CancellationToken.None); protected override bool TryComputeLength(out long length) { @@ -63,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/System.Net.Http.Json/src/System/Net/Http/Json/TranscodingReadStream.cs b/src/System.Net.Http.Json/src/System/Net/Http/Json/TranscodingReadStream.cs index 63333319858d..51efb040cc87 100644 --- a/src/System.Net.Http.Json/src/System/Net/Http/Json/TranscodingReadStream.cs +++ b/src/System.Net.Http.Json/src/System/Net/Http/Json/TranscodingReadStream.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. +// 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; @@ -20,7 +22,6 @@ internal sealed class TranscodingReadStream : Stream private readonly Stream _stream; private readonly Decoder _decoder; - private readonly Encoder _encoder; private ArraySegment _byteBuffer; @@ -34,28 +35,18 @@ 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(); - _encoder = Encoding.UTF8.GetEncoder(); } @@ -74,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); @@ -103,110 +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; + } } - 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); } + _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. - _encoder.Convert(_charBuffer.Array, _charBuffer.Offset, _charBuffer.Count, _overflowBuffer.Array, _overflowBuffer.Offset, _overflowBuffer.Count, - false, out int overFlowChars, out int overflowBytes, out bool _); + _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 = - await _stream.ReadAsync(_byteBuffer.Array, _byteBuffer.Count, _byteBuffer.Array.Length, cancellationToken).ConfigureAwait(false); - _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"); _decoder.Convert(_byteBuffer.Array, _byteBuffer.Offset, _byteBuffer.Count, _charBuffer.Array, 0, _charBuffer.Array.Length, - flush: readBytes == 0, out int bytesUsed, out int charsUsed, out _); + 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) @@ -232,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/System.Net.Http.Json/src/System/Net/Http/Json/TranscodingWriteStream.cs b/src/System.Net.Http.Json/src/System/Net/Http/Json/TranscodingWriteStream.cs index ac03a88dc954..0eb6d5f9791c 100644 --- a/src/System.Net.Http.Json/src/System/Net/Http/Json/TranscodingWriteStream.cs +++ b/src/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,20 +72,37 @@ 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) { - _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); _charsDecoded += charsDecoded; @@ -110,7 +127,6 @@ private async Task WriteBufferAsync(CancellationToken cancellationToken) flush: false, out int charsEncoded, out int bytesUsed, out encoderCompleted); await _stream.WriteAsync(byteBuffer, 0, bytesUsed, cancellationToken).ConfigureAwait(false); - charsWritten += charsEncoded; } @@ -120,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!; } } diff --git a/src/System.Net.Http.Json/tests/FunctionalTests/System.Net.Http.Json.Functional.Tests.csproj b/src/System.Net.Http.Json/tests/FunctionalTests/System.Net.Http.Json.Functional.Tests.csproj index aca738836a5f..51a88a6b7385 100644 --- a/src/System.Net.Http.Json/tests/FunctionalTests/System.Net.Http.Json.Functional.Tests.csproj +++ b/src/System.Net.Http.Json/tests/FunctionalTests/System.Net.Http.Json.Functional.Tests.csproj @@ -10,7 +10,7 @@ - + Common\System\Net\Capability.Security.cs @@ -39,9 +39,11 @@ Common\System\Threading\Tasks\TaskTimeoutExtensions.cs + + + - - - + + diff --git a/src/System.Net.Http.Json/tests/FunctionalTests/TranscodingReadStreamTests.cs b/src/System.Net.Http.Json/tests/FunctionalTests/TranscodingReadStreamTests.cs index 19ca65039a5c..3b879412f20a 100644 --- a/src/System.Net.Http.Json/tests/FunctionalTests/TranscodingReadStreamTests.cs +++ b/src/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 d0d1fd5e641ee28b0a9a1ed35ca1785324e79c06 Mon Sep 17 00:00:00 2001 From: David Cantu Date: Thu, 19 Mar 2020 10:44:04 -0700 Subject: [PATCH 19/25] Mark TransportContext as nullable to fix CI issues. --- src/System.Net.Http.Json/ref/System.Net.Http.Json.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/System.Net.Http.Json/ref/System.Net.Http.Json.cs b/src/System.Net.Http.Json/ref/System.Net.Http.Json.cs index c44ee208adb8..6831447bab31 100644 --- a/src/System.Net.Http.Json/ref/System.Net.Http.Json.cs +++ b/src/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; } } } From 988ca9f9895f2e86e31ec8adfd9321b9375d4648 Mon Sep 17 00:00:00 2001 From: David Cantu Date: Thu, 19 Mar 2020 12:09:58 -0700 Subject: [PATCH 20/25] 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 +++--- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Get.cs b/src/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Get.cs index 289fd5c73479..240040e2e3f4 100644 --- a/src/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Get.cs +++ b/src/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/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Post.cs b/src/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Post.cs index 5b188baae4fa..6a651c648d30 100644 --- a/src/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Post.cs +++ b/src/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/System.Net.Http.Json/src/System/Net/Http/Json/HttpContentJsonExtensions.cs b/src/System.Net.Http.Json/src/System/Net/Http/Json/HttpContentJsonExtensions.cs index 3b687f873a93..8895013dbbbf 100644 --- a/src/System.Net.Http.Json/src/System/Net/Http/Json/HttpContentJsonExtensions.cs +++ b/src/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/System.Net.Http.Json/src/System/Net/Http/Json/JsonContent.cs b/src/System.Net.Http.Json/src/System/Net/Http/Json/JsonContent.cs index 78a1cdd6e64c..c9cf0f0117d7 100644 --- a/src/System.Net.Http.Json/src/System/Net/Http/Json/JsonContent.cs +++ b/src/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; From 12dc58b3137774224a5f388ea50ab8d0133940e8 Mon Sep 17 00:00:00 2001 From: David Cantu Date: Fri, 20 Mar 2020 10:24:03 -0700 Subject: [PATCH 21/25] 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/System.Net.Http.Json/ref/System.Net.Http.Json.cs b/src/System.Net.Http.Json/ref/System.Net.Http.Json.cs index 6831447bab31..687092ae2fab 100644 --- a/src/System.Net.Http.Json/ref/System.Net.Http.Json.cs +++ b/src/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/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Get.cs b/src/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Get.cs index 240040e2e3f4..88b33be34130 100644 --- a/src/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Get.cs +++ b/src/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/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Put.cs b/src/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Put.cs index a7e59a66d4d1..ed35819adee8 100644 --- a/src/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Put.cs +++ b/src/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/System.Net.Http.Json/src/System/Net/Http/Json/HttpContentJsonExtensions.cs b/src/System.Net.Http.Json/src/System/Net/Http/Json/HttpContentJsonExtensions.cs index 8895013dbbbf..05bbe8009c12 100644 --- a/src/System.Net.Http.Json/src/System/Net/Http/Json/HttpContentJsonExtensions.cs +++ b/src/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/System.Net.Http.Json/src/System/Net/Http/Json/JsonContent.cs b/src/System.Net.Http.Json/src/System/Net/Http/Json/JsonContent.cs index c9cf0f0117d7..70c2bf62ba39 100644 --- a/src/System.Net.Http.Json/src/System/Net/Http/Json/JsonContent.cs +++ b/src/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/System.Net.Http.Json/src/System/Net/Http/Json/TranscodingWriteStream.cs b/src/System.Net.Http.Json/src/System/Net/Http/Json/TranscodingWriteStream.cs index 0eb6d5f9791c..aed04bacebaa 100644 --- a/src/System.Net.Http.Json/src/System/Net/Http/Json/TranscodingWriteStream.cs +++ b/src/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/System.Net.Http.Json/tests/FunctionalTests/HttpClientJsonExtensionsTests.cs b/src/System.Net.Http.Json/tests/FunctionalTests/HttpClientJsonExtensionsTests.cs index 2e17003a738f..0884505c33ac 100644 --- a/src/System.Net.Http.Json/tests/FunctionalTests/HttpClientJsonExtensionsTests.cs +++ b/src/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/System.Net.Http.Json/tests/FunctionalTests/HttpContentJsonExtensionsTests.cs b/src/System.Net.Http.Json/tests/FunctionalTests/HttpContentJsonExtensionsTests.cs index 47fa5991a7c8..e94da9d2038f 100644 --- a/src/System.Net.Http.Json/tests/FunctionalTests/HttpContentJsonExtensionsTests.cs +++ b/src/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/System.Net.Http.Json/tests/FunctionalTests/JsonContentTests.cs b/src/System.Net.Http.Json/tests/FunctionalTests/JsonContentTests.cs index 2ba52806583c..7ed830777de0 100644 --- a/src/System.Net.Http.Json/tests/FunctionalTests/JsonContentTests.cs +++ b/src/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/System.Net.Http.Json/tests/FunctionalTests/System.Net.Http.Json.Functional.Tests.csproj b/src/System.Net.Http.Json/tests/FunctionalTests/System.Net.Http.Json.Functional.Tests.csproj index 51a88a6b7385..7355a2806545 100644 --- a/src/System.Net.Http.Json/tests/FunctionalTests/System.Net.Http.Json.Functional.Tests.csproj +++ b/src/System.Net.Http.Json/tests/FunctionalTests/System.Net.Http.Json.Functional.Tests.csproj @@ -3,14 +3,14 @@ netcoreapp-Debug;netcoreapp-Release;netfx-Debug;netfx-Release - - - + + + - + Common\System\Net\Capability.Security.cs diff --git a/src/System.Net.Http.Json/tests/FunctionalTests/TranscodingReadStreamTests.cs b/src/System.Net.Http.Json/tests/FunctionalTests/TranscodingReadStreamTests.cs index 3b879412f20a..74748d01a609 100644 --- a/src/System.Net.Http.Json/tests/FunctionalTests/TranscodingReadStreamTests.cs +++ b/src/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 845ac1387ecdbee02dbaf2bc7aad5e5bc9f6d55c Mon Sep 17 00:00:00 2001 From: David Cantu Date: Fri, 20 Mar 2020 11:08:58 -0700 Subject: [PATCH 22/25] 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/System.Net.Http.Json/tests/FunctionalTests/Person.cs create mode 100644 src/System.Net.Http.Json/tests/FunctionalTests/TestClasses.cs diff --git a/src/System.Net.Http.Json/src/System/Net/Http/Json/HttpContentJsonExtensions.cs b/src/System.Net.Http.Json/src/System/Net/Http/Json/HttpContentJsonExtensions.cs index 05bbe8009c12..3e45cd8eef2b 100644 --- a/src/System.Net.Http.Json/src/System/Net/Http/Json/HttpContentJsonExtensions.cs +++ b/src/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/System.Net.Http.Json/src/System/Net/Http/Json/JsonContent.cs b/src/System.Net.Http.Json/src/System/Net/Http/Json/JsonContent.cs index 70c2bf62ba39..7cce2770b0c1 100644 --- a/src/System.Net.Http.Json/src/System/Net/Http/Json/JsonContent.cs +++ b/src/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/System.Net.Http.Json/tests/FunctionalTests/HttpClientJsonExtensionsTests.cs b/src/System.Net.Http.Json/tests/FunctionalTests/HttpClientJsonExtensionsTests.cs index 0884505c33ac..6b5aa8045292 100644 --- a/src/System.Net.Http.Json/tests/FunctionalTests/HttpClientJsonExtensionsTests.cs +++ b/src/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/System.Net.Http.Json/tests/FunctionalTests/HttpContentJsonExtensionsTests.cs b/src/System.Net.Http.Json/tests/FunctionalTests/HttpContentJsonExtensionsTests.cs index e94da9d2038f..e61a9a3ed5f8 100644 --- a/src/System.Net.Http.Json/tests/FunctionalTests/HttpContentJsonExtensionsTests.cs +++ b/src/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/System.Net.Http.Json/tests/FunctionalTests/JsonContentTests.cs b/src/System.Net.Http.Json/tests/FunctionalTests/JsonContentTests.cs index 7ed830777de0..abe641e2a37f 100644 --- a/src/System.Net.Http.Json/tests/FunctionalTests/JsonContentTests.cs +++ b/src/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/System.Net.Http.Json/tests/FunctionalTests/Person.cs b/src/System.Net.Http.Json/tests/FunctionalTests/Person.cs deleted file mode 100644 index a8595866c89c..000000000000 --- a/src/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/System.Net.Http.Json/tests/FunctionalTests/System.Net.Http.Json.Functional.Tests.csproj b/src/System.Net.Http.Json/tests/FunctionalTests/System.Net.Http.Json.Functional.Tests.csproj index 7355a2806545..a48301dadc69 100644 --- a/src/System.Net.Http.Json/tests/FunctionalTests/System.Net.Http.Json.Functional.Tests.csproj +++ b/src/System.Net.Http.Json/tests/FunctionalTests/System.Net.Http.Json.Functional.Tests.csproj @@ -6,7 +6,7 @@ - + diff --git a/src/System.Net.Http.Json/tests/FunctionalTests/TestClasses.cs b/src/System.Net.Http.Json/tests/FunctionalTests/TestClasses.cs new file mode 100644 index 000000000000..8dc962bae915 --- /dev/null +++ b/src/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 d6745fe1b34264394d8e2e61016891ad007e334a Mon Sep 17 00:00:00 2001 From: David Cantu Date: Fri, 20 Mar 2020 11:37:29 -0700 Subject: [PATCH 23/25] 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/System.Net.Http.Json/tests/FunctionalTests/TranscodingReadStreamTests.cs b/src/System.Net.Http.Json/tests/FunctionalTests/TranscodingReadStreamTests.cs index 74748d01a609..aa576b38a693 100644 --- a/src/System.Net.Http.Json/tests/FunctionalTests/TranscodingReadStreamTests.cs +++ b/src/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 653f26df4ddf8c90ca8756b40f4d7fab7916ace9 Mon Sep 17 00:00:00 2001 From: David Cantu Date: Fri, 20 Mar 2020 12:57:03 -0700 Subject: [PATCH 24/25] 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/System.Net.Http.Json/src/System/Net/Http/Json/HttpContentJsonExtensions.cs b/src/System.Net.Http.Json/src/System/Net/Http/Json/HttpContentJsonExtensions.cs index 3e45cd8eef2b..31ca33bff5ce 100644 --- a/src/System.Net.Http.Json/src/System/Net/Http/Json/HttpContentJsonExtensions.cs +++ b/src/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/System.Net.Http.Json/src/System/Net/Http/Json/TranscodingReadStream.cs b/src/System.Net.Http.Json/src/System/Net/Http/Json/TranscodingReadStream.cs index 51efb040cc87..554fc3c4932c 100644 --- a/src/System.Net.Http.Json/src/System/Net/Http/Json/TranscodingReadStream.cs +++ b/src/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/System.Net.Http.Json/src/System/Net/Http/Json/TranscodingWriteStream.cs b/src/System.Net.Http.Json/src/System/Net/Http/Json/TranscodingWriteStream.cs index aed04bacebaa..5cad1b9c8566 100644 --- a/src/System.Net.Http.Json/src/System/Net/Http/Json/TranscodingWriteStream.cs +++ b/src/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 f9112c40bf45f7548557b54b7fb27ac73de4994e Mon Sep 17 00:00:00 2001 From: David Cantu Date: Fri, 20 Mar 2020 12:59:11 -0700 Subject: [PATCH 25/25] 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/System.Net.Http.Json/src/Resources/Strings.resx b/src/System.Net.Http.Json/src/Resources/Strings.resx index 5f592a90a4f2..60bb3ba55e5c 100644 --- a/src/System.Net.Http.Json/src/Resources/Strings.resx +++ b/src/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/System.Net.Http.Json/src/System/Net/Http/Json/JsonContent.cs b/src/System.Net.Http.Json/src/System/Net/Http/Json/JsonContent.cs index 7cce2770b0c1..c4919533a8b7 100644 --- a/src/System.Net.Http.Json/src/System/Net/Http/Json/JsonContent.cs +++ b/src/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/System.Net.Http.Json/tests/FunctionalTests/JsonContentTests.cs b/src/System.Net.Http.Json/tests/FunctionalTests/JsonContentTests.cs index abe641e2a37f..a69597ac4a2e 100644 --- a/src/System.Net.Http.Json/tests/FunctionalTests/JsonContentTests.cs +++ b/src/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]