Add support for System.Text.Json based JsonResult executor (#9558)
Fixes https://github.com/aspnet/AspNetCore/issues/9554
This commit is contained in:
parent
b893777898
commit
5e953124e6
|
|
@ -24,6 +24,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Json
|
|||
private ArraySegment<byte> _byteBuffer;
|
||||
private ArraySegment<char> _charBuffer;
|
||||
private ArraySegment<byte> _overflowBuffer;
|
||||
private bool _disposed;
|
||||
|
||||
public TranscodingReadStream(Stream input, Encoding sourceEncoding)
|
||||
{
|
||||
|
|
@ -150,9 +151,9 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Json
|
|||
readBuffer = readBuffer.Slice(bytesEncoded);
|
||||
}
|
||||
|
||||
// We need to exit in one of the 2 conditions:
|
||||
// * encoderCompleted will return false if "buffer" was too small for all the chars to be encoded.
|
||||
// * no bytes were converted in an iteration. This can occur if there wasn't any input.
|
||||
// We need to exit in one of the 2 conditions:
|
||||
// * encoderCompleted will return false if "buffer" was too small for all the chars to be encoded.
|
||||
// * no bytes were converted in an iteration. This can occur if there wasn't any input.
|
||||
} while (encoderCompleted && bytesEncoded > 0);
|
||||
|
||||
return totalBytes;
|
||||
|
|
@ -224,11 +225,13 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Json
|
|||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
ArrayPool<char>.Shared.Return(_charBuffer.Array);
|
||||
ArrayPool<byte>.Shared.Return(_byteBuffer.Array);
|
||||
ArrayPool<byte>.Shared.Return(_overflowBuffer.Array);
|
||||
|
||||
base.Dispose(disposing);
|
||||
if (!_disposed)
|
||||
{
|
||||
_disposed = true;
|
||||
ArrayPool<char>.Shared.Return(_charBuffer.Array);
|
||||
ArrayPool<byte>.Shared.Return(_byteBuffer.Array);
|
||||
ArrayPool<byte>.Shared.Return(_overflowBuffer.Array);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Json
|
|||
private readonly Encoder _encoder;
|
||||
private readonly char[] _charBuffer;
|
||||
private int _charsDecoded;
|
||||
private bool _disposed;
|
||||
|
||||
public TranscodingWriteStream(Stream stream, Encoding targetEncoding)
|
||||
{
|
||||
|
|
@ -154,13 +155,11 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Json
|
|||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
if (!_disposed)
|
||||
{
|
||||
_disposed = true;
|
||||
ArrayPool<char>.Shared.Return(_charBuffer);
|
||||
}
|
||||
|
||||
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,133 @@
|
|||
// 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.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.Core;
|
||||
using Microsoft.AspNetCore.Mvc.Formatters;
|
||||
using Microsoft.AspNetCore.Mvc.Formatters.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||
{
|
||||
internal sealed class SystemTextJsonResultExecutor : IActionResultExecutor<JsonResult>
|
||||
{
|
||||
private static readonly string DefaultContentType = new MediaTypeHeaderValue("application/json")
|
||||
{
|
||||
Encoding = Encoding.UTF8
|
||||
}.ToString();
|
||||
|
||||
private readonly MvcOptions _mvcOptions;
|
||||
private readonly ILogger<SystemTextJsonResultExecutor> _logger;
|
||||
|
||||
public SystemTextJsonResultExecutor(
|
||||
IOptions<MvcOptions> mvcOptions,
|
||||
ILogger<SystemTextJsonResultExecutor> logger)
|
||||
{
|
||||
_mvcOptions = mvcOptions.Value;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task ExecuteAsync(ActionContext context, JsonResult result)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(result));
|
||||
}
|
||||
|
||||
var jsonSerializerOptions = GetSerializerOptions(result);
|
||||
|
||||
var response = context.HttpContext.Response;
|
||||
|
||||
ResponseContentTypeHelper.ResolveContentTypeAndEncoding(
|
||||
result.ContentType,
|
||||
response.ContentType,
|
||||
DefaultContentType,
|
||||
out var resolvedContentType,
|
||||
out var resolvedContentTypeEncoding);
|
||||
|
||||
response.ContentType = resolvedContentType;
|
||||
|
||||
if (result.StatusCode != null)
|
||||
{
|
||||
response.StatusCode = result.StatusCode.Value;
|
||||
}
|
||||
|
||||
Log.JsonResultExecuting(_logger, result.Value);
|
||||
|
||||
// Keep this code in sync with SystemTextJsonOutputFormatter
|
||||
var writeStream = GetWriteStream(context.HttpContext, resolvedContentTypeEncoding);
|
||||
try
|
||||
{
|
||||
var type = result.Value?.GetType() ?? typeof(object);
|
||||
await JsonSerializer.WriteAsync(result.Value, type, writeStream, jsonSerializerOptions);
|
||||
await writeStream.FlushAsync();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (writeStream is TranscodingWriteStream transcoding)
|
||||
{
|
||||
await transcoding.DisposeAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Stream GetWriteStream(HttpContext httpContext, Encoding selectedEncoding)
|
||||
{
|
||||
if (selectedEncoding.CodePage == Encoding.UTF8.CodePage)
|
||||
{
|
||||
// JsonSerializer does not write a BOM. Therefore we do not have to handle it
|
||||
// in any special way.
|
||||
return httpContext.Response.Body;
|
||||
}
|
||||
|
||||
return new TranscodingWriteStream(httpContext.Response.Body, selectedEncoding);
|
||||
}
|
||||
|
||||
private JsonSerializerOptions GetSerializerOptions(JsonResult result)
|
||||
{
|
||||
var serializerSettings = result.SerializerSettings;
|
||||
if (serializerSettings == null)
|
||||
{
|
||||
return _mvcOptions.SerializerOptions;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!(serializerSettings is JsonSerializerOptions settingsFromResult))
|
||||
{
|
||||
throw new InvalidOperationException(Resources.FormatProperty_MustBeInstanceOfType(
|
||||
nameof(JsonResult),
|
||||
nameof(JsonResult.SerializerSettings),
|
||||
typeof(JsonSerializerOptions)));
|
||||
}
|
||||
|
||||
return settingsFromResult;
|
||||
}
|
||||
}
|
||||
|
||||
private static class Log
|
||||
{
|
||||
private static readonly Action<ILogger, string, Exception> _jsonResultExecuting = LoggerMessage.Define<string>(
|
||||
LogLevel.Information,
|
||||
new EventId(1, "JsonResultExecuting"),
|
||||
"Executing JsonResult, writing value of type '{Type}'.");
|
||||
|
||||
public static void JsonResultExecuting(ILogger logger, object value)
|
||||
{
|
||||
var type = value == null ? "null" : value.GetType().FullName;
|
||||
_jsonResultExecuting(logger, type, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -68,17 +68,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
}
|
||||
|
||||
var services = context.HttpContext.RequestServices;
|
||||
var executor = services.GetService<IActionResultExecutor<JsonResult>>();
|
||||
if (executor == null)
|
||||
{
|
||||
throw new InvalidOperationException(Resources.FormatReferenceToNewtonsoftJsonRequired(
|
||||
$"{nameof(JsonResult)}.{nameof(ExecuteResultAsync)}",
|
||||
"Microsoft.AspNetCore.Mvc.NewtonsoftJson",
|
||||
nameof(IMvcBuilder),
|
||||
"AddNewtonsoftJson",
|
||||
"ConfigureServices(...)"));
|
||||
}
|
||||
|
||||
var executor = services.GetRequiredService<IActionResultExecutor<JsonResult>>();
|
||||
return executor.ExecuteAsync(context, this);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ using Microsoft.AspNetCore.Mvc.Formatters;
|
|||
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Mvc.Formatters.Xml.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
|
||||
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Mvc.IntegrationTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
|
||||
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Mvc.Localization.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
|
||||
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Mvc.NewtonsoftJson.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
|
||||
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Mvc.Razor.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
|
||||
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Mvc.RazorPages.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
|
||||
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Mvc.TagHelpers.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
|
||||
|
|
|
|||
|
|
@ -1705,7 +1705,7 @@ namespace Microsoft.AspNetCore.Mvc.Core
|
|||
=> string.Format(CultureInfo.CurrentCulture, GetString("ReferenceToNewtonsoftJsonRequired"), p0, p1, p2, p3, p4);
|
||||
|
||||
/// <summary>
|
||||
/// Collection bound to '{0}' exceeded {1}.{2} ({3}). This limit is a safeguard against incorrect model binders and models. Address issues in '{4}'. For example, this type may have a property with a model binder that always succeeds. See the {0}.{1} documentation for more information.
|
||||
/// Collection bound to '{0}' exceeded {1}.{2} ({3}). This limit is a safeguard against incorrect model binders and models. Address issues in '{4}'. For example, this type may have a property with a model binder that always succeeds. See the {1}.{2} documentation for more information.
|
||||
/// </summary>
|
||||
internal static string ModelBinding_ExceededMaxModelBindingCollectionSize
|
||||
{
|
||||
|
|
@ -1713,7 +1713,7 @@ namespace Microsoft.AspNetCore.Mvc.Core
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Collection bound to '{0}' exceeded {1}.{2} ({3}). This limit is a safeguard against incorrect model binders and models. Address issues in '{4}'. For example, this type may have a property with a model binder that always succeeds. See the {0}.{1} documentation for more information.
|
||||
/// Collection bound to '{0}' exceeded {1}.{2} ({3}). This limit is a safeguard against incorrect model binders and models. Address issues in '{4}'. For example, this type may have a property with a model binder that always succeeds. See the {1}.{2} documentation for more information.
|
||||
/// </summary>
|
||||
internal static string FormatModelBinding_ExceededMaxModelBindingCollectionSize(object p0, object p1, object p2, object p3, object p4)
|
||||
=> string.Format(CultureInfo.CurrentCulture, GetString("ModelBinding_ExceededMaxModelBindingCollectionSize"), p0, p1, p2, p3, p4);
|
||||
|
|
@ -1732,6 +1732,20 @@ namespace Microsoft.AspNetCore.Mvc.Core
|
|||
internal static string FormatModelBinding_ExceededMaxModelBindingRecursionDepth(object p0, object p1, object p2, object p3)
|
||||
=> string.Format(CultureInfo.CurrentCulture, GetString("ModelBinding_ExceededMaxModelBindingRecursionDepth"), p0, p1, p2, p3);
|
||||
|
||||
/// <summary>
|
||||
/// Property '{0}.{1}' must be an instance of type '{2}'.
|
||||
/// </summary>
|
||||
internal static string Property_MustBeInstanceOfType
|
||||
{
|
||||
get => GetString("Property_MustBeInstanceOfType");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Property '{0}.{1}' must be an instance of type '{2}'.
|
||||
/// </summary>
|
||||
internal static string FormatProperty_MustBeInstanceOfType(object p0, object p1, object p2)
|
||||
=> string.Format(CultureInfo.CurrentCulture, GetString("Property_MustBeInstanceOfType"), p0, p1, p2);
|
||||
|
||||
private static string GetString(string name, params string[] formatterNames)
|
||||
{
|
||||
var value = _resourceManager.GetString(name);
|
||||
|
|
|
|||
|
|
@ -501,4 +501,7 @@
|
|||
<value>Model binding system exceeded {0}.{1} ({2}). Reduce the potential nesting of '{3}'. For example, this type may have a property with a model binder that always succeeds. See the {0}.{1} documentation for more information.</value>
|
||||
<comment>{0} is MvcOptions, {1} is MaxModelBindingRecursionDepth, {2} is option value, {3} is (loopy or deeply nested) top-level model type.</comment>
|
||||
</data>
|
||||
</root>
|
||||
<data name="Property_MustBeInstanceOfType" xml:space="preserve">
|
||||
<value>Property '{0}.{1}' must be an instance of type '{2}'.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
|
|
|||
|
|
@ -2,11 +2,10 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
|
@ -15,21 +14,20 @@ using Microsoft.AspNetCore.Routing;
|
|||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Extensions.Logging.Testing;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
using Moq;
|
||||
using Newtonsoft.Json;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson
|
||||
namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||
{
|
||||
public class JsonResultExecutorTest
|
||||
public abstract class JsonResultExecutorTestBase
|
||||
{
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_UsesDefaultContentType_IfNoContentTypeSpecified()
|
||||
{
|
||||
// Arrange
|
||||
var expected = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(new { foo = "abcd" }));
|
||||
var expected = Encoding.UTF8.GetBytes(JsonSerializer.ToString(new { foo = "abcd" }));
|
||||
|
||||
var context = GetActionContext();
|
||||
|
||||
|
|
@ -49,7 +47,7 @@ namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson
|
|||
public async Task ExecuteAsync_NullEncoding_DoesNotSetCharsetOnContentType()
|
||||
{
|
||||
// Arrange
|
||||
var expected = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(new { foo = "abcd" }));
|
||||
var expected = Encoding.UTF8.GetBytes(JsonSerializer.ToString(new { foo = "abcd" }));
|
||||
|
||||
var context = GetActionContext();
|
||||
|
||||
|
|
@ -66,11 +64,55 @@ namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson
|
|||
Assert.Equal("text/json", context.HttpContext.Response.ContentType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_UsesEncodingSpecifiedInContentType()
|
||||
{
|
||||
// Arrange
|
||||
var expected = Encoding.Unicode.GetBytes(JsonSerializer.ToString(new { foo = "abcd" }));
|
||||
|
||||
var context = GetActionContext();
|
||||
context.HttpContext.Response.ContentType = "text/json; charset=utf-8";
|
||||
|
||||
var result = new JsonResult(new { foo = "abcd" })
|
||||
{
|
||||
ContentType = "text/json; charset=utf-16",
|
||||
};
|
||||
var executor = CreateExecutor();
|
||||
|
||||
// Act
|
||||
await executor.ExecuteAsync(context, result);
|
||||
|
||||
// Assert
|
||||
var written = GetWrittenBytes(context.HttpContext);
|
||||
Assert.Equal(expected, written);
|
||||
Assert.Equal("text/json; charset=utf-16", context.HttpContext.Response.ContentType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_UsesEncodingSpecifiedInResponseContentType()
|
||||
{
|
||||
// Arrange
|
||||
var expected = Encoding.Unicode.GetBytes(JsonSerializer.ToString(new { foo = "abcd" }));
|
||||
|
||||
var context = GetActionContext();
|
||||
context.HttpContext.Response.ContentType = "text/json; charset=utf-16";
|
||||
var result = new JsonResult(new { foo = "abcd" });
|
||||
var executor = CreateExecutor();
|
||||
|
||||
// Act
|
||||
await executor.ExecuteAsync(context, result);
|
||||
|
||||
// Assert
|
||||
var written = GetWrittenBytes(context.HttpContext);
|
||||
Assert.Equal(expected, written);
|
||||
Assert.Equal("text/json; charset=utf-16", context.HttpContext.Response.ContentType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_SetsContentTypeAndEncoding()
|
||||
{
|
||||
// Arrange
|
||||
var expected = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(new { foo = "abcd" }));
|
||||
var expected = Encoding.UTF8.GetBytes(JsonSerializer.ToString(new { foo = "abcd" }));
|
||||
|
||||
var context = GetActionContext();
|
||||
|
||||
|
|
@ -94,7 +136,7 @@ namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson
|
|||
public async Task ExecuteAsync_NoResultContentTypeSet_UsesResponseContentType_AndSuppliedEncoding()
|
||||
{
|
||||
// Arrange
|
||||
var expected = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(new { foo = "abcd" }));
|
||||
var expected = Encoding.UTF8.GetBytes(JsonSerializer.ToString(new { foo = "abcd" }));
|
||||
var expectedContentType = "text/foo; p1=p1-value; charset=us-ascii";
|
||||
|
||||
var context = GetActionContext();
|
||||
|
|
@ -120,7 +162,7 @@ namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson
|
|||
string expectedContentType)
|
||||
{
|
||||
// Arrange
|
||||
var expected = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(new { foo = "abcd" }));
|
||||
var expected = Encoding.UTF8.GetBytes(JsonSerializer.ToString(new { foo = "abcd" }));
|
||||
|
||||
var context = GetActionContext();
|
||||
context.HttpContext.Response.ContentType = responseContentType;
|
||||
|
|
@ -141,14 +183,13 @@ namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson
|
|||
public async Task ExecuteAsync_UsesPassedInSerializerSettings()
|
||||
{
|
||||
// Arrange
|
||||
var expected = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(
|
||||
var expected = Encoding.UTF8.GetBytes(JsonSerializer.ToString(
|
||||
new { foo = "abcd" },
|
||||
Formatting.Indented));
|
||||
new JsonSerializerOptions { WriteIndented = true }));
|
||||
|
||||
var context = GetActionContext();
|
||||
|
||||
var serializerSettings = new JsonSerializerSettings();
|
||||
serializerSettings.Formatting = Formatting.Indented;
|
||||
var serializerSettings = GetIndentedSettings();
|
||||
|
||||
var result = new JsonResult(new { foo = "abcd" }, serializerSettings);
|
||||
var executor = CreateExecutor();
|
||||
|
|
@ -162,6 +203,8 @@ namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson
|
|||
Assert.Equal("application/json; charset=utf-8", context.HttpContext.Response.ContentType);
|
||||
}
|
||||
|
||||
protected abstract object GetIndentedSettings();
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_ErrorDuringSerialization_DoesNotWriteContent()
|
||||
{
|
||||
|
|
@ -175,7 +218,11 @@ namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson
|
|||
{
|
||||
await executor.ExecuteAsync(context, result);
|
||||
}
|
||||
catch (JsonSerializationException serializerException)
|
||||
catch (NotImplementedException ex)
|
||||
{
|
||||
Assert.Equal("Property Age has not been implemented", ex.Message);
|
||||
}
|
||||
catch (Exception serializerException)
|
||||
{
|
||||
var expectedException = Assert.IsType<NotImplementedException>(serializerException.InnerException);
|
||||
Assert.Equal("Property Age has not been implemented", expectedException.Message);
|
||||
|
|
@ -192,15 +239,16 @@ namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson
|
|||
// Arrange
|
||||
var expected = "Executing JsonResult, writing value of type 'System.String'.";
|
||||
var context = GetActionContext();
|
||||
var logger = new StubLogger();
|
||||
var executer = CreateExecutor(logger);
|
||||
var sink = new TestSink();
|
||||
var executor = CreateExecutor(new TestLoggerFactory(sink, enabled: true));
|
||||
var result = new JsonResult("result_value");
|
||||
|
||||
// Act
|
||||
await executer.ExecuteAsync(context, result);
|
||||
await executor.ExecuteAsync(context, result);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, logger.MostRecentMessage);
|
||||
var write = Assert.Single(sink.Writes);
|
||||
Assert.Equal(expected, write.State.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -209,26 +257,28 @@ namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson
|
|||
// Arrange
|
||||
var expected = "Executing JsonResult, writing value of type 'null'.";
|
||||
var context = GetActionContext();
|
||||
var logger = new StubLogger();
|
||||
var executer = CreateExecutor(logger);
|
||||
var sink = new TestSink();
|
||||
var executor = CreateExecutor(new TestLoggerFactory(sink, enabled: true));
|
||||
var result = new JsonResult(null);
|
||||
|
||||
// Act
|
||||
await executer.ExecuteAsync(context, result);
|
||||
await executor.ExecuteAsync(context, result);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, logger.MostRecentMessage);
|
||||
var write = Assert.Single(sink.Writes);
|
||||
Assert.Equal(expected, write.State.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_LargePayload_DoesNotPerformSynchronousWrites()
|
||||
{
|
||||
// Arrange
|
||||
var model = Enumerable.Range(0, 1000).Select(p => new TestModel { Property = new string('a', 5000)});
|
||||
var model = Enumerable.Range(0, 1000).Select(p => new TestModel { Property = new string('a', 5000) }).ToArray();
|
||||
|
||||
var stream = new Mock<Stream> { CallBase = true };
|
||||
var stream = new Mock<Stream>();
|
||||
stream.Setup(v => v.WriteAsync(It.IsAny<byte[]>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<CancellationToken>()))
|
||||
.Returns(Task.CompletedTask);
|
||||
.Returns(Task.CompletedTask)
|
||||
.Verifiable();
|
||||
stream.SetupGet(s => s.CanWrite).Returns(true);
|
||||
var context = GetActionContext();
|
||||
context.HttpContext.Response.Body = stream.Object;
|
||||
|
|
@ -240,22 +290,30 @@ namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson
|
|||
await executor.ExecuteAsync(context, result);
|
||||
|
||||
// Assert
|
||||
stream.Verify(v => v.WriteAsync(It.IsAny<byte[]>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<CancellationToken>()), Times.AtLeastOnce());
|
||||
|
||||
stream.Verify(v => v.Write(It.IsAny<byte[]>(), It.IsAny<int>(), It.IsAny<int>()), Times.Never());
|
||||
stream.Verify(v => v.Flush(), Times.Never());
|
||||
}
|
||||
|
||||
private static JsonResultExecutor CreateExecutor(ILogger<JsonResultExecutor> logger = null)
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_ThrowsIfSerializerSettingIsNotTheCorrectType()
|
||||
{
|
||||
return new JsonResultExecutor(
|
||||
new TestHttpResponseStreamWriterFactory(),
|
||||
logger ?? NullLogger<JsonResultExecutor>.Instance,
|
||||
Options.Create(new MvcOptions()),
|
||||
Options.Create(new MvcNewtonsoftJsonOptions()),
|
||||
ArrayPool<char>.Shared);
|
||||
// Arrange
|
||||
var context = GetActionContext();
|
||||
|
||||
var result = new JsonResult(new { foo = "abcd" }, new object());
|
||||
var executor = CreateExecutor();
|
||||
|
||||
// Act & Assert
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => executor.ExecuteAsync(context, result));
|
||||
|
||||
// Assert
|
||||
Assert.StartsWith("Property 'JsonResult.SerializerSettings' must be an instance of type", ex.Message);
|
||||
}
|
||||
|
||||
protected IActionResultExecutor<JsonResult> CreateExecutor() => CreateExecutor(NullLoggerFactory.Instance);
|
||||
|
||||
protected abstract IActionResultExecutor<JsonResult> CreateExecutor(ILoggerFactory loggerFactory);
|
||||
|
||||
private static HttpContext GetHttpContext()
|
||||
{
|
||||
var httpContext = new DefaultHttpContext();
|
||||
|
|
@ -292,20 +350,6 @@ namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson
|
|||
}
|
||||
}
|
||||
|
||||
private class StubLogger : ILogger<JsonResultExecutor>
|
||||
{
|
||||
public string MostRecentMessage { get; private set; }
|
||||
|
||||
public IDisposable BeginScope<TState>(TState state) => throw new NotImplementedException();
|
||||
|
||||
public bool IsEnabled(LogLevel logLevel) => true;
|
||||
|
||||
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
|
||||
{
|
||||
MostRecentMessage = formatter(state, exception);
|
||||
}
|
||||
}
|
||||
|
||||
private class TestModel
|
||||
{
|
||||
public string Property { get; set; }
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
// 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.Json.Serialization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||
{
|
||||
public class SystemTextJsonResultExecutorTest : JsonResultExecutorTestBase
|
||||
{
|
||||
protected override IActionResultExecutor<JsonResult> CreateExecutor(ILoggerFactory loggerFactory)
|
||||
{
|
||||
return new SystemTextJsonResultExecutor(Options.Create(new MvcOptions()), loggerFactory.CreateLogger<SystemTextJsonResultExecutor>());
|
||||
}
|
||||
|
||||
protected override object GetIndentedSettings()
|
||||
{
|
||||
return new JsonSerializerOptions { WriteIndented = true };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,32 +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.
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Core
|
||||
{
|
||||
public class JsonResultTest
|
||||
{
|
||||
[Fact]
|
||||
public async Task ExecuteResultAsync_ThrowsIfExecutorIsNotAvailableInServices()
|
||||
{
|
||||
// Arrange
|
||||
var jsonResult = new JsonResult("Hello");
|
||||
var message = "'JsonResult.ExecuteResultAsync' requires a reference to 'Microsoft.AspNetCore.Mvc.NewtonsoftJson'. " +
|
||||
"Configure your application by adding a reference to the 'Microsoft.AspNetCore.Mvc.NewtonsoftJson' package and calling 'IMvcBuilder.AddNewtonsoftJson' " +
|
||||
"inside the call to 'ConfigureServices(...)' in the application startup code.";
|
||||
var actionContext = new ActionContext
|
||||
{
|
||||
HttpContext = new DefaultHttpContext { RequestServices = Mock.Of<IServiceProvider>() }
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => jsonResult.ExecuteResultAsync(actionContext));
|
||||
Assert.Equal(message, ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,11 +2,9 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.ApiExplorer;
|
||||
using Microsoft.AspNetCore.Mvc.Formatters;
|
||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||
using Microsoft.AspNetCore.Mvc.NewtonsoftJson;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
|
|
@ -73,14 +71,22 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
ServiceDescriptor.Transient<IConfigureOptions<MvcOptions>, NewtonsoftJsonMvcOptionsSetup>());
|
||||
services.TryAddEnumerable(
|
||||
ServiceDescriptor.Transient<IApiDescriptionProvider, JsonPatchOperationsArrayProvider>());
|
||||
services.TryAddSingleton<IActionResultExecutor<JsonResult>, JsonResultExecutor>();
|
||||
|
||||
|
||||
var jsonResultExecutor = services.FirstOrDefault(f =>
|
||||
f.ServiceType == typeof(IActionResultExecutor<JsonResult>) &&
|
||||
f.ImplementationType?.Assembly == typeof(JsonResult).Assembly);
|
||||
|
||||
if (jsonResultExecutor != null)
|
||||
{
|
||||
services.Remove(jsonResultExecutor);
|
||||
}
|
||||
services.TryAddSingleton<IActionResultExecutor<JsonResult>, NewtonsoftJsonResultExecutor>();
|
||||
|
||||
var viewFeaturesAssembly = typeof(IHtmlHelper).Assembly;
|
||||
|
||||
var tempDataSerializer = services.FirstOrDefault(f =>
|
||||
f.ServiceType == typeof(TempDataSerializer) &&
|
||||
f.ImplementationType?.Assembly == viewFeaturesAssembly &&
|
||||
f.ImplementationType.FullName == "Microsoft.AspNetCore.Mvc.ViewFeatures.Infrastructure.DefaultTempDataSerializer");
|
||||
f.ImplementationType?.Assembly == viewFeaturesAssembly);
|
||||
|
||||
if (tempDataSerializer != null)
|
||||
{
|
||||
|
|
@ -94,8 +100,7 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
//
|
||||
var jsonHelper = services.FirstOrDefault(
|
||||
f => f.ServiceType == typeof(IJsonHelper) &&
|
||||
f.ImplementationType?.Assembly == viewFeaturesAssembly &&
|
||||
f.ImplementationType.FullName == "Microsoft.AspNetCore.Mvc.Rendering.DefaultJsonHelper");
|
||||
f.ImplementationType?.Assembly == viewFeaturesAssembly);
|
||||
if (jsonHelper != null)
|
||||
{
|
||||
services.Remove(jsonHelper);
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson
|
|||
/// <summary>
|
||||
/// Executes a <see cref="JsonResult"/> to write to the response.
|
||||
/// </summary>
|
||||
internal class JsonResultExecutor : IActionResultExecutor<JsonResult>
|
||||
internal class NewtonsoftJsonResultExecutor : IActionResultExecutor<JsonResult>
|
||||
{
|
||||
private static readonly string DefaultContentType = new MediaTypeHeaderValue("application/json")
|
||||
{
|
||||
|
|
@ -34,16 +34,16 @@ namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson
|
|||
private readonly IArrayPool<char> _charPool;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="JsonResultExecutor"/>.
|
||||
/// Creates a new <see cref="NewtonsoftJsonResultExecutor"/>.
|
||||
/// </summary>
|
||||
/// <param name="writerFactory">The <see cref="IHttpResponseStreamWriterFactory"/>.</param>
|
||||
/// <param name="logger">The <see cref="ILogger{JsonResultExecutor}"/>.</param>
|
||||
/// <param name="logger">The <see cref="ILogger{NewtonsoftJsonResultExecutor}"/>.</param>
|
||||
/// <param name="mvcOptions">Accessor to <see cref="MvcOptions"/>.</param>
|
||||
/// <param name="jsonOptions">Accessor to <see cref="MvcNewtonsoftJsonOptions"/>.</param>
|
||||
/// <param name="charPool">The <see cref="ArrayPool{Char}"/> for creating <see cref="T:char[]"/> buffers.</param>
|
||||
public JsonResultExecutor(
|
||||
public NewtonsoftJsonResultExecutor(
|
||||
IHttpResponseStreamWriterFactory writerFactory,
|
||||
ILogger<JsonResultExecutor> logger,
|
||||
ILogger<NewtonsoftJsonResultExecutor> logger,
|
||||
IOptions<MvcOptions> mvcOptions,
|
||||
IOptions<MvcNewtonsoftJsonOptions> jsonOptions,
|
||||
ArrayPool<char> charPool)
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||
using Microsoft.AspNetCore.Mvc.NewtonsoftJson;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using Microsoft.AspNetCore.Mvc.ViewFeatures.Infrastructure;
|
||||
|
|
@ -59,5 +60,20 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
var tempDataSerializer = Assert.Single(services, d => d.ServiceType == typeof(TempDataSerializer));
|
||||
Assert.Same(typeof(BsonTempDataSerializer), tempDataSerializer.ImplementationType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddServicesCore_ReplacesDefaultJsonResultExecutor()
|
||||
{
|
||||
// Arrange
|
||||
var services = new ServiceCollection()
|
||||
.AddSingleton<IActionResultExecutor<JsonResult>, SystemTextJsonResultExecutor>();
|
||||
|
||||
// Act
|
||||
NewtonsoftJsonMvcCoreBuilderExtensions.AddServicesCore(services);
|
||||
|
||||
// Assert
|
||||
var jsonResultExecutor = Assert.Single(services, d => d.ServiceType == typeof(IActionResultExecutor<JsonResult>));
|
||||
Assert.Same(typeof(NewtonsoftJsonResultExecutor), jsonResultExecutor.ImplementationType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,9 +44,9 @@ namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson
|
|||
var httpContext = new DefaultHttpContext();
|
||||
httpContext.Response.Body = new MemoryStream();
|
||||
|
||||
var executor = new JsonResultExecutor(
|
||||
var executor = new NewtonsoftJsonResultExecutor(
|
||||
new TestHttpResponseStreamWriterFactory(),
|
||||
NullLogger<JsonResultExecutor>.Instance,
|
||||
NullLogger<NewtonsoftJsonResultExecutor>.Instance,
|
||||
Options.Create(new MvcOptions()),
|
||||
Options.Create(new MvcNewtonsoftJsonOptions()),
|
||||
ArrayPool<char>.Shared);
|
||||
|
|
@ -69,4 +69,4 @@ namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson
|
|||
return Assert.IsType<MemoryStream>(context.Response.Body).ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,11 +8,13 @@
|
|||
<Reference Include="Microsoft.AspNetCore.Mvc" />
|
||||
<Reference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" />
|
||||
<ProjectReference Include="..\..\shared\Mvc.Core.TestCommon\Microsoft.AspNetCore.Mvc.Core.TestCommon.csproj" />
|
||||
<Reference Include="Microsoft.Extensions.Logging.Testing" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="..\..\Mvc.Core\test\Formatters\JsonInputFormatterTestBase.cs" />
|
||||
<Compile Include="..\..\Mvc.Core\test\Formatters\JsonOutputFormatterTestBase.cs" />
|
||||
<Compile Include="..\..\Mvc.Core\test\Infrastructure\JsonResultExecutorTestBase.cs" />
|
||||
<Compile Include="..\..\Mvc.ViewFeatures\test\Infrastructure\TempDataSerializerTestBase.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
// 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.Buffers;
|
||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson
|
||||
{
|
||||
public class NewtonsoftJsonResultExecutorTest : JsonResultExecutorTestBase
|
||||
{
|
||||
protected override IActionResultExecutor<JsonResult> CreateExecutor(ILoggerFactory loggerFactory)
|
||||
{
|
||||
return new NewtonsoftJsonResultExecutor(
|
||||
new TestHttpResponseStreamWriterFactory(),
|
||||
loggerFactory.CreateLogger< NewtonsoftJsonResultExecutor>(),
|
||||
Options.Create(new MvcOptions()),
|
||||
Options.Create(new MvcNewtonsoftJsonOptions()),
|
||||
ArrayPool<char>.Shared);
|
||||
}
|
||||
|
||||
protected override object GetIndentedSettings()
|
||||
{
|
||||
return new JsonSerializerSettings { Formatting = Formatting.Indented };
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue