Merge pull request #22453 from dotnet-maestro-bot/merge/release/5.0-preview6-to-master

[automated] Merge branch 'release/5.0-preview6' => 'master'
This commit is contained in:
msftbot[bot] 2020-06-03 02:41:08 +00:00 committed by GitHub
commit 912ab2bcb3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 1065 additions and 1 deletions

View File

@ -0,0 +1,7 @@
<Project>
<Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory)..\, Directory.Build.props))\Directory.Build.props" />
<PropertyGroup>
<Nullable>annotations</Nullable>
</PropertyGroup>
</Project>

View File

@ -5,6 +5,7 @@
</PropertyGroup>
<ItemGroup Condition="'$(TargetFramework)' == '$(DefaultNetCoreTargetFramework)'">
<Compile Include="Microsoft.AspNetCore.Http.Extensions.netcoreapp.cs" />
<Compile Include="../src/Properties/AssemblyInfo.cs" />
<Reference Include="Microsoft.AspNetCore.Http.Abstractions" />
<Reference Include="Microsoft.Net.Http.Headers" />
<Reference Include="Microsoft.Extensions.FileProviders.Abstractions" />

View File

@ -13,6 +13,10 @@ namespace Microsoft.AspNetCore.Http
{
public static string GetServerVariable(this Microsoft.AspNetCore.Http.HttpContext context, string variableName) { throw null; }
}
public static partial class HttpRequestExtensions
{
public static bool HasJsonContentType(this Microsoft.AspNetCore.Http.HttpRequest request) { throw null; }
}
public static partial class ResponseExtensions
{
public static void Clear(this Microsoft.AspNetCore.Http.HttpResponse response) { }
@ -127,3 +131,29 @@ namespace Microsoft.AspNetCore.Http.Headers
public void SetList<T>(string name, System.Collections.Generic.IList<T> values) { }
}
}
namespace Microsoft.AspNetCore.Http.Json
{
public static partial class HttpRequestJsonExtensions
{
[System.Diagnostics.DebuggerStepThroughAttribute]
public static System.Threading.Tasks.ValueTask<object?> ReadFromJsonAsync(this Microsoft.AspNetCore.Http.HttpRequest request, System.Type type, System.Text.Json.JsonSerializerOptions? options, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
public static System.Threading.Tasks.ValueTask<object?> ReadFromJsonAsync(this Microsoft.AspNetCore.Http.HttpRequest request, System.Type type, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
[System.Diagnostics.DebuggerStepThroughAttribute]
public static System.Threading.Tasks.ValueTask<TValue> ReadFromJsonAsync<TValue>(this Microsoft.AspNetCore.Http.HttpRequest request, System.Text.Json.JsonSerializerOptions? options, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
public static System.Threading.Tasks.ValueTask<TValue> ReadFromJsonAsync<TValue>(this Microsoft.AspNetCore.Http.HttpRequest request, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
}
public static partial class HttpResponseJsonExtensions
{
public static System.Threading.Tasks.Task WriteAsJsonAsync(this Microsoft.AspNetCore.Http.HttpResponse response, object? value, System.Type type, System.Text.Json.JsonSerializerOptions? options, string? contentType, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
public static System.Threading.Tasks.Task WriteAsJsonAsync(this Microsoft.AspNetCore.Http.HttpResponse response, object? value, System.Type type, System.Text.Json.JsonSerializerOptions? options, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
public static System.Threading.Tasks.Task WriteAsJsonAsync(this Microsoft.AspNetCore.Http.HttpResponse response, object? value, System.Type type, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
public static System.Threading.Tasks.Task WriteAsJsonAsync<TValue>(this Microsoft.AspNetCore.Http.HttpResponse response, [System.Diagnostics.CodeAnalysis.AllowNullAttribute] TValue value, System.Text.Json.JsonSerializerOptions? options, string? contentType, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
public static System.Threading.Tasks.Task WriteAsJsonAsync<TValue>(this Microsoft.AspNetCore.Http.HttpResponse response, [System.Diagnostics.CodeAnalysis.AllowNullAttribute] TValue value, System.Text.Json.JsonSerializerOptions? options, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
public static System.Threading.Tasks.Task WriteAsJsonAsync<TValue>(this Microsoft.AspNetCore.Http.HttpResponse response, [System.Diagnostics.CodeAnalysis.AllowNullAttribute] TValue value, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
}
public partial class JsonOptions
{
public JsonOptions() { }
public System.Text.Json.JsonSerializerOptions SerializerOptions { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } }
}
}

View File

@ -0,0 +1,54 @@
// 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.
using System;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
#nullable enable
namespace Microsoft.AspNetCore.Http
{
public static class HttpRequestExtensions
{
/// <summary>
/// Checks the Content-Type header for JSON types.
/// </summary>
/// <returns>true if the Content-Type header represents a JSON content type; otherwise, false.</returns>
public static bool HasJsonContentType(this HttpRequest request)
{
return request.HasJsonContentType(out _);
}
internal static bool HasJsonContentType(this HttpRequest request, out StringSegment charset)
{
if (request == null)
{
throw new ArgumentNullException(nameof(request));
}
if (!MediaTypeHeaderValue.TryParse(request.ContentType, out var mt))
{
charset = StringSegment.Empty;
return false;
}
// Matches application/json
if (mt.MediaType.Equals(JsonConstants.JsonContentType, StringComparison.OrdinalIgnoreCase))
{
charset = mt.Charset;
return true;
}
// Matches +json, e.g. application/ld+json
if (mt.Suffix.Equals("json", StringComparison.OrdinalIgnoreCase))
{
charset = mt.Charset;
return true;
}
charset = StringSegment.Empty;
return false;
}
}
}

View File

@ -0,0 +1,182 @@
// 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.
using System;
using System.IO;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
#nullable enable
namespace Microsoft.AspNetCore.Http.Json
{
public static class HttpRequestJsonExtensions
{
/// <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.
/// </summary>
/// <typeparam name="TValue">The type of object to read.</typeparam>
/// <param name="request">The request to read from.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> used to cancel the operation.</param>
/// <returns>The task object representing the asynchronous operation.</returns>
public static ValueTask<TValue> ReadFromJsonAsync<TValue>(
this HttpRequest request,
CancellationToken cancellationToken = default)
{
return request.ReadFromJsonAsync<TValue>(options: null, cancellationToken);
}
/// <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.
/// </summary>
/// <typeparam name="TValue">The type of object to read.</typeparam>
/// <param name="request">The request to read from.</param>
/// <param name="options">The serializer options use when deserializing the content.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> used to cancel the operation.</param>
/// <returns>The task object representing the asynchronous operation.</returns>
public static async ValueTask<TValue> ReadFromJsonAsync<TValue>(
this HttpRequest request,
JsonSerializerOptions? options,
CancellationToken cancellationToken = default)
{
if (request == null)
{
throw new ArgumentNullException(nameof(request));
}
if (!request.HasJsonContentType(out var charset))
{
throw CreateContentTypeError(request);
}
options ??= ResolveSerializerOptions(request.HttpContext);
var encoding = GetEncodingFromCharset(charset);
var (inputStream, usesTranscodingStream) = GetInputStream(request.HttpContext, encoding);
try
{
return await JsonSerializer.DeserializeAsync<TValue>(inputStream, options, 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.
/// </summary>
/// <param name="request">The request to read from.</param>
/// <param name="type">The type of object to read.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> used to cancel the operation.</param>
/// <returns>The task object representing the asynchronous operation.</returns>
public static ValueTask<object?> ReadFromJsonAsync(
this HttpRequest request,
Type type,
CancellationToken cancellationToken = default)
{
return request.ReadFromJsonAsync(type, options: null, cancellationToken);
}
/// <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.
/// </summary>
/// <param name="request">The request to read from.</param>
/// <param name="type">The type of object to read.</param>
/// <param name="options">The serializer options use when deserializing the content.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> used to cancel the operation.</param>
/// <returns>The task object representing the asynchronous operation.</returns>
public static async ValueTask<object?> ReadFromJsonAsync(
this HttpRequest request,
Type type,
JsonSerializerOptions? options,
CancellationToken cancellationToken = default)
{
if (request == null)
{
throw new ArgumentNullException(nameof(request));
}
if (type == null)
{
throw new ArgumentNullException(nameof(type));
}
if (!request.HasJsonContentType(out var charset))
{
throw CreateContentTypeError(request);
}
options ??= ResolveSerializerOptions(request.HttpContext);
var encoding = GetEncodingFromCharset(charset);
var (inputStream, usesTranscodingStream) = GetInputStream(request.HttpContext, encoding);
try
{
return await JsonSerializer.DeserializeAsync(inputStream, type, options, cancellationToken);
}
finally
{
if (usesTranscodingStream)
{
await inputStream.DisposeAsync();
}
}
}
private static JsonSerializerOptions ResolveSerializerOptions(HttpContext httpContext)
{
// Attempt to resolve options from DI then fallback to default options
return httpContext.RequestServices?.GetService<IOptions<JsonOptions>>()?.Value?.SerializerOptions ?? JsonOptions.DefaultSerializerOptions;
}
private static InvalidOperationException CreateContentTypeError(HttpRequest request)
{
return new InvalidOperationException($"Unable to read the request as JSON because the request content type '{request.ContentType}' is not a known JSON content type.");
}
private static (Stream inputStream, bool usesTranscodingStream) GetInputStream(HttpContext httpContext, Encoding? encoding)
{
if (encoding == null || encoding.CodePage == Encoding.UTF8.CodePage)
{
return (httpContext.Request.Body, false);
}
var inputStream = Encoding.CreateTranscodingStream(httpContext.Request.Body, encoding, Encoding.UTF8, leaveOpen: true);
return (inputStream, true);
}
private static Encoding? GetEncodingFromCharset(StringSegment charset)
{
if (charset.Equals("utf-8", StringComparison.OrdinalIgnoreCase))
{
// This is an optimization for utf-8 that prevents the Substring caused by
// charset.Value
return Encoding.UTF8;
}
try
{
// charset.Value might be an invalid encoding name as in charset=invalid.
return charset.HasValue ? Encoding.GetEncoding(charset.Value) : null;
}
catch (Exception ex)
{
throw new InvalidOperationException($"Unable to read the request as JSON because the request content type charset '{charset}' is not a known encoding.", ex);
}
}
}
}

View File

@ -0,0 +1,163 @@
// 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.
using System;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
#nullable enable
namespace Microsoft.AspNetCore.Http.Json
{
public static partial class HttpResponseJsonExtensions
{
/// <summary>
/// Write the specified value as JSON to the response body. The response content-type will be set to
/// <c>application/json; charset=utf-8</c> and the status code set to <c>200</c>.
/// </summary>
/// <typeparam name="TValue">The type of object to write.</typeparam>
/// <param name="response">The response to write JSON to.</param>
/// <param name="value">The value to write as JSON.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> used to cancel the operation.</param>
/// <returns>The task object representing the asynchronous operation.</returns>
public static Task WriteAsJsonAsync<TValue>(
this HttpResponse response,
[AllowNull] TValue value,
CancellationToken cancellationToken = default)
{
return response.WriteAsJsonAsync<TValue>(value, options: null, contentType: null, cancellationToken);
}
/// <summary>
/// Write the specified value as JSON to the response body. The response content-type will be set to
/// <c>application/json; charset=utf-8</c> and the status code set to <c>200</c>.
/// </summary>
/// <typeparam name="TValue">The type of object to write.</typeparam>
/// <param name="response">The response to write JSON to.</param>
/// <param name="value">The value to write as JSON.</param>
/// <param name="options">The serializer options use when serializing the value.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> used to cancel the operation.</param>
/// <returns>The task object representing the asynchronous operation.</returns>
public static Task WriteAsJsonAsync<TValue>(
this HttpResponse response,
[AllowNull] TValue value,
JsonSerializerOptions? options,
CancellationToken cancellationToken = default)
{
return response.WriteAsJsonAsync<TValue>(value, options, contentType: null, cancellationToken);
}
/// <summary>
/// Write the specified value as JSON to the response body. The response content-type will be set to
/// the specified content-type and the status code set to <c>200</c>.
/// </summary>
/// <typeparam name="TValue">The type of object to write.</typeparam>
/// <param name="response">The response to write JSON to.</param>
/// <param name="value">The value to write as JSON.</param>
/// <param name="options">The serializer options use when serializing the value.</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>
public static Task WriteAsJsonAsync<TValue>(
this HttpResponse response,
[AllowNull] TValue value,
JsonSerializerOptions? options,
string? contentType,
CancellationToken cancellationToken = default)
{
if (response == null)
{
throw new ArgumentNullException(nameof(response));
}
options ??= ResolveSerializerOptions(response.HttpContext);
response.ContentType = contentType ?? JsonConstants.JsonContentTypeWithCharset;
response.StatusCode = StatusCodes.Status200OK;
return JsonSerializer.SerializeAsync<TValue>(response.Body, value!, options, cancellationToken);
}
/// <summary>
/// Write the specified value as JSON to the response body. The response content-type will be set to
/// <c>application/json; charset=utf-8</c> and the status code set to <c>200</c>.
/// </summary>
/// <param name="response">The response to write JSON to.</param>
/// <param name="value">The value to write as JSON.</param>
/// <param name="type">The type of object to write.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> used to cancel the operation.</param>
/// <returns>The task object representing the asynchronous operation.</returns>
public static Task WriteAsJsonAsync(
this HttpResponse response,
object? value,
Type type,
CancellationToken cancellationToken = default)
{
return response.WriteAsJsonAsync(value, type, options: null, contentType: null, cancellationToken);
}
/// <summary>
/// Write the specified value as JSON to the response body. The response content-type will be set to
/// <c>application/json; charset=utf-8</c> and the status code set to <c>200</c>.
/// </summary>
/// <param name="response">The response to write JSON to.</param>
/// <param name="value">The value to write as JSON.</param>
/// <param name="type">The type of object to write.</param>
/// <param name="options">The serializer options use when serializing the value.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> used to cancel the operation.</param>
/// <returns>The task object representing the asynchronous operation.</returns>
public static Task WriteAsJsonAsync(
this HttpResponse response,
object? value,
Type type,
JsonSerializerOptions? options,
CancellationToken cancellationToken = default)
{
return response.WriteAsJsonAsync(value, type, options, contentType: null, cancellationToken);
}
/// <summary>
/// Write the specified value as JSON to the response body. The response content-type will be set to
/// the specified content-type and the status code set to <c>200</c>.
/// </summary>
/// <param name="response">The response to write JSON to.</param>
/// <param name="value">The value to write as JSON.</param>
/// <param name="type">The type of object to write.</param>
/// <param name="options">The serializer options use when serializing the value.</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>
public static Task WriteAsJsonAsync(
this HttpResponse response,
object? value,
Type type,
JsonSerializerOptions? options,
string? contentType,
CancellationToken cancellationToken = default)
{
if (response == null)
{
throw new ArgumentNullException(nameof(response));
}
if (type == null)
{
throw new ArgumentNullException(nameof(type));
}
options ??= ResolveSerializerOptions(response.HttpContext);
response.ContentType = contentType ?? JsonConstants.JsonContentTypeWithCharset;
response.StatusCode = StatusCodes.Status200OK;
return JsonSerializer.SerializeAsync(response.Body, value, type, options, cancellationToken);
}
private static JsonSerializerOptions ResolveSerializerOptions(HttpContext httpContext)
{
// Attempt to resolve options from DI then fallback to default options
return httpContext.RequestServices?.GetService<IOptions<JsonOptions>>()?.Value?.SerializerOptions ?? JsonOptions.DefaultSerializerOptions;
}
}
}

View File

@ -0,0 +1,11 @@
// 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.
namespace Microsoft.AspNetCore.Http
{
internal static class JsonConstants
{
public const string JsonContentType = "application/json";
public const string JsonContentTypeWithCharset = "application/json; charset=utf-8";
}
}

View File

@ -0,0 +1,25 @@
// 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.
using System.Text.Encodings.Web;
using System.Text.Json;
#nullable enable
namespace Microsoft.AspNetCore.Http.Json
{
public class JsonOptions
{
internal static readonly JsonSerializerOptions DefaultSerializerOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web)
{
// Web defaults don't use the relex JSON escaping encoder.
//
// Because these options are for producing content that is written directly to the request
// (and not embedded in an HTML page for example), we can use UnsafeRelaxedJsonEscaping.
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
};
// Use a copy so the defaults are not modified.
public JsonSerializerOptions SerializerOptions { get; } = new JsonSerializerOptions(DefaultSerializerOptions);
}
}

View File

@ -0,0 +1,6 @@
// 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.
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Http.Extensions.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]

View File

@ -0,0 +1,32 @@
// 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.
using Xunit;
#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());
}
}
}

View File

@ -0,0 +1,215 @@
// 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.
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http.Json;
using Xunit;
#nullable enable
namespace Microsoft.AspNetCore.Http.Extensions.Tests
{
public class HttpRequestJsonExtensionsTests
{
[Fact]
public async Task ReadFromJsonAsyncGeneric_NonJsonContentType_ThrowError()
{
// Arrange
var context = new DefaultHttpContext();
context.Request.ContentType = "text/json";
// Act
var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () => await context.Request.ReadFromJsonAsync<int>());
// Assert
var exceptedMessage = $"Unable to read the request as JSON because the request content type 'text/json' is not a known JSON content type.";
Assert.Equal(exceptedMessage, ex.Message);
}
[Fact]
public async Task ReadFromJsonAsyncGeneric_NoBodyContent_ThrowError()
{
// Arrange
var context = new DefaultHttpContext();
context.Request.ContentType = "application/json";
// Act
var ex = await Assert.ThrowsAsync<JsonException>(async () => await context.Request.ReadFromJsonAsync<int>());
// Assert
var exceptedMessage = $"The input does not contain any JSON tokens. Expected the input to start with a valid JSON token, when isFinalBlock is true. Path: $ | LineNumber: 0 | BytePositionInLine: 0.";
Assert.Equal(exceptedMessage, ex.Message);
}
[Fact]
public async Task ReadFromJsonAsyncGeneric_ValidBodyContent_ReturnValue()
{
// Arrange
var context = new DefaultHttpContext();
context.Request.ContentType = "application/json";
context.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes("1"));
// Act
var result = await context.Request.ReadFromJsonAsync<int>();
// Assert
Assert.Equal(1, result);
}
[Fact]
public async Task ReadFromJsonAsyncGeneric_WithOptions_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;
// Act
var result = await context.Request.ReadFromJsonAsync<List<int>>(options);
// Assert
Assert.Collection(result,
i => Assert.Equal(1, i),
i => Assert.Equal(2, i));
}
[Fact]
public async Task ReadFromJsonAsyncGeneric_Utf8Encoding_ReturnValue()
{
// Arrange
var context = new DefaultHttpContext();
context.Request.ContentType = "application/json; charset=utf-8";
context.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes("[1,2]"));
// Act
var result = await context.Request.ReadFromJsonAsync<List<int>>();
// Assert
Assert.Collection(result,
i => Assert.Equal(1, i),
i => Assert.Equal(2, i));
}
[Fact]
public async Task ReadFromJsonAsyncGeneric_Utf16Encoding_ReturnValue()
{
// Arrange
var context = new DefaultHttpContext();
context.Request.ContentType = "application/json; charset=utf-16";
context.Request.Body = new MemoryStream(Encoding.Unicode.GetBytes(@"{""name"": ""激光這兩個字是甚麼意思""}"));
// Act
var result = await context.Request.ReadFromJsonAsync<Dictionary<string, string>>();
// Assert
Assert.Equal("激光這兩個字是甚麼意思", result["name"]);
}
[Fact]
public async Task ReadFromJsonAsyncGeneric_WithCancellationToken_CancellationRaised()
{
// Arrange
var context = new DefaultHttpContext();
context.Request.ContentType = "application /json";
context.Request.Body = new TestStream();
var cts = new CancellationTokenSource();
// Act
var readTask = context.Request.ReadFromJsonAsync<List<int>>(cts.Token);
Assert.False(readTask.IsCompleted);
cts.Cancel();
// Assert
await Assert.ThrowsAsync<TaskCanceledException>(async () => await readTask);
}
[Fact]
public async Task ReadFromJsonAsyncGeneric_InvalidEncoding_ThrowError()
{
// Arrange
var context = new DefaultHttpContext();
context.Request.ContentType = "application/json; charset=invalid";
// Act
var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () => await context.Request.ReadFromJsonAsync<object>());
// Assert
Assert.Equal("Unable to read the request as JSON because the request content type charset 'invalid' is not a known encoding.", ex.Message);
}
[Fact]
public async Task ReadFromJsonAsync_ValidBodyContent_ReturnValue()
{
// Arrange
var context = new DefaultHttpContext();
context.Request.ContentType = "application/json";
context.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes("1"));
// Act
var result = (int?)await context.Request.ReadFromJsonAsync(typeof(int));
// Assert
Assert.Equal(1, result);
}
[Fact]
public async Task ReadFromJsonAsync_Utf16Encoding_ReturnValue()
{
// Arrange
var context = new DefaultHttpContext();
context.Request.ContentType = "application/json; charset=utf-16";
context.Request.Body = new MemoryStream(Encoding.Unicode.GetBytes(@"{""name"": ""激光這兩個字是甚麼意思""}"));
// Act
var result = (Dictionary<string, string>?)await context.Request.ReadFromJsonAsync(typeof(Dictionary<string, string>));
// Assert
Assert.Equal("激光這兩個字是甚麼意思", result!["name"]);
}
[Fact]
public async Task ReadFromJsonAsync_InvalidEncoding_ThrowError()
{
// Arrange
var context = new DefaultHttpContext();
context.Request.ContentType = "application/json; charset=invalid";
// Act
var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () => await context.Request.ReadFromJsonAsync(typeof(object)));
// Assert
Assert.Equal("Unable to read the request as JSON because the request content type charset 'invalid' is not a known encoding.", ex.Message);
}
[Fact]
public async Task ReadFromJsonAsync_WithOptions_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;
// Act
var result = (List<int>?)await context.Request.ReadFromJsonAsync(typeof(List<int>), options);
// Assert
Assert.Collection(result,
i => Assert.Equal(1, i),
i => Assert.Equal(2, i));
}
}
}

