Skip to content

Adding untyped JsonTypeInfo overloads #45593

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions src/Http/Http.Extensions/src/HttpRequestJsonExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,47 @@ public static class HttpRequestJsonExtensions
}
}

/// <summary>
/// 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.
/// </summary>
/// <param name="request">The request to read from.</param>
/// <param name="jsonTypeInfo">Metadata about the type to convert.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> used to cancel the operation.</param>
/// <returns>The deserialized value.</returns>
#pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters
public static async ValueTask<object?> 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();
}
}
}

/// <summary>
/// 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.
Expand Down
44 changes: 44 additions & 0 deletions src/Http/Http.Extensions/src/HttpResponseJsonExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,50 @@ static async Task WriteAsJsonAsyncSlow(HttpResponse response, TValue value, Json
}
}

/// <summary>
/// Write the specified value as JSON to the response body. The response content-type will be set to
/// the specified content-type.
/// </summary>
/// <param name="response">The response to write JSON to.</param>
/// <param name="value">The value to write as JSON.</param>
/// <param name="jsonTypeInfo">Metadata about the type to convert.</param>
/// <param name="contentType">The content-type to set on the response.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> used to cancel the operation.</param>
/// <returns>The task object representing the asynchronous operation.</returns>
#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<TValue>(
Stream body,
Expand Down
2 changes: 2 additions & 0 deletions src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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<object?>
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<object?>
static Microsoft.AspNetCore.Http.HttpRequestJsonExtensions.ReadFromJsonAsync<TValue>(this Microsoft.AspNetCore.Http.HttpRequest! request, System.Text.Json.Serialization.Metadata.JsonTypeInfo<TValue>! jsonTypeInfo, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask<TValue?>
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<TValue>(this Microsoft.AspNetCore.Http.HttpResponse! response, TValue value, System.Text.Json.Serialization.Metadata.JsonTypeInfo<TValue>! 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!
Expand Down
29 changes: 0 additions & 29 deletions src/Http/Http.Extensions/test/HttpRequestExtensionsTests.cs

This file was deleted.

66 changes: 66 additions & 0 deletions src/Http/Http.Extensions/test/HttpRequestJsonExtensionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,34 @@

using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization.Metadata;

#nullable enable

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()
{
Expand Down Expand Up @@ -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<int>?)await context.Request.ReadFromJsonAsync(options.GetTypeInfo(typeof(List<int>)));

// 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<List<int>>)options.GetTypeInfo(typeof(List<int>));
var result = await context.Request.ReadFromJsonAsync(typeInfo);

// Assert
Assert.NotNull(result);
Assert.Collection(result,
i => Assert.Equal(1, i),
i => Assert.Equal(2, i));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -438,6 +439,48 @@ async IAsyncEnumerable<int> 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<int[]>)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; }
Expand Down