Cleanup MvcJsonHelper (#6529)
* Cleanup MvcJsonHelper * Remove dependency on JsonOutputFormatter * Cache JsonSerializer for the default case
This commit is contained in:
parent
26c78ee542
commit
55ec35bb80
|
|
@ -23,6 +23,10 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
|
|||
/// The <see cref="JsonSerializerSettings"/> to be used by the serializer.
|
||||
/// </param>
|
||||
/// <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(
|
||||
this IJsonHelper jsonHelper,
|
||||
object value,
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ using System.Buffers;
|
|||
using System.Globalization;
|
||||
using System.IO;
|
||||
using Microsoft.AspNetCore.Html;
|
||||
using Microsoft.AspNetCore.Mvc.Formatters;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson
|
||||
|
|
@ -17,42 +17,39 @@ namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson
|
|||
/// </summary>
|
||||
internal class NewtonsoftJsonHelper : IJsonHelper
|
||||
{
|
||||
private readonly NewtonsoftJsonOutputFormatter _jsonOutputFormatter;
|
||||
private readonly ArrayPool<char> _charPool;
|
||||
// Perf: JsonSerializers are relatively expensive to create, and are thread safe. Cache the serializer
|
||||
private readonly JsonSerializer _defaultSettingsJsonSerializer;
|
||||
private readonly IArrayPool<char> _charPool;
|
||||
|
||||
/// <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>
|
||||
/// <param name="jsonOutputFormatter">The <see cref="NewtonsoftJsonOutputFormatter"/> used to serialize JSON.</param>
|
||||
/// <param name="options">The <see cref="MvcNewtonsoftJsonOptions"/>.</param>
|
||||
/// <param name="charPool">
|
||||
/// The <see cref="ArrayPool{Char}"/> for use with custom <see cref="JsonSerializerSettings"/> (see
|
||||
/// <see cref="Serialize(object, JsonSerializerSettings)"/>).
|
||||
/// </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)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(charPool));
|
||||
}
|
||||
|
||||
_jsonOutputFormatter = jsonOutputFormatter;
|
||||
_charPool = charPool;
|
||||
_defaultSettingsJsonSerializer = CreateHtmlSafeSerializer(options.Value.SerializerSettings);
|
||||
_charPool = new JsonArrayPool<char>(charPool);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IHtmlContent Serialize(object value)
|
||||
{
|
||||
var settings = ShallowCopy(_jsonOutputFormatter.PublicSerializerSettings);
|
||||
settings.StringEscapeHandling = StringEscapeHandling.EscapeHtml;
|
||||
|
||||
return Serialize(value, settings);
|
||||
return Serialize(value, _defaultSettingsJsonSerializer);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IHtmlContent Serialize(object value, JsonSerializerSettings serializerSettings)
|
||||
{
|
||||
if (serializerSettings == null)
|
||||
|
|
@ -60,54 +57,34 @@ namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson
|
|||
throw new ArgumentNullException(nameof(serializerSettings));
|
||||
}
|
||||
|
||||
var jsonOutputFormatter = new NewtonsoftJsonOutputFormatter(serializerSettings, _charPool);
|
||||
|
||||
return SerializeInternal(jsonOutputFormatter, value);
|
||||
var jsonSerializer = CreateHtmlSafeSerializer(serializerSettings);
|
||||
return Serialize(value, jsonSerializer);
|
||||
}
|
||||
|
||||
private IHtmlContent SerializeInternal(NewtonsoftJsonOutputFormatter jsonOutputFormatter, object value)
|
||||
private IHtmlContent Serialize(object value, JsonSerializer jsonSerializer)
|
||||
{
|
||||
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
|
||||
using (var stringWriter = new StringWriter(CultureInfo.InvariantCulture))
|
||||
{
|
||||
FloatParseHandling = settings.FloatParseHandling,
|
||||
FloatFormatHandling = settings.FloatFormatHandling,
|
||||
DateParseHandling = settings.DateParseHandling,
|
||||
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,
|
||||
};
|
||||
var jsonWriter = new JsonTextWriter(stringWriter)
|
||||
{
|
||||
ArrayPool = _charPool,
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
|
@ -63,36 +62,6 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
/// </remarks>
|
||||
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>
|
||||
/// Called during serialization to create the <see cref="JsonWriter"/>.
|
||||
/// </summary>
|
||||
|
|
@ -145,7 +114,11 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
var response = context.HttpContext.Response;
|
||||
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
|
||||
// buffers. This is better than just letting dispose handle it (which would result in a synchronous
|
||||
|
|
|
|||
|
|
@ -265,7 +265,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
public async Task JsonHelperWithSettings_RendersJson_WithNamesUnchanged()
|
||||
{
|
||||
// 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(
|
||||
@"<script type=""text/javascript"">
|
||||
var json = {0};
|
||||
|
|
@ -287,7 +287,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
public async Task JsonHelperWithSettings_RendersJson_WithSnakeCaseNames()
|
||||
{
|
||||
// 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(
|
||||
@"<script type=""text/javascript"">
|
||||
var json = {0};
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -10,7 +10,6 @@ using System.Text;
|
|||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||
using Microsoft.AspNetCore.Mvc.Formatters;
|
||||
using Microsoft.AspNetCore.Mvc.NewtonsoftJson;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
|
|
|||
Loading…
Reference in New Issue