View File

@ -0,0 +1,279 @@
// 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.
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http.Json;
using Xunit;
#nullable enable
namespace Microsoft.AspNetCore.Http.Extensions.Tests
{
public class HttpResponseJsonExtensionsTests
{
[Fact]
public async Task WriteAsJsonAsyncGeneric_SimpleValue_JsonResponse()
{
// Arrange
var body = new MemoryStream();
var context = new DefaultHttpContext();
context.Response.Body = body;
// Act
await context.Response.WriteAsJsonAsync(1);
// Assert
Assert.Equal(JsonConstants.JsonContentTypeWithCharset, context.Response.ContentType);
Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);
var data = body.ToArray();
Assert.Collection(data, b => Assert.Equal((byte)'1', b));
}
[Fact]
public async Task WriteAsJsonAsyncGeneric_NullValue_JsonResponse()
{
// Arrange
var body = new MemoryStream();
var context = new DefaultHttpContext();
context.Response.Body = body;
// Act
await context.Response.WriteAsJsonAsync<Uri>(value: null);
// Assert
Assert.Equal(JsonConstants.JsonContentTypeWithCharset, context.Response.ContentType);
var data = Encoding.UTF8.GetString(body.ToArray());
Assert.Equal("null", data);
}
[Fact]
public async Task WriteAsJsonAsyncGeneric_WithOptions_JsonResponse()
{
// Arrange
var body = new MemoryStream();
var context = new DefaultHttpContext();
context.Response.Body = body;
// Act
var options = new JsonSerializerOptions();
options.Converters.Add(new IntegerConverter());
await context.Response.WriteAsJsonAsync(new int[] { 1, 2, 3 }, options);
// Assert
Assert.Equal(JsonConstants.JsonContentTypeWithCharset, context.Response.ContentType);
var data = Encoding.UTF8.GetString(body.ToArray());
Assert.Equal("[false,true,false]", data);
}
private class IntegerConverter : JsonConverter<int>
{
public override int Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
throw new NotImplementedException();
}
public override void Write(Utf8JsonWriter writer, int value, JsonSerializerOptions options)
{
writer.WriteBooleanValue(value % 2 == 0);
}
}
[Fact]
public async Task WriteAsJsonAsyncGeneric_WithContentType_JsonResponseWithCustomContentType()
{
// Arrange
var body = new MemoryStream();
var context = new DefaultHttpContext();
context.Response.Body = body;
// Act
await context.Response.WriteAsJsonAsync(1, options: null, contentType: "application/custom-type");
// Assert
Assert.Equal("application/custom-type", context.Response.ContentType);
}
[Fact]
public async Task WriteAsJsonAsyncGeneric_WithCancellationToken_CancellationRaised()
{
// Arrange
var context = new DefaultHttpContext();
context.Response.Body = new TestStream();
var cts = new CancellationTokenSource();
// Act
var writeTask = context.Response.WriteAsJsonAsync(1, cts.Token);
Assert.False(writeTask.IsCompleted);
cts.Cancel();
// Assert
await Assert.ThrowsAsync<TaskCanceledException>(async () => await writeTask);
}
[Fact]
public async Task WriteAsJsonAsyncGeneric_ObjectWithStrings_CamcelCaseAndNotEscaped()
{
// Arrange
var body = new MemoryStream();
var context = new DefaultHttpContext();
context.Response.Body = body;
var value = new TestObject
{
StringProperty = "激光這兩個字是甚麼意思"
};
// Act
await context.Response.WriteAsJsonAsync(value);
// Assert
var data = Encoding.UTF8.GetString(body.ToArray());
Assert.Equal(@"{""stringProperty"":""激光這兩個字是甚麼意思""}", data);
}
[Fact]
public async Task WriteAsJsonAsync_SimpleValue_JsonResponse()
{
// Arrange
var body = new MemoryStream();
var context = new DefaultHttpContext();
context.Response.Body = body;
// Act
await context.Response.WriteAsJsonAsync(1, typeof(int));
// Assert
Assert.Equal(JsonConstants.JsonContentTypeWithCharset, context.Response.ContentType);
Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);
var data = body.ToArray();
Assert.Collection(data, b => Assert.Equal((byte)'1', b));
}
[Fact]
public async Task WriteAsJsonAsync_NullValue_JsonResponse()
{
// Arrange
var body = new MemoryStream();
var context = new DefaultHttpContext();
context.Response.Body = body;
// Act
await context.Response.WriteAsJsonAsync(value: null, typeof(int?));
// Assert
Assert.Equal(JsonConstants.JsonContentTypeWithCharset, context.Response.ContentType);
var data = Encoding.UTF8.GetString(body.ToArray());
Assert.Equal("null", data);
}
[Fact]
public async Task WriteAsJsonAsync_NullType_ThrowsArgumentNullException()
{
// Arrange
var body = new MemoryStream();
var context = new DefaultHttpContext();
context.Response.Body = body;
// Act & Assert
await Assert.ThrowsAsync<ArgumentNullException>(async () => await context.Response.WriteAsJsonAsync(value: null, type: null!));
}
[Fact]
public async Task WriteAsJsonAsync_NullResponse_ThrowsArgumentNullException()
{
// Arrange
var body = new MemoryStream();
var context = new DefaultHttpContext();
context.Response.Body = body;
// Act & Assert
await Assert.ThrowsAsync<ArgumentNullException>(async () => await HttpResponseJsonExtensions.WriteAsJsonAsync(response: null!, value: null, typeof(int?)));
}
[Fact]
public async Task WriteAsJsonAsync_ObjectWithStrings_CamcelCaseAndNotEscaped()
{
// Arrange
var body = new MemoryStream();
var context = new DefaultHttpContext();
context.Response.Body = body;
var value = new TestObject
{
StringProperty = "激光這兩個字是甚麼意思"
};
// Act
await context.Response.WriteAsJsonAsync(value, typeof(TestObject));
// Assert
var data = Encoding.UTF8.GetString(body.ToArray());
Assert.Equal(@"{""stringProperty"":""激光這兩個字是甚麼意思""}", data);
}
public class TestObject
{
public string? StringProperty { get; set; }
}
private class TestStream : Stream
{
public override bool CanRead { get; }
public override bool CanSeek { get; }
public override bool CanWrite { get; }
public override long Length { get; }
public override long Position { get; set; }
public override void Flush()
{
throw new NotImplementedException();
}
public override int Read(byte[] buffer, int offset, int count)
{
throw new NotImplementedException();
}
public override long Seek(long offset, SeekOrigin origin)
{
throw new NotImplementedException();
}
public override void SetLength(long value)
{
throw new NotImplementedException();
}
public override void Write(byte[] buffer, int offset, int count)
{
throw new NotImplementedException();
}
public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
{
var tcs = new TaskCompletionSource<int>();
cancellationToken.Register(s => ((TaskCompletionSource<int>)s!).SetCanceled(), tcs);
return new ValueTask<int>(tcs.Task);
}
public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default)
{
var tcs = new TaskCompletionSource<int>();
cancellationToken.Register(s => ((TaskCompletionSource<int>)s!).SetCanceled(), tcs);
return new ValueTask(tcs.Task);
}
}
}
}

