Skip to content

Commit 55ec35b

Browse files
authored
Cleanup MvcJsonHelper (#6529)
* Cleanup MvcJsonHelper * Remove dependency on JsonOutputFormatter * Cache JsonSerializer for the default case
1 parent 26c78ee commit 55ec35b

File tree

7 files changed

+140
-160
lines changed

7 files changed

+140
-160
lines changed

src/Mvc/src/Microsoft.AspNetCore.Mvc.NewtonsoftJson/JsonHelperExtensions.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ public static class JsonHelperExtensions
2323
/// The <see cref="JsonSerializerSettings"/> to be used by the serializer.
2424
/// </param>
2525
/// <returns>A new <see cref="IHtmlContent"/> containing the serialized JSON.</returns>
26+
/// <remarks>
27+
/// The value for <see cref="JsonSerializerSettings.StringEscapeHandling" /> from <paramref name="serializerSettings"/>
28+
/// is ignored by this method and <see cref="StringEscapeHandling.EscapeHtml"/> is always used.
29+
/// </remarks>
2630
public static IHtmlContent Serialize(
2731
this IJsonHelper jsonHelper,
2832
object value,

src/Mvc/src/Microsoft.AspNetCore.Mvc.NewtonsoftJson/NewtonsoftJsonHelper.cs

Lines changed: 34 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
using System.Globalization;
77
using System.IO;
88
using Microsoft.AspNetCore.Html;
9-
using Microsoft.AspNetCore.Mvc.Formatters;
109
using Microsoft.AspNetCore.Mvc.Rendering;
10+
using Microsoft.Extensions.Options;
1111
using Newtonsoft.Json;
1212

1313
namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson
@@ -17,97 +17,74 @@ namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson
1717
/// </summary>
1818
internal class NewtonsoftJsonHelper : IJsonHelper
1919
{
20-
private readonly NewtonsoftJsonOutputFormatter _jsonOutputFormatter;
21-
private readonly ArrayPool<char> _charPool;
20+
// Perf: JsonSerializers are relatively expensive to create, and are thread safe. Cache the serializer
21+
private readonly JsonSerializer _defaultSettingsJsonSerializer;
22+
private readonly IArrayPool<char> _charPool;
2223

2324
/// <summary>
24-
/// Initializes a new instance of <see cref="NewtonsoftJsonHelper"/> that is backed by <paramref name="jsonOutputFormatter"/>.
25+
/// Initializes a new instance of <see cref="NewtonsoftJsonHelper"/>.
2526
/// </summary>
26-
/// <param name="jsonOutputFormatter">The <see cref="NewtonsoftJsonOutputFormatter"/> used to serialize JSON.</param>
27+
/// <param name="options">The <see cref="MvcNewtonsoftJsonOptions"/>.</param>
2728
/// <param name="charPool">
2829
/// The <see cref="ArrayPool{Char}"/> for use with custom <see cref="JsonSerializerSettings"/> (see
2930
/// <see cref="Serialize(object, JsonSerializerSettings)"/>).
3031
/// </param>
31-
public NewtonsoftJsonHelper(NewtonsoftJsonOutputFormatter jsonOutputFormatter, ArrayPool<char> charPool)
32+
public NewtonsoftJsonHelper(IOptions<MvcNewtonsoftJsonOptions> options, ArrayPool<char> charPool)
3233
{
33-
if (jsonOutputFormatter == null)
34+
if (options == null)
3435
{
35-
throw new ArgumentNullException(nameof(jsonOutputFormatter));
36+
throw new ArgumentNullException(nameof(options));
3637
}
38+
3739
if (charPool == null)
3840
{
3941
throw new ArgumentNullException(nameof(charPool));
4042
}
4143

42-
_jsonOutputFormatter = jsonOutputFormatter;
43-
_charPool = charPool;
44+
_defaultSettingsJsonSerializer = CreateHtmlSafeSerializer(options.Value.SerializerSettings);
45+
_charPool = new JsonArrayPool<char>(charPool);
4446
}
4547

46-
/// <inheritdoc />
4748
public IHtmlContent Serialize(object value)
4849
{
49-
var settings = ShallowCopy(_jsonOutputFormatter.PublicSerializerSettings);
50-
settings.StringEscapeHandling = StringEscapeHandling.EscapeHtml;
51-
52-
return Serialize(value, settings);
50+
return Serialize(value, _defaultSettingsJsonSerializer);
5351
}
5452

55-
/// <inheritdoc />
5653
public IHtmlContent Serialize(object value, JsonSerializerSettings serializerSettings)
5754
{
5855
if (serializerSettings == null)
5956
{
6057
throw new ArgumentNullException(nameof(serializerSettings));
6158
}
6259

63-
var jsonOutputFormatter = new NewtonsoftJsonOutputFormatter(serializerSettings, _charPool);
64-
65-
return SerializeInternal(jsonOutputFormatter, value);
60+
var jsonSerializer = CreateHtmlSafeSerializer(serializerSettings);
61+
return Serialize(value, jsonSerializer);
6662
}
6763

68-
private IHtmlContent SerializeInternal(NewtonsoftJsonOutputFormatter jsonOutputFormatter, object value)
64+
private IHtmlContent Serialize(object value, JsonSerializer jsonSerializer)
6965
{
70-
var stringWriter = new StringWriter(CultureInfo.InvariantCulture);
71-
jsonOutputFormatter.WriteObject(stringWriter, value);
66+
using (var stringWriter = new StringWriter(CultureInfo.InvariantCulture))
67+
{
68+
var jsonWriter = new JsonTextWriter(stringWriter)
69+
{
70+
ArrayPool = _charPool,
71+
};
7272

73-
return new HtmlString(stringWriter.ToString());
73+
using (jsonWriter)
74+
{
75+
jsonSerializer.Serialize(jsonWriter, value);
76+
}
77+
78+
return new HtmlString(stringWriter.ToString());
79+
}
7480
}
7581

76-
private static JsonSerializerSettings ShallowCopy(JsonSerializerSettings settings)
82+
private static JsonSerializer CreateHtmlSafeSerializer(JsonSerializerSettings serializerSettings)
7783
{
78-
var copiedSettings = new JsonSerializerSettings
79-
{
80-
FloatParseHandling = settings.FloatParseHandling,
81-
FloatFormatHandling = settings.FloatFormatHandling,
82-
DateParseHandling = settings.DateParseHandling,
83-
DateTimeZoneHandling = settings.DateTimeZoneHandling,
84-
DateFormatHandling = settings.DateFormatHandling,
85-
Formatting = settings.Formatting,
86-
MaxDepth = settings.MaxDepth,
87-
DateFormatString = settings.DateFormatString,
88-
Context = settings.Context,
89-
Error = settings.Error,
90-
SerializationBinder = settings.SerializationBinder,
91-
TraceWriter = settings.TraceWriter,
92-
Culture = settings.Culture,
93-
ReferenceResolverProvider = settings.ReferenceResolverProvider,
94-
EqualityComparer = settings.EqualityComparer,
95-
ContractResolver = settings.ContractResolver,
96-
ConstructorHandling = settings.ConstructorHandling,
97-
TypeNameAssemblyFormatHandling = settings.TypeNameAssemblyFormatHandling,
98-
MetadataPropertyHandling = settings.MetadataPropertyHandling,
99-
TypeNameHandling = settings.TypeNameHandling,
100-
PreserveReferencesHandling = settings.PreserveReferencesHandling,
101-
Converters = settings.Converters,
102-
DefaultValueHandling = settings.DefaultValueHandling,
103-
NullValueHandling = settings.NullValueHandling,
104-
ObjectCreationHandling = settings.ObjectCreationHandling,
105-
MissingMemberHandling = settings.MissingMemberHandling,
106-
ReferenceLoopHandling = settings.ReferenceLoopHandling,
107-
CheckAdditionalContent = settings.CheckAdditionalContent,
108-
};
109-
110-
return copiedSettings;
84+
var jsonSerializer = JsonSerializer.Create(serializerSettings);
85+
// Ignore the user configured StringEscapeHandling and always escape it.
86+
jsonSerializer.StringEscapeHandling = StringEscapeHandling.EscapeHtml;
87+
return jsonSerializer;
11188
}
11289
}
11390
}

src/Mvc/src/Microsoft.AspNetCore.Mvc.NewtonsoftJson/NewtonsoftJsonOutputFormatter.cs

Lines changed: 5 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
using System;
55
using System.Buffers;
6-
using System.ComponentModel;
76
using System.IO;
87
using System.Text;
98
using System.Threading.Tasks;
@@ -63,36 +62,6 @@ public NewtonsoftJsonOutputFormatter(JsonSerializerSettings serializerSettings,
6362
/// </remarks>
6463
protected JsonSerializerSettings SerializerSettings { get; }
6564

66-
/// <summary>
67-
/// Gets the <see cref="JsonSerializerSettings"/> used to configure the <see cref="JsonSerializer"/>.
68-
/// </summary>
69-
/// <remarks>
70-
/// Any modifications to the <see cref="JsonSerializerSettings"/> object after this
71-
/// <see cref="NewtonsoftJsonOutputFormatter"/> has been used will have no effect.
72-
/// </remarks>
73-
[EditorBrowsable(EditorBrowsableState.Never)]
74-
public JsonSerializerSettings PublicSerializerSettings => SerializerSettings;
75-
76-
/// <summary>
77-
/// Writes the given <paramref name="value"/> as JSON using the given
78-
/// <paramref name="writer"/>.
79-
/// </summary>
80-
/// <param name="writer">The <see cref="TextWriter"/> used to write the <paramref name="value"/></param>
81-
/// <param name="value">The value to write as JSON.</param>
82-
public void WriteObject(TextWriter writer, object value)
83-
{
84-
if (writer == null)
85-
{
86-
throw new ArgumentNullException(nameof(writer));
87-
}
88-
89-
using (var jsonWriter = CreateJsonWriter(writer))
90-
{
91-
var jsonSerializer = CreateJsonSerializer();
92-
jsonSerializer.Serialize(jsonWriter, value);
93-
}
94-
}
95-
9665
/// <summary>
9766
/// Called during serialization to create the <see cref="JsonWriter"/>.
9867
/// </summary>
@@ -145,7 +114,11 @@ public override async Task WriteResponseBodyAsync(OutputFormatterWriteContext co
145114
var response = context.HttpContext.Response;
146115
using (var writer = context.WriterFactory(response.Body, selectedEncoding))
147116
{
148-
WriteObject(writer, context.Object);
117+
using (var jsonWriter = CreateJsonWriter(writer))
118+
{
119+
var jsonSerializer = CreateJsonSerializer();
120+
jsonSerializer.Serialize(jsonWriter, context.Object);
121+
}
149122

150123
// Perf: call FlushAsync to call WriteAsync on the stream with any content left in the TextWriter's
151124
// buffers. This is better than just letting dispose handle it (which would result in a synchronous

src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/BasicTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ public async Task JsonHelper_RendersJson_WithCamelCaseNames()
265265
public async Task JsonHelperWithSettings_RendersJson_WithNamesUnchanged()
266266
{
267267
// Arrange
268-
var json = "{\"id\":9000,\"FullName\":\"John <b>Smith</b>\"}";
268+
var json = "{\"id\":9000,\"FullName\":\"John \\u003cb\\u003eSmith\\u003c/b\\u003e\"}";
269269
var expectedBody = string.Format(
270270
@"<script type=""text/javascript"">
271271
var json = {0};
@@ -287,7 +287,7 @@ public async Task JsonHelperWithSettings_RendersJson_WithNamesUnchanged()
287287
public async Task JsonHelperWithSettings_RendersJson_WithSnakeCaseNames()
288288
{
289289
// Arrange
290-
var json = "{\"id\":9000,\"full_name\":\"John <b>Smith</b>\"}";
290+
var json = "{\"id\":9000,\"full_name\":\"John \\u003cb\\u003eSmith\\u003c/b\\u003e\"}";
291291
var expectedBody = string.Format(
292292
@"<script type=""text/javascript"">
293293
var json = {0};

src/Mvc/test/Microsoft.AspNetCore.Mvc.NewtonsoftJson.Test/JsonHelperTest.cs

Lines changed: 0 additions & 68 deletions
This file was deleted.
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System.Buffers;
5+
using Microsoft.AspNetCore.Html;
6+
using Microsoft.Extensions.Options;
7+
using Newtonsoft.Json;
8+
using Newtonsoft.Json.Serialization;
9+
using Xunit;
10+
11+
namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson
12+
{
13+
public class NewtonsoftJsonHelperTest
14+
{
15+
[Fact]
16+
public void Serialize_EscapesHtmlByDefault()
17+
{
18+
// Arrange
19+
var options = new MvcNewtonsoftJsonOptions();
20+
options.SerializerSettings.StringEscapeHandling = StringEscapeHandling.EscapeNonAscii;
21+
var helper = new NewtonsoftJsonHelper(
22+
Options.Create(options),
23+
ArrayPool<char>.Shared);
24+
var obj = new
25+
{
26+
HTML = "<b>John Doe</b>"
27+
};
28+
var expectedOutput = "{\"html\":\"\\u003cb\\u003eJohn Doe\\u003c/b\\u003e\"}";
29+
30+
// Act
31+
var result = helper.Serialize(obj);
32+
33+
// Assert
34+
var htmlString = Assert.IsType<HtmlString>(result);
35+
Assert.Equal(expectedOutput, htmlString.ToString());
36+
}
37+
38+
[Fact]
39+
public void Serialize_MaintainsSettingsAndEscapesHtml()
40+
{
41+
// Arrange
42+
var options = new MvcNewtonsoftJsonOptions();
43+
options.SerializerSettings.ContractResolver = new DefaultContractResolver
44+
{
45+
NamingStrategy = new DefaultNamingStrategy(),
46+
};
47+
var helper = new NewtonsoftJsonHelper(
48+
Options.Create(options),
49+
ArrayPool<char>.Shared);
50+
var obj = new
51+
{
52+
FullHtml = "<b>John Doe</b>"
53+
};
54+
var expectedOutput = "{\"FullHtml\":\"\\u003cb\\u003eJohn Doe\\u003c/b\\u003e\"}";
55+
56+
// Act
57+
var result = helper.Serialize(obj);
58+
59+
// Assert
60+
var htmlString = Assert.IsType<HtmlString>(result);
61+
Assert.Equal(expectedOutput, htmlString.ToString());
62+
}
63+
64+
[Fact]
65+
public void Serialize_UsesHtmlSafeVersionOfPassedInSettings()
66+
{
67+
// Arrange
68+
var options = new MvcNewtonsoftJsonOptions();
69+
var helper = new NewtonsoftJsonHelper(
70+
Options.Create(options),
71+
ArrayPool<char>.Shared);
72+
var obj = new
73+
{
74+
FullHtml = "<b>John Doe</b>"
75+
};
76+
var serializerSettings = new JsonSerializerSettings
77+
{
78+
StringEscapeHandling = StringEscapeHandling.Default,
79+
ContractResolver = new DefaultContractResolver
80+
{
81+
NamingStrategy = new SnakeCaseNamingStrategy(),
82+
}
83+
};
84+
85+
var expectedOutput = "{\"full_html\":\"\\u003cb\\u003eJohn Doe\\u003c/b\\u003e\"}";
86+
87+
// Act
88+
var result = helper.Serialize(obj, serializerSettings);
89+
90+
// Assert
91+
var htmlString = Assert.IsType<HtmlString>(result);
92+
Assert.Equal(expectedOutput, htmlString.ToString());
93+
}
94+
}
95+
}

0 commit comments

Comments
 (0)