Skip to content

Add a System.Text.Json based IJsonHelper #9566

New issue

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

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

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Apr 21, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 11 additions & 8 deletions src/Mvc/Mvc.Core/src/Formatters/TranscodingReadStream.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ internal sealed class TranscodingReadStream : Stream
private ArraySegment<byte> _byteBuffer;
private ArraySegment<char> _charBuffer;
private ArraySegment<byte> _overflowBuffer;
private bool _disposed;

public TranscodingReadStream(Stream input, Encoding sourceEncoding)
{
Expand Down Expand Up @@ -150,9 +151,9 @@ public override async Task<int> ReadAsync(byte[] buffer, int offset, int count,
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;
Expand Down Expand Up @@ -224,11 +225,13 @@ public override void Write(byte[] buffer, int offset, int count)

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);
}
}
}
}
7 changes: 3 additions & 4 deletions src/Mvc/Mvc.Core/src/Formatters/TranscodingWriteStream.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ internal sealed class TranscodingWriteStream : Stream
private readonly Encoder _encoder;
private readonly char[] _charBuffer;
private int _charsDecoded;
private bool _disposed;

public TranscodingWriteStream(Stream stream, Encoding targetEncoding)
{
Expand Down Expand Up @@ -154,13 +155,11 @@ private static void ThrowArgumentException(byte[] buffer, int offset, int count)

protected override void Dispose(bool disposing)
{
if (disposing)
if (!_disposed)
{
_disposed = true;
ArrayPool<char>.Shared.Return(_charBuffer);
}


base.Dispose(disposing);
}
}
}
133 changes: 133 additions & 0 deletions src/Mvc/Mvc.Core/src/Infrastructure/SystemTextJsonResultExecutor.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
}
12 changes: 1 addition & 11 deletions src/Mvc/Mvc.Core/src/JsonResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,17 +68,7 @@ public override Task ExecuteResultAsync(ActionContext context)
}

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);
}
}
Expand Down
1 change: 1 addition & 0 deletions src/Mvc/Mvc.Core/src/Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
[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")]
Expand Down
18 changes: 16 additions & 2 deletions src/Mvc/Mvc.Core/src/Properties/Resources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion src/Mvc/Mvc.Core/src/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Loading