System.Text.Json based formatters (#8362)
* System.Text.Json based formatters Fixes: https://github.com/aspnet/AspNetCore/issues/7256
This commit is contained in:
parent
26fa19e080
commit
f5ff181222
|
|
@ -876,6 +876,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
public bool RequireHttpsPermanent { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||
public bool RespectBrowserAcceptHeader { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||
public bool ReturnHttpNotAcceptable { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||
public System.Text.Json.Serialization.JsonSerializerOptions SerializerOptions { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
|
||||
public int? SslPort { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||
public bool SuppressAsyncSuffixInActionNames { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||
public bool SuppressInputFormatterBuffering { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||
|
|
@ -1855,6 +1856,21 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
public override bool CanWriteResult(Microsoft.AspNetCore.Mvc.Formatters.OutputFormatterCanWriteContext context) { throw null; }
|
||||
public override System.Threading.Tasks.Task WriteResponseBodyAsync(Microsoft.AspNetCore.Mvc.Formatters.OutputFormatterWriteContext context, System.Text.Encoding encoding) { throw null; }
|
||||
}
|
||||
public partial class SystemTextJsonInputFormatter : Microsoft.AspNetCore.Mvc.Formatters.TextInputFormatter, Microsoft.AspNetCore.Mvc.Formatters.IInputFormatterExceptionPolicy
|
||||
{
|
||||
public SystemTextJsonInputFormatter(Microsoft.AspNetCore.Mvc.MvcOptions options) { }
|
||||
Microsoft.AspNetCore.Mvc.Formatters.InputFormatterExceptionPolicy Microsoft.AspNetCore.Mvc.Formatters.IInputFormatterExceptionPolicy.ExceptionPolicy { get { throw null; } }
|
||||
public System.Text.Json.Serialization.JsonSerializerOptions SerializerOptions { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
|
||||
[System.Diagnostics.DebuggerStepThroughAttribute]
|
||||
public sealed override System.Threading.Tasks.Task<Microsoft.AspNetCore.Mvc.Formatters.InputFormatterResult> ReadRequestBodyAsync(Microsoft.AspNetCore.Mvc.Formatters.InputFormatterContext context, System.Text.Encoding encoding) { throw null; }
|
||||
}
|
||||
public partial class SystemTextJsonOutputFormatter : Microsoft.AspNetCore.Mvc.Formatters.TextOutputFormatter
|
||||
{
|
||||
public SystemTextJsonOutputFormatter(Microsoft.AspNetCore.Mvc.MvcOptions options) { }
|
||||
public System.Text.Json.Serialization.JsonSerializerOptions SerializerOptions { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
|
||||
[System.Diagnostics.DebuggerStepThroughAttribute]
|
||||
public sealed override System.Threading.Tasks.Task WriteResponseBodyAsync(Microsoft.AspNetCore.Mvc.Formatters.OutputFormatterWriteContext context, System.Text.Encoding selectedEncoding) { throw null; }
|
||||
}
|
||||
public abstract partial class TextInputFormatter : Microsoft.AspNetCore.Mvc.Formatters.InputFormatter
|
||||
{
|
||||
protected static readonly System.Text.Encoding UTF16EncodingLittleEndian;
|
||||
|
|
|
|||
|
|
@ -3,10 +3,19 @@
|
|||
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Formatters.Xml
|
||||
namespace Microsoft.AspNetCore.Mvc.Formatters
|
||||
{
|
||||
internal static class MediaTypeHeaderValues
|
||||
{
|
||||
public static readonly MediaTypeHeaderValue ApplicationJson
|
||||
= MediaTypeHeaderValue.Parse("application/json").CopyAsReadOnly();
|
||||
|
||||
public static readonly MediaTypeHeaderValue TextJson
|
||||
= MediaTypeHeaderValue.Parse("text/json").CopyAsReadOnly();
|
||||
|
||||
public static readonly MediaTypeHeaderValue ApplicationAnyJsonSyntax
|
||||
= MediaTypeHeaderValue.Parse("application/*+json").CopyAsReadOnly();
|
||||
|
||||
public static readonly MediaTypeHeaderValue ApplicationXml
|
||||
= MediaTypeHeaderValue.Parse("application/xml").CopyAsReadOnly();
|
||||
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
// 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.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.Formatters.Json;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Formatters
|
||||
{
|
||||
/// <summary>
|
||||
/// A <see cref="TextInputFormatter"/> for JSON content that uses <see cref="JsonSerializer"/>.
|
||||
/// </summary>
|
||||
public class SystemTextJsonInputFormatter : TextInputFormatter, IInputFormatterExceptionPolicy
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of <see cref="SystemTextJsonInputFormatter"/>.
|
||||
/// </summary>
|
||||
/// <param name="options">The <see cref="MvcOptions"/>.</param>
|
||||
public SystemTextJsonInputFormatter(MvcOptions options)
|
||||
{
|
||||
SerializerOptions = options.SerializerOptions;
|
||||
|
||||
SupportedEncodings.Add(UTF8EncodingWithoutBOM);
|
||||
SupportedEncodings.Add(UTF16EncodingLittleEndian);
|
||||
|
||||
SupportedMediaTypes.Add(MediaTypeHeaderValues.ApplicationJson);
|
||||
SupportedMediaTypes.Add(MediaTypeHeaderValues.TextJson);
|
||||
SupportedMediaTypes.Add(MediaTypeHeaderValues.ApplicationAnyJsonSyntax);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="JsonSerializerOptions"/> used to configure the <see cref="JsonSerializer"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A single instance of <see cref="SystemTextJsonInputFormatter"/> is used for all JSON formatting. Any
|
||||
/// changes to the options will affect all input formatting.
|
||||
/// </remarks>
|
||||
public JsonSerializerOptions SerializerOptions { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
InputFormatterExceptionPolicy IInputFormatterExceptionPolicy.ExceptionPolicy => InputFormatterExceptionPolicy.MalformedInputExceptions;
|
||||
|
||||
/// <inheritdoc />
|
||||
public sealed override async Task<InputFormatterResult> ReadRequestBodyAsync(
|
||||
InputFormatterContext context,
|
||||
Encoding encoding)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
if (encoding == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(encoding));
|
||||
}
|
||||
|
||||
var httpContext = context.HttpContext;
|
||||
var inputStream = GetInputStream(httpContext, encoding);
|
||||
|
||||
object model;
|
||||
try
|
||||
{
|
||||
model = await JsonSerializer.ReadAsync(inputStream, context.ModelType, SerializerOptions);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (inputStream is TranscodingReadStream transcoding)
|
||||
{
|
||||
transcoding.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
if (model == null && !context.TreatEmptyInputAsDefaultValue)
|
||||
{
|
||||
// Some nonempty inputs might deserialize as null, for example whitespace,
|
||||
// or the JSON-encoded value "null". The upstream BodyModelBinder needs to
|
||||
// be notified that we don't regard this as a real input so it can register
|
||||
// a model binding error.
|
||||
return InputFormatterResult.NoValue();
|
||||
}
|
||||
else
|
||||
{
|
||||
return InputFormatterResult.Success(model);
|
||||
}
|
||||
}
|
||||
|
||||
private Stream GetInputStream(HttpContext httpContext, Encoding encoding)
|
||||
{
|
||||
if (encoding.CodePage == Encoding.UTF8.CodePage)
|
||||
{
|
||||
return httpContext.Request.Body;
|
||||
}
|
||||
|
||||
return new TranscodingReadStream(httpContext.Request.Body, encoding);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
// 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.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.Formatters.Json;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Formatters
|
||||
{
|
||||
/// <summary>
|
||||
/// A <see cref="TextOutputFormatter"/> for JSON content that uses <see cref="JsonSerializer"/>.
|
||||
/// </summary>
|
||||
public class SystemTextJsonOutputFormatter : TextOutputFormatter
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new <see cref="SystemTextJsonOutputFormatter"/> instance.
|
||||
/// </summary>
|
||||
/// <param name="options">The <see cref="MvcOptions"/>.</param>
|
||||
public SystemTextJsonOutputFormatter(MvcOptions options)
|
||||
{
|
||||
SerializerOptions = options.SerializerOptions;
|
||||
|
||||
SupportedEncodings.Add(Encoding.UTF8);
|
||||
SupportedEncodings.Add(Encoding.Unicode);
|
||||
SupportedMediaTypes.Add(MediaTypeHeaderValues.ApplicationJson);
|
||||
SupportedMediaTypes.Add(MediaTypeHeaderValues.TextJson);
|
||||
SupportedMediaTypes.Add(MediaTypeHeaderValues.ApplicationAnyJsonSyntax);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="JsonSerializerOptions"/> used to configure the <see cref="JsonSerializer"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A single instance of <see cref="SystemTextJsonOutputFormatter"/> is used for all JSON formatting. Any
|
||||
/// changes to the options will affect all output formatting.
|
||||
/// </remarks>
|
||||
public JsonSerializerOptions SerializerOptions { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public sealed override async Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
if (selectedEncoding == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(selectedEncoding));
|
||||
}
|
||||
|
||||
var httpContext = context.HttpContext;
|
||||
|
||||
var writeStream = GetWriteStream(httpContext, selectedEncoding);
|
||||
try
|
||||
{
|
||||
await JsonSerializer.WriteAsync(context.Object, context.ObjectType, writeStream, SerializerOptions);
|
||||
await writeStream.FlushAsync();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (writeStream is TranscodingWriteStream transcoding)
|
||||
{
|
||||
transcoding.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,234 @@
|
|||
// 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.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Formatters.Json
|
||||
{
|
||||
internal sealed class TranscodingReadStream : Stream
|
||||
{
|
||||
internal const int MaxByteBufferSize = 4096;
|
||||
internal const int MaxCharBufferSize = 3 * MaxByteBufferSize;
|
||||
private static readonly int MaxByteCountForUTF8Char = Encoding.UTF8.GetMaxByteCount(charCount: 1);
|
||||
|
||||
private readonly Stream _stream;
|
||||
private readonly Encoder _encoder;
|
||||
private readonly Decoder _decoder;
|
||||
|
||||
private ArraySegment<byte> _byteBuffer;
|
||||
private ArraySegment<char> _charBuffer;
|
||||
private ArraySegment<byte> _overflowBuffer;
|
||||
|
||||
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(MaxByteCountForUTF8Char),
|
||||
0,
|
||||
count: 0);
|
||||
|
||||
_encoder = Encoding.UTF8.GetEncoder();
|
||||
_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; set; }
|
||||
|
||||
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);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
var totalBytes = 0;
|
||||
bool encoderCompleted;
|
||||
int bytesEncoded;
|
||||
|
||||
do
|
||||
{
|
||||
// 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.
|
||||
var eof = false;
|
||||
if (_charBuffer.Count == 0)
|
||||
{
|
||||
// Only read more content from the input stream if we have exhausted all the buffered chars.
|
||||
eof = await ReadInputChars(cancellationToken);
|
||||
}
|
||||
|
||||
// We need to flush on the last write. This is true when we exhaust the input Stream and any buffered content.
|
||||
var allContentRead = eof && _charBuffer.Count == 0 && _byteBuffer.Count == 0;
|
||||
|
||||
if (_charBuffer.Count > 0 && readBuffer.Count < MaxByteCountForUTF8Char && readBuffer.Count < Encoding.UTF8.GetByteCount(_charBuffer.AsSpan(0, 1)))
|
||||
{
|
||||
// It's possible that the passed in buffer is smaller than the size required to encode a single
|
||||
// char. For instance, the JsonSerializer may pass in a buffer of size 1 or 2 which
|
||||
// is insufficient if the character requires more than 2 bytes to represent. In this case, read
|
||||
// content in to an overflow buffer and fill up the passed in buffer.
|
||||
_encoder.Convert(
|
||||
_charBuffer,
|
||||
_overflowBuffer.Array,
|
||||
flush: false,
|
||||
out var charsUsed,
|
||||
out var bytesUsed,
|
||||
out _);
|
||||
|
||||
_charBuffer = _charBuffer.Slice(charsUsed);
|
||||
|
||||
Debug.Assert(readBuffer.Count < bytesUsed);
|
||||
_overflowBuffer.Array.AsSpan(0, readBuffer.Count).CopyTo(readBuffer);
|
||||
|
||||
_overflowBuffer = new ArraySegment<byte>(
|
||||
_overflowBuffer.Array,
|
||||
readBuffer.Count,
|
||||
bytesUsed - readBuffer.Count);
|
||||
|
||||
totalBytes += readBuffer.Count;
|
||||
// At this point we're done writing.
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
_encoder.Convert(
|
||||
_charBuffer,
|
||||
readBuffer,
|
||||
flush: allContentRead,
|
||||
out var charsUsed,
|
||||
out bytesEncoded,
|
||||
out encoderCompleted);
|
||||
|
||||
totalBytes += bytesEncoded;
|
||||
_charBuffer = _charBuffer.Slice(charsUsed);
|
||||
readBuffer = readBuffer.Slice(bytesEncoded);
|
||||
}
|
||||
|
||||
// We need to exit in one of the 2 conditions:
|
||||
// * encoderCompleted will return false if "buffer" was too small for all the chars to be encoded.
|
||||
// * no bytes were converted in an iteration. This can occur if there wasn't any input.
|
||||
} while (encoderCompleted && bytesEncoded > 0);
|
||||
|
||||
return totalBytes;
|
||||
}
|
||||
|
||||
private async ValueTask<bool> 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);
|
||||
|
||||
return readBytes == 0;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
ArrayPool<char>.Shared.Return(_charBuffer.Array);
|
||||
ArrayPool<byte>.Shared.Return(_byteBuffer.Array);
|
||||
ArrayPool<byte>.Shared.Return(_overflowBuffer.Array);
|
||||
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,166 @@
|
|||
// 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;
|
||||
|
||||
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 WriteAsync(ArraySegment<byte>.Empty, flush: true, 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, flush: false, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task WriteAsync(
|
||||
ArraySegment<byte> bufferSegment,
|
||||
bool flush,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var decoderCompleted = false;
|
||||
while (!decoderCompleted)
|
||||
{
|
||||
_decoder.Convert(
|
||||
bufferSegment,
|
||||
_charBuffer.AsSpan(_charsDecoded),
|
||||
flush,
|
||||
out var bytesDecoded,
|
||||
out var charsDecoded,
|
||||
out decoderCompleted);
|
||||
|
||||
_charsDecoded += charsDecoded;
|
||||
bufferSegment = bufferSegment.Slice(bytesDecoded);
|
||||
|
||||
if (flush || !decoderCompleted)
|
||||
{
|
||||
// This is being invoked from FlushAsync or the char buffer is not large enough
|
||||
// to accomodate all writes.
|
||||
await WriteBufferAsync(flush, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task WriteBufferAsync(bool flush, CancellationToken cancellationToken)
|
||||
{
|
||||
var encoderCompletd = false;
|
||||
var charsWritten = 0;
|
||||
var byteBuffer = ArrayPool<byte>.Shared.Rent(_maxByteBufferSize);
|
||||
|
||||
try
|
||||
{
|
||||
while (!encoderCompletd && charsWritten < _charsDecoded)
|
||||
{
|
||||
_encoder.Convert(
|
||||
_charBuffer.AsSpan(charsWritten, _charsDecoded - charsWritten),
|
||||
byteBuffer,
|
||||
flush,
|
||||
out var charsEncoded,
|
||||
out var bytesUsed,
|
||||
out encoderCompletd);
|
||||
|
||||
await _stream.WriteAsync(byteBuffer.AsMemory(0, bytesUsed), cancellationToken);
|
||||
charsWritten += charsEncoded;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
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 (disposing)
|
||||
{
|
||||
ArrayPool<char>.Shared.Return(_charBuffer);
|
||||
}
|
||||
|
||||
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -65,10 +65,14 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
// Set up filters
|
||||
options.Filters.Add(new UnsupportedContentTypeFilter());
|
||||
|
||||
// Set up default input formatters.
|
||||
options.InputFormatters.Add(new SystemTextJsonInputFormatter(options));
|
||||
|
||||
// Set up default output formatters.
|
||||
options.OutputFormatters.Add(new HttpNoContentOutputFormatter());
|
||||
options.OutputFormatters.Add(new StringOutputFormatter());
|
||||
options.OutputFormatters.Add(new StreamOutputFormatter());
|
||||
options.OutputFormatters.Add(new SystemTextJsonOutputFormatter(options));
|
||||
|
||||
// Set up ValueProviders
|
||||
options.ValueProviderFactories.Add(new FormValueProviderFactory());
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.AspNetCore.Mvc.ApplicationModels;
|
||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
|
|
@ -317,6 +319,24 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="JsonSerializerOptions"/> used by <see cref="SystemTextJsonInputFormatter"/> and
|
||||
/// <see cref="SystemTextJsonOutputFormatter"/>.
|
||||
/// </summary>
|
||||
public JsonSerializerOptions SerializerOptions { get; } = new JsonSerializerOptions
|
||||
{
|
||||
// Allow for the payload to have null values for some inputs (under-binding)
|
||||
IgnoreNullPropertyValueOnRead = true,
|
||||
|
||||
ReaderOptions = new JsonReaderOptions
|
||||
{
|
||||
// Limit the object graph we'll consume to a fixed depth. This prevents stackoverflow exceptions
|
||||
// from deserialization errors that might occur from deeply nested objects.
|
||||
// This value is to be kept in sync with JsonSerializerSettingsProvider.DefaultMaxDepth
|
||||
MaxDepth = DefaultMaxModelBindingRecursionDepth,
|
||||
},
|
||||
};
|
||||
|
||||
IEnumerator<ICompatibilitySwitch> IEnumerable<ICompatibilitySwitch>.GetEnumerator() => _switches.GetEnumerator();
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() => _switches.GetEnumerator();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,367 @@
|
|||
// 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.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Formatters
|
||||
{
|
||||
public abstract class JsonInputFormatterTestBase
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("application/json", true)]
|
||||
[InlineData("application/*", false)]
|
||||
[InlineData("*/*", false)]
|
||||
[InlineData("text/json", true)]
|
||||
[InlineData("text/*", false)]
|
||||
[InlineData("text/xml", false)]
|
||||
[InlineData("application/xml", false)]
|
||||
[InlineData("application/some.entity+json", true)]
|
||||
[InlineData("application/some.entity+json;v=2", true)]
|
||||
[InlineData("application/some.entity+xml", false)]
|
||||
[InlineData("application/some.entity+*", false)]
|
||||
[InlineData("text/some.entity+json", true)]
|
||||
[InlineData("", false)]
|
||||
[InlineData(null, false)]
|
||||
[InlineData("invalid", false)]
|
||||
public void CanRead_ReturnsTrueForAnySupportedContentType(string requestContentType, bool expectedCanRead)
|
||||
{
|
||||
// Arrange
|
||||
var formatter = GetInputFormatter();
|
||||
|
||||
var contentBytes = Encoding.UTF8.GetBytes("content");
|
||||
var httpContext = GetHttpContext(contentBytes, contentType: requestContentType);
|
||||
|
||||
var formatterContext = CreateInputFormatterContext(typeof(string), httpContext);
|
||||
|
||||
// Act
|
||||
var result = formatter.CanRead(formatterContext);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedCanRead, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DefaultMediaType_ReturnsApplicationJson()
|
||||
{
|
||||
// Arrange
|
||||
var formatter = GetInputFormatter();
|
||||
|
||||
// Act
|
||||
var mediaType = formatter.SupportedMediaTypes[0];
|
||||
|
||||
// Assert
|
||||
Assert.Equal("application/json", mediaType.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task JsonFormatterReadsIntValue()
|
||||
{
|
||||
// Arrange
|
||||
var content = "100";
|
||||
var formatter = GetInputFormatter();
|
||||
|
||||
var contentBytes = Encoding.UTF8.GetBytes(content);
|
||||
var httpContext = GetHttpContext(contentBytes);
|
||||
|
||||
var formatterContext = CreateInputFormatterContext(typeof(int), httpContext);
|
||||
|
||||
// Act
|
||||
var result = await formatter.ReadAsync(formatterContext);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.HasError);
|
||||
var intValue = Assert.IsType<int>(result.Model);
|
||||
Assert.Equal(100, intValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task JsonFormatterReadsStringValue()
|
||||
{
|
||||
// Arrange
|
||||
var content = "\"abcd\"";
|
||||
var formatter = GetInputFormatter();
|
||||
|
||||
var contentBytes = Encoding.UTF8.GetBytes(content);
|
||||
var httpContext = GetHttpContext(contentBytes);
|
||||
|
||||
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("abcd", stringValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public virtual async Task JsonFormatterReadsDateTimeValue()
|
||||
{
|
||||
// Arrange
|
||||
var content = "\"2012-02-01 12:45 AM\"";
|
||||
var formatter = GetInputFormatter();
|
||||
|
||||
var contentBytes = Encoding.UTF8.GetBytes(content);
|
||||
var httpContext = GetHttpContext(contentBytes);
|
||||
|
||||
var formatterContext = CreateInputFormatterContext(typeof(DateTime), httpContext);
|
||||
|
||||
// Act
|
||||
var result = await formatter.ReadAsync(formatterContext);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.HasError);
|
||||
var dateValue = Assert.IsType<DateTime>(result.Model);
|
||||
Assert.Equal(new DateTime(2012, 02, 01, 00, 45, 00), dateValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task JsonFormatterReadsComplexTypes()
|
||||
{
|
||||
// Arrange
|
||||
var formatter = GetInputFormatter();
|
||||
|
||||
var content = "{\"Name\": \"Person Name\", \"Age\": 30}";
|
||||
var contentBytes = Encoding.UTF8.GetBytes(content);
|
||||
var httpContext = GetHttpContext(contentBytes);
|
||||
|
||||
var formatterContext = CreateInputFormatterContext(typeof(ComplexModel), httpContext);
|
||||
|
||||
// Act
|
||||
var result = await formatter.ReadAsync(formatterContext);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.HasError);
|
||||
var userModel = Assert.IsType<ComplexModel>(result.Model);
|
||||
Assert.Equal("Person Name", userModel.Name);
|
||||
Assert.Equal(30, userModel.Age);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReadAsync_ReadsValidArray()
|
||||
{
|
||||
// Arrange
|
||||
var formatter = GetInputFormatter();
|
||||
|
||||
var content = "[0, 23, 300]";
|
||||
var contentBytes = Encoding.UTF8.GetBytes(content);
|
||||
var httpContext = GetHttpContext(contentBytes);
|
||||
|
||||
var formatterContext = CreateInputFormatterContext(typeof(int[]), httpContext);
|
||||
|
||||
// Act
|
||||
var result = await formatter.ReadAsync(formatterContext);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.HasError);
|
||||
var integers = Assert.IsType<int[]>(result.Model);
|
||||
Assert.Equal(new int[] { 0, 23, 300 }, integers);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public virtual Task ReadAsync_ReadsValidArray_AsListOfT() => ReadAsync_ReadsValidArray_AsList(typeof(List<int>));
|
||||
|
||||
[Fact]
|
||||
public virtual Task ReadAsync_ReadsValidArray_AsIListOfT() => ReadAsync_ReadsValidArray_AsList(typeof(IList<int>));
|
||||
|
||||
[Fact]
|
||||
public virtual Task ReadAsync_ReadsValidArray_AsCollectionOfT() => ReadAsync_ReadsValidArray_AsList(typeof(ICollection<int>));
|
||||
|
||||
[Fact]
|
||||
public virtual Task ReadAsync_ReadsValidArray_AsEnumerableOfT() => ReadAsync_ReadsValidArray_AsList(typeof(IEnumerable<int>));
|
||||
|
||||
protected async Task ReadAsync_ReadsValidArray_AsList(Type requestedType)
|
||||
{
|
||||
// Arrange
|
||||
var formatter = GetInputFormatter();
|
||||
|
||||
var content = "[0, 23, 300]";
|
||||
var contentBytes = Encoding.UTF8.GetBytes(content);
|
||||
var httpContext = GetHttpContext(contentBytes);
|
||||
|
||||
var formatterContext = CreateInputFormatterContext(requestedType, httpContext);
|
||||
|
||||
// Act
|
||||
var result = await formatter.ReadAsync(formatterContext);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.HasError);
|
||||
var integers = Assert.IsType<List<int>>(result.Model);
|
||||
Assert.Equal(new int[] { 0, 23, 300 }, integers);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public virtual async Task ReadAsync_AddsModelValidationErrorsToModelState()
|
||||
{
|
||||
// Arrange
|
||||
var formatter = GetInputFormatter();
|
||||
|
||||
var content = "{ \"Name\": \"Person Name\", \"Age\": \"not-an-age\" }";
|
||||
var contentBytes = Encoding.UTF8.GetBytes(content);
|
||||
var httpContext = GetHttpContext(contentBytes);
|
||||
|
||||
var formatterContext = CreateInputFormatterContext(typeof(ComplexModel), httpContext);
|
||||
|
||||
// Act
|
||||
var result = await formatter.ReadAsync(formatterContext);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.HasError);
|
||||
Assert.Equal(
|
||||
"Could not convert string to decimal: not-an-age. Path 'Age', line 1, position 44.",
|
||||
formatterContext.ModelState["Age"].Errors[0].ErrorMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public virtual async Task ReadAsync_InvalidArray_AddsOverflowErrorsToModelState()
|
||||
{
|
||||
// Arrange
|
||||
var formatter = GetInputFormatter();
|
||||
|
||||
var content = "[0, 23, 300]";
|
||||
var contentBytes = Encoding.UTF8.GetBytes(content);
|
||||
var httpContext = GetHttpContext(contentBytes);
|
||||
|
||||
var formatterContext = CreateInputFormatterContext(typeof(byte[]), httpContext);
|
||||
|
||||
// Act
|
||||
var result = await formatter.ReadAsync(formatterContext);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.HasError);
|
||||
Assert.Equal("The supplied value is invalid.", formatterContext.ModelState["[2]"].Errors[0].ErrorMessage);
|
||||
Assert.Null(formatterContext.ModelState["[2]"].Errors[0].Exception);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public virtual async Task ReadAsync_InvalidComplexArray_AddsOverflowErrorsToModelState()
|
||||
{
|
||||
// Arrange
|
||||
var formatter = GetInputFormatter();
|
||||
|
||||
var content = "[{ \"Name\": \"Name One\", \"Age\": 30}, { \"Name\": \"Name Two\", \"Small\": 300}]";
|
||||
var contentBytes = Encoding.UTF8.GetBytes(content);
|
||||
var httpContext = GetHttpContext(contentBytes);
|
||||
|
||||
var formatterContext = CreateInputFormatterContext(typeof(ComplexModel[]), httpContext, modelName: "names");
|
||||
|
||||
// Act
|
||||
var result = await formatter.ReadAsync(formatterContext);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.HasError);
|
||||
Assert.Equal(
|
||||
"Error converting value 300 to type 'System.Byte'. Path '[1].Small', line 1, position 69.",
|
||||
formatterContext.ModelState["names[1].Small"].Errors[0].ErrorMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public virtual async Task ReadAsync_UsesTryAddModelValidationErrorsToModelState()
|
||||
{
|
||||
// Arrange
|
||||
var formatter = GetInputFormatter();
|
||||
|
||||
var content = "{ \"Name\": \"Person Name\", \"Age\": \"not-an-age\"}";
|
||||
var contentBytes = Encoding.UTF8.GetBytes(content);
|
||||
var httpContext = GetHttpContext(contentBytes);
|
||||
|
||||
var formatterContext = CreateInputFormatterContext(typeof(ComplexModel), httpContext);
|
||||
formatterContext.ModelState.MaxAllowedErrors = 3;
|
||||
formatterContext.ModelState.AddModelError("key1", "error1");
|
||||
formatterContext.ModelState.AddModelError("key2", "error2");
|
||||
|
||||
// Act
|
||||
var result = await formatter.ReadAsync(formatterContext);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.HasError);
|
||||
|
||||
Assert.False(formatterContext.ModelState.ContainsKey("age"));
|
||||
var error = Assert.Single(formatterContext.ModelState[""].Errors);
|
||||
Assert.IsType<TooManyModelErrorsException>(error.Exception);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("null", true, true)]
|
||||
[InlineData("null", false, false)]
|
||||
public async Task ReadAsync_WithInputThatDeserializesToNull_SetsModelOnlyIfAllowingEmptyInput(
|
||||
string content,
|
||||
bool treatEmptyInputAsDefaultValue,
|
||||
bool expectedIsModelSet)
|
||||
{
|
||||
// Arrange
|
||||
var formatter = GetInputFormatter();
|
||||
|
||||
var contentBytes = Encoding.UTF8.GetBytes(content);
|
||||
var httpContext = GetHttpContext(contentBytes);
|
||||
|
||||
var formatterContext = CreateInputFormatterContext(
|
||||
typeof(string),
|
||||
httpContext,
|
||||
treatEmptyInputAsDefaultValue: treatEmptyInputAsDefaultValue);
|
||||
|
||||
// Act
|
||||
var result = await formatter.ReadAsync(formatterContext);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.HasError);
|
||||
Assert.Equal(expectedIsModelSet, result.IsModelSet);
|
||||
Assert.Null(result.Model);
|
||||
}
|
||||
|
||||
protected abstract TextInputFormatter GetInputFormatter();
|
||||
|
||||
protected static HttpContext GetHttpContext(
|
||||
byte[] contentBytes,
|
||||
string contentType = "application/json")
|
||||
{
|
||||
return GetHttpContext(new MemoryStream(contentBytes), contentType);
|
||||
}
|
||||
|
||||
protected static HttpContext GetHttpContext(
|
||||
Stream requestStream,
|
||||
string contentType = "application/json")
|
||||
{
|
||||
var httpContext = new DefaultHttpContext();
|
||||
httpContext.Request.Body = requestStream;
|
||||
httpContext.Request.ContentType = contentType;
|
||||
|
||||
return httpContext;
|
||||
}
|
||||
|
||||
protected static InputFormatterContext CreateInputFormatterContext(
|
||||
Type modelType,
|
||||
HttpContext httpContext,
|
||||
string modelName = null,
|
||||
bool treatEmptyInputAsDefaultValue = false)
|
||||
{
|
||||
var provider = new EmptyModelMetadataProvider();
|
||||
var metadata = provider.GetMetadataForType(modelType);
|
||||
|
||||
return new InputFormatterContext(
|
||||
httpContext,
|
||||
modelName: modelName ?? string.Empty,
|
||||
modelState: new ModelStateDictionary(),
|
||||
metadata: metadata,
|
||||
readerFactory: new TestHttpRequestStreamReaderFactory().CreateReader,
|
||||
treatEmptyInputAsDefaultValue: treatEmptyInputAsDefaultValue);
|
||||
}
|
||||
|
||||
protected sealed class ComplexModel
|
||||
{
|
||||
public string Name { get; set; }
|
||||
|
||||
public decimal Age { get; set; }
|
||||
|
||||
public byte Small { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,168 @@
|
|||
// 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.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Formatters
|
||||
{
|
||||
public abstract class JsonOutputFormatterTestBase
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("application/json", false, "application/json")]
|
||||
[InlineData("application/json", true, "application/json")]
|
||||
[InlineData("application/xml", false, null)]
|
||||
[InlineData("application/xml", true, null)]
|
||||
[InlineData("application/*", false, "application/json")]
|
||||
[InlineData("text/*", false, "text/json")]
|
||||
[InlineData("custom/*", false, null)]
|
||||
[InlineData("application/json;v=2", false, null)]
|
||||
[InlineData("application/json;v=2", true, null)]
|
||||
[InlineData("application/some.entity+json", false, null)]
|
||||
[InlineData("application/some.entity+json", true, "application/some.entity+json")]
|
||||
[InlineData("application/some.entity+json;v=2", true, "application/some.entity+json;v=2")]
|
||||
[InlineData("application/some.entity+xml", true, null)]
|
||||
public void CanWriteResult_ReturnsExpectedValueForMediaType(
|
||||
string mediaType,
|
||||
bool isServerDefined,
|
||||
string expectedResult)
|
||||
{
|
||||
// Arrange
|
||||
var formatter = GetOutputFormatter();
|
||||
|
||||
var body = new MemoryStream();
|
||||
var actionContext = GetActionContext(MediaTypeHeaderValue.Parse(mediaType), body);
|
||||
var outputFormatterContext = new OutputFormatterWriteContext(
|
||||
actionContext.HttpContext,
|
||||
new TestHttpResponseStreamWriterFactory().CreateWriter,
|
||||
typeof(string),
|
||||
new object())
|
||||
{
|
||||
ContentType = new StringSegment(mediaType),
|
||||
ContentTypeIsServerDefined = isServerDefined,
|
||||
};
|
||||
|
||||
// Act
|
||||
var actualCanWriteValue = formatter.CanWriteResult(outputFormatterContext);
|
||||
|
||||
// Assert
|
||||
var expectedContentType = expectedResult ?? mediaType;
|
||||
Assert.Equal(expectedResult != null, actualCanWriteValue);
|
||||
Assert.Equal(new StringSegment(expectedContentType), outputFormatterContext.ContentType);
|
||||
}
|
||||
|
||||
public static TheoryData<string, string, bool> WriteCorrectCharacterEncoding
|
||||
{
|
||||
get
|
||||
{
|
||||
var data = new TheoryData<string, string, bool>
|
||||
{
|
||||
{ "This is a test 激光這兩個字是甚麼意思 string written using utf-8", "utf-8", true },
|
||||
{ "This is a test 激光這兩個字是甚麼意思 string written using utf-16", "utf-16", true },
|
||||
{ "This is a test 激光這兩個字是甚麼意思 string written using utf-32", "utf-32", false },
|
||||
{ "This is a test æøå string written using iso-8859-1", "iso-8859-1", false },
|
||||
};
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ErrorDuringSerialization_DoesNotCloseTheBrackets()
|
||||
{
|
||||
// Arrange
|
||||
var outputFormatterContext = GetOutputFormatterContext(
|
||||
new ModelWithSerializationError(),
|
||||
typeof(ModelWithSerializationError));
|
||||
var jsonFormatter = GetOutputFormatter();
|
||||
|
||||
// Act
|
||||
await Record.ExceptionAsync(() => jsonFormatter.WriteResponseBodyAsync(outputFormatterContext, Encoding.UTF8));
|
||||
|
||||
// Assert
|
||||
var body = outputFormatterContext.HttpContext.Response.Body;
|
||||
|
||||
Assert.NotNull(body);
|
||||
body.Position = 0;
|
||||
|
||||
var content = new StreamReader(body, Encoding.UTF8).ReadToEnd();
|
||||
Assert.DoesNotContain("}", content);
|
||||
}
|
||||
|
||||
protected static ActionContext GetActionContext(
|
||||
MediaTypeHeaderValue contentType,
|
||||
MemoryStream responseStream = null)
|
||||
{
|
||||
var httpContext = new DefaultHttpContext();
|
||||
httpContext.Request.ContentType = contentType.ToString();
|
||||
httpContext.Request.Headers[HeaderNames.AcceptCharset] = contentType.Charset.ToString();
|
||||
|
||||
|
||||
httpContext.Response.Body = responseStream ?? new MemoryStream();
|
||||
return new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
|
||||
}
|
||||
|
||||
protected static OutputFormatterWriteContext GetOutputFormatterContext(
|
||||
object outputValue,
|
||||
Type outputType,
|
||||
string contentType = "application/xml; charset=utf-8",
|
||||
MemoryStream responseStream = null)
|
||||
{
|
||||
var mediaTypeHeaderValue = MediaTypeHeaderValue.Parse(contentType);
|
||||
|
||||
var actionContext = GetActionContext(mediaTypeHeaderValue, responseStream);
|
||||
return new OutputFormatterWriteContext(
|
||||
actionContext.HttpContext,
|
||||
new TestHttpResponseStreamWriterFactory().CreateWriter,
|
||||
outputType,
|
||||
outputValue)
|
||||
{
|
||||
ContentType = new StringSegment(contentType),
|
||||
};
|
||||
}
|
||||
|
||||
protected static Encoding CreateOrGetSupportedEncoding(
|
||||
TextOutputFormatter formatter,
|
||||
string encodingAsString,
|
||||
bool isDefaultEncoding)
|
||||
{
|
||||
Encoding encoding = null;
|
||||
if (isDefaultEncoding)
|
||||
{
|
||||
encoding = formatter
|
||||
.SupportedEncodings
|
||||
.First((e) => e.WebName.Equals(encodingAsString, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
else
|
||||
{
|
||||
encoding = Encoding.GetEncoding(encodingAsString);
|
||||
formatter.SupportedEncodings.Add(encoding);
|
||||
}
|
||||
|
||||
return encoding;
|
||||
}
|
||||
|
||||
protected abstract TextOutputFormatter GetOutputFormatter();
|
||||
|
||||
protected sealed class ModelWithSerializationError
|
||||
{
|
||||
public string Name { get; } = "Robert";
|
||||
public int Age
|
||||
{
|
||||
get
|
||||
{
|
||||
throw new NotImplementedException($"Property {nameof(Age)} has not been implemented");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
// 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.Threading.Tasks;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Formatters
|
||||
{
|
||||
public class SystemTextJsonInputFormatterTest : JsonInputFormatterTestBase
|
||||
{
|
||||
[Fact(Skip = "https://github.com/aspnet/AspNetCore/issues/8489")]
|
||||
public override Task JsonFormatterReadsDateTimeValue()
|
||||
{
|
||||
return base.JsonFormatterReadsDateTimeValue();
|
||||
}
|
||||
|
||||
[Fact(Skip = "https://github.com/aspnet/AspNetCore/issues/8474")]
|
||||
public override Task ReadAsync_AddsModelValidationErrorsToModelState()
|
||||
{
|
||||
return base.ReadAsync_AddsModelValidationErrorsToModelState();
|
||||
}
|
||||
|
||||
[Fact(Skip = "https://github.com/aspnet/AspNetCore/issues/8474")]
|
||||
public override Task ReadAsync_InvalidArray_AddsOverflowErrorsToModelState()
|
||||
{
|
||||
return base.ReadAsync_InvalidArray_AddsOverflowErrorsToModelState();
|
||||
}
|
||||
|
||||
[Fact(Skip = "https://github.com/aspnet/AspNetCore/issues/8474")]
|
||||
public override Task ReadAsync_InvalidComplexArray_AddsOverflowErrorsToModelState()
|
||||
{
|
||||
return base.ReadAsync_InvalidComplexArray_AddsOverflowErrorsToModelState();
|
||||
}
|
||||
|
||||
[Fact(Skip = "https://github.com/aspnet/AspNetCore/issues/8474")]
|
||||
public override Task ReadAsync_UsesTryAddModelValidationErrorsToModelState()
|
||||
{
|
||||
return base.ReadAsync_UsesTryAddModelValidationErrorsToModelState();
|
||||
}
|
||||
|
||||
[Fact(Skip = "https://github.com/dotnet/corefx/issues/36026")]
|
||||
public override Task ReadAsync_ReadsValidArray_AsCollectionOfT()
|
||||
{
|
||||
return base.ReadAsync_ReadsValidArray_AsCollectionOfT();
|
||||
}
|
||||
|
||||
[Fact(Skip = "https://github.com/dotnet/corefx/issues/36026")]
|
||||
public override Task ReadAsync_ReadsValidArray_AsEnumerableOfT()
|
||||
{
|
||||
return base.ReadAsync_ReadsValidArray_AsEnumerableOfT();
|
||||
}
|
||||
|
||||
[Fact(Skip = "https://github.com/dotnet/corefx/issues/36026")]
|
||||
public override Task ReadAsync_ReadsValidArray_AsIListOfT()
|
||||
{
|
||||
return base.ReadAsync_ReadsValidArray_AsIListOfT();
|
||||
}
|
||||
|
||||
protected override TextInputFormatter GetInputFormatter()
|
||||
{
|
||||
return new SystemTextJsonInputFormatter(new MvcOptions());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
// 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.Threading.Tasks;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Formatters
|
||||
{
|
||||
public class SystemTextJsonOutputFormatterTest : JsonOutputFormatterTestBase
|
||||
{
|
||||
protected override TextOutputFormatter GetOutputFormatter()
|
||||
{
|
||||
return new SystemTextJsonOutputFormatter(new MvcOptions());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(WriteCorrectCharacterEncoding))]
|
||||
public async Task WriteToStreamAsync_UsesCorrectCharacterEncoding(
|
||||
string content,
|
||||
string encodingAsString,
|
||||
bool isDefaultEncoding)
|
||||
{
|
||||
// Arrange
|
||||
var formatter = GetOutputFormatter();
|
||||
var expectedContent = "\"" + JavaScriptEncoder.Default.Encode(content) + "\"";
|
||||
var mediaType = MediaTypeHeaderValue.Parse(string.Format("application/json; charset={0}", encodingAsString));
|
||||
var encoding = CreateOrGetSupportedEncoding(formatter, encodingAsString, isDefaultEncoding);
|
||||
|
||||
|
||||
var body = new MemoryStream();
|
||||
var actionContext = GetActionContext(mediaType, body);
|
||||
|
||||
var outputFormatterContext = new OutputFormatterWriteContext(
|
||||
actionContext.HttpContext,
|
||||
new TestHttpResponseStreamWriterFactory().CreateWriter,
|
||||
typeof(string),
|
||||
content)
|
||||
{
|
||||
ContentType = new StringSegment(mediaType.ToString()),
|
||||
};
|
||||
|
||||
// Act
|
||||
await formatter.WriteResponseBodyAsync(outputFormatterContext, Encoding.GetEncoding(encodingAsString));
|
||||
|
||||
// Assert
|
||||
var actualContent = encoding.GetString(body.ToArray());
|
||||
Assert.Equal(expectedContent, actualContent, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,212 @@
|
|||
// 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.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json.Serialization;
|
||||
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;
|
||||
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;
|
||||
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;
|
||||
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 = new string('A', 4096 + 4);
|
||||
var encoding = Encoding.Unicode;
|
||||
var stream = new TranscodingReadStream(new MemoryStream(encoding.GetBytes(input)), encoding);
|
||||
var bytes = new byte[4096];
|
||||
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(0, stream.CharBufferCount);
|
||||
Assert.Equal(4, stream.OverflowCount);
|
||||
|
||||
readBytes = await stream.ReadAsync(bytes, 0, bytes.Length);
|
||||
Assert.Equal(4, readBytes);
|
||||
Assert.Equal(0, stream.ByteBufferCount);
|
||||
Assert.Equal(0, stream.CharBufferCount);
|
||||
Assert.Equal(0, stream.OverflowCount);
|
||||
}
|
||||
|
||||
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.ReadAsync(transcodingStream, typeof(TestModel));
|
||||
var testModel = Assert.IsType<TestModel>(model);
|
||||
|
||||
Assert.Equal(message, testModel.Message);
|
||||
}
|
||||
|
||||
public class TestModel
|
||||
{
|
||||
public string Message { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
// 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.Serialization;
|
||||
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.WriteAsync(model, model.GetType(), transcodingStream);
|
||||
await transcodingStream.FlushAsync();
|
||||
|
||||
var actual = targetEncoding.GetString(stream.ToArray());
|
||||
Assert.Equal(expected, actual, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private class TestModel
|
||||
{
|
||||
public string Message { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1793,7 +1793,7 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
|||
public class TaskDerivedType : Task
|
||||
{
|
||||
public TaskDerivedType()
|
||||
: base(() => Console.WriteLine("In The Constructor"))
|
||||
: base(() => { })
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,8 +59,10 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
|
||||
public void Configure(MvcOptions options)
|
||||
{
|
||||
options.OutputFormatters.RemoveType<SystemTextJsonOutputFormatter>();
|
||||
options.OutputFormatters.Add(new NewtonsoftJsonOutputFormatter(_jsonOptions.SerializerSettings, _charPool));
|
||||
|
||||
options.InputFormatters.RemoveType<SystemTextJsonInputFormatter>();
|
||||
// Register JsonPatchInputFormatter before JsonInputFormatter, otherwise
|
||||
// JsonInputFormatter would consume "application/json-patch+json" requests
|
||||
// before JsonPatchInputFormatter gets to see them.
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ using Newtonsoft.Json;
|
|||
namespace Microsoft.AspNetCore.Mvc
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides programmatic configuration for JSON in the MVC framework.
|
||||
/// Provides programmatic configuration for JSON formatters using Newtonsoft.JSON.
|
||||
/// </summary>
|
||||
public class MvcNewtonsoftJsonOptions : IEnumerable<ICompatibilitySwitch>
|
||||
{
|
||||
|
|
|
|||
|
|
@ -10,4 +10,9 @@
|
|||
<ProjectReference Include="..\..\shared\Mvc.Core.TestCommon\Microsoft.AspNetCore.Mvc.Core.TestCommon.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="..\..\Mvc.Core\test\Formatters\JsonInputFormatterTestBase.cs" />
|
||||
<Compile Include="..\..\Mvc.Core\test\Formatters\JsonOutputFormatterTestBase.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -3,14 +3,12 @@
|
|||
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.ObjectPool;
|
||||
|
|
@ -21,7 +19,7 @@ using Xunit;
|
|||
|
||||
namespace Microsoft.AspNetCore.Mvc.Formatters
|
||||
{
|
||||
public class NewtonsoftJsonInputFormatterTest
|
||||
public class NewtonsoftJsonInputFormatterTest : JsonInputFormatterTestBase
|
||||
{
|
||||
private static readonly ObjectPoolProvider _objectPoolProvider = new DefaultObjectPoolProvider();
|
||||
private static readonly JsonSerializerSettings _serializerSettings = new JsonSerializerSettings();
|
||||
|
|
@ -158,271 +156,6 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
Assert.Null(result.Model);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("application/json", true)]
|
||||
[InlineData("application/*", false)]
|
||||
[InlineData("*/*", false)]
|
||||
[InlineData("text/json", true)]
|
||||
[InlineData("text/*", false)]
|
||||
[InlineData("text/xml", false)]
|
||||
[InlineData("application/xml", false)]
|
||||
[InlineData("application/some.entity+json", true)]
|
||||
[InlineData("application/some.entity+json;v=2", true)]
|
||||
[InlineData("application/some.entity+xml", false)]
|
||||
[InlineData("application/some.entity+*", false)]
|
||||
[InlineData("text/some.entity+json", true)]
|
||||
[InlineData("", false)]
|
||||
[InlineData(null, false)]
|
||||
[InlineData("invalid", false)]
|
||||
public void CanRead_ReturnsTrueForAnySupportedContentType(string requestContentType, bool expectedCanRead)
|
||||
{
|
||||
// Arrange
|
||||
var formatter = CreateFormatter();
|
||||
|
||||
var contentBytes = Encoding.UTF8.GetBytes("content");
|
||||
var httpContext = GetHttpContext(contentBytes, contentType: requestContentType);
|
||||
|
||||
var formatterContext = CreateInputFormatterContext(typeof(string), httpContext);
|
||||
|
||||
// Act
|
||||
var result = formatter.CanRead(formatterContext);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedCanRead, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DefaultMediaType_ReturnsApplicationJson()
|
||||
{
|
||||
// Arrange
|
||||
var formatter = CreateFormatter();
|
||||
|
||||
// Act
|
||||
var mediaType = formatter.SupportedMediaTypes[0];
|
||||
|
||||
// Assert
|
||||
Assert.Equal("application/json", mediaType.ToString());
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> JsonFormatterReadSimpleTypesData
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return new object[] { "100", typeof(int), 100 };
|
||||
yield return new object[] { "'abcd'", typeof(string), "abcd" };
|
||||
yield return new object[] { "'2012-02-01 12:45 AM'", typeof(DateTime), new DateTime(2012, 02, 01, 00, 45, 00) };
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(JsonFormatterReadSimpleTypesData))]
|
||||
public async Task JsonFormatterReadsSimpleTypes(string content, Type type, object expected)
|
||||
{
|
||||
// Arrange
|
||||
var formatter = CreateFormatter();
|
||||
|
||||
var contentBytes = Encoding.UTF8.GetBytes(content);
|
||||
var httpContext = GetHttpContext(contentBytes);
|
||||
|
||||
var formatterContext = CreateInputFormatterContext(type, httpContext);
|
||||
|
||||
// Act
|
||||
var result = await formatter.ReadAsync(formatterContext);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.HasError);
|
||||
Assert.Equal(expected, result.Model);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task JsonFormatterReadsComplexTypes()
|
||||
{
|
||||
// Arrange
|
||||
var formatter = CreateFormatter();
|
||||
|
||||
var content = "{name: 'Person Name', Age: '30'}";
|
||||
var contentBytes = Encoding.UTF8.GetBytes(content);
|
||||
var httpContext = GetHttpContext(contentBytes);
|
||||
|
||||
var formatterContext = CreateInputFormatterContext(typeof(User), httpContext);
|
||||
|
||||
// Act
|
||||
var result = await formatter.ReadAsync(formatterContext);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.HasError);
|
||||
var userModel = Assert.IsType<User>(result.Model);
|
||||
Assert.Equal("Person Name", userModel.Name);
|
||||
Assert.Equal(30, userModel.Age);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReadAsync_ReadsValidArray()
|
||||
{
|
||||
// Arrange
|
||||
var formatter = CreateFormatter();
|
||||
|
||||
var content = "[0, 23, 300]";
|
||||
var contentBytes = Encoding.UTF8.GetBytes(content);
|
||||
var httpContext = GetHttpContext(contentBytes);
|
||||
|
||||
var formatterContext = CreateInputFormatterContext(typeof(int[]), httpContext);
|
||||
|
||||
// Act
|
||||
var result = await formatter.ReadAsync(formatterContext);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.HasError);
|
||||
var integers = Assert.IsType<int[]>(result.Model);
|
||||
Assert.Equal(new int[] { 0, 23, 300 }, integers);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(typeof(ICollection<int>))]
|
||||
[InlineData(typeof(IEnumerable<int>))]
|
||||
[InlineData(typeof(IList<int>))]
|
||||
[InlineData(typeof(List<int>))]
|
||||
public async Task ReadAsync_ReadsValidArray_AsList(Type requestedType)
|
||||
{
|
||||
// Arrange
|
||||
var formatter = CreateFormatter();
|
||||
|
||||
var content = "[0, 23, 300]";
|
||||
var contentBytes = Encoding.UTF8.GetBytes(content);
|
||||
var httpContext = GetHttpContext(contentBytes);
|
||||
|
||||
var formatterContext = CreateInputFormatterContext(requestedType, httpContext);
|
||||
|
||||
// Act
|
||||
var result = await formatter.ReadAsync(formatterContext);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.HasError);
|
||||
var integers = Assert.IsType<List<int>>(result.Model);
|
||||
Assert.Equal(new int[] { 0, 23, 300 }, integers);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReadAsync_AddsModelValidationErrorsToModelState()
|
||||
{
|
||||
// Arrange
|
||||
var formatter = CreateFormatter(allowInputFormatterExceptionMessages: true);
|
||||
|
||||
var content = "{name: 'Person Name', Age: 'not-an-age'}";
|
||||
var contentBytes = Encoding.UTF8.GetBytes(content);
|
||||
var httpContext = GetHttpContext(contentBytes);
|
||||
|
||||
var formatterContext = CreateInputFormatterContext(typeof(User), httpContext);
|
||||
|
||||
// Act
|
||||
var result = await formatter.ReadAsync(formatterContext);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.HasError);
|
||||
Assert.Equal(
|
||||
"Could not convert string to decimal: not-an-age. Path 'Age', line 1, position 39.",
|
||||
formatterContext.ModelState["Age"].Errors[0].ErrorMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReadAsync_InvalidArray_AddsOverflowErrorsToModelState()
|
||||
{
|
||||
// Arrange
|
||||
var formatter = CreateFormatter(allowInputFormatterExceptionMessages: true);
|
||||
|
||||
var content = "[0, 23, 300]";
|
||||
var contentBytes = Encoding.UTF8.GetBytes(content);
|
||||
var httpContext = GetHttpContext(contentBytes);
|
||||
|
||||
var formatterContext = CreateInputFormatterContext(typeof(byte[]), httpContext);
|
||||
|
||||
// Act
|
||||
var result = await formatter.ReadAsync(formatterContext);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.HasError);
|
||||
Assert.Equal("The supplied value is invalid.", formatterContext.ModelState["[2]"].Errors[0].ErrorMessage);
|
||||
Assert.Null(formatterContext.ModelState["[2]"].Errors[0].Exception);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReadAsync_InvalidComplexArray_AddsOverflowErrorsToModelState()
|
||||
{
|
||||
// Arrange
|
||||
var formatter = CreateFormatter(allowInputFormatterExceptionMessages: true);
|
||||
|
||||
var content = "[{name: 'Name One', Age: 30}, {name: 'Name Two', Small: 300}]";
|
||||
var contentBytes = Encoding.UTF8.GetBytes(content);
|
||||
var httpContext = GetHttpContext(contentBytes);
|
||||
|
||||
var formatterContext = CreateInputFormatterContext(typeof(User[]), httpContext, modelName: "names");
|
||||
|
||||
// Act
|
||||
var result = await formatter.ReadAsync(formatterContext);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.HasError);
|
||||
Assert.Equal(
|
||||
"Error converting value 300 to type 'System.Byte'. Path '[1].Small', line 1, position 59.",
|
||||
formatterContext.ModelState["names[1].Small"].Errors[0].ErrorMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReadAsync_UsesTryAddModelValidationErrorsToModelState()
|
||||
{
|
||||
// Arrange
|
||||
var formatter = CreateFormatter();
|
||||
|
||||
var content = "{name: 'Person Name', Age: 'not-an-age'}";
|
||||
var contentBytes = Encoding.UTF8.GetBytes(content);
|
||||
var httpContext = GetHttpContext(contentBytes);
|
||||
|
||||
var formatterContext = CreateInputFormatterContext(typeof(User), httpContext);
|
||||
formatterContext.ModelState.MaxAllowedErrors = 3;
|
||||
formatterContext.ModelState.AddModelError("key1", "error1");
|
||||
formatterContext.ModelState.AddModelError("key2", "error2");
|
||||
|
||||
// Act
|
||||
var result = await formatter.ReadAsync(formatterContext);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.HasError);
|
||||
|
||||
Assert.False(formatterContext.ModelState.ContainsKey("age"));
|
||||
var error = Assert.Single(formatterContext.ModelState[""].Errors);
|
||||
Assert.IsType<TooManyModelErrorsException>(error.Exception);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("null", true, true)]
|
||||
[InlineData("null", false, false)]
|
||||
[InlineData(" ", true, true)]
|
||||
[InlineData(" ", false, false)]
|
||||
public async Task ReadAsync_WithInputThatDeserializesToNull_SetsModelOnlyIfAllowingEmptyInput(
|
||||
string content,
|
||||
bool treatEmptyInputAsDefaultValue,
|
||||
bool expectedIsModelSet)
|
||||
{
|
||||
// Arrange
|
||||
var formatter = CreateFormatter();
|
||||
|
||||
var contentBytes = Encoding.UTF8.GetBytes(content);
|
||||
var httpContext = GetHttpContext(contentBytes);
|
||||
|
||||
var formatterContext = CreateInputFormatterContext(
|
||||
typeof(object),
|
||||
httpContext,
|
||||
treatEmptyInputAsDefaultValue: treatEmptyInputAsDefaultValue);
|
||||
|
||||
// Act
|
||||
var result = await formatter.ReadAsync(formatterContext);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.HasError);
|
||||
Assert.Equal(expectedIsModelSet, result.IsModelSet);
|
||||
Assert.Null(result.Model);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_UsesSerializerSettings()
|
||||
{
|
||||
|
|
@ -482,6 +215,14 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
Assert.Equal(settings.DateTimeZoneHandling, actual.DateTimeZoneHandling);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(" ", true, true)]
|
||||
[InlineData(" ", false, false)]
|
||||
public Task ReadAsync_WithInputThatDeserializesToNull_SetsModelOnlyIfAllowingEmptyInput_WhenValueIsWhitespaceString(string content, bool treatEmptyInputAsDefaultValue, bool expectedIsModelSet)
|
||||
{
|
||||
return base.ReadAsync_WithInputThatDeserializesToNull_SetsModelOnlyIfAllowingEmptyInput(content, treatEmptyInputAsDefaultValue, expectedIsModelSet);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("{", "", "Unexpected end when reading JSON. Path '', line 1, position 1.")]
|
||||
[InlineData("{\"a\":{\"b\"}}", "a", "Invalid character after parsing property name. Expected ':' but got: }. Path 'a', line 1, position 9.")]
|
||||
|
|
@ -537,7 +278,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReadAsync_AllowInputFormatterExceptionMessages_DoesNotWrapJsonInputExceptions()
|
||||
public async Task ReadAsync_lowInputFormatterExceptionMessages_DoesNotWrapJsonInputExceptions()
|
||||
{
|
||||
// Arrange
|
||||
var formatter = new NewtonsoftJsonInputFormatter(
|
||||
|
|
@ -586,6 +327,9 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
return NullLogger.Instance;
|
||||
}
|
||||
|
||||
protected override TextInputFormatter GetInputFormatter()
|
||||
=> CreateFormatter(allowInputFormatterExceptionMessages: true);
|
||||
|
||||
private NewtonsoftJsonInputFormatter CreateFormatter(JsonSerializerSettings serializerSettings = null, bool allowInputFormatterExceptionMessages = false)
|
||||
{
|
||||
return new NewtonsoftJsonInputFormatter(
|
||||
|
|
@ -600,45 +344,19 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
});
|
||||
}
|
||||
|
||||
private static HttpContext GetHttpContext(
|
||||
byte[] contentBytes,
|
||||
string contentType = "application/json")
|
||||
private class Location
|
||||
{
|
||||
return GetHttpContext(new MemoryStream(contentBytes), contentType);
|
||||
public int Id { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
}
|
||||
|
||||
private static HttpContext GetHttpContext(
|
||||
Stream requestStream,
|
||||
string contentType = "application/json")
|
||||
private class TestResponseFeature : HttpResponseFeature
|
||||
{
|
||||
var request = new Mock<HttpRequest>();
|
||||
var headers = new Mock<IHeaderDictionary>();
|
||||
request.SetupGet(r => r.Headers).Returns(headers.Object);
|
||||
request.SetupGet(f => f.Body).Returns(requestStream);
|
||||
request.SetupGet(f => f.ContentType).Returns(contentType);
|
||||
|
||||
var httpContext = new Mock<HttpContext>();
|
||||
httpContext.SetupGet(c => c.Request).Returns(request.Object);
|
||||
httpContext.SetupGet(c => c.Request).Returns(request.Object);
|
||||
return httpContext.Object;
|
||||
}
|
||||
|
||||
private InputFormatterContext CreateInputFormatterContext(
|
||||
Type modelType,
|
||||
HttpContext httpContext,
|
||||
string modelName = null,
|
||||
bool treatEmptyInputAsDefaultValue = false)
|
||||
{
|
||||
var provider = new EmptyModelMetadataProvider();
|
||||
var metadata = provider.GetMetadataForType(modelType);
|
||||
|
||||
return new InputFormatterContext(
|
||||
httpContext,
|
||||
modelName: modelName ?? string.Empty,
|
||||
modelState: new ModelStateDictionary(),
|
||||
metadata: metadata,
|
||||
readerFactory: new TestHttpRequestStreamReaderFactory().CreateReader,
|
||||
treatEmptyInputAsDefaultValue: treatEmptyInputAsDefaultValue);
|
||||
public override void OnCompleted(Func<object, Task> callback, object state)
|
||||
{
|
||||
// do not do anything
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class User
|
||||
|
|
@ -660,20 +378,5 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
[JsonProperty(Required = Required.Always)]
|
||||
public string Password { get; set; }
|
||||
}
|
||||
|
||||
private class Location
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
}
|
||||
|
||||
private class TestResponseFeature : HttpResponseFeature
|
||||
{
|
||||
public override void OnCompleted(Func<object, Task> callback, object state)
|
||||
{
|
||||
// do not do anything
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,18 +5,8 @@ using System;
|
|||
using System.Buffers;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||
using Microsoft.AspNetCore.Mvc.NewtonsoftJson;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
using Moq;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
|
|
@ -24,8 +14,13 @@ using Xunit;
|
|||
|
||||
namespace Microsoft.AspNetCore.Mvc.Formatters
|
||||
{
|
||||
public class NewtonsoftJsonOutputFormatterTest
|
||||
public class NewtonsoftJsonOutputFormatterTest : JsonOutputFormatterTestBase
|
||||
{
|
||||
protected override TextOutputFormatter GetOutputFormatter()
|
||||
{
|
||||
return new NewtonsoftJsonOutputFormatter(new JsonSerializerSettings(), ArrayPool<char>.Shared);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Creates_SerializerSettings_ByDefault()
|
||||
{
|
||||
|
|
@ -299,188 +294,6 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
Assert.Equal(beforeMessage, afterMessage);
|
||||
}
|
||||
|
||||
public static TheoryData<string, string, bool> WriteCorrectCharacterEncoding
|
||||
{
|
||||
get
|
||||
{
|
||||
var data = new TheoryData<string, string, bool>
|
||||
{
|
||||
{ "This is a test 激光這兩個字是甚麼意思 string written using utf-8", "utf-8", true },
|
||||
{ "This is a test 激光這兩個字是甚麼意思 string written using utf-16", "utf-16", true },
|
||||
{ "This is a test 激光這兩個字是甚麼意思 string written using utf-32", "utf-32", false },
|
||||
{ "This is a test æøå string written using iso-8859-1", "iso-8859-1", false },
|
||||
};
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(WriteCorrectCharacterEncoding))]
|
||||
public async Task WriteToStreamAsync_UsesCorrectCharacterEncoding(
|
||||
string content,
|
||||
string encodingAsString,
|
||||
bool isDefaultEncoding)
|
||||
{
|
||||
// Arrange
|
||||
var formatter = new NewtonsoftJsonOutputFormatter(new JsonSerializerSettings(), ArrayPool<char>.Shared);
|
||||
var formattedContent = "\"" + content + "\"";
|
||||
var mediaType = MediaTypeHeaderValue.Parse(string.Format("application/json; charset={0}", encodingAsString));
|
||||
var encoding = CreateOrGetSupportedEncoding(formatter, encodingAsString, isDefaultEncoding);
|
||||
var expectedData = encoding.GetBytes(formattedContent);
|
||||
|
||||
|
||||
var body = new MemoryStream();
|
||||
var actionContext = GetActionContext(mediaType, body);
|
||||
|
||||
var outputFormatterContext = new OutputFormatterWriteContext(
|
||||
actionContext.HttpContext,
|
||||
new TestHttpResponseStreamWriterFactory().CreateWriter,
|
||||
typeof(string),
|
||||
content)
|
||||
{
|
||||
ContentType = new StringSegment(mediaType.ToString()),
|
||||
};
|
||||
|
||||
// Act
|
||||
await formatter.WriteResponseBodyAsync(outputFormatterContext, Encoding.GetEncoding(encodingAsString));
|
||||
|
||||
// Assert
|
||||
var actualData = body.ToArray();
|
||||
Assert.Equal(expectedData, actualData);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ErrorDuringSerialization_DoesNotCloseTheBrackets()
|
||||
{
|
||||
// Arrange
|
||||
var expectedOutput = "{\"name\":\"Robert\"";
|
||||
var outputFormatterContext = GetOutputFormatterContext(
|
||||
new ModelWithSerializationError(),
|
||||
typeof(ModelWithSerializationError));
|
||||
var serializerSettings = JsonSerializerSettingsProvider.CreateSerializerSettings();
|
||||
var jsonFormatter = new NewtonsoftJsonOutputFormatter(serializerSettings, ArrayPool<char>.Shared);
|
||||
|
||||
// Act
|
||||
try
|
||||
{
|
||||
await jsonFormatter.WriteResponseBodyAsync(outputFormatterContext, Encoding.UTF8);
|
||||
}
|
||||
catch (JsonSerializationException serializerException)
|
||||
{
|
||||
var expectedException = Assert.IsType<NotImplementedException>(serializerException.InnerException);
|
||||
Assert.Equal("Property Age has not been implemented", expectedException.Message);
|
||||
}
|
||||
|
||||
// Assert
|
||||
var body = outputFormatterContext.HttpContext.Response.Body;
|
||||
|
||||
Assert.NotNull(body);
|
||||
body.Position = 0;
|
||||
|
||||
var content = new StreamReader(body, Encoding.UTF8).ReadToEnd();
|
||||
Assert.Equal(expectedOutput, content);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("application/json", false, "application/json")]
|
||||
[InlineData("application/json", true, "application/json")]
|
||||
[InlineData("application/xml", false, null)]
|
||||
[InlineData("application/xml", true, null)]
|
||||
[InlineData("application/*", false, "application/json")]
|
||||
[InlineData("text/*", false, "text/json")]
|
||||
[InlineData("custom/*", false, null)]
|
||||
[InlineData("application/json;v=2", false, null)]
|
||||
[InlineData("application/json;v=2", true, null)]
|
||||
[InlineData("application/some.entity+json", false, null)]
|
||||
[InlineData("application/some.entity+json", true, "application/some.entity+json")]
|
||||
[InlineData("application/some.entity+json;v=2", true, "application/some.entity+json;v=2")]
|
||||
[InlineData("application/some.entity+xml", true, null)]
|
||||
public void CanWriteResult_ReturnsExpectedValueForMediaType(
|
||||
string mediaType,
|
||||
bool isServerDefined,
|
||||
string expectedResult)
|
||||
{
|
||||
// Arrange
|
||||
var formatter = new NewtonsoftJsonOutputFormatter(new JsonSerializerSettings(), ArrayPool<char>.Shared);
|
||||
|
||||
var body = new MemoryStream();
|
||||
var actionContext = GetActionContext(MediaTypeHeaderValue.Parse(mediaType), body);
|
||||
var outputFormatterContext = new OutputFormatterWriteContext(
|
||||
actionContext.HttpContext,
|
||||
new TestHttpResponseStreamWriterFactory().CreateWriter,
|
||||
typeof(string),
|
||||
new object())
|
||||
{
|
||||
ContentType = new StringSegment(mediaType),
|
||||
ContentTypeIsServerDefined = isServerDefined,
|
||||
};
|
||||
|
||||
// Act
|
||||
var actualCanWriteValue = formatter.CanWriteResult(outputFormatterContext);
|
||||
|
||||
// Assert
|
||||
var expectedContentType = expectedResult ?? mediaType;
|
||||
Assert.Equal(expectedResult != null, actualCanWriteValue);
|
||||
Assert.Equal(new StringSegment(expectedContentType), outputFormatterContext.ContentType);
|
||||
}
|
||||
|
||||
private static Encoding CreateOrGetSupportedEncoding(
|
||||
NewtonsoftJsonOutputFormatter formatter,
|
||||
string encodingAsString,
|
||||
bool isDefaultEncoding)
|
||||
{
|
||||
Encoding encoding = null;
|
||||
if (isDefaultEncoding)
|
||||
{
|
||||
encoding = formatter.SupportedEncodings
|
||||
.First((e) => e.WebName.Equals(encodingAsString, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
else
|
||||
{
|
||||
encoding = Encoding.GetEncoding(encodingAsString);
|
||||
formatter.SupportedEncodings.Add(encoding);
|
||||
}
|
||||
|
||||
return encoding;
|
||||
}
|
||||
|
||||
private static ILogger GetLogger()
|
||||
{
|
||||
return NullLogger.Instance;
|
||||
}
|
||||
|
||||
private static OutputFormatterWriteContext GetOutputFormatterContext(
|
||||
object outputValue,
|
||||
Type outputType,
|
||||
string contentType = "application/xml; charset=utf-8",
|
||||
MemoryStream responseStream = null)
|
||||
{
|
||||
var mediaTypeHeaderValue = MediaTypeHeaderValue.Parse(contentType);
|
||||
|
||||
var actionContext = GetActionContext(mediaTypeHeaderValue, responseStream);
|
||||
return new OutputFormatterWriteContext(
|
||||
actionContext.HttpContext,
|
||||
new TestHttpResponseStreamWriterFactory().CreateWriter,
|
||||
outputType,
|
||||
outputValue)
|
||||
{
|
||||
ContentType = new StringSegment(contentType),
|
||||
};
|
||||
}
|
||||
|
||||
private static ActionContext GetActionContext(
|
||||
MediaTypeHeaderValue contentType,
|
||||
MemoryStream responseStream = null)
|
||||
{
|
||||
var context = new DefaultHttpContext();
|
||||
context.Request.ContentType = contentType.ToString();
|
||||
context.Request.Headers[HeaderNames.AcceptCharset] = contentType.Charset.ToString();
|
||||
context.Response.Body = responseStream ?? new MemoryStream();
|
||||
|
||||
return new ActionContext(context, new RouteData(), new ActionDescriptor());
|
||||
}
|
||||
|
||||
private class TestableJsonOutputFormatter : NewtonsoftJsonOutputFormatter
|
||||
{
|
||||
public TestableJsonOutputFormatter(JsonSerializerSettings serializerSettings)
|
||||
|
|
@ -518,17 +331,5 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
|
||||
public string FullName { get; set; }
|
||||
}
|
||||
|
||||
private class ModelWithSerializationError
|
||||
{
|
||||
public string Name { get; } = "Robert";
|
||||
public int Age
|
||||
{
|
||||
get
|
||||
{
|
||||
throw new NotImplementedException($"Property {nameof(Age)} has not been implemented");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -95,7 +95,8 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
Assert.Collection(options.OutputFormatters,
|
||||
formatter => Assert.IsType<HttpNoContentOutputFormatter>(formatter),
|
||||
formatter => Assert.IsType<StringOutputFormatter>(formatter),
|
||||
formatter => Assert.IsType<StreamOutputFormatter>(formatter));
|
||||
formatter => Assert.IsType<StreamOutputFormatter>(formatter),
|
||||
formatter => Assert.IsType<SystemTextJsonOutputFormatter>(formatter));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -105,7 +106,9 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
var options = GetOptions<MvcOptions>();
|
||||
|
||||
// Assert
|
||||
Assert.Empty(options.InputFormatters);
|
||||
Assert.Collection(
|
||||
options.InputFormatters,
|
||||
formatter => Assert.IsType<SystemTextJsonInputFormatter>(formatter));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -42,148 +42,6 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
Assert.Equal(sampleInputInt.ToString(), await response.Content.ReadAsStringAsync());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("application/json")]
|
||||
[InlineData("text/json")]
|
||||
public async Task JsonInputFormatter_IsSelectedForJsonRequest(string requestContentType)
|
||||
{
|
||||
// Arrange
|
||||
var sampleInputInt = 10;
|
||||
var input = "{\"SampleInt\":10}";
|
||||
var content = new StringContent(input, Encoding.UTF8, requestContentType);
|
||||
|
||||
// Act
|
||||
var response = await Client.PostAsync("http://localhost/Home/Index", content);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.Equal(sampleInputInt.ToString(), await response.Content.ReadAsStringAsync());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("application/json", "{\"SampleInt\":10}", 10)]
|
||||
[InlineData("application/json", "{}", 0)]
|
||||
public async Task JsonInputFormatter_IsModelStateValid_ForValidContentType(
|
||||
string requestContentType,
|
||||
string jsonInput,
|
||||
int expectedSampleIntValue)
|
||||
{
|
||||
// Arrange
|
||||
var content = new StringContent(jsonInput, Encoding.UTF8, requestContentType);
|
||||
|
||||
// Act
|
||||
var response = await Client.PostAsync("http://localhost/JsonFormatter/ReturnInput/", content);
|
||||
var responseBody = await response.Content.ReadAsStringAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.Equal(expectedSampleIntValue.ToString(), responseBody);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("application/json", "")]
|
||||
[InlineData("application/json", " ")]
|
||||
public async Task JsonInputFormatter_ReturnsBadRequest_ForEmptyRequestBody(
|
||||
string requestContentType,
|
||||
string jsonInput)
|
||||
{
|
||||
// Arrange
|
||||
var content = new StringContent(jsonInput, Encoding.UTF8, requestContentType);
|
||||
|
||||
// Act
|
||||
var response = await Client.PostAsync("http://localhost/JsonFormatter/ReturnInput/", content);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact] // This test covers the 2.0 behavior. JSON.Net error messages are not preserved.
|
||||
public async Task JsonInputFormatter_SuppliedJsonDeserializationErrorMessage()
|
||||
{
|
||||
// Arrange
|
||||
var content = new StringContent("{", Encoding.UTF8, "application/json");
|
||||
|
||||
// Act
|
||||
var response = await Client.PostAsync("http://localhost/JsonFormatter/ReturnInput/", content);
|
||||
var responseBody = await response.Content.ReadAsStringAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
Assert.Equal("{\"\":[\"Unexpected end when reading JSON. Path '', line 1, position 1.\"]}", responseBody);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("\"I'm a JSON string!\"")]
|
||||
[InlineData("true")]
|
||||
[InlineData("\"\"")] // Empty string
|
||||
public async Task JsonInputFormatter_ReturnsDefaultValue_ForValueTypes(string input)
|
||||
{
|
||||
// Arrange
|
||||
var content = new StringContent(input, Encoding.UTF8, "application/json");
|
||||
|
||||
// Act
|
||||
var response = await Client.PostAsync("http://localhost/JsonFormatter/ValueTypeAsBody/", content);
|
||||
var responseBody = await response.Content.ReadAsStringAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
Assert.Equal("0", responseBody);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task JsonInputFormatter_ReadsPrimitiveTypes()
|
||||
{
|
||||
// Arrange
|
||||
var expected = "1773";
|
||||
var content = new StringContent(expected, Encoding.UTF8, "application/json");
|
||||
|
||||
// Act
|
||||
var response = await Client.PostAsync("http://localhost/JsonFormatter/ValueTypeAsBody/", content);
|
||||
var responseBody = await response.Content.ReadAsStringAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.Equal(expected, responseBody);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task JsonInputFormatter_Returns415UnsupportedMediaType_ForEmptyContentType()
|
||||
{
|
||||
// Arrange
|
||||
var jsonInput = "{\"SampleInt\":10}";
|
||||
var content = new StringContent(jsonInput, Encoding.UTF8, "application/json");
|
||||
content.Headers.Clear();
|
||||
|
||||
// Act
|
||||
var response = await Client.PostAsync("http://localhost/JsonFormatter/ReturnInput/", content);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.UnsupportedMediaType, response.StatusCode);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("application/json", "{\"SampleInt\":10}", 10)]
|
||||
[InlineData("application/json", "{}", 0)]
|
||||
public async Task JsonInputFormatter_IsModelStateValid_ForTransferEncodingChunk(
|
||||
string requestContentType,
|
||||
string jsonInput,
|
||||
int expectedSampleIntValue)
|
||||
{
|
||||
// Arrange
|
||||
var content = new StringContent(jsonInput, Encoding.UTF8, requestContentType);
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, "http://localhost/JsonFormatter/ReturnInput/");
|
||||
request.Headers.TransferEncodingChunked = true;
|
||||
request.Content = content;
|
||||
|
||||
// Act
|
||||
var response = await Client.SendAsync(request);
|
||||
var responseBody = await response.Content.ReadAsStringAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.Equal(expectedSampleIntValue.ToString(), responseBody);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("utf-8")]
|
||||
[InlineData("unicode")]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,138 @@
|
|||
// 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.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
||||
{
|
||||
public abstract class JsonInputFormatterTestBase<TStartup> : IClassFixture<MvcTestFixture<TStartup>> where TStartup : class
|
||||
{
|
||||
protected JsonInputFormatterTestBase(MvcTestFixture<TStartup> fixture)
|
||||
{
|
||||
var factory = fixture.Factories.FirstOrDefault() ?? fixture.WithWebHostBuilder(ConfigureWebHostBuilder);
|
||||
Client = factory.CreateDefaultClient();
|
||||
}
|
||||
|
||||
private static void ConfigureWebHostBuilder(IWebHostBuilder builder) =>
|
||||
builder.UseStartup<TStartup>();
|
||||
|
||||
public HttpClient Client { get; }
|
||||
|
||||
[Theory]
|
||||
[InlineData("application/json")]
|
||||
[InlineData("text/json")]
|
||||
public async Task JsonInputFormatter_IsSelectedForJsonRequest(string requestContentType)
|
||||
{
|
||||
// Arrange
|
||||
var sampleInputInt = 10;
|
||||
var input = "{\"SampleInt\":10}";
|
||||
var content = new StringContent(input, Encoding.UTF8, requestContentType);
|
||||
|
||||
// Act
|
||||
var response = await Client.PostAsync("http://localhost/Home/Index", content);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.Equal(sampleInputInt.ToString(), await response.Content.ReadAsStringAsync());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("application/json", "{\"SampleInt\":10}", 10)]
|
||||
[InlineData("application/json", "{}", 0)]
|
||||
public async Task JsonInputFormatter_IsModelStateValid_ForValidContentType(
|
||||
string requestContentType,
|
||||
string jsonInput,
|
||||
int expectedSampleIntValue)
|
||||
{
|
||||
// Arrange
|
||||
var content = new StringContent(jsonInput, Encoding.UTF8, requestContentType);
|
||||
|
||||
// Act
|
||||
var response = await Client.PostAsync("http://localhost/JsonFormatter/ReturnInput/", content);
|
||||
var responseBody = await response.Content.ReadAsStringAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.Equal(expectedSampleIntValue.ToString(), responseBody);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("\"I'm a JSON string!\"")]
|
||||
[InlineData("true")]
|
||||
[InlineData("\"\"")] // Empty string
|
||||
public virtual async Task JsonInputFormatter_ReturnsDefaultValue_ForValueTypes(string input)
|
||||
{
|
||||
// Arrange
|
||||
var content = new StringContent(input, Encoding.UTF8, "application/json");
|
||||
|
||||
// Act
|
||||
var response = await Client.PostAsync("http://localhost/JsonFormatter/ValueTypeAsBody/", content);
|
||||
var responseBody = await response.Content.ReadAsStringAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
Assert.Equal("0", responseBody);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task JsonInputFormatter_ReadsPrimitiveTypes()
|
||||
{
|
||||
// Arrange
|
||||
var expected = "1773";
|
||||
var content = new StringContent(expected, Encoding.UTF8, "application/json");
|
||||
|
||||
// Act
|
||||
var response = await Client.PostAsync("http://localhost/JsonFormatter/ValueTypeAsBody/", content);
|
||||
var responseBody = await response.Content.ReadAsStringAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.Equal(expected, responseBody);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task JsonInputFormatter_Returns415UnsupportedMediaType_ForEmptyContentType()
|
||||
{
|
||||
// Arrange
|
||||
var jsonInput = "{\"SampleInt\":10}";
|
||||
var content = new StringContent(jsonInput, Encoding.UTF8, "application/json");
|
||||
content.Headers.Clear();
|
||||
|
||||
// Act
|
||||
var response = await Client.PostAsync("http://localhost/JsonFormatter/ReturnInput/", content);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.UnsupportedMediaType, response.StatusCode);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("application/json", "{\"SampleInt\":10}", 10)]
|
||||
[InlineData("application/json", "{}", 0)]
|
||||
public async Task JsonInputFormatter_IsModelStateValid_ForTransferEncodingChunk(
|
||||
string requestContentType,
|
||||
string jsonInput,
|
||||
int expectedSampleIntValue)
|
||||
{
|
||||
// Arrange
|
||||
var content = new StringContent(jsonInput, Encoding.UTF8, requestContentType);
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, "http://localhost/JsonFormatter/ReturnInput/");
|
||||
request.Headers.TransferEncodingChunked = true;
|
||||
request.Content = content;
|
||||
|
||||
// Act
|
||||
var response = await Client.SendAsync(request);
|
||||
var responseBody = await response.Content.ReadAsStringAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.Equal(expectedSampleIntValue.ToString(), responseBody);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,189 @@
|
|||
// 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.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using FormatterWebSite.Controllers;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Newtonsoft.Json;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
||||
{
|
||||
public abstract class JsonOutputFormatterTestBase<TStartup> : IClassFixture<MvcTestFixture<TStartup>> where TStartup : class
|
||||
{
|
||||
protected JsonOutputFormatterTestBase(MvcTestFixture<TStartup> fixture)
|
||||
{
|
||||
var factory = fixture.Factories.FirstOrDefault() ?? fixture.WithWebHostBuilder(ConfigureWebHostBuilder);
|
||||
Client = factory.CreateDefaultClient();
|
||||
}
|
||||
|
||||
private static void ConfigureWebHostBuilder(IWebHostBuilder builder) =>
|
||||
builder.UseStartup<TStartup>();
|
||||
|
||||
public HttpClient Client { get; }
|
||||
|
||||
[Fact]
|
||||
public virtual async Task SerializableErrorIsReturnedInExpectedFormat()
|
||||
{
|
||||
// Arrange
|
||||
var input = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
|
||||
"<Employee xmlns=\"http://schemas.datacontract.org/2004/07/FormatterWebSite\">" +
|
||||
"<Id>2</Id><Name>foo</Name></Employee>";
|
||||
|
||||
var expectedOutput = "{\"Id\":[\"The field Id must be between 10 and 100." +
|
||||
"\"],\"Name\":[\"The field Name must be a string or array type with" +
|
||||
" a minimum length of '15'.\"]}";
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, "http://localhost/SerializableError/CreateEmployee");
|
||||
request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json"));
|
||||
request.Content = new StringContent(input, Encoding.UTF8, "application/xml");
|
||||
|
||||
// Act
|
||||
var response = await Client.SendAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
|
||||
var actualContent = await response.Content.ReadAsStringAsync();
|
||||
Assert.Equal(expectedOutput, actualContent);
|
||||
|
||||
var modelStateErrors = JsonConvert.DeserializeObject<Dictionary<string, string[]>>(actualContent);
|
||||
Assert.Equal(2, modelStateErrors.Count);
|
||||
|
||||
var errors = Assert.Single(modelStateErrors, kvp => kvp.Key == "Id").Value;
|
||||
|
||||
var error = Assert.Single(errors);
|
||||
Assert.Equal("The field Id must be between 10 and 100.", error);
|
||||
|
||||
errors = Assert.Single(modelStateErrors, kvp => kvp.Key == "Name").Value;
|
||||
error = Assert.Single(errors);
|
||||
Assert.Equal("The field Name must be a string or array type with a minimum length of '15'.", error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public virtual async Task Formatting_IntValue()
|
||||
{
|
||||
// Act
|
||||
var response = await Client.GetAsync($"/JsonOutputFormatter/{nameof(JsonOutputFormatterController.IntResult)}");
|
||||
|
||||
// Assert
|
||||
await response.AssertStatusCodeAsync(HttpStatusCode.OK);
|
||||
Assert.Equal("2", await response.Content.ReadAsStringAsync());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public virtual async Task Formatting_StringValue()
|
||||
{
|
||||
// Act
|
||||
var response = await Client.GetAsync($"/JsonOutputFormatter/{nameof(JsonOutputFormatterController.StringResult)}");
|
||||
|
||||
// Assert
|
||||
await response.AssertStatusCodeAsync(HttpStatusCode.OK);
|
||||
Assert.Equal("\"Hello world\"", await response.Content.ReadAsStringAsync());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public virtual async Task Formatting_StringValueWithUnicodeContent()
|
||||
{
|
||||
// Act
|
||||
var response = await Client.GetAsync($"/JsonOutputFormatter/{nameof(JsonOutputFormatterController.StringWithUnicodeResult)}");
|
||||
|
||||
// Assert
|
||||
await response.AssertStatusCodeAsync(HttpStatusCode.OK);
|
||||
Assert.Equal("\"Hello Mr. 🦊\"", await response.Content.ReadAsStringAsync());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public virtual async Task Formatting_SimpleModel()
|
||||
{
|
||||
// Arrange
|
||||
var expected = "{\"id\":10,\"name\":\"Test\",\"streetName\":\"Some street\"}";
|
||||
|
||||
// Act
|
||||
var response = await Client.GetAsync($"/JsonOutputFormatter/{nameof(JsonOutputFormatterController.SimpleModelResult)}");
|
||||
|
||||
// Assert
|
||||
await response.AssertStatusCodeAsync(HttpStatusCode.OK);
|
||||
Assert.Equal(expected, await response.Content.ReadAsStringAsync());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public virtual async Task Formatting_CollectionType()
|
||||
{
|
||||
// Arrange
|
||||
var expected = "[{\"id\":10,\"name\":\"TestName\",\"streetName\":null},{\"id\":11,\"name\":\"TestName1\",\"streetName\":\"Some street\"}]";
|
||||
|
||||
// Act
|
||||
var response = await Client.GetAsync($"/JsonOutputFormatter/{nameof(JsonOutputFormatterController.CollectionModelResult)}");
|
||||
|
||||
// Assert
|
||||
await response.AssertStatusCodeAsync(HttpStatusCode.OK);
|
||||
Assert.Equal(expected, await response.Content.ReadAsStringAsync());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public virtual async Task Formatting_DictionaryType()
|
||||
{
|
||||
// Arrange
|
||||
var expected = "{\"SomeKey\":\"Value0\",\"DifferentKey\":\"Value1\",\"Key3\":null}";
|
||||
|
||||
// Act
|
||||
var response = await Client.GetAsync($"/JsonOutputFormatter/{nameof(JsonOutputFormatterController.DictionaryResult)}");
|
||||
|
||||
// Assert
|
||||
await response.AssertStatusCodeAsync(HttpStatusCode.OK);
|
||||
Assert.Equal(expected, await response.Content.ReadAsStringAsync());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public virtual async Task Formatting_LargeObject()
|
||||
{
|
||||
// Arrange
|
||||
var expectedName = "This is long so we can test large objects " + new string('a', 1024 * 65);
|
||||
var expected = $"{{\"id\":10,\"name\":\"{expectedName}\",\"streetName\":null}}";
|
||||
|
||||
// Act
|
||||
var response = await Client.GetAsync($"/JsonOutputFormatter/{nameof(JsonOutputFormatterController.LargeObjectResult)}");
|
||||
|
||||
// Assert
|
||||
await response.AssertStatusCodeAsync(HttpStatusCode.OK);
|
||||
Assert.Equal(expected, await response.Content.ReadAsStringAsync());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public virtual async Task Formatting_ProblemDetails()
|
||||
{
|
||||
using var _ = new ActivityReplacer();
|
||||
|
||||
// Arrange
|
||||
var expected = $"{{\"type\":\"https://tools.ietf.org/html/rfc7231#section-6.5.4\",\"title\":\"Not Found\",\"status\":404,\"traceId\":\"{Activity.Current.Id}\"}}";
|
||||
|
||||
// Act
|
||||
var response = await Client.GetAsync($"/JsonOutputFormatter/{nameof(JsonOutputFormatterController.ProblemDetailsResult)}");
|
||||
|
||||
// Assert
|
||||
await response.AssertStatusCodeAsync(HttpStatusCode.NotFound);
|
||||
Assert.Equal(expected, await response.Content.ReadAsStringAsync());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public virtual async Task Formatting_PolymorphicModel()
|
||||
{
|
||||
// Arrange
|
||||
var expected = "{\"address\":\"Some address\",\"id\":10,\"name\":\"test\",\"streetName\":null}";
|
||||
|
||||
// Act
|
||||
var response = await Client.GetAsync($"/JsonOutputFormatter/{nameof(JsonOutputFormatterController.PolymorphicResult)}");
|
||||
|
||||
// Assert
|
||||
await response.AssertStatusCodeAsync(HttpStatusCode.OK);
|
||||
Assert.Equal(expected, await response.Content.ReadAsStringAsync());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,91 +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.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc.Formatters;
|
||||
using Microsoft.AspNetCore.Mvc.NewtonsoftJson;
|
||||
using Microsoft.AspNetCore.Testing.xunit;
|
||||
using Newtonsoft.Json;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
||||
{
|
||||
public class JsonOutputFormatterTests : IClassFixture<MvcTestFixture<FormatterWebSite.Startup>>
|
||||
{
|
||||
public JsonOutputFormatterTests(MvcTestFixture<FormatterWebSite.Startup> fixture)
|
||||
{
|
||||
Client = fixture.CreateDefaultClient();
|
||||
}
|
||||
|
||||
public HttpClient Client { get; }
|
||||
|
||||
[Fact]
|
||||
public async Task JsonOutputFormatter_ReturnsIndentedJson()
|
||||
{
|
||||
// Arrange
|
||||
var user = new FormatterWebSite.User()
|
||||
{
|
||||
Id = 1,
|
||||
Alias = "john",
|
||||
description = "This is long so we can test large objects " + new string('a', 1024 * 65),
|
||||
Designation = "Administrator",
|
||||
Name = "John Williams"
|
||||
};
|
||||
|
||||
var serializerSettings = JsonSerializerSettingsProvider.CreateSerializerSettings();
|
||||
serializerSettings.Formatting = Formatting.Indented;
|
||||
var expectedBody = JsonConvert.SerializeObject(user, serializerSettings);
|
||||
|
||||
// Act
|
||||
var response = await Client.GetAsync("http://localhost/JsonFormatter/ReturnsIndentedJson");
|
||||
|
||||
// Assert
|
||||
var actualBody = await response.Content.ReadAsStringAsync();
|
||||
Assert.Equal(expectedBody, actualBody);
|
||||
}
|
||||
|
||||
[ConditionalFact]
|
||||
// Mono issue - https://github.com/aspnet/External/issues/18
|
||||
[FrameworkSkipCondition(RuntimeFrameworks.Mono)]
|
||||
public async Task SerializableErrorIsReturnedInExpectedFormat()
|
||||
{
|
||||
// Arrange
|
||||
var input = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
|
||||
"<Employee xmlns=\"http://schemas.datacontract.org/2004/07/FormatterWebSite\">" +
|
||||
"<Id>2</Id><Name>foo</Name></Employee>";
|
||||
|
||||
var expectedOutput = "{\"Id\":[\"The field Id must be between 10 and 100." +
|
||||
"\"],\"Name\":[\"The field Name must be a string or array type with" +
|
||||
" a minimum length of '15'.\"]}";
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, "http://localhost/SerializableError/CreateEmployee");
|
||||
request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json"));
|
||||
request.Content = new StringContent(input, Encoding.UTF8, "application/xml");
|
||||
|
||||
// Act
|
||||
var response = await Client.SendAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
|
||||
var actualContent = await response.Content.ReadAsStringAsync();
|
||||
Assert.Equal(expectedOutput, actualContent);
|
||||
|
||||
var modelStateErrors = JsonConvert.DeserializeObject<Dictionary<string, string[]>>(actualContent);
|
||||
Assert.Equal(2, modelStateErrors.Count);
|
||||
|
||||
var errors = Assert.Single(modelStateErrors, kvp => kvp.Key == "Id").Value;
|
||||
|
||||
var error = Assert.Single(errors);
|
||||
Assert.Equal("The field Id must be between 10 and 100.", error);
|
||||
|
||||
errors = Assert.Single(modelStateErrors, kvp => kvp.Key == "Name").Value;
|
||||
error = Assert.Single(errors);
|
||||
Assert.Equal("The field Name must be a string or array type with a minimum length of '15'.", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
// 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.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
||||
{
|
||||
public class NewtonsoftJsonInputFormatterTest : JsonInputFormatterTestBase<FormatterWebSite.Startup>
|
||||
{
|
||||
public NewtonsoftJsonInputFormatterTest(MvcTestFixture<FormatterWebSite.Startup> fixture)
|
||||
: base(fixture)
|
||||
{
|
||||
}
|
||||
|
||||
[Fact] // This test covers the 2.0 behavior. JSON.Net error messages are not preserved.
|
||||
public virtual async Task JsonInputFormatter_SuppliedJsonDeserializationErrorMessage()
|
||||
{
|
||||
// Arrange
|
||||
var content = new StringContent("{", Encoding.UTF8, "application/json");
|
||||
|
||||
// Act
|
||||
var response = await Client.PostAsync("http://localhost/JsonFormatter/ReturnInput/", content);
|
||||
var responseBody = await response.Content.ReadAsStringAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
Assert.Equal("{\"\":[\"Unexpected end when reading JSON. Path '', line 1, position 1.\"]}", responseBody);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("application/json", "")]
|
||||
[InlineData("application/json", " ")]
|
||||
public async Task JsonInputFormatter_ReturnsBadRequest_ForEmptyRequestBody(
|
||||
string requestContentType,
|
||||
string jsonInput)
|
||||
{
|
||||
// Arrange
|
||||
var content = new StringContent(jsonInput, Encoding.UTF8, requestContentType);
|
||||
|
||||
// Act
|
||||
var response = await Client.PostAsync("http://localhost/JsonFormatter/ReturnInput/", content);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
// 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.Net;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc.NewtonsoftJson;
|
||||
using Newtonsoft.Json;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
||||
{
|
||||
public class NewtonsoftJsonOutputFormatterTest : JsonOutputFormatterTestBase<FormatterWebSite.Startup>
|
||||
{
|
||||
public NewtonsoftJsonOutputFormatterTest(MvcTestFixture<FormatterWebSite.Startup> fixture)
|
||||
: base(fixture)
|
||||
{
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task JsonOutputFormatter_ReturnsIndentedJson()
|
||||
{
|
||||
// Arrange
|
||||
var user = new FormatterWebSite.User()
|
||||
{
|
||||
Id = 1,
|
||||
Alias = "john",
|
||||
description = "This is long so we can test large objects " + new string('a', 1024 * 65),
|
||||
Designation = "Administrator",
|
||||
Name = "John Williams"
|
||||
};
|
||||
|
||||
var serializerSettings = JsonSerializerSettingsProvider.CreateSerializerSettings();
|
||||
serializerSettings.Formatting = Formatting.Indented;
|
||||
var expectedBody = JsonConvert.SerializeObject(user, serializerSettings);
|
||||
|
||||
// Act
|
||||
var response = await Client.GetAsync("http://localhost/JsonFormatter/ReturnsIndentedJson");
|
||||
|
||||
// Assert
|
||||
await response.AssertStatusCodeAsync(HttpStatusCode.OK);
|
||||
var actualBody = await response.Content.ReadAsStringAsync();
|
||||
Assert.Equal(expectedBody, actualBody);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
// 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.Threading.Tasks;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
||||
{
|
||||
public class SystemTextJsonInputFormatterTest : JsonInputFormatterTestBase<FormatterWebSite.StartupWithJsonFormatter>
|
||||
{
|
||||
public SystemTextJsonInputFormatterTest(MvcTestFixture<FormatterWebSite.StartupWithJsonFormatter> fixture)
|
||||
: base(fixture)
|
||||
{
|
||||
}
|
||||
|
||||
[Theory(Skip = "https://github.com/dotnet/corefx/issues/36025")]
|
||||
[InlineData("\"I'm a JSON string!\"")]
|
||||
[InlineData("true")]
|
||||
[InlineData("\"\"")] // Empty string
|
||||
public override Task JsonInputFormatter_ReturnsDefaultValue_ForValueTypes(string input)
|
||||
{
|
||||
return base.JsonInputFormatter_ReturnsDefaultValue_ForValueTypes(input);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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.Net;
|
||||
using System.Threading.Tasks;
|
||||
using FormatterWebSite.Controllers;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
||||
{
|
||||
public class SystemTextJsonOutputFormatterTest : JsonOutputFormatterTestBase<FormatterWebSite.StartupWithJsonFormatter>
|
||||
{
|
||||
public SystemTextJsonOutputFormatterTest(MvcTestFixture<FormatterWebSite.StartupWithJsonFormatter> fixture)
|
||||
: base(fixture)
|
||||
{
|
||||
}
|
||||
|
||||
[Fact(Skip = "Insert issue here")]
|
||||
public override Task SerializableErrorIsReturnedInExpectedFormat() => base.SerializableErrorIsReturnedInExpectedFormat();
|
||||
|
||||
[Fact]
|
||||
public override async Task Formatting_StringValueWithUnicodeContent()
|
||||
{
|
||||
// Act
|
||||
var response = await Client.GetAsync($"/JsonOutputFormatter/{nameof(JsonOutputFormatterController.StringWithUnicodeResult)}");
|
||||
|
||||
// Assert
|
||||
await response.AssertStatusCodeAsync(HttpStatusCode.OK);
|
||||
Assert.Equal("\"Hello Mr. \\ud83e\\udd8a\"", await response.Content.ReadAsStringAsync());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public override async Task Formatting_SimpleModel()
|
||||
{
|
||||
// Arrange
|
||||
var expected = "{\"Id\":10,\"Name\":\"Test\",\"StreetName\":\"Some street\"}";
|
||||
|
||||
// Act
|
||||
var response = await Client.GetAsync($"/JsonOutputFormatter/{nameof(JsonOutputFormatterController.SimpleModelResult)}");
|
||||
|
||||
// Assert
|
||||
await response.AssertStatusCodeAsync(HttpStatusCode.OK);
|
||||
Assert.Equal(expected, await response.Content.ReadAsStringAsync());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public override async Task Formatting_CollectionType()
|
||||
{
|
||||
// Arrange
|
||||
var expected = "[{\"Id\":10,\"Name\":\"TestName\",\"StreetName\":null},{\"Id\":11,\"Name\":\"TestName1\",\"StreetName\":\"Some street\"}]";
|
||||
|
||||
// Act
|
||||
var response = await Client.GetAsync($"/JsonOutputFormatter/{nameof(JsonOutputFormatterController.CollectionModelResult)}");
|
||||
|
||||
// Assert
|
||||
await response.AssertStatusCodeAsync(HttpStatusCode.OK);
|
||||
Assert.Equal(expected, await response.Content.ReadAsStringAsync());
|
||||
}
|
||||
|
||||
[Fact(Skip = "Dictionary serialization does not correctly work.")]
|
||||
public override Task Formatting_DictionaryType() => base.Formatting_DictionaryType();
|
||||
|
||||
[Fact(Skip = "Dictionary serialization does not correctly work.")]
|
||||
public override Task Formatting_ProblemDetails() => base.Formatting_ProblemDetails();
|
||||
|
||||
[Fact]
|
||||
public override async Task Formatting_PolymorphicModel()
|
||||
{
|
||||
// Arrange
|
||||
var expected = "{\"Id\":10,\"Name\":\"test\",\"StreetName\":null}";
|
||||
|
||||
// Act
|
||||
var response = await Client.GetAsync($"/JsonOutputFormatter/{nameof(JsonOutputFormatterController.PolymorphicResult)}");
|
||||
|
||||
// Assert
|
||||
await response.AssertStatusCodeAsync(HttpStatusCode.OK);
|
||||
Assert.Equal(expected, await response.Content.ReadAsStringAsync());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public override async Task Formatting_LargeObject()
|
||||
{
|
||||
// Arrange
|
||||
var expectedName = "This is long so we can test large objects " + new string('a', 1024 * 65);
|
||||
var expected = $"{{\"Id\":10,\"Name\":\"{expectedName}\",\"StreetName\":null}}";
|
||||
|
||||
// Act
|
||||
var response = await Client.GetAsync($"/JsonOutputFormatter/{nameof(JsonOutputFormatterController.LargeObjectResult)}");
|
||||
|
||||
// Assert
|
||||
await response.AssertStatusCodeAsync(HttpStatusCode.OK);
|
||||
Assert.Equal(expected, await response.Content.ReadAsStringAsync());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
// 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 Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace FormatterWebSite.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[Route("[controller]/[action]")]
|
||||
public class JsonInputFormatterController
|
||||
{
|
||||
[HttpPost]
|
||||
public ActionResult<int> IntValue(int value)
|
||||
{
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
// 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.Collections.Generic;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace FormatterWebSite.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[Route("[controller]/[action]")]
|
||||
[Produces("application/json")]
|
||||
public class JsonOutputFormatterController : ControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
public ActionResult<int> IntResult() => 2;
|
||||
|
||||
[HttpGet]
|
||||
public ActionResult<string> StringResult() => "Hello world";
|
||||
|
||||
[HttpGet]
|
||||
public ActionResult<string> StringWithUnicodeResult() => "Hello Mr. 🦊";
|
||||
|
||||
[HttpGet]
|
||||
public ActionResult<SimpleModel> SimpleModelResult() =>
|
||||
new SimpleModel { Id = 10, Name = "Test", StreetName = "Some street" };
|
||||
|
||||
[HttpGet]
|
||||
public ActionResult<IEnumerable<SimpleModel>> CollectionModelResult() =>
|
||||
new[]
|
||||
{
|
||||
new SimpleModel { Id = 10, Name = "TestName" },
|
||||
new SimpleModel { Id = 11, Name = "TestName1", StreetName = "Some street" },
|
||||
};
|
||||
|
||||
[HttpGet]
|
||||
public ActionResult<Dictionary<string, string>> DictionaryResult() =>
|
||||
new Dictionary<string, string>
|
||||
{
|
||||
["SomeKey"] = "Value0",
|
||||
["DifferentKey"] = "Value1",
|
||||
["Key3"] = null,
|
||||
};
|
||||
|
||||
[HttpGet]
|
||||
public ActionResult<SimpleModel> LargeObjectResult() =>
|
||||
new SimpleModel
|
||||
{
|
||||
Id = 10,
|
||||
Name = "This is long so we can test large objects " + new string('a', 1024 * 65),
|
||||
};
|
||||
|
||||
[HttpGet]
|
||||
public ActionResult<SimpleModel> PolymorphicResult() => new DeriviedModel
|
||||
{
|
||||
Id = 10,
|
||||
Name = "test",
|
||||
Address = "Some address",
|
||||
};
|
||||
|
||||
[HttpGet]
|
||||
public ActionResult<ProblemDetails> ProblemDetailsResult() => NotFound();
|
||||
|
||||
public class SimpleModel
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
|
||||
public string StreetName { get; set; }
|
||||
}
|
||||
|
||||
public class DeriviedModel : SimpleModel
|
||||
{
|
||||
public string Address { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp3.0</TargetFramework>
|
||||
|
|
@ -9,6 +9,7 @@
|
|||
<Reference Include="Microsoft.AspNetCore.Mvc" />
|
||||
<Reference Include="Microsoft.AspNetCore.Mvc.Formatters.Xml" />
|
||||
<Reference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" />
|
||||
<Reference Include="Microsoft.AspNetCore.Diagnostics" />
|
||||
|
||||
<Reference Include="Microsoft.AspNetCore.Server.IISIntegration" />
|
||||
<Reference Include="Microsoft.AspNetCore.Server.Kestrel" />
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
// 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 Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace FormatterWebSite
|
||||
{
|
||||
public class StartupWithJsonFormatter
|
||||
{
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.AddMvc(options =>
|
||||
{
|
||||
options.ModelMetadataDetailsProviders.Add(new SuppressChildValidationMetadataProvider(typeof(Developer)));
|
||||
options.ModelMetadataDetailsProviders.Add(new SuppressChildValidationMetadataProvider(typeof(Supplier)));
|
||||
})
|
||||
.AddXmlDataContractSerializerFormatters()
|
||||
.SetCompatibilityVersion(CompatibilityVersion.Latest);
|
||||
}
|
||||
|
||||
public void Configure(IApplicationBuilder app)
|
||||
{
|
||||
app.UseRouting(routes =>
|
||||
{
|
||||
routes.MapDefaultControllerRoute();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue