System.Text.Json based formatters (#8362)

* System.Text.Json based formatters

Fixes: https://github.com/aspnet/AspNetCore/issues/7256
This commit is contained in:
Pranav K 2019-03-21 13:45:21 -07:00 committed by GitHub
parent 26fa19e080
commit f5ff181222
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 2306 additions and 763 deletions

View File

@ -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;

View File

@ -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();

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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());

View File

@ -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();

View File

@ -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; }
}
}
}

View File

@ -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");
}
}
}
}
}

View File

@ -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());
}
}
}

View File

@ -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);
}
}
}

View File

@ -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; }
}
}
}

View File

@ -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; }
}
}
}

View File

@ -1793,7 +1793,7 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
public class TaskDerivedType : Task
{
public TaskDerivedType()
: base(() => Console.WriteLine("In The Constructor"))
: base(() => { })
{
}
}

View File

@ -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.

View File

@ -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>
{

View File

@ -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>

View File

@ -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
}
}
}
}

View File

@ -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");
}
}
}
}
}

View File

@ -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]

View File

@ -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")]

View File

@ -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);
}
}
}

View File

@ -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());
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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());
}
}
}

View File

@ -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;
}
}
}

View File

@ -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; }
}
}
}

View File

@ -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" />

View File

@ -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();
});
}
}
}