Cleanup MvcJsonHelper (#6529)

* Cleanup MvcJsonHelper

* Remove dependency on JsonOutputFormatter
* Cache JsonSerializer for the default case
This commit is contained in:
Pranav K 2019-01-10 12:34:27 -08:00 committed by GitHub
parent 26c78ee542
commit 55ec35bb80
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 142 additions and 162 deletions

View File

@ -23,6 +23,10 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
/// The <see cref="JsonSerializerSettings"/> to be used by the serializer. /// The <see cref="JsonSerializerSettings"/> to be used by the serializer.
/// </param> /// </param>
/// <returns>A new <see cref="IHtmlContent"/> containing the serialized JSON.</returns> /// <returns>A new <see cref="IHtmlContent"/> containing the serialized JSON.</returns>
/// <remarks>
/// The value for <see cref="JsonSerializerSettings.StringEscapeHandling" /> from <paramref name="serializerSettings"/>
/// is ignored by this method and <see cref="StringEscapeHandling.EscapeHtml"/> is always used.
/// </remarks>
public static IHtmlContent Serialize( public static IHtmlContent Serialize(
this IJsonHelper jsonHelper, this IJsonHelper jsonHelper,
object value, object value,

View File

@ -6,8 +6,8 @@ using System.Buffers;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.Extensions.Options;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson
@ -17,42 +17,39 @@ namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson
/// </summary> /// </summary>
internal class NewtonsoftJsonHelper : IJsonHelper internal class NewtonsoftJsonHelper : IJsonHelper
{ {
private readonly NewtonsoftJsonOutputFormatter _jsonOutputFormatter; // Perf: JsonSerializers are relatively expensive to create, and are thread safe. Cache the serializer
private readonly ArrayPool<char> _charPool; private readonly JsonSerializer _defaultSettingsJsonSerializer;
private readonly IArrayPool<char> _charPool;
/// <summary> /// <summary>
/// Initializes a new instance of <see cref="NewtonsoftJsonHelper"/> that is backed by <paramref name="jsonOutputFormatter"/>. /// Initializes a new instance of <see cref="NewtonsoftJsonHelper"/>.
/// </summary> /// </summary>
/// <param name="jsonOutputFormatter">The <see cref="NewtonsoftJsonOutputFormatter"/> used to serialize JSON.</param> /// <param name="options">The <see cref="MvcNewtonsoftJsonOptions"/>.</param>
/// <param name="charPool"> /// <param name="charPool">
/// The <see cref="ArrayPool{Char}"/> for use with custom <see cref="JsonSerializerSettings"/> (see /// The <see cref="ArrayPool{Char}"/> for use with custom <see cref="JsonSerializerSettings"/> (see
/// <see cref="Serialize(object, JsonSerializerSettings)"/>). /// <see cref="Serialize(object, JsonSerializerSettings)"/>).
/// </param> /// </param>
public NewtonsoftJsonHelper(NewtonsoftJsonOutputFormatter jsonOutputFormatter, ArrayPool<char> charPool) public NewtonsoftJsonHelper(IOptions<MvcNewtonsoftJsonOptions> options, ArrayPool<char> charPool)
{ {
if (jsonOutputFormatter == null) if (options == null)
{ {
throw new ArgumentNullException(nameof(jsonOutputFormatter)); throw new ArgumentNullException(nameof(options));
} }
if (charPool == null) if (charPool == null)
{ {
throw new ArgumentNullException(nameof(charPool)); throw new ArgumentNullException(nameof(charPool));
} }
_jsonOutputFormatter = jsonOutputFormatter; _defaultSettingsJsonSerializer = CreateHtmlSafeSerializer(options.Value.SerializerSettings);
_charPool = charPool; _charPool = new JsonArrayPool<char>(charPool);
} }
/// <inheritdoc />
public IHtmlContent Serialize(object value) public IHtmlContent Serialize(object value)
{ {
var settings = ShallowCopy(_jsonOutputFormatter.PublicSerializerSettings); return Serialize(value, _defaultSettingsJsonSerializer);
settings.StringEscapeHandling = StringEscapeHandling.EscapeHtml;
return Serialize(value, settings);
} }
/// <inheritdoc />
public IHtmlContent Serialize(object value, JsonSerializerSettings serializerSettings) public IHtmlContent Serialize(object value, JsonSerializerSettings serializerSettings)
{ {
if (serializerSettings == null) if (serializerSettings == null)
@ -60,54 +57,34 @@ namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson
throw new ArgumentNullException(nameof(serializerSettings)); throw new ArgumentNullException(nameof(serializerSettings));
} }
var jsonOutputFormatter = new NewtonsoftJsonOutputFormatter(serializerSettings, _charPool); var jsonSerializer = CreateHtmlSafeSerializer(serializerSettings);
return Serialize(value, jsonSerializer);
return SerializeInternal(jsonOutputFormatter, value);
} }
private IHtmlContent SerializeInternal(NewtonsoftJsonOutputFormatter jsonOutputFormatter, object value) private IHtmlContent Serialize(object value, JsonSerializer jsonSerializer)
{ {
var stringWriter = new StringWriter(CultureInfo.InvariantCulture); using (var stringWriter = new StringWriter(CultureInfo.InvariantCulture))
jsonOutputFormatter.WriteObject(stringWriter, value);
return new HtmlString(stringWriter.ToString());
}
private static JsonSerializerSettings ShallowCopy(JsonSerializerSettings settings)
{
var copiedSettings = new JsonSerializerSettings
{ {
FloatParseHandling = settings.FloatParseHandling, var jsonWriter = new JsonTextWriter(stringWriter)
FloatFormatHandling = settings.FloatFormatHandling, {
DateParseHandling = settings.DateParseHandling, ArrayPool = _charPool,
DateTimeZoneHandling = settings.DateTimeZoneHandling, };
DateFormatHandling = settings.DateFormatHandling,
Formatting = settings.Formatting,
MaxDepth = settings.MaxDepth,
DateFormatString = settings.DateFormatString,
Context = settings.Context,
Error = settings.Error,
SerializationBinder = settings.SerializationBinder,
TraceWriter = settings.TraceWriter,
Culture = settings.Culture,
ReferenceResolverProvider = settings.ReferenceResolverProvider,
EqualityComparer = settings.EqualityComparer,
ContractResolver = settings.ContractResolver,
ConstructorHandling = settings.ConstructorHandling,
TypeNameAssemblyFormatHandling = settings.TypeNameAssemblyFormatHandling,
MetadataPropertyHandling = settings.MetadataPropertyHandling,
TypeNameHandling = settings.TypeNameHandling,
PreserveReferencesHandling = settings.PreserveReferencesHandling,
Converters = settings.Converters,
DefaultValueHandling = settings.DefaultValueHandling,
NullValueHandling = settings.NullValueHandling,
ObjectCreationHandling = settings.ObjectCreationHandling,
MissingMemberHandling = settings.MissingMemberHandling,
ReferenceLoopHandling = settings.ReferenceLoopHandling,
CheckAdditionalContent = settings.CheckAdditionalContent,
};
return copiedSettings; using (jsonWriter)
{
jsonSerializer.Serialize(jsonWriter, value);
}
return new HtmlString(stringWriter.ToString());
}
}
private static JsonSerializer CreateHtmlSafeSerializer(JsonSerializerSettings serializerSettings)
{
var jsonSerializer = JsonSerializer.Create(serializerSettings);
// Ignore the user configured StringEscapeHandling and always escape it.
jsonSerializer.StringEscapeHandling = StringEscapeHandling.EscapeHtml;
return jsonSerializer;
} }
} }
} }

