Use Encoding.CreateTranscodingStream (#21509)
* Use Encoding.CreateTranscodingStream Fixes https://github.com/dotnet/aspnetcore/issues/21243
This commit is contained in:
parent
5fd4f87977
commit
51f18b5652
|
|
@ -7,7 +7,6 @@ using System.Text;
|
|||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.Formatters.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Formatters
|
||||
|
|
@ -67,7 +66,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
}
|
||||
|
||||
var httpContext = context.HttpContext;
|
||||
var inputStream = GetInputStream(httpContext, encoding);
|
||||
var (inputStream, usesTranscodingStream) = GetInputStream(httpContext, encoding);
|
||||
|
||||
object model;
|
||||
try
|
||||
|
|
@ -98,9 +97,9 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
}
|
||||
finally
|
||||
{
|
||||
if (inputStream is TranscodingReadStream transcoding)
|
||||
if (usesTranscodingStream)
|
||||
{
|
||||
await transcoding.DisposeAsync();
|
||||
await inputStream.DisposeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -119,14 +118,15 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
}
|
||||
}
|
||||
|
||||
private Stream GetInputStream(HttpContext httpContext, Encoding encoding)
|
||||
private (Stream inputStream, bool usesTranscodingStream) GetInputStream(HttpContext httpContext, Encoding encoding)
|
||||
{
|
||||
if (encoding.CodePage == Encoding.UTF8.CodePage)
|
||||
{
|
||||
return httpContext.Request.Body;
|
||||
return (httpContext.Request.Body, false);
|
||||
}
|
||||
|
||||
return new TranscodingReadStream(httpContext.Request.Body, encoding);
|
||||
var inputStream = Encoding.CreateTranscodingStream(httpContext.Request.Body, encoding, Encoding.UTF8, leaveOpen: true);
|
||||
return (inputStream, true);
|
||||
}
|
||||
|
||||
private static class Log
|
||||
|
|
|
|||
|
|
@ -2,14 +2,11 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.ExceptionServices;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.Formatters.Json;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Formatters
|
||||
{
|
||||
|
|
@ -73,44 +70,50 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
|
||||
var httpContext = context.HttpContext;
|
||||
|
||||
var writeStream = GetWriteStream(httpContext, selectedEncoding);
|
||||
try
|
||||
{
|
||||
// context.ObjectType reflects the declared model type when specified.
|
||||
// For polymorphic scenarios where the user declares a return type, but returns a derived type,
|
||||
// we want to serialize all the properties on the derived type. This keeps parity with
|
||||
// the behavior you get when the user does not declare the return type and with Json.Net at least at the top level.
|
||||
var objectType = context.Object?.GetType() ?? context.ObjectType ?? typeof(object);
|
||||
await JsonSerializer.SerializeAsync(writeStream, context.Object, objectType, SerializerOptions);
|
||||
// context.ObjectType reflects the declared model type when specified.
|
||||
// For polymorphic scenarios where the user declares a return type, but returns a derived type,
|
||||
// we want to serialize all the properties on the derived type. This keeps parity with
|
||||
// the behavior you get when the user does not declare the return type and with Json.Net at least at the top level.
|
||||
var objectType = context.Object?.GetType() ?? context.ObjectType ?? typeof(object);
|
||||
|
||||
// The transcoding streams use Encoders and Decoders that have internal buffers. We need to flush these
|
||||
// when there is no more data to be written. Stream.FlushAsync isn't suitable since it's
|
||||
// acceptable to Flush a Stream (multiple times) prior to completion.
|
||||
if (writeStream is TranscodingWriteStream transcodingStream)
|
||||
{
|
||||
await transcodingStream.FinalWriteAsync(CancellationToken.None);
|
||||
}
|
||||
await writeStream.FlushAsync();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (writeStream is TranscodingWriteStream transcodingStream)
|
||||
{
|
||||
await transcodingStream.DisposeAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Stream GetWriteStream(HttpContext httpContext, Encoding selectedEncoding)
|
||||
{
|
||||
var responseStream = httpContext.Response.Body;
|
||||
if (selectedEncoding.CodePage == Encoding.UTF8.CodePage)
|
||||
{
|
||||
// JsonSerializer does not write a BOM. Therefore we do not have to handle it
|
||||
// in any special way.
|
||||
return httpContext.Response.Body;
|
||||
await JsonSerializer.SerializeAsync(responseStream, context.Object, objectType, SerializerOptions);
|
||||
await responseStream.FlushAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
// JsonSerializer only emits UTF8 encoded output, but we need to write the response in the encoding specified by
|
||||
// selectedEncoding
|
||||
var transcodingStream = Encoding.CreateTranscodingStream(httpContext.Response.Body, selectedEncoding, Encoding.UTF8, leaveOpen: true);
|
||||
|
||||
return new TranscodingWriteStream(httpContext.Response.Body, selectedEncoding);
|
||||
ExceptionDispatchInfo exceptionDispatchInfo = null;
|
||||
try
|
||||
{
|
||||
await JsonSerializer.SerializeAsync(transcodingStream, context.Object, objectType, SerializerOptions);
|
||||
await transcodingStream.FlushAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// TranscodingStream may write to the inner stream as part of it's disposal.
|
||||
// We do not want this exception "ex" to be eclipsed by any exception encountered during the write. We will stash it and
|
||||
// explicitly rethrow it during the finally block.
|
||||
exceptionDispatchInfo = ExceptionDispatchInfo.Capture(ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
await transcodingStream.DisposeAsync();
|
||||
}
|
||||
catch when (exceptionDispatchInfo != null)
|
||||
{
|
||||
}
|
||||
|
||||
exceptionDispatchInfo?.Throw();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,223 +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;
|
||||
using System.Buffers;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Text.Unicode;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Formatters.Json
|
||||
{
|
||||
internal sealed class TranscodingReadStream : Stream
|
||||
{
|
||||
private static readonly int OverflowBufferSize = Encoding.UTF8.GetMaxByteCount(1); // The most number of bytes used to represent a single UTF char
|
||||
|
||||
internal const int MaxByteBufferSize = 4096;
|
||||
internal const int MaxCharBufferSize = 3 * MaxByteBufferSize;
|
||||
|
||||
private readonly Stream _stream;
|
||||
private readonly Decoder _decoder;
|
||||
|
||||
private ArraySegment<byte> _byteBuffer;
|
||||
private ArraySegment<char> _charBuffer;
|
||||
private ArraySegment<byte> _overflowBuffer;
|
||||
private bool _disposed;
|
||||
|
||||
public TranscodingReadStream(Stream input, Encoding sourceEncoding)
|
||||
{
|
||||
_stream = input;
|
||||
|
||||
// The "count" in the buffer is the size of any content from a previous read.
|
||||
// Initialize them to 0 since nothing has been read so far.
|
||||
_byteBuffer = new ArraySegment<byte>(
|
||||
ArrayPool<byte>.Shared.Rent(MaxByteBufferSize),
|
||||
0,
|
||||
count: 0);
|
||||
|
||||
// Attempt to allocate a char buffer than can tolerate the worst-case scenario for this
|
||||
// encoding. This would allow the byte -> char conversion to complete in a single call.
|
||||
// However limit the buffer size to prevent an encoding that has a very poor worst-case scenario.
|
||||
// The conversion process is tolerant of char buffer that is not large enough to convert all the bytes at once.
|
||||
var maxCharBufferSize = Math.Min(MaxCharBufferSize, sourceEncoding.GetMaxCharCount(MaxByteBufferSize));
|
||||
_charBuffer = new ArraySegment<char>(
|
||||
ArrayPool<char>.Shared.Rent(maxCharBufferSize),
|
||||
0,
|
||||
count: 0);
|
||||
|
||||
_overflowBuffer = new ArraySegment<byte>(
|
||||
ArrayPool<byte>.Shared.Rent(OverflowBufferSize),
|
||||
0,
|
||||
count: 0);
|
||||
|
||||
_decoder = sourceEncoding.GetDecoder();
|
||||
}
|
||||
|
||||
public override bool CanRead => true;
|
||||
public override bool CanSeek => false;
|
||||
public override bool CanWrite => false;
|
||||
public override long Length => throw new NotSupportedException();
|
||||
|
||||
public override long Position
|
||||
{
|
||||
get => throw new NotSupportedException();
|
||||
set => throw new NotSupportedException();
|
||||
}
|
||||
|
||||
internal int ByteBufferCount => _byteBuffer.Count;
|
||||
internal int CharBufferCount => _charBuffer.Count;
|
||||
internal int OverflowCount => _overflowBuffer.Count;
|
||||
|
||||
public override void Flush()
|
||||
=> throw new NotSupportedException();
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count)
|
||||
=> throw new NotSupportedException();
|
||||
|
||||
public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
|
||||
{
|
||||
ThrowArgumentOutOfRangeException(buffer, offset, count);
|
||||
|
||||
if (count == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var readBuffer = new ArraySegment<byte>(buffer, offset, count);
|
||||
|
||||
if (_overflowBuffer.Count > 0)
|
||||
{
|
||||
var bytesToCopy = Math.Min(count, _overflowBuffer.Count);
|
||||
_overflowBuffer.Slice(0, bytesToCopy).CopyTo(readBuffer);
|
||||
|
||||
_overflowBuffer = _overflowBuffer.Slice(bytesToCopy);
|
||||
|
||||
// If we have any overflow bytes, avoid complicating the remainder of the code, by returning as
|
||||
// soon as we copy any content.
|
||||
return bytesToCopy;
|
||||
}
|
||||
|
||||
if (_charBuffer.Count == 0)
|
||||
{
|
||||
// Only read more content from the input stream if we have exhausted all the buffered chars.
|
||||
await ReadInputChars(cancellationToken);
|
||||
}
|
||||
|
||||
var operationStatus = Utf8.FromUtf16(_charBuffer, readBuffer, out var charsRead, out var bytesWritten, isFinalBlock: false);
|
||||
_charBuffer = _charBuffer.Slice(charsRead);
|
||||
|
||||
switch (operationStatus)
|
||||
{
|
||||
case OperationStatus.Done:
|
||||
return bytesWritten;
|
||||
|
||||
case OperationStatus.DestinationTooSmall:
|
||||
if (bytesWritten != 0)
|
||||
{
|
||||
return bytesWritten;
|
||||
}
|
||||
|
||||
// Overflow buffer is always empty when we get here and we can use it's full length to write contents to.
|
||||
Utf8.FromUtf16(_charBuffer, _overflowBuffer.Array, out var overFlowChars, out var overflowBytes, isFinalBlock: false);
|
||||
|
||||
Debug.Assert(overflowBytes > 0 && overFlowChars > 0, "We expect writes to the overflow buffer to always succeed since it is large enough to accommodate at least one char.");
|
||||
|
||||
_charBuffer = _charBuffer.Slice(overFlowChars);
|
||||
|
||||
// readBuffer: [ 0, 0, ], overflowBuffer: [ 7, 13, 34, ]
|
||||
// Fill up the readBuffer to capacity, so the result looks like so:
|
||||
// readBuffer: [ 7, 13 ], overflowBuffer: [ 34 ]
|
||||
Debug.Assert(readBuffer.Count < overflowBytes);
|
||||
_overflowBuffer.Array.AsSpan(0, readBuffer.Count).CopyTo(readBuffer);
|
||||
|
||||
_overflowBuffer = new ArraySegment<byte>(
|
||||
_overflowBuffer.Array,
|
||||
readBuffer.Count,
|
||||
overflowBytes - readBuffer.Count);
|
||||
|
||||
Debug.Assert(_overflowBuffer.Count != 0);
|
||||
|
||||
return readBuffer.Count;
|
||||
|
||||
default:
|
||||
Debug.Fail("We should never see this");
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ReadInputChars(CancellationToken cancellationToken)
|
||||
{
|
||||
// If we had left-over bytes from a previous read, move it to the start of the buffer and read content in to
|
||||
// the segment that follows.
|
||||
Buffer.BlockCopy(
|
||||
_byteBuffer.Array,
|
||||
_byteBuffer.Offset,
|
||||
_byteBuffer.Array,
|
||||
0,
|
||||
_byteBuffer.Count);
|
||||
|
||||
var readBytes = await _stream.ReadAsync(_byteBuffer.Array.AsMemory(_byteBuffer.Count), cancellationToken);
|
||||
_byteBuffer = new ArraySegment<byte>(_byteBuffer.Array, 0, _byteBuffer.Count + readBytes);
|
||||
|
||||
Debug.Assert(_charBuffer.Count == 0, "We should only expect to read more input chars once all buffered content is read");
|
||||
|
||||
_decoder.Convert(
|
||||
_byteBuffer.AsSpan(),
|
||||
_charBuffer.Array,
|
||||
flush: readBytes == 0,
|
||||
out var bytesUsed,
|
||||
out var charsUsed,
|
||||
out _);
|
||||
|
||||
_byteBuffer = _byteBuffer.Slice(bytesUsed);
|
||||
_charBuffer = new ArraySegment<char>(_charBuffer.Array, 0, charsUsed);
|
||||
}
|
||||
|
||||
private static void ThrowArgumentOutOfRangeException(byte[] buffer, int offset, int count)
|
||||
{
|
||||
if (count < 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(count));
|
||||
}
|
||||
|
||||
if (offset < 0 || offset >= buffer.Length)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(offset));
|
||||
}
|
||||
|
||||
if (buffer.Length - offset < count)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(count));
|
||||
}
|
||||
}
|
||||
|
||||
public override long Seek(long offset, SeekOrigin origin)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public override void SetLength(long value)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public override void Write(byte[] buffer, int offset, int count)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (!_disposed)
|
||||
{
|
||||
_disposed = true;
|
||||
ArrayPool<char>.Shared.Return(_charBuffer.Array);
|
||||
ArrayPool<byte>.Shared.Return(_byteBuffer.Array);
|
||||
ArrayPool<byte>.Shared.Return(_overflowBuffer.Array);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,181 +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;
|
||||
using System.Buffers;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Formatters.Json
|
||||
{
|
||||
internal sealed class TranscodingWriteStream : Stream
|
||||
{
|
||||
internal const int MaxCharBufferSize = 4096;
|
||||
internal const int MaxByteBufferSize = 4 * MaxCharBufferSize;
|
||||
private readonly int _maxByteBufferSize;
|
||||
|
||||
private readonly Stream _stream;
|
||||
private readonly Decoder _decoder;
|
||||
private readonly Encoder _encoder;
|
||||
private readonly char[] _charBuffer;
|
||||
private int _charsDecoded;
|
||||
private bool _disposed;
|
||||
|
||||
public TranscodingWriteStream(Stream stream, Encoding targetEncoding)
|
||||
{
|
||||
_stream = stream;
|
||||
|
||||
_charBuffer = ArrayPool<char>.Shared.Rent(MaxCharBufferSize);
|
||||
|
||||
// Attempt to allocate a byte buffer than can tolerate the worst-case scenario for this
|
||||
// encoding. This would allow the char -> byte conversion to complete in a single call.
|
||||
// However limit the buffer size to prevent an encoding that has a very poor worst-case scenario.
|
||||
_maxByteBufferSize = Math.Min(MaxByteBufferSize, targetEncoding.GetMaxByteCount(MaxCharBufferSize));
|
||||
|
||||
_decoder = Encoding.UTF8.GetDecoder();
|
||||
_encoder = targetEncoding.GetEncoder();
|
||||
}
|
||||
|
||||
public override bool CanRead => false;
|
||||
public override bool CanSeek => false;
|
||||
public override bool CanWrite => true;
|
||||
public override long Length => throw new NotSupportedException();
|
||||
public override long Position { get; set; }
|
||||
|
||||
public override void Flush()
|
||||
=> throw new NotSupportedException();
|
||||
|
||||
public override async Task FlushAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await _stream.FlushAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count)
|
||||
=> throw new NotSupportedException();
|
||||
|
||||
public override long Seek(long offset, SeekOrigin origin)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public override void SetLength(long value)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public override void Write(byte[] buffer, int offset, int count)
|
||||
=> throw new NotSupportedException();
|
||||
|
||||
public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
|
||||
{
|
||||
ThrowArgumentException(buffer, offset, count);
|
||||
var bufferSegment = new ArraySegment<byte>(buffer, offset, count);
|
||||
return WriteAsync(bufferSegment, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task WriteAsync(
|
||||
ArraySegment<byte> bufferSegment,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var decoderCompleted = false;
|
||||
while (!decoderCompleted)
|
||||
{
|
||||
_decoder.Convert(
|
||||
bufferSegment,
|
||||
_charBuffer.AsSpan(_charsDecoded),
|
||||
flush: false,
|
||||
out var bytesDecoded,
|
||||
out var charsDecoded,
|
||||
out decoderCompleted);
|
||||
|
||||
_charsDecoded += charsDecoded;
|
||||
bufferSegment = bufferSegment.Slice(bytesDecoded);
|
||||
|
||||
if (!decoderCompleted)
|
||||
{
|
||||
await WriteBufferAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task WriteBufferAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var encoderCompleted = false;
|
||||
var charsWritten = 0;
|
||||
var byteBuffer = ArrayPool<byte>.Shared.Rent(_maxByteBufferSize);
|
||||
|
||||
while (!encoderCompleted && charsWritten < _charsDecoded)
|
||||
{
|
||||
_encoder.Convert(
|
||||
_charBuffer.AsSpan(charsWritten, _charsDecoded - charsWritten),
|
||||
byteBuffer,
|
||||
flush: false,
|
||||
out var charsEncoded,
|
||||
out var bytesUsed,
|
||||
out encoderCompleted);
|
||||
|
||||
await _stream.WriteAsync(byteBuffer.AsMemory(0, bytesUsed), cancellationToken);
|
||||
charsWritten += charsEncoded;
|
||||
}
|
||||
|
||||
ArrayPool<byte>.Shared.Return(byteBuffer);
|
||||
|
||||
// At this point, we've written all the buffered chars to the underlying Stream.
|
||||
_charsDecoded = 0;
|
||||
}
|
||||
|
||||
private static void ThrowArgumentException(byte[] buffer, int offset, int count)
|
||||
{
|
||||
if (count <= 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(count));
|
||||
}
|
||||
|
||||
if (offset < 0 || offset >= buffer.Length)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(offset));
|
||||
}
|
||||
|
||||
if (buffer.Length - offset < count)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(count));
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (!_disposed)
|
||||
{
|
||||
_disposed = true;
|
||||
ArrayPool<char>.Shared.Return(_charBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task FinalWriteAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
// First write any buffered content
|
||||
await WriteBufferAsync(cancellationToken);
|
||||
|
||||
// Now flush the encoder.
|
||||
var byteBuffer = ArrayPool<byte>.Shared.Rent(_maxByteBufferSize);
|
||||
var encoderCompleted = false;
|
||||
|
||||
while (!encoderCompleted)
|
||||
{
|
||||
_encoder.Convert(
|
||||
Array.Empty<char>(),
|
||||
byteBuffer,
|
||||
flush: true,
|
||||
out _,
|
||||
out var bytesUsed,
|
||||
out encoderCompleted);
|
||||
|
||||
await _stream.WriteAsync(byteBuffer.AsMemory(0, bytesUsed), cancellationToken);
|
||||
}
|
||||
|
||||
ArrayPool<byte>.Shared.Return(byteBuffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,16 +2,12 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Runtime.ExceptionServices;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.Core;
|
||||
using Microsoft.AspNetCore.Mvc.Formatters;
|
||||
using Microsoft.AspNetCore.Mvc.Formatters.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
|
@ -71,50 +67,56 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
|||
|
||||
Log.JsonResultExecuting(_logger, result.Value);
|
||||
|
||||
var value = result.Value;
|
||||
if (value != null && _asyncEnumerableReaderFactory.TryGetReader(value.GetType(), out var reader))
|
||||
{
|
||||
Log.BufferingAsyncEnumerable(_logger, value);
|
||||
value = await reader(value);
|
||||
}
|
||||
|
||||
var objectType = value?.GetType() ?? typeof(object);
|
||||
|
||||
// Keep this code in sync with SystemTextJsonOutputFormatter
|
||||
var writeStream = GetWriteStream(context.HttpContext, resolvedContentTypeEncoding);
|
||||
try
|
||||
var responseStream = response.Body;
|
||||
if (resolvedContentTypeEncoding.CodePage == Encoding.UTF8.CodePage)
|
||||
{
|
||||
var value = result.Value;
|
||||
if (value != null && _asyncEnumerableReaderFactory.TryGetReader(value.GetType(), out var reader))
|
||||
{
|
||||
Log.BufferingAsyncEnumerable(_logger, value);
|
||||
value = await reader(value);
|
||||
}
|
||||
|
||||
var type = value?.GetType() ?? typeof(object);
|
||||
await JsonSerializer.SerializeAsync(writeStream, value, type, jsonSerializerOptions);
|
||||
|
||||
// The transcoding streams use Encoders and Decoders that have internal buffers. We need to flush these
|
||||
// when there is no more data to be written. Stream.FlushAsync isn't suitable since it's
|
||||
// acceptable to Flush a Stream (multiple times) prior to completion.
|
||||
if (writeStream is TranscodingWriteStream transcodingStream)
|
||||
{
|
||||
await transcodingStream.FinalWriteAsync(CancellationToken.None);
|
||||
}
|
||||
await writeStream.FlushAsync();
|
||||
await JsonSerializer.SerializeAsync(responseStream, value, objectType, jsonSerializerOptions);
|
||||
await responseStream.FlushAsync();
|
||||
}
|
||||
finally
|
||||
else
|
||||
{
|
||||
if (writeStream is TranscodingWriteStream transcodingStream)
|
||||
// JsonSerializer only emits UTF8 encoded output, but we need to write the response in the encoding specified by
|
||||
// selectedEncoding
|
||||
var transcodingStream = Encoding.CreateTranscodingStream(response.Body, resolvedContentTypeEncoding, Encoding.UTF8, leaveOpen: true);
|
||||
|
||||
ExceptionDispatchInfo exceptionDispatchInfo = null;
|
||||
try
|
||||
{
|
||||
await transcodingStream.DisposeAsync();
|
||||
await JsonSerializer.SerializeAsync(transcodingStream, value, objectType, jsonSerializerOptions);
|
||||
await transcodingStream.FlushAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// TranscodingStream may write to the inner stream as part of it's disposal.
|
||||
// We do not want this exception "ex" to be eclipsed by any exception encountered during the write. We will stash it and
|
||||
// explicitly rethrow it during the finally block.
|
||||
exceptionDispatchInfo = ExceptionDispatchInfo.Capture(ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
await transcodingStream.DisposeAsync();
|
||||
}
|
||||
catch when (exceptionDispatchInfo != null)
|
||||
{
|
||||
}
|
||||
|
||||
exceptionDispatchInfo?.Throw();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Stream GetWriteStream(HttpContext httpContext, Encoding selectedEncoding)
|
||||
{
|
||||
if (selectedEncoding.CodePage == Encoding.UTF8.CodePage)
|
||||
{
|
||||
// JsonSerializer does not write a BOM. Therefore we do not have to handle it
|
||||
// in any special way.
|
||||
return httpContext.Response.Body;
|
||||
}
|
||||
|
||||
return new TranscodingWriteStream(httpContext.Response.Body, selectedEncoding);
|
||||
}
|
||||
|
||||
private JsonSerializerOptions GetSerializerOptions(JsonResult result)
|
||||
{
|
||||
var serializerSettings = result.SerializerSettings;
|
||||
|
|
|
|||
|
|
@ -110,6 +110,28 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
Assert.Equal("abcd", stringValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task JsonFormatterReadsNonUtf8Content()
|
||||
{
|
||||
// Arrange
|
||||
var content = "☀☁☂☃☄★☆☇☈☉☊☋☌☍☎☏☐☑☒☓☚☛☜☝☞☟☠☡☢☣☤☥☦☧☨☩☪☫☬☮☯☰☱☲☳☴☵☶☷☸";
|
||||
var formatter = GetInputFormatter();
|
||||
|
||||
var contentBytes = Encoding.Unicode.GetBytes($"\"{content}\"");
|
||||
var httpContext = GetHttpContext(contentBytes, "application/json;charset=utf-16");
|
||||
|
||||
var formatterContext = CreateInputFormatterContext(typeof(string), httpContext);
|
||||
|
||||
// Act
|
||||
var result = await formatter.ReadAsync(formatterContext);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.HasError);
|
||||
var stringValue = Assert.IsType<string>(result.Model);
|
||||
Assert.Equal(content, stringValue);
|
||||
Assert.True(httpContext.Request.Body.CanRead, "Verify that the request stream hasn't been disposed");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public virtual async Task JsonFormatter_EscapedKeys_Bracket()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -108,6 +108,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
// Assert
|
||||
var actualContent = encoding.GetString(body.ToArray());
|
||||
Assert.Equal(expectedContent, actualContent, StringComparer.OrdinalIgnoreCase);
|
||||
Assert.True(body.CanWrite, "Response body should not be disposed.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
// 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;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
|
|
@ -55,6 +57,30 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
Assert.Equal(expectedContent, actualContent);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WriteResponseBodyAsync_WithNonUtf8Encoding_FormattingErrorsAreThrown()
|
||||
{
|
||||
// Arrange
|
||||
var formatter = GetOutputFormatter();
|
||||
var mediaType = MediaTypeHeaderValue.Parse("application/json; charset=utf-16");
|
||||
var encoding = CreateOrGetSupportedEncoding(formatter, "utf-16", isDefaultEncoding: true);
|
||||
|
||||
var body = new MemoryStream();
|
||||
var actionContext = GetActionContext(mediaType, body);
|
||||
|
||||
var outputFormatterContext = new OutputFormatterWriteContext(
|
||||
actionContext.HttpContext,
|
||||
new TestHttpResponseStreamWriterFactory().CreateWriter,
|
||||
typeof(Person),
|
||||
new ThrowingFormatterModel())
|
||||
{
|
||||
ContentType = new StringSegment(mediaType.ToString()),
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<TimeZoneNotFoundException>(() => formatter.WriteResponseBodyAsync(outputFormatterContext, Encoding.GetEncoding("utf-16")));
|
||||
}
|
||||
|
||||
private class Person
|
||||
{
|
||||
public string Name { get; set; }
|
||||
|
|
@ -63,5 +89,24 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
|
||||
public Person Parent { get; set; }
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(ThrowingFormatterPersonConverter))]
|
||||
private class ThrowingFormatterModel
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
private class ThrowingFormatterPersonConverter : JsonConverter<ThrowingFormatterModel>
|
||||
{
|
||||
public override ThrowingFormatterModel Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, ThrowingFormatterModel value, JsonSerializerOptions options)
|
||||
{
|
||||
throw new TimeZoneNotFoundException();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,254 +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.IO;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Formatters.Json
|
||||
{
|
||||
public class TranscodingReadStreamTest
|
||||
{
|
||||
[Fact]
|
||||
public async Task ReadAsync_SingleByte()
|
||||
{
|
||||
// Arrange
|
||||
var input = "Hello world";
|
||||
var encoding = Encoding.Unicode;
|
||||
using var stream = new TranscodingReadStream(new MemoryStream(encoding.GetBytes(input)), encoding);
|
||||
var bytes = new byte[4];
|
||||
|
||||
// Act
|
||||
var readBytes = await stream.ReadAsync(bytes, 0, 1);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(1, readBytes);
|
||||
Assert.Equal((byte)'H', bytes[0]);
|
||||
Assert.Equal(0, bytes[1]);
|
||||
|
||||
Assert.Equal(0, stream.ByteBufferCount);
|
||||
Assert.Equal(10, stream.CharBufferCount);
|
||||
Assert.Equal(0, stream.OverflowCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReadAsync_FillsBuffer()
|
||||
{
|
||||
// Arrange
|
||||
var input = "Hello world";
|
||||
var encoding = Encoding.Unicode;
|
||||
using var stream = new TranscodingReadStream(new MemoryStream(encoding.GetBytes(input)), encoding);
|
||||
var bytes = new byte[3];
|
||||
var expected = Encoding.UTF8.GetBytes(input.Substring(0, bytes.Length));
|
||||
|
||||
// Act
|
||||
var readBytes = await stream.ReadAsync(bytes, 0, bytes.Length);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(3, readBytes);
|
||||
Assert.Equal(expected, bytes);
|
||||
Assert.Equal(0, stream.ByteBufferCount);
|
||||
Assert.Equal(8, stream.CharBufferCount);
|
||||
Assert.Equal(0, stream.OverflowCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReadAsync_CompletedInSecondIteration()
|
||||
{
|
||||
// Arrange
|
||||
var input = new string('A', 1024 + 10);
|
||||
var encoding = Encoding.Unicode;
|
||||
using var stream = new TranscodingReadStream(new MemoryStream(encoding.GetBytes(input)), encoding);
|
||||
var bytes = new byte[1024];
|
||||
var expected = Encoding.UTF8.GetBytes(input.Substring(0, bytes.Length));
|
||||
|
||||
// Act
|
||||
var readBytes = await stream.ReadAsync(bytes, 0, bytes.Length);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(bytes.Length, readBytes);
|
||||
Assert.Equal(expected, bytes);
|
||||
Assert.Equal(0, stream.ByteBufferCount);
|
||||
Assert.Equal(10, stream.CharBufferCount);
|
||||
Assert.Equal(0, stream.OverflowCount);
|
||||
|
||||
readBytes = await stream.ReadAsync(bytes, 0, bytes.Length);
|
||||
Assert.Equal(10, readBytes);
|
||||
Assert.Equal(0, stream.ByteBufferCount);
|
||||
Assert.Equal(0, stream.CharBufferCount);
|
||||
Assert.Equal(0, stream.OverflowCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReadAsync_WithOverflowBuffer()
|
||||
{
|
||||
// Arrange
|
||||
// Test ensures that the overflow buffer works correctly
|
||||
var input = "☀";
|
||||
var encoding = Encoding.Unicode;
|
||||
using var stream = new TranscodingReadStream(new MemoryStream(encoding.GetBytes(input)), encoding);
|
||||
var bytes = new byte[1];
|
||||
var expected = Encoding.UTF8.GetBytes(input);
|
||||
|
||||
// Act
|
||||
var readBytes = await stream.ReadAsync(bytes, 0, bytes.Length);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(1, readBytes);
|
||||
Assert.Equal(expected[0], bytes[0]);
|
||||
Assert.Equal(0, stream.ByteBufferCount);
|
||||
Assert.Equal(0, stream.CharBufferCount);
|
||||
Assert.Equal(2, stream.OverflowCount);
|
||||
|
||||
bytes = new byte[expected.Length - 1];
|
||||
readBytes = await stream.ReadAsync(bytes, 0, bytes.Length);
|
||||
Assert.Equal(bytes.Length, readBytes);
|
||||
Assert.Equal(0, stream.ByteBufferCount);
|
||||
Assert.Equal(0, stream.CharBufferCount);
|
||||
Assert.Equal(0, stream.OverflowCount);
|
||||
|
||||
readBytes = await stream.ReadAsync(bytes, 0, bytes.Length);
|
||||
Assert.Equal(0, readBytes);
|
||||
}
|
||||
|
||||
public static TheoryData<string> ReadAsync_WithOverflowBuffer_AtBoundariesData => new TheoryData<string>
|
||||
{
|
||||
new string('a', TranscodingReadStream.MaxCharBufferSize - 1) + "☀",
|
||||
new string('a', TranscodingReadStream.MaxCharBufferSize - 2) + "☀",
|
||||
new string('a', TranscodingReadStream.MaxCharBufferSize) + "☀",
|
||||
};
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(ReadAsync_WithOverflowBuffer_AtBoundariesData))]
|
||||
public Task ReadAsync_WithOverflowBuffer_WithBufferSize1(string input) => ReadAsync_WithOverflowBufferAtCharBufferBoundaries(input, bufferSize: 1);
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(ReadAsync_WithOverflowBuffer_AtBoundariesData))]
|
||||
public Task ReadAsync_WithOverflowBuffer_WithBufferSize2(string input) => ReadAsync_WithOverflowBufferAtCharBufferBoundaries(input, bufferSize: 1);
|
||||
|
||||
private static async Task<TranscodingReadStream> ReadAsync_WithOverflowBufferAtCharBufferBoundaries(string input, int bufferSize)
|
||||
{
|
||||
// Arrange
|
||||
// Test ensures that the overflow buffer works correctly
|
||||
var encoding = Encoding.Unicode;
|
||||
var stream = new TranscodingReadStream(new MemoryStream(encoding.GetBytes(input)), encoding);
|
||||
var bytes = new byte[1];
|
||||
var expected = Encoding.UTF8.GetBytes(input);
|
||||
|
||||
// Act
|
||||
int read;
|
||||
var buffer = new byte[bufferSize];
|
||||
var actual = new List<byte>();
|
||||
|
||||
while ((read = await stream.ReadAsync(buffer)) != 0)
|
||||
{
|
||||
actual.AddRange(buffer);
|
||||
}
|
||||
|
||||
Assert.Equal(expected, actual);
|
||||
return stream;
|
||||
}
|
||||
|
||||
public static TheoryData ReadAsyncInputLatin =>
|
||||
GetLatinTextInput(TranscodingReadStream.MaxCharBufferSize, TranscodingReadStream.MaxByteBufferSize);
|
||||
|
||||
public static TheoryData ReadAsyncInputUnicode =>
|
||||
GetUnicodeText(TranscodingReadStream.MaxCharBufferSize);
|
||||
|
||||
internal static TheoryData GetLatinTextInput(int maxCharBufferSize, int maxByteBufferSize)
|
||||
{
|
||||
return new TheoryData<string>
|
||||
{
|
||||
"Hello world",
|
||||
string.Join(string.Empty, Enumerable.Repeat("AB", 9000)),
|
||||
new string('A', count: maxByteBufferSize),
|
||||
new string('A', count: maxCharBufferSize),
|
||||
new string('A', count: maxByteBufferSize + 1),
|
||||
new string('A', count: maxCharBufferSize + 1),
|
||||
};
|
||||
}
|
||||
|
||||
internal static TheoryData GetUnicodeText(int maxCharBufferSize)
|
||||
{
|
||||
return new TheoryData<string>
|
||||
{
|
||||
new string('Æ', count: 7),
|
||||
new string('A', count: maxCharBufferSize - 1) + 'Æ',
|
||||
"AbĀāĂ㥹ĆŊŋŌōŎŏŐőŒœŔŕŖŗŘřŚşŠšŢţŤťŦŧŨũŪūŬŭŮůŰűŲųŴŵŶŷŸŹźŻżŽžſAbc",
|
||||
"Abcஐஒஓஔகஙசஜஞடணதநனபமயரறலளழவஷஸஹ",
|
||||
"☀☁☂☃☄★☆☇☈☉☊☋☌☍☎☏☐☑☒☓☚☛☜☝☞☟☠☡☢☣☤☥☦☧☨☩☪☫☬☭☮☯☰☱☲☳☴☵☶☷☸",
|
||||
new string('Æ', count: 64 * 1024),
|
||||
new string('Æ', count: 64 * 1024 + 1),
|
||||
"pingüino",
|
||||
new string('ऄ', count: maxCharBufferSize + 1), // This uses 3 bytes to represent in UTF8
|
||||
};
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(ReadAsyncInputLatin))]
|
||||
[MemberData(nameof(ReadAsyncInputUnicode))]
|
||||
public Task ReadAsync_Works_WhenInputIs_UTF32(string message)
|
||||
{
|
||||
var sourceEncoding = Encoding.UTF32;
|
||||
return ReadAsyncTest(sourceEncoding, message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(ReadAsyncInputLatin))]
|
||||
[MemberData(nameof(ReadAsyncInputUnicode))]
|
||||
public Task ReadAsync_Works_WhenInputIs_Unicode(string message)
|
||||
{
|
||||
var sourceEncoding = Encoding.Unicode;
|
||||
return ReadAsyncTest(sourceEncoding, message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(ReadAsyncInputLatin))]
|
||||
[MemberData(nameof(ReadAsyncInputUnicode))]
|
||||
public Task ReadAsync_Works_WhenInputIs_UTF7(string message)
|
||||
{
|
||||
var sourceEncoding = Encoding.UTF7;
|
||||
return ReadAsyncTest(sourceEncoding, message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(ReadAsyncInputLatin))]
|
||||
public Task ReadAsync_Works_WhenInputIs_WesternEuropeanEncoding(string message)
|
||||
{
|
||||
// Arrange
|
||||
var sourceEncoding = Encoding.GetEncoding(28591);
|
||||
return ReadAsyncTest(sourceEncoding, message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(ReadAsyncInputLatin))]
|
||||
public Task ReadAsync_Works_WhenInputIs_ASCII(string message)
|
||||
{
|
||||
// Arrange
|
||||
var sourceEncoding = Encoding.ASCII;
|
||||
return ReadAsyncTest(sourceEncoding, message);
|
||||
}
|
||||
|
||||
private static async Task ReadAsyncTest(Encoding sourceEncoding, string message)
|
||||
{
|
||||
var input = $"{{ \"Message\": \"{message}\" }}";
|
||||
var stream = new MemoryStream(sourceEncoding.GetBytes(input));
|
||||
|
||||
var transcodingStream = new TranscodingReadStream(stream, sourceEncoding);
|
||||
|
||||
var model = await JsonSerializer.DeserializeAsync(transcodingStream, typeof(TestModel));
|
||||
var testModel = Assert.IsType<TestModel>(model);
|
||||
|
||||
Assert.Equal(message, testModel.Message);
|
||||
}
|
||||
|
||||
public class TestModel
|
||||
{
|
||||
public string Message { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,87 +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;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Formatters.Json
|
||||
{
|
||||
public class TranscodingWriteStreamTest
|
||||
{
|
||||
public static TheoryData WriteAsyncInputLatin =>
|
||||
TranscodingReadStreamTest.GetLatinTextInput(TranscodingWriteStream.MaxCharBufferSize, TranscodingWriteStream.MaxByteBufferSize);
|
||||
|
||||
public static TheoryData WriteAsyncInputUnicode =>
|
||||
TranscodingReadStreamTest.GetUnicodeText(TranscodingWriteStream.MaxCharBufferSize);
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(WriteAsyncInputLatin))]
|
||||
[MemberData(nameof(WriteAsyncInputUnicode))]
|
||||
public Task WriteAsync_Works_WhenOutputIs_UTF32(string message)
|
||||
{
|
||||
var targetEncoding = Encoding.UTF32;
|
||||
return WriteAsyncTest(targetEncoding, message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(WriteAsyncInputLatin))]
|
||||
[MemberData(nameof(WriteAsyncInputUnicode))]
|
||||
public Task WriteAsync_Works_WhenOutputIs_Unicode(string message)
|
||||
{
|
||||
var targetEncoding = Encoding.Unicode;
|
||||
return WriteAsyncTest(targetEncoding, message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(WriteAsyncInputLatin))]
|
||||
public Task WriteAsync_Works_WhenOutputIs_UTF7(string message)
|
||||
{
|
||||
var targetEncoding = Encoding.UTF7;
|
||||
return WriteAsyncTest(targetEncoding, message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(WriteAsyncInputLatin))]
|
||||
public Task WriteAsync_Works_WhenOutputIs_WesternEuropeanEncoding(string message)
|
||||
{
|
||||
// Arrange
|
||||
var targetEncoding = Encoding.GetEncoding(28591);
|
||||
return WriteAsyncTest(targetEncoding, message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(WriteAsyncInputLatin))]
|
||||
public Task WriteAsync_Works_WhenOutputIs_ASCII(string message)
|
||||
{
|
||||
// Arrange
|
||||
var targetEncoding = Encoding.ASCII;
|
||||
return WriteAsyncTest(targetEncoding, message);
|
||||
}
|
||||
|
||||
private static async Task WriteAsyncTest(Encoding targetEncoding, string message)
|
||||
{
|
||||
var expected = $"{{\"Message\":\"{JavaScriptEncoder.Default.Encode(message)}\"}}";
|
||||
|
||||
var model = new TestModel { Message = message };
|
||||
var stream = new MemoryStream();
|
||||
|
||||
var transcodingStream = new TranscodingWriteStream(stream, targetEncoding);
|
||||
await JsonSerializer.SerializeAsync(transcodingStream, model, model.GetType());
|
||||
await transcodingStream.FinalWriteAsync(default);
|
||||
await transcodingStream.FlushAsync();
|
||||
|
||||
var actual = targetEncoding.GetString(stream.ToArray());
|
||||
Assert.Equal(expected, actual, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private class TestModel
|
||||
{
|
||||
public string Message { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -382,7 +382,7 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
|||
return httpContext;
|
||||
}
|
||||
|
||||
private static ActionContext GetActionContext()
|
||||
protected static ActionContext GetActionContext()
|
||||
{
|
||||
return new ActionContext(GetHttpContext(), new RouteData(), new ActionDescriptor());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,14 @@
|
|||
// 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;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||
{
|
||||
|
|
@ -17,9 +22,44 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
|||
Options.Create(new MvcOptions()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WriteResponseBodyAsync_WithNonUtf8Encoding_FormattingErrorsAreThrown()
|
||||
{
|
||||
// Arrange
|
||||
var context = GetActionContext();
|
||||
|
||||
var result = new JsonResult(new ThrowingFormatterModel())
|
||||
{
|
||||
ContentType = "application/json; charset=utf-16",
|
||||
};
|
||||
var executor = CreateExecutor();
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<TimeZoneNotFoundException>(() => executor.ExecuteAsync(context, result));
|
||||
}
|
||||
|
||||
protected override object GetIndentedSettings()
|
||||
{
|
||||
return new JsonSerializerOptions { WriteIndented = true };
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(ThrowingFormatterPersonConverter))]
|
||||
private class ThrowingFormatterModel
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
private class ThrowingFormatterPersonConverter : JsonConverter<ThrowingFormatterModel>
|
||||
{
|
||||
public override ThrowingFormatterModel Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, ThrowingFormatterModel value, JsonSerializerOptions options)
|
||||
{
|
||||
throw new TimeZoneNotFoundException();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue