diff --git a/src/Http/Http.Extensions/src/HttpRequestJsonExtensions.cs b/src/Http/Http.Extensions/src/HttpRequestJsonExtensions.cs index a204e74c0477..1bd0b4b28b65 100644 --- a/src/Http/Http.Extensions/src/HttpRequestJsonExtensions.cs +++ b/src/Http/Http.Extensions/src/HttpRequestJsonExtensions.cs @@ -122,6 +122,47 @@ public static class HttpRequestJsonExtensions } } + /// + /// Read JSON from the request and deserialize to object type. + /// If the request's content-type is not a known JSON type then an error will be thrown. + /// + /// The request to read from. + /// Metadata about the type to convert. + /// A used to cancel the operation. + /// The deserialized value. +#pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters + public static async ValueTask ReadFromJsonAsync( +#pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters + this HttpRequest request, + JsonTypeInfo jsonTypeInfo, + CancellationToken cancellationToken = default) + { + if (request == null) + { + throw new ArgumentNullException(nameof(request)); + } + + if (!request.HasJsonContentType(out var charset)) + { + ThrowContentTypeError(request); + } + + var encoding = GetEncodingFromCharset(charset); + var (inputStream, usesTranscodingStream) = GetInputStream(request.HttpContext, encoding); + + try + { + return await JsonSerializer.DeserializeAsync(inputStream, jsonTypeInfo, cancellationToken); + } + finally + { + if (usesTranscodingStream) + { + await inputStream.DisposeAsync(); + } + } + } + /// /// Read JSON from the request and deserialize to the specified type. /// If the request's content-type is not a known JSON type then an error will be thrown. diff --git a/src/Http/Http.Extensions/src/HttpResponseJsonExtensions.cs b/src/Http/Http.Extensions/src/HttpResponseJsonExtensions.cs index 300b836a7d34..34f92a3e78f8 100644 --- a/src/Http/Http.Extensions/src/HttpResponseJsonExtensions.cs +++ b/src/Http/Http.Extensions/src/HttpResponseJsonExtensions.cs @@ -139,6 +139,50 @@ static async Task WriteAsJsonAsyncSlow(HttpResponse response, TValue value, Json } } + /// + /// Write the specified value as JSON to the response body. The response content-type will be set to + /// the specified content-type. + /// + /// The response to write JSON to. + /// The value to write as JSON. + /// Metadata about the type to convert. + /// The content-type to set on the response. + /// A used to cancel the operation. + /// The task object representing the asynchronous operation. +#pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters + public static Task WriteAsJsonAsync( +#pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters + this HttpResponse response, + object? value, + JsonTypeInfo jsonTypeInfo, + string? contentType = default, + CancellationToken cancellationToken = default) + { + if (response == null) + { + throw new ArgumentNullException(nameof(response)); + } + + response.ContentType = contentType ?? JsonConstants.JsonContentTypeWithCharset; + + // if no user provided token, pass the RequestAborted token and ignore OperationCanceledException + if (!cancellationToken.CanBeCanceled) + { + return WriteAsJsonAsyncSlow(response, value, jsonTypeInfo); + } + + return JsonSerializer.SerializeAsync(response.Body, value, jsonTypeInfo, cancellationToken); + + static async Task WriteAsJsonAsyncSlow(HttpResponse response, object? value, JsonTypeInfo jsonTypeInfo) + { + try + { + await JsonSerializer.SerializeAsync(response.Body, value, jsonTypeInfo, response.HttpContext.RequestAborted); + } + catch (OperationCanceledException) { } + } + } + [RequiresUnreferencedCode(RequiresUnreferencedCodeMessage)] private static async Task WriteAsJsonAsyncSlow( Stream body, diff --git a/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt index 6945deb350e5..7182ff420d4f 100644 --- a/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt @@ -45,8 +45,10 @@ Microsoft.AspNetCore.Mvc.ProblemDetails.Type.get -> string? (forwarded, containe Microsoft.AspNetCore.Mvc.ProblemDetails.Type.set -> void (forwarded, contained in Microsoft.AspNetCore.Http.Abstractions) Microsoft.Extensions.DependencyInjection.ProblemDetailsServiceCollectionExtensions Microsoft.Extensions.DependencyInjection.HttpJsonServiceExtensions +static Microsoft.AspNetCore.Http.HttpRequestJsonExtensions.ReadFromJsonAsync(this Microsoft.AspNetCore.Http.HttpRequest! request, System.Text.Json.Serialization.Metadata.JsonTypeInfo! jsonTypeInfo, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask static Microsoft.AspNetCore.Http.HttpRequestJsonExtensions.ReadFromJsonAsync(this Microsoft.AspNetCore.Http.HttpRequest! request, System.Type! type, System.Text.Json.Serialization.JsonSerializerContext! context, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask static Microsoft.AspNetCore.Http.HttpRequestJsonExtensions.ReadFromJsonAsync(this Microsoft.AspNetCore.Http.HttpRequest! request, System.Text.Json.Serialization.Metadata.JsonTypeInfo! jsonTypeInfo, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +static Microsoft.AspNetCore.Http.HttpResponseJsonExtensions.WriteAsJsonAsync(this Microsoft.AspNetCore.Http.HttpResponse! response, object? value, System.Text.Json.Serialization.Metadata.JsonTypeInfo! jsonTypeInfo, string? contentType = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! static Microsoft.AspNetCore.Http.HttpResponseJsonExtensions.WriteAsJsonAsync(this Microsoft.AspNetCore.Http.HttpResponse! response, object? value, System.Type! type, System.Text.Json.Serialization.JsonSerializerContext! context, string? contentType = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! static Microsoft.AspNetCore.Http.HttpResponseJsonExtensions.WriteAsJsonAsync(this Microsoft.AspNetCore.Http.HttpResponse! response, TValue value, System.Text.Json.Serialization.Metadata.JsonTypeInfo! jsonTypeInfo, string? contentType = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! *REMOVED*static Microsoft.AspNetCore.Http.RequestDelegateFactory.Create(System.Delegate! handler, Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions? options = null) -> Microsoft.AspNetCore.Http.RequestDelegateResult! diff --git a/src/Http/Http.Extensions/test/HttpRequestExtensionsTests.cs b/src/Http/Http.Extensions/test/HttpRequestExtensionsTests.cs deleted file mode 100644 index bd6735818e55..000000000000 --- a/src/Http/Http.Extensions/test/HttpRequestExtensionsTests.cs +++ /dev/null @@ -1,29 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -#nullable enable - -namespace Microsoft.AspNetCore.Http.Extensions.Tests; - -public class HttpRequestExtensionsTests -{ - [Theory] - [InlineData(null, false)] - [InlineData("", false)] - [InlineData("application/xml", false)] - [InlineData("text/json", false)] - [InlineData("text/json; charset=utf-8", false)] - [InlineData("application/json", true)] - [InlineData("application/json; charset=utf-8", true)] - [InlineData("application/ld+json", true)] - [InlineData("APPLICATION/JSON", true)] - [InlineData("APPLICATION/JSON; CHARSET=UTF-8", true)] - [InlineData("APPLICATION/LD+JSON", true)] - public void HasJsonContentType(string contentType, bool hasJsonContentType) - { - var request = new DefaultHttpContext().Request; - request.ContentType = contentType; - - Assert.Equal(hasJsonContentType, request.HasJsonContentType()); - } -} diff --git a/src/Http/Http.Extensions/test/HttpRequestJsonExtensionsTests.cs b/src/Http/Http.Extensions/test/HttpRequestJsonExtensionsTests.cs index 9ac9ddd2a66c..4d63107214e9 100644 --- a/src/Http/Http.Extensions/test/HttpRequestJsonExtensionsTests.cs +++ b/src/Http/Http.Extensions/test/HttpRequestJsonExtensionsTests.cs @@ -3,6 +3,7 @@ using System.Text; using System.Text.Json; +using System.Text.Json.Serialization.Metadata; #nullable enable @@ -10,6 +11,26 @@ namespace Microsoft.AspNetCore.Http.Extensions.Tests; public class HttpRequestJsonExtensionsTests { + [Theory] + [InlineData(null, false)] + [InlineData("", false)] + [InlineData("application/xml", false)] + [InlineData("text/json", false)] + [InlineData("text/json; charset=utf-8", false)] + [InlineData("application/json", true)] + [InlineData("application/json; charset=utf-8", true)] + [InlineData("application/ld+json", true)] + [InlineData("APPLICATION/JSON", true)] + [InlineData("APPLICATION/JSON; CHARSET=UTF-8", true)] + [InlineData("APPLICATION/LD+JSON", true)] + public void HasJsonContentType(string contentType, bool hasJsonContentType) + { + var request = new DefaultHttpContext().Request; + request.ContentType = contentType; + + Assert.Equal(hasJsonContentType, request.HasJsonContentType()); + } + [Fact] public async Task ReadFromJsonAsyncGeneric_NonJsonContentType_ThrowError() { @@ -207,4 +228,49 @@ public async Task ReadFromJsonAsync_WithOptions_ReturnValue() i => Assert.Equal(1, i), i => Assert.Equal(2, i)); } + + [Fact] + public async Task ReadFromJsonAsync_WithTypeInfo_ReturnValue() + { + // Arrange + var context = new DefaultHttpContext(); + context.Request.ContentType = "application/json"; + context.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes("[1,2,]")); + + var options = new JsonSerializerOptions(); + options.AllowTrailingCommas = true; + options.TypeInfoResolver = new DefaultJsonTypeInfoResolver(); + + // Act + var result = (List?)await context.Request.ReadFromJsonAsync(options.GetTypeInfo(typeof(List))); + + // Assert + Assert.NotNull(result); + Assert.Collection(result, + i => Assert.Equal(1, i), + i => Assert.Equal(2, i)); + } + + [Fact] + public async Task ReadFromJsonAsync_WithGenericTypeInfo_ReturnValue() + { + // Arrange + var context = new DefaultHttpContext(); + context.Request.ContentType = "application/json"; + context.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes("[1,2,]")); + + var options = new JsonSerializerOptions(); + options.AllowTrailingCommas = true; + options.TypeInfoResolver = new DefaultJsonTypeInfoResolver(); + + // Act + var typeInfo = (JsonTypeInfo>)options.GetTypeInfo(typeof(List)); + var result = await context.Request.ReadFromJsonAsync(typeInfo); + + // Assert + Assert.NotNull(result); + Assert.Collection(result, + i => Assert.Equal(1, i), + i => Assert.Equal(2, i)); + } } diff --git a/src/Http/Http.Extensions/test/HttpResponseJsonExtensionsTests.cs b/src/Http/Http.Extensions/test/HttpResponseJsonExtensionsTests.cs index 0f93ce83e644..9562ee94c099 100644 --- a/src/Http/Http.Extensions/test/HttpResponseJsonExtensionsTests.cs +++ b/src/Http/Http.Extensions/test/HttpResponseJsonExtensionsTests.cs @@ -5,6 +5,7 @@ using System.Text; using System.Text.Json; using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; using Microsoft.AspNetCore.Testing; #nullable enable @@ -438,6 +439,48 @@ async IAsyncEnumerable AsyncEnumerable([EnumeratorCancellation] Cancellatio } } + [Fact] + public async Task WriteAsJsonAsyncGeneric_WithJsonTypeInfo_JsonResponse() + { + // Arrange + var body = new MemoryStream(); + var context = new DefaultHttpContext(); + context.Response.Body = body; + + // Act + var options = new JsonSerializerOptions(); + options.TypeInfoResolver = new DefaultJsonTypeInfoResolver(); + + await context.Response.WriteAsJsonAsync(new int[] { 1, 2, 3 }, (JsonTypeInfo)options.GetTypeInfo(typeof(int[]))); + + // Assert + Assert.Equal(JsonConstants.JsonContentTypeWithCharset, context.Response.ContentType); + + var data = Encoding.UTF8.GetString(body.ToArray()); + Assert.Equal("[1,2,3]", data); + } + + [Fact] + public async Task WriteAsJsonAsync_NullValue_WithJsonTypeInfo_JsonResponse() + { + // Arrange + var body = new MemoryStream(); + var context = new DefaultHttpContext(); + context.Response.Body = body; + + // Act + var options = new JsonSerializerOptions(); + options.TypeInfoResolver = new DefaultJsonTypeInfoResolver(); + + await context.Response.WriteAsJsonAsync(value : null, options.GetTypeInfo(typeof(Uri))); + + // Assert + Assert.Equal(JsonConstants.JsonContentTypeWithCharset, context.Response.ContentType); + + var data = Encoding.UTF8.GetString(body.ToArray()); + Assert.Equal("null", data); + } + public class TestObject { public string? StringProperty { get; set; }