Add support for System.Text.Json based JsonResult executor (#9558)

Fixes https://github.com/aspnet/AspNetCore/issues/9554
This commit is contained in:
Pranav K 2019-04-21 09:13:30 -07:00 committed by GitHub
parent b893777898
commit 5e953124e6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 353 additions and 124 deletions

View File

@ -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);
}
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -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);
}
}

View File

@ -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")]

View File

@ -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);

View File

@ -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>

View File

@ -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; }

View File

@ -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 };
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);

View File

@ -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)

View File

@ -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);
}
}
}

View File

@ -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();
}
}
}
}

View File

@ -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>

View File

@ -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 };
}
}
}