Skip to content

Commit 3f45874

Browse files
committed
add standard JsonOutputFormatter
1 parent 273060e commit 3f45874

3 files changed

Lines changed: 197 additions & 0 deletions

File tree

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Copyright (c) Arjen Post. See License.txt and Notice.txt in the project root for license information.
2+
3+
using System;
4+
using System.Buffers;
5+
using Newtonsoft.Json;
6+
7+
namespace PartialResponse.AspNetCore.Mvc.Formatters.Json.Internal
8+
{
9+
public class JsonArrayPool<T> : IArrayPool<T>
10+
{
11+
private readonly ArrayPool<T> _inner;
12+
13+
public JsonArrayPool(ArrayPool<T> inner)
14+
{
15+
if (inner == null)
16+
{
17+
throw new ArgumentNullException(nameof(inner));
18+
}
19+
20+
_inner = inner;
21+
}
22+
23+
public T[] Rent(int minimumLength)
24+
{
25+
return _inner.Rent(minimumLength);
26+
}
27+
28+
public void Return(T[] array)
29+
{
30+
if (array == null)
31+
{
32+
throw new ArgumentNullException(nameof(array));
33+
}
34+
35+
_inner.Return(array);
36+
}
37+
}
38+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Copyright (c) Arjen Post. See License.txt and Notice.txt in the project root for license information.
2+
3+
using Microsoft.Net.Http.Headers;
4+
5+
namespace PartialResponse.AspNetCore.Mvc.Formatters.Json.Internal
6+
{
7+
internal class MediaTypeHeaderValues
8+
{
9+
public static readonly MediaTypeHeaderValue ApplicationJson
10+
= MediaTypeHeaderValue.Parse("application/json").CopyAsReadOnly();
11+
12+
public static readonly MediaTypeHeaderValue TextJson
13+
= MediaTypeHeaderValue.Parse("text/json").CopyAsReadOnly();
14+
}
15+
}
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
// Copyright (c) Arjen Post. See License.txt and Notice.txt in the project root for license information.
2+
3+
using System;
4+
using System.Buffers;
5+
using System.IO;
6+
using System.Text;
7+
using System.Threading.Tasks;
8+
using Microsoft.AspNetCore.Mvc.Formatters;
9+
using Newtonsoft.Json;
10+
using PartialResponse.AspNetCore.Mvc.Formatters.Json.Internal;
11+
12+
namespace PartialResponse.AspNetCore.Mvc.Formatters
13+
{
14+
/// <summary>
15+
/// A <see cref="TextOutputFormatter"/> for JSON content.
16+
/// </summary>
17+
public class PartialJsonOutputFormatter : TextOutputFormatter
18+
{
19+
private readonly IArrayPool<char> _charPool;
20+
21+
// Perf: JsonSerializers are relatively expensive to create, and are thread safe. We cache
22+
// the serializer and invalidate it when the settings change.
23+
private JsonSerializer _serializer;
24+
25+
/// <summary>
26+
/// Initializes a new <see cref="PartialJsonOutputFormatter"/> instance.
27+
/// </summary>
28+
/// <param name="serializerSettings">
29+
/// The <see cref="JsonSerializerSettings"/>. Should be either the application-wide settings
30+
/// (<see cref="MvcJsonOptions.SerializerSettings"/>) or an instance
31+
/// <see cref="JsonSerializerSettingsProvider.CreateSerializerSettings"/> initially returned.
32+
/// </param>
33+
/// <param name="charPool">The <see cref="ArrayPool{Char}"/>.</param>
34+
public PartialJsonOutputFormatter(JsonSerializerSettings serializerSettings, ArrayPool<char> charPool)
35+
{
36+
if (serializerSettings == null)
37+
{
38+
throw new ArgumentNullException(nameof(serializerSettings));
39+
}
40+
41+
if (charPool == null)
42+
{
43+
throw new ArgumentNullException(nameof(charPool));
44+
}
45+
46+
SerializerSettings = serializerSettings;
47+
_charPool = new JsonArrayPool<char>(charPool);
48+
49+
SupportedEncodings.Add(Encoding.UTF8);
50+
SupportedEncodings.Add(Encoding.Unicode);
51+
SupportedMediaTypes.Add(MediaTypeHeaderValues.ApplicationJson);
52+
SupportedMediaTypes.Add(MediaTypeHeaderValues.TextJson);
53+
}
54+
55+
/// <summary>
56+
/// Gets the <see cref="JsonSerializerSettings"/> used to configure the <see cref="JsonSerializer"/>.
57+
/// </summary>
58+
/// <remarks>
59+
/// Any modifications to the <see cref="JsonSerializerSettings"/> object after this
60+
/// <see cref="PartialJsonOutputFormatter"/> has been used will have no effect.
61+
/// </remarks>
62+
protected JsonSerializerSettings SerializerSettings { get; }
63+
64+
/// <summary>
65+
/// Writes the given <paramref name="value"/> as JSON using the given
66+
/// <paramref name="writer"/>.
67+
/// </summary>
68+
/// <param name="writer">The <see cref="TextWriter"/> used to write the <paramref name="value"/></param>
69+
/// <param name="value">The value to write as JSON.</param>
70+
public void WriteObject(TextWriter writer, object value)
71+
{
72+
if (writer == null)
73+
{
74+
throw new ArgumentNullException(nameof(writer));
75+
}
76+
77+
using (var jsonWriter = CreateJsonWriter(writer))
78+
{
79+
var jsonSerializer = CreateJsonSerializer();
80+
jsonSerializer.Serialize(jsonWriter, value);
81+
}
82+
}
83+
84+
/// <summary>
85+
/// Called during serialization to create the <see cref="JsonWriter"/>.
86+
/// </summary>
87+
/// <param name="writer">The <see cref="TextWriter"/> used to write.</param>
88+
/// <returns>The <see cref="JsonWriter"/> used during serialization.</returns>
89+
protected virtual JsonWriter CreateJsonWriter(TextWriter writer)
90+
{
91+
if (writer == null)
92+
{
93+
throw new ArgumentNullException(nameof(writer));
94+
}
95+
96+
var jsonWriter = new JsonTextWriter(writer)
97+
{
98+
ArrayPool = _charPool,
99+
CloseOutput = false,
100+
};
101+
102+
return jsonWriter;
103+
}
104+
105+
/// <summary>
106+
/// Called during serialization to create the <see cref="JsonSerializer"/>.
107+
/// </summary>
108+
/// <returns>The <see cref="JsonSerializer"/> used during serialization and deserialization.</returns>
109+
protected virtual JsonSerializer CreateJsonSerializer()
110+
{
111+
if (_serializer == null)
112+
{
113+
_serializer = JsonSerializer.Create(SerializerSettings);
114+
}
115+
116+
return _serializer;
117+
}
118+
119+
/// <inheritdoc />
120+
public override async Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
121+
{
122+
if (context == null)
123+
{
124+
throw new ArgumentNullException(nameof(context));
125+
}
126+
127+
if (selectedEncoding == null)
128+
{
129+
throw new ArgumentNullException(nameof(selectedEncoding));
130+
}
131+
132+
var response = context.HttpContext.Response;
133+
using (var writer = context.WriterFactory(response.Body, selectedEncoding))
134+
{
135+
WriteObject(writer, context.Object);
136+
137+
// Perf: call FlushAsync to call WriteAsync on the stream with any content left in the TextWriter's
138+
// buffers. This is better than just letting dispose handle it (which would result in a synchronous
139+
// write).
140+
await writer.FlushAsync();
141+
}
142+
}
143+
}
144+
}

0 commit comments

Comments
 (0)