View File

@ -1,4 +1,5 @@
// Copyright (c) .NET Foundation. All rights reserved. See License.txt in the project root for license information.
// 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.
using System.IO;
using System.IO.Pipelines;

View File

@ -0,0 +1,58 @@
// 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.
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.AspNetCore.Http.Extensions.Tests
{
public class TestStream : Stream
{
public override bool CanRead { get; }
public override bool CanSeek { get; }
public override bool CanWrite { get; }
public override long Length { get; }
public override long Position { get; set; }
public override void Flush()
{
throw new NotImplementedException();
}
public override int Read(byte[] buffer, int offset, int count)
{
throw new NotImplementedException();
}
public override long Seek(long offset, SeekOrigin origin)
{
throw new NotImplementedException();
}
public override void SetLength(long value)
{
throw new NotImplementedException();
}
public override void Write(byte[] buffer, int offset, int count)
{
throw new NotImplementedException();
}
public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
{
var tcs = new TaskCompletionSource<int>();
cancellationToken.Register(s => ((TaskCompletionSource<int>)s).SetCanceled(), tcs);
return new ValueTask<int>(tcs.Task);
}
public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default)
{
var tcs = new TaskCompletionSource<int>();
cancellationToken.Register(s => ((TaskCompletionSource<int>)s).SetCanceled(), tcs);
return new ValueTask(tcs.Task);
}
}
}