View File

@ -3,7 +3,6 @@
using System; using System;
using System.Buffers; using System.Buffers;
using System.ComponentModel;
using System.IO; using System.IO;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -63,36 +62,6 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
/// </remarks> /// </remarks>
protected JsonSerializerSettings SerializerSettings { get; } protected JsonSerializerSettings SerializerSettings { get; }
/// <summary>
/// Gets the <see cref="JsonSerializerSettings"/> used to configure the <see cref="JsonSerializer"/>.
/// </summary>
/// <remarks>
/// Any modifications to the <see cref="JsonSerializerSettings"/> object after this
/// <see cref="NewtonsoftJsonOutputFormatter"/> has been used will have no effect.
/// </remarks>
[EditorBrowsable(EditorBrowsableState.Never)]
public JsonSerializerSettings PublicSerializerSettings => SerializerSettings;
/// <summary>
/// Writes the given <paramref name="value"/> as JSON using the given
/// <paramref name="writer"/>.
/// </summary>
/// <param name="writer">The <see cref="TextWriter"/> used to write the <paramref name="value"/></param>
/// <param name="value">The value to write as JSON.</param>
public void WriteObject(TextWriter writer, object value)
{
if (writer == null)
{
throw new ArgumentNullException(nameof(writer));
}
using (var jsonWriter = CreateJsonWriter(writer))
{
var jsonSerializer = CreateJsonSerializer();
jsonSerializer.Serialize(jsonWriter, value);
}
}
/// <summary> /// <summary>
/// Called during serialization to create the <see cref="JsonWriter"/>. /// Called during serialization to create the <see cref="JsonWriter"/>.
/// </summary> /// </summary>
@ -145,7 +114,11 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
var response = context.HttpContext.Response; var response = context.HttpContext.Response;
using (var writer = context.WriterFactory(response.Body, selectedEncoding)) using (var writer = context.WriterFactory(response.Body, selectedEncoding))
{ {
WriteObject(writer, context.Object); using (var jsonWriter = CreateJsonWriter(writer))
{
var jsonSerializer = CreateJsonSerializer();
jsonSerializer.Serialize(jsonWriter, context.Object);
}
// Perf: call FlushAsync to call WriteAsync on the stream with any content left in the TextWriter's // Perf: call FlushAsync to call WriteAsync on the stream with any content left in the TextWriter's
// buffers. This is better than just letting dispose handle it (which would result in a synchronous // buffers. This is better than just letting dispose handle it (which would result in a synchronous

View File

@ -265,7 +265,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
public async Task JsonHelperWithSettings_RendersJson_WithNamesUnchanged() public async Task JsonHelperWithSettings_RendersJson_WithNamesUnchanged()
{ {
// Arrange // Arrange
var json = "{\"id\":9000,\"FullName\":\"John <b>Smith</b>\"}"; var json = "{\"id\":9000,\"FullName\":\"John \\u003cb\\u003eSmith\\u003c/b\\u003e\"}";
var expectedBody = string.Format( var expectedBody = string.Format(
@"<script type=""text/javascript""> @"<script type=""text/javascript"">
var json = {0}; var json = {0};
@ -287,7 +287,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
public async Task JsonHelperWithSettings_RendersJson_WithSnakeCaseNames() public async Task JsonHelperWithSettings_RendersJson_WithSnakeCaseNames()
{ {
// Arrange // Arrange
var json = "{\"id\":9000,\"full_name\":\"John <b>Smith</b>\"}"; var json = "{\"id\":9000,\"full_name\":\"John \\u003cb\\u003eSmith\\u003c/b\\u003e\"}";
var expectedBody = string.Format( var expectedBody = string.Format(
@"<script type=""text/javascript""> @"<script type=""text/javascript"">
var json = {0}; var json = {0};

View File

@ -1,68 +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.Buffers;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc.Formatters;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson
{
public class JsonHelperTest
{
[Fact]
public void Serialize_EscapesHtmlByDefault()
{
// Arrange
var settings = new JsonSerializerSettings()
{
StringEscapeHandling = StringEscapeHandling.EscapeNonAscii,
};
var helper = new NewtonsoftJsonHelper(
new NewtonsoftJsonOutputFormatter(settings, ArrayPool<char>.Shared),
ArrayPool<char>.Shared);
var obj = new
{
HTML = "<b>John Doe</b>"
};
var expectedOutput = "{\"HTML\":\"\\u003cb\\u003eJohn Doe\\u003c/b\\u003e\"}";
// Act
var result = helper.Serialize(obj);
// Assert
var htmlString = Assert.IsType<HtmlString>(result);
Assert.Equal(expectedOutput, htmlString.ToString());
}
[Fact]
public void Serialize_MaintainsSettingsAndEscapesHtml()
{
// Arrange
var settings = new JsonSerializerSettings()
{
ContractResolver = new DefaultContractResolver
{
NamingStrategy = new CamelCaseNamingStrategy(),
},
};
var helper = new NewtonsoftJsonHelper(
new NewtonsoftJsonOutputFormatter(settings, ArrayPool<char>.Shared),
ArrayPool<char>.Shared);
var obj = new
{
FullHtml = "<b>John Doe</b>"
};
var expectedOutput = "{\"fullHtml\":\"\\u003cb\\u003eJohn Doe\\u003c/b\\u003e\"}";
// Act
var result = helper.Serialize(obj);
// Assert
var htmlString = Assert.IsType<HtmlString>(result);
Assert.Equal(expectedOutput, htmlString.ToString());
}
}
}

View File

@ -0,0 +1,95 @@
// 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.Html;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson
{
public class NewtonsoftJsonHelperTest
{
[Fact]
public void Serialize_EscapesHtmlByDefault()
{
// Arrange
var options = new MvcNewtonsoftJsonOptions();
options.SerializerSettings.StringEscapeHandling = StringEscapeHandling.EscapeNonAscii;
var helper = new NewtonsoftJsonHelper(
Options.Create(options),
ArrayPool<char>.Shared);
var obj = new
{
HTML = "<b>John Doe</b>"
};
var expectedOutput = "{\"html\":\"\\u003cb\\u003eJohn Doe\\u003c/b\\u003e\"}";
// Act
var result = helper.Serialize(obj);
// Assert
var htmlString = Assert.IsType<HtmlString>(result);
Assert.Equal(expectedOutput, htmlString.ToString());
}
[Fact]
public void Serialize_MaintainsSettingsAndEscapesHtml()
{
// Arrange
var options = new MvcNewtonsoftJsonOptions();
options.SerializerSettings.ContractResolver = new DefaultContractResolver
{
NamingStrategy = new DefaultNamingStrategy(),
};
var helper = new NewtonsoftJsonHelper(
Options.Create(options),
ArrayPool<char>.Shared);
var obj = new
{
FullHtml = "<b>John Doe</b>"
};
var expectedOutput = "{\"FullHtml\":\"\\u003cb\\u003eJohn Doe\\u003c/b\\u003e\"}";
// Act
var result = helper.Serialize(obj);
// Assert
var htmlString = Assert.IsType<HtmlString>(result);
Assert.Equal(expectedOutput, htmlString.ToString());
}
[Fact]
public void Serialize_UsesHtmlSafeVersionOfPassedInSettings()
{
// Arrange
var options = new MvcNewtonsoftJsonOptions();
var helper = new NewtonsoftJsonHelper(
Options.Create(options),
ArrayPool<char>.Shared);
var obj = new
{
FullHtml = "<b>John Doe</b>"
};
var serializerSettings = new JsonSerializerSettings
{
StringEscapeHandling = StringEscapeHandling.Default,
ContractResolver = new DefaultContractResolver
{
NamingStrategy = new SnakeCaseNamingStrategy(),
}
};
var expectedOutput = "{\"full_html\":\"\\u003cb\\u003eJohn Doe\\u003c/b\\u003e\"}";
// Act
var result = helper.Serialize(obj, serializerSettings);
// Assert
var htmlString = Assert.IsType<HtmlString>(result);
Assert.Equal(expectedOutput, htmlString.ToString());
}
}
}

View File

@ -10,7 +10,6 @@ using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.AspNetCore.Mvc.NewtonsoftJson; using Microsoft.AspNetCore.Mvc.NewtonsoftJson;
using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;