Skip to content

Commit 7842d08

Browse files
committed
Use TranscodingStream in JSON and XML formatters
- remove `ReadOnlyStreamWithEncodingPreamble` and its tests - was insufficient in general and doesn't work correctly in `netstandard1.3` - see dotnet/runtime#80160 Now support a much broader set of encodings in JSON and XML formatters - not just UTF8, UTF16BE, and UTF16LE - JSON formatter already supported every encoding when `UseDataContractJsonSerializer` was `false` - no longer have this downside when property is `true` - however, quotas remain unsupported in `netstandard1.3` version of this formatter when property is `true` - XML formatter requirement for an XML declaration in non-UTF8 content was general and is no longer an issue
1 parent a52832a commit 7842d08

File tree

8 files changed

+73
-337
lines changed

8 files changed

+73
-337
lines changed

src/System.Net.Http.Formatting/Formatting/JsonMediaTypeFormatter.cs

Lines changed: 41 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@ namespace System.Net.Http.Formatting
2222
/// </summary>
2323
public class JsonMediaTypeFormatter : BaseJsonMediaTypeFormatter
2424
{
25-
private ConcurrentDictionary<Type, DataContractJsonSerializer> _dataContractSerializerCache = new ConcurrentDictionary<Type, DataContractJsonSerializer>();
26-
private XmlDictionaryReaderQuotas _readerQuotas = FormattingUtilities.CreateDefaultReaderQuotas();
27-
private RequestHeaderMapping _requestHeaderMapping;
25+
private readonly ConcurrentDictionary<Type, DataContractJsonSerializer> _dataContractSerializerCache = new ConcurrentDictionary<Type, DataContractJsonSerializer>();
26+
private readonly XmlDictionaryReaderQuotas _readerQuotas = FormattingUtilities.CreateDefaultReaderQuotas();
27+
private readonly RequestHeaderMapping _requestHeaderMapping;
2828

2929
/// <summary>
3030
/// Initializes a new instance of the <see cref="JsonMediaTypeFormatter"/> class.
@@ -216,17 +216,23 @@ public override object ReadFromStream(Type type, Stream readStream, Encoding eff
216216
{
217217
DataContractJsonSerializer dataContractSerializer = GetDataContractSerializer(type);
218218

219-
#if NETFX_CORE // JsonReaderWriterFactory is internal in netstandard1.3. Unfortunately, ignoring _readerQuotas.
220-
// Force a preamble into the stream since DataContractJsonSerializer only supports auto-detecting
221-
// encoding in netstandard1.3 ].
222-
readStream = new ReadOnlyStreamWithEncodingPreamble(readStream, effectiveEncoding);
219+
// JsonReaderWriterFactory is internal, CreateTextReader only supports auto-detecting the encoding
220+
// and auto-detection fails in some cases for the NETFX_CORE project. In addition, DCS encodings are
221+
// limited to UTF8, UTF16BE, and UTF16LE. Convert to UTF8 as we read.
222+
Stream innerStream = string.Equals(effectiveEncoding.WebName, Utf8Encoding.WebName, StringComparison.OrdinalIgnoreCase) ?
223+
new NonClosingDelegatingStream(readStream) :
224+
new TranscodingStream(readStream, effectiveEncoding, Utf8Encoding, leaveOpen: true);
223225

224-
return dataContractSerializer.ReadObject(new NonClosingDelegatingStream(readStream));
225-
#else
226-
using (XmlReader reader = JsonReaderWriterFactory.CreateJsonReader(new NonClosingDelegatingStream(readStream), effectiveEncoding, _readerQuotas, null))
226+
#if NETFX_CORE
227+
using (innerStream)
227228
{
228-
return dataContractSerializer.ReadObject(reader);
229+
// Unfortunately, we're ignoring _readerQuotas.
230+
return dataContractSerializer.ReadObject(innerStream);
229231
}
232+
#else
233+
// XmlReader will always dispose of innerStream when we dispose of the reader.
234+
using var reader = JsonReaderWriterFactory.CreateJsonReader(innerStream, Utf8Encoding, _readerQuotas, onClose: null);
235+
return dataContractSerializer.ReadObject(reader);
230236
#endif
231237
}
232238
else
@@ -277,14 +283,6 @@ public override void WriteToStream(Type type, object value, Stream writeStream,
277283

278284
if (UseDataContractJsonSerializer)
279285
{
280-
#if NETFX_CORE // DataContractJsonSerializer writes only UTF8 in netstandard1.3. Later versions of (now public)
281-
// JsonReaderWriterFactory can compensate.
282-
if (!string.Equals(Encoding.UTF8.WebName, effectiveEncoding.WebName, StringComparison.OrdinalIgnoreCase))
283-
{
284-
throw new NotSupportedException("!!! To be added !!!");
285-
}
286-
#endif
287-
288286
if (MediaTypeFormatter.TryGetDelegatingTypeForIQueryableGenericOrSame(ref type))
289287
{
290288
if (value != null)
@@ -293,23 +291,39 @@ public override void WriteToStream(Type type, object value, Stream writeStream,
293291
}
294292
}
295293

296-
DataContractJsonSerializer dataContractSerializer = GetDataContractSerializer(type);
297-
298-
#if NETFX_CORE // JsonReaderWriterFactory is internal in netstandard1.3.
299-
dataContractSerializer.WriteObject(writeStream, value);
300-
#else
301-
using (XmlWriter writer = JsonReaderWriterFactory.CreateJsonWriter(writeStream, effectiveEncoding, ownsStream: false))
294+
WritePreamble(writeStream, effectiveEncoding);
295+
if (string.Equals(effectiveEncoding.WebName, Utf8Encoding.WebName, StringComparison.OrdinalIgnoreCase))
302296
{
303-
dataContractSerializer.WriteObject(writer, value);
297+
WriteObject(writeStream, type, value);
298+
}
299+
else
300+
{
301+
// JsonReaderWriterFactory is internal and DataContractJsonSerializer only writes UTF8 for the
302+
// NETFX_CORE project. In addition, DCS encodings are limited to UTF8, UTF16BE, and UTF16LE.
303+
// Convert to UTF8 as we write.
304+
using var innerStream = new TranscodingStream(writeStream, effectiveEncoding, Utf8Encoding, leaveOpen: true);
305+
WriteObject(innerStream, type, value);
304306
}
305-
#endif
306307
}
307308
else
308309
{
309310
base.WriteToStream(type, value, writeStream, effectiveEncoding);
310311
}
311312
}
312313

314+
private void WriteObject(Stream stream, Type type, object value)
315+
{
316+
DataContractJsonSerializer dataContractSerializer = GetDataContractSerializer(type);
317+
318+
// Do not dispose of the stream. WriteToStream handles that where it's needed.
319+
#if NETFX_CORE
320+
dataContractSerializer.WriteObject(stream, value);
321+
#else
322+
using XmlWriter writer = JsonReaderWriterFactory.CreateJsonWriter(stream, Utf8Encoding, ownsStream: false);
323+
dataContractSerializer.WriteObject(writer, value);
324+
#endif
325+
}
326+
313327
[SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Catch all is around an extensibile method")]
314328
private DataContractJsonSerializer CreateDataContractSerializer(Type type, bool throwOnError)
315329
{

src/System.Net.Http.Formatting/Formatting/MediaTypeFormatter.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ namespace System.Net.Http.Formatting
2020
/// </summary>
2121
public abstract class MediaTypeFormatter
2222
{
23+
private protected static readonly Encoding Utf8Encoding =
24+
new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true);
2325
private const int DefaultMinHttpCollectionKeys = 1;
2426
private const int DefaultMaxHttpCollectionKeys = 1000; // same default as ASPNET
2527
private const string IWellKnownComparerTypeName = "System.IWellKnownStringEqualityComparer, mscorlib, Version=4.0.0.0, PublicKeyToken=b77a5c561934e089";
@@ -499,6 +501,15 @@ public static object GetDefaultValueForType(Type type)
499501
return null;
500502
}
501503

504+
private protected static void WritePreamble(Stream stream, Encoding encoding)
505+
{
506+
byte[] bytes = encoding.GetPreamble();
507+
if (bytes.Length > 0)
508+
{
509+
stream.Write(bytes, 0, bytes.Length);
510+
}
511+
}
512+
502513
/// <summary>
503514
/// Collection class that validates it contains only <see cref="MediaTypeHeaderValue"/> instances
504515
/// that are not null and not media ranges.

src/System.Net.Http.Formatting/Formatting/XmlMediaTypeFormatter.cs

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -337,12 +337,14 @@ protected internal virtual XmlReader CreateXmlReader(Stream readStream, HttpCont
337337
{
338338
// Get the character encoding for the content
339339
Encoding effectiveEncoding = SelectCharacterEncoding(content == null ? null : content.Headers);
340-
#if NETFX_CORE
341-
// Force a preamble into the stream, since CreateTextReader in WinRT only supports auto-detecting encoding.
342-
return XmlDictionaryReader.CreateTextReader(new ReadOnlyStreamWithEncodingPreamble(readStream, effectiveEncoding), _readerQuotas);
343-
#else
344-
return XmlDictionaryReader.CreateTextReader(new NonClosingDelegatingStream(readStream), effectiveEncoding, _readerQuotas, null);
345-
#endif
340+
341+
// DCS encodings are limited to UTF8, UTF16BE, and UTF16LE. Convert to UTF8 as we read.
342+
Stream innerStream = string.Equals(effectiveEncoding.WebName, Utf8Encoding.WebName, StringComparison.OrdinalIgnoreCase) ?
343+
new NonClosingDelegatingStream(readStream) :
344+
new TranscodingStream(readStream, effectiveEncoding, Utf8Encoding, leaveOpen: true);
345+
346+
// XmlReader will always dispose of innerStream when caller disposes of the reader.
347+
return XmlDictionaryReader.CreateTextReader(innerStream, Utf8Encoding, _readerQuotas, onClose: null);
346348
}
347349

348350
/// <inheritdoc/>
@@ -433,9 +435,20 @@ protected internal virtual object GetSerializer(Type type, object value, HttpCon
433435
protected internal virtual XmlWriter CreateXmlWriter(Stream writeStream, HttpContent content)
434436
{
435437
Encoding effectiveEncoding = SelectCharacterEncoding(content != null ? content.Headers : null);
438+
WritePreamble(writeStream, effectiveEncoding);
439+
440+
// DCS encodings are limited to UTF8, UTF16BE, and UTF16LE. Convert to UTF8 as we read.
441+
Stream innerStream = string.Equals(effectiveEncoding.WebName, Utf8Encoding.WebName, StringComparison.OrdinalIgnoreCase) ?
442+
writeStream :
443+
new TranscodingStream(writeStream, effectiveEncoding, Utf8Encoding, leaveOpen: true);
444+
436445
XmlWriterSettings writerSettings = WriterSettings.Clone();
437-
writerSettings.Encoding = effectiveEncoding;
438-
return XmlWriter.Create(writeStream, writerSettings);
446+
writerSettings.Encoding = Utf8Encoding;
447+
448+
// Have XmlWriter dispose of innerStream when caller disposes of the writer if using a TranscodingStream.
449+
writerSettings.CloseOutput = writeStream != innerStream;
450+
451+
return XmlWriter.Create(innerStream, writerSettings);
439452
}
440453

441454
/// <summary>

src/System.Net.Http.Formatting/Internal/ReadOnlyStreamWithEncodingPreamble.cs

Lines changed: 0 additions & 175 deletions
This file was deleted.

test/System.Net.Http.Formatting.Test/Formatting/DataContractJsonMediaTypeFormatterTests.cs

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -218,12 +218,6 @@ public void UseDataContractJsonSerializer_True_Indent_Throws()
218218

219219
public override Task ReadFromStreamAsync_UsesCorrectCharacterEncoding(string content, string encoding, bool isDefaultEncoding)
220220
{
221-
if (!isDefaultEncoding)
222-
{
223-
// XmlDictionaryReader/Writer only supports utf-8 and 16
224-
return TaskHelpers.Completed();
225-
}
226-
227221
// Arrange
228222
DataContractJsonMediaTypeFormatter formatter = new DataContractJsonMediaTypeFormatter();
229223
string formattedContent = "\"" + content + "\"";
@@ -236,14 +230,6 @@ public override Task ReadFromStreamAsync_UsesCorrectCharacterEncoding(string con
236230

237231
public override Task WriteToStreamAsync_UsesCorrectCharacterEncoding(string content, string encoding, bool isDefaultEncoding)
238232
{
239-
// DataContractJsonSerializer does not honor the value of byteOrderMark in the UnicodeEncoding ctor.
240-
// It doesn't include the BOM when byteOrderMark is set to true.
241-
if (!isDefaultEncoding || encoding != "utf-8")
242-
{
243-
// XmlDictionaryReader/Writer only supports utf-8 and 16
244-
return TaskHelpers.Completed();
245-
}
246-
247233
// Arrange
248234
DataContractJsonMediaTypeFormatter formatter = new DataContractJsonMediaTypeFormatter();
249235
string formattedContent = "\"" + content + "\"";

0 commit comments

Comments
 (0)