diff --git a/src/Mvc/Mvc.Core/ref/Microsoft.AspNetCore.Mvc.Core.netcoreapp3.0.cs b/src/Mvc/Mvc.Core/ref/Microsoft.AspNetCore.Mvc.Core.netcoreapp3.0.cs index 5cb7488011df..ccfbff6d84b0 100644 --- a/src/Mvc/Mvc.Core/ref/Microsoft.AspNetCore.Mvc.Core.netcoreapp3.0.cs +++ b/src/Mvc/Mvc.Core/ref/Microsoft.AspNetCore.Mvc.Core.netcoreapp3.0.cs @@ -876,6 +876,7 @@ public MvcOptions() { } 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 @@ public StringOutputFormatter() { } 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 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; diff --git a/src/Mvc/Mvc.Formatters.Xml/src/MediaTypeHeaderValues.cs b/src/Mvc/Mvc.Core/src/Formatters/MediaTypeHeaderValues.cs similarity index 59% rename from src/Mvc/Mvc.Formatters.Xml/src/MediaTypeHeaderValues.cs rename to src/Mvc/Mvc.Core/src/Formatters/MediaTypeHeaderValues.cs index 24ddce0b3f86..8d1eaae71708 100644 --- a/src/Mvc/Mvc.Formatters.Xml/src/MediaTypeHeaderValues.cs +++ b/src/Mvc/Mvc.Core/src/Formatters/MediaTypeHeaderValues.cs @@ -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(); diff --git a/src/Mvc/Mvc.Core/src/Formatters/SystemTextJsonInputFormatter.cs b/src/Mvc/Mvc.Core/src/Formatters/SystemTextJsonInputFormatter.cs new file mode 100644 index 000000000000..a99775da126f --- /dev/null +++ b/src/Mvc/Mvc.Core/src/Formatters/SystemTextJsonInputFormatter.cs @@ -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 +{ + /// + /// A for JSON content that uses . + /// + public class SystemTextJsonInputFormatter : TextInputFormatter, IInputFormatterExceptionPolicy + { + /// + /// Initializes a new instance of . + /// + /// The . + 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); + } + + /// + /// Gets the used to configure the . + /// + /// + /// A single instance of is used for all JSON formatting. Any + /// changes to the options will affect all input formatting. + /// + public JsonSerializerOptions SerializerOptions { get; } + + /// + InputFormatterExceptionPolicy IInputFormatterExceptionPolicy.ExceptionPolicy => InputFormatterExceptionPolicy.MalformedInputExceptions; + + /// + public sealed override async Task 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); + } + } +} diff --git a/src/Mvc/Mvc.Core/src/Formatters/SystemTextJsonOutputFormatter.cs b/src/Mvc/Mvc.Core/src/Formatters/SystemTextJsonOutputFormatter.cs new file mode 100644 index 000000000000..b39ab6ded60a --- /dev/null +++ b/src/Mvc/Mvc.Core/src/Formatters/SystemTextJsonOutputFormatter.cs @@ -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 +{ + /// + /// A for JSON content that uses . + /// + public class SystemTextJsonOutputFormatter : TextOutputFormatter + { + + /// + /// Initializes a new instance. + /// + /// The . + 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); + } + + /// + /// Gets the used to configure the . + /// + /// + /// A single instance of is used for all JSON formatting. Any + /// changes to the options will affect all output formatting. + /// + public JsonSerializerOptions SerializerOptions { get; } + + /// + 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); + } + } +} diff --git a/src/Mvc/Mvc.Core/src/Formatters/TranscodingReadStream.cs b/src/Mvc/Mvc.Core/src/Formatters/TranscodingReadStream.cs new file mode 100644 index 000000000000..297ad3cc3375 --- /dev/null +++ b/src/Mvc/Mvc.Core/src/Formatters/TranscodingReadStream.cs @@ -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 _byteBuffer; + private ArraySegment _charBuffer; + private ArraySegment _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( + ArrayPool.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( + ArrayPool.Shared.Rent(maxCharBufferSize), + 0, + count: 0); + + _overflowBuffer = new ArraySegment( + ArrayPool.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 ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + ThrowArgumentOutOfRangeException(buffer, offset, count); + + var readBuffer = new ArraySegment(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( + _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 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(_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(_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.Shared.Return(_charBuffer.Array); + ArrayPool.Shared.Return(_byteBuffer.Array); + ArrayPool.Shared.Return(_overflowBuffer.Array); + + base.Dispose(disposing); + } + } +} diff --git a/src/Mvc/Mvc.Core/src/Formatters/TranscodingWriteStream.cs b/src/Mvc/Mvc.Core/src/Formatters/TranscodingWriteStream.cs new file mode 100644 index 000000000000..5062778d894e --- /dev/null +++ b/src/Mvc/Mvc.Core/src/Formatters/TranscodingWriteStream.cs @@ -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.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.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(buffer, offset, count); + return WriteAsync(bufferSegment, flush: false, cancellationToken); + } + + private async Task WriteAsync( + ArraySegment 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.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.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.Shared.Return(_charBuffer); + } + + + base.Dispose(disposing); + } + } +} diff --git a/src/Mvc/Mvc.Core/src/Infrastructure/MvcCoreMvcOptionsSetup.cs b/src/Mvc/Mvc.Core/src/Infrastructure/MvcCoreMvcOptionsSetup.cs index 81ba0b2ace7e..d856b684b8b7 100644 --- a/src/Mvc/Mvc.Core/src/Infrastructure/MvcCoreMvcOptionsSetup.cs +++ b/src/Mvc/Mvc.Core/src/Infrastructure/MvcCoreMvcOptionsSetup.cs @@ -65,10 +65,14 @@ public void Configure(MvcOptions options) // 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()); diff --git a/src/Mvc/Mvc.Core/src/MvcOptions.cs b/src/Mvc/Mvc.Core/src/MvcOptions.cs index c05fdcfd2110..c5d2ea5d161b 100644 --- a/src/Mvc/Mvc.Core/src/MvcOptions.cs +++ b/src/Mvc/Mvc.Core/src/MvcOptions.cs @@ -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 @@ public int MaxModelBindingRecursionDepth } } + /// + /// Gets the used by and + /// . + /// + 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 IEnumerable.GetEnumerator() => _switches.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => _switches.GetEnumerator(); diff --git a/src/Mvc/Mvc.Core/test/Formatters/JsonInputFormatterTestBase.cs b/src/Mvc/Mvc.Core/test/Formatters/JsonInputFormatterTestBase.cs new file mode 100644 index 000000000000..fe7024b0893d --- /dev/null +++ b/src/Mvc/Mvc.Core/test/Formatters/JsonInputFormatterTestBase.cs @@ -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(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(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(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(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(result.Model); + Assert.Equal(new int[] { 0, 23, 300 }, integers); + } + + [Fact] + public virtual Task ReadAsync_ReadsValidArray_AsListOfT() => ReadAsync_ReadsValidArray_AsList(typeof(List)); + + [Fact] + public virtual Task ReadAsync_ReadsValidArray_AsIListOfT() => ReadAsync_ReadsValidArray_AsList(typeof(IList)); + + [Fact] + public virtual Task ReadAsync_ReadsValidArray_AsCollectionOfT() => ReadAsync_ReadsValidArray_AsList(typeof(ICollection)); + + [Fact] + public virtual Task ReadAsync_ReadsValidArray_AsEnumerableOfT() => ReadAsync_ReadsValidArray_AsList(typeof(IEnumerable)); + + 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>(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(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; } + } + } +} diff --git a/src/Mvc/Mvc.Core/test/Formatters/JsonOutputFormatterTestBase.cs b/src/Mvc/Mvc.Core/test/Formatters/JsonOutputFormatterTestBase.cs new file mode 100644 index 000000000000..0039a4a9ade1 --- /dev/null +++ b/src/Mvc/Mvc.Core/test/Formatters/JsonOutputFormatterTestBase.cs @@ -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 WriteCorrectCharacterEncoding + { + get + { + var data = new TheoryData + { + { "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"); + } + } + } + } +} diff --git a/src/Mvc/Mvc.Core/test/Formatters/SystemTextJsonInputFormatterTest.cs b/src/Mvc/Mvc.Core/test/Formatters/SystemTextJsonInputFormatterTest.cs new file mode 100644 index 000000000000..3f8c91b68901 --- /dev/null +++ b/src/Mvc/Mvc.Core/test/Formatters/SystemTextJsonInputFormatterTest.cs @@ -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()); + } + } +} diff --git a/src/Mvc/Mvc.Core/test/Formatters/SystemTextJsonOutputFormatterTest.cs b/src/Mvc/Mvc.Core/test/Formatters/SystemTextJsonOutputFormatterTest.cs new file mode 100644 index 000000000000..edd5d64f2c99 --- /dev/null +++ b/src/Mvc/Mvc.Core/test/Formatters/SystemTextJsonOutputFormatterTest.cs @@ -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); + } + } +} diff --git a/src/Mvc/Mvc.Core/test/Formatters/TranscodingReadStreamTest.cs b/src/Mvc/Mvc.Core/test/Formatters/TranscodingReadStreamTest.cs new file mode 100644 index 000000000000..546e5eb3a734 --- /dev/null +++ b/src/Mvc/Mvc.Core/test/Formatters/TranscodingReadStreamTest.cs @@ -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 + { + "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 + { + 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(model); + + Assert.Equal(message, testModel.Message); + } + + public class TestModel + { + public string Message { get; set; } + } + } +} diff --git a/src/Mvc/Mvc.Core/test/Formatters/TranscodingWriteStreamTest.cs b/src/Mvc/Mvc.Core/test/Formatters/TranscodingWriteStreamTest.cs new file mode 100644 index 000000000000..2c94113e2fe6 --- /dev/null +++ b/src/Mvc/Mvc.Core/test/Formatters/TranscodingWriteStreamTest.cs @@ -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; } + } + } +} diff --git a/src/Mvc/Mvc.Core/test/Infrastructure/ControllerActionInvokerTest.cs b/src/Mvc/Mvc.Core/test/Infrastructure/ControllerActionInvokerTest.cs index e74cdcfdee7c..70ce4124e1cf 100644 --- a/src/Mvc/Mvc.Core/test/Infrastructure/ControllerActionInvokerTest.cs +++ b/src/Mvc/Mvc.Core/test/Infrastructure/ControllerActionInvokerTest.cs @@ -1793,7 +1793,7 @@ public IConvertToActionResult ConvertibleToActionResultReturningNull() public class TaskDerivedType : Task { public TaskDerivedType() - : base(() => Console.WriteLine("In The Constructor")) + : base(() => { }) { } } diff --git a/src/Mvc/Mvc.NewtonsoftJson/src/DependencyInjection/NewtonsoftJsonMvcOptionsSetup.cs b/src/Mvc/Mvc.NewtonsoftJson/src/DependencyInjection/NewtonsoftJsonMvcOptionsSetup.cs index 56adf9a238ac..c466d8d11416 100644 --- a/src/Mvc/Mvc.NewtonsoftJson/src/DependencyInjection/NewtonsoftJsonMvcOptionsSetup.cs +++ b/src/Mvc/Mvc.NewtonsoftJson/src/DependencyInjection/NewtonsoftJsonMvcOptionsSetup.cs @@ -59,8 +59,10 @@ public NewtonsoftJsonMvcOptionsSetup( public void Configure(MvcOptions options) { + options.OutputFormatters.RemoveType(); options.OutputFormatters.Add(new NewtonsoftJsonOutputFormatter(_jsonOptions.SerializerSettings, _charPool)); + options.InputFormatters.RemoveType(); // Register JsonPatchInputFormatter before JsonInputFormatter, otherwise // JsonInputFormatter would consume "application/json-patch+json" requests // before JsonPatchInputFormatter gets to see them. diff --git a/src/Mvc/Mvc.NewtonsoftJson/src/MvcNewtonsoftJsonOptions.cs b/src/Mvc/Mvc.NewtonsoftJson/src/MvcNewtonsoftJsonOptions.cs index 57c0d7684d81..19e459b4c0a5 100644 --- a/src/Mvc/Mvc.NewtonsoftJson/src/MvcNewtonsoftJsonOptions.cs +++ b/src/Mvc/Mvc.NewtonsoftJson/src/MvcNewtonsoftJsonOptions.cs @@ -13,7 +13,7 @@ namespace Microsoft.AspNetCore.Mvc { /// - /// Provides programmatic configuration for JSON in the MVC framework. + /// Provides programmatic configuration for JSON formatters using Newtonsoft.JSON. /// public class MvcNewtonsoftJsonOptions : IEnumerable { diff --git a/src/Mvc/Mvc.NewtonsoftJson/test/Microsoft.AspNetCore.Mvc.NewtonsoftJson.Test.csproj b/src/Mvc/Mvc.NewtonsoftJson/test/Microsoft.AspNetCore.Mvc.NewtonsoftJson.Test.csproj index 03621c7963f8..412691fd2d3c 100644 --- a/src/Mvc/Mvc.NewtonsoftJson/test/Microsoft.AspNetCore.Mvc.NewtonsoftJson.Test.csproj +++ b/src/Mvc/Mvc.NewtonsoftJson/test/Microsoft.AspNetCore.Mvc.NewtonsoftJson.Test.csproj @@ -10,4 +10,9 @@ + + + + + diff --git a/src/Mvc/Mvc.NewtonsoftJson/test/NewtonsoftJsonInputFormatterTest.cs b/src/Mvc/Mvc.NewtonsoftJson/test/NewtonsoftJsonInputFormatterTest.cs index 35b2cadebf21..04d47d106537 100644 --- a/src/Mvc/Mvc.NewtonsoftJson/test/NewtonsoftJsonInputFormatterTest.cs +++ b/src/Mvc/Mvc.NewtonsoftJson/test/NewtonsoftJsonInputFormatterTest.cs @@ -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 @@ 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 @@ public async Task Version_2_1_Constructor_SuppressInputFormatterBufferingSetToTr 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 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(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(result.Model); - Assert.Equal(new int[] { 0, 23, 300 }, integers); - } - - [Theory] - [InlineData(typeof(ICollection))] - [InlineData(typeof(IEnumerable))] - [InlineData(typeof(IList))] - [InlineData(typeof(List))] - 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>(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(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 @@ public void CreateJsonSerializer_UsesJsonSerializerSettings() 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 @@ public async Task ReadAsync_DoNotAllowInputFormatterExceptionMessages_DoesNotWra } [Fact] - public async Task ReadAsync_AllowInputFormatterExceptionMessages_DoesNotWrapJsonInputExceptions() + public async Task ReadAsync_lowInputFormatterExceptionMessages_DoesNotWrapJsonInputExceptions() { // Arrange var formatter = new NewtonsoftJsonInputFormatter( @@ -586,6 +327,9 @@ private static ILogger GetLogger() 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 @@ private NewtonsoftJsonInputFormatter CreateFormatter(JsonSerializerSettings seri }); } - private static HttpContext GetHttpContext( - byte[] contentBytes, - string contentType = "application/json") + private class Location { - return GetHttpContext(new MemoryStream(contentBytes), contentType); - } + public int Id { get; set; } - private static HttpContext GetHttpContext( - Stream requestStream, - string contentType = "application/json") - { - var request = new Mock(); - var headers = new Mock(); - 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.SetupGet(c => c.Request).Returns(request.Object); - httpContext.SetupGet(c => c.Request).Returns(request.Object); - return httpContext.Object; + public string Name { get; set; } } - private InputFormatterContext CreateInputFormatterContext( - Type modelType, - HttpContext httpContext, - string modelName = null, - bool treatEmptyInputAsDefaultValue = false) + private class TestResponseFeature : HttpResponseFeature { - 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 callback, object state) + { + // do not do anything + } } private sealed class User @@ -660,20 +378,5 @@ private sealed class UserLogin [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 callback, object state) - { - // do not do anything - } - } } } diff --git a/src/Mvc/Mvc.NewtonsoftJson/test/NewtonsoftJsonOutputFormatterTest.cs b/src/Mvc/Mvc.NewtonsoftJson/test/NewtonsoftJsonOutputFormatterTest.cs index 1fff43320e87..e5cce46201ca 100644 --- a/src/Mvc/Mvc.NewtonsoftJson/test/NewtonsoftJsonOutputFormatterTest.cs +++ b/src/Mvc/Mvc.NewtonsoftJson/test/NewtonsoftJsonOutputFormatterTest.cs @@ -5,18 +5,8 @@ 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 @@ namespace Microsoft.AspNetCore.Mvc.Formatters { - public class NewtonsoftJsonOutputFormatterTest + public class NewtonsoftJsonOutputFormatterTest : JsonOutputFormatterTestBase { + protected override TextOutputFormatter GetOutputFormatter() + { + return new NewtonsoftJsonOutputFormatter(new JsonSerializerSettings(), ArrayPool.Shared); + } + [Fact] public void Creates_SerializerSettings_ByDefault() { @@ -299,188 +294,6 @@ public async Task WriteToStreamAsync_RoundTripsJToken() Assert.Equal(beforeMessage, afterMessage); } - public static TheoryData WriteCorrectCharacterEncoding - { - get - { - var data = new TheoryData - { - { "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.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.Shared); - - // Act - try - { - await jsonFormatter.WriteResponseBodyAsync(outputFormatterContext, Encoding.UTF8); - } - catch (JsonSerializationException serializerException) - { - var expectedException = Assert.IsType(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.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 @@ private class UserWithJsonObject 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"); } - } - } - } } \ No newline at end of file diff --git a/src/Mvc/Mvc/test/MvcOptionsSetupTest.cs b/src/Mvc/Mvc/test/MvcOptionsSetupTest.cs index 8e7b93cb3e38..ccd04a0cd440 100644 --- a/src/Mvc/Mvc/test/MvcOptionsSetupTest.cs +++ b/src/Mvc/Mvc/test/MvcOptionsSetupTest.cs @@ -95,7 +95,8 @@ public void Setup_SetsUpOutputFormatters() Assert.Collection(options.OutputFormatters, formatter => Assert.IsType(formatter), formatter => Assert.IsType(formatter), - formatter => Assert.IsType(formatter)); + formatter => Assert.IsType(formatter), + formatter => Assert.IsType(formatter)); } [Fact] @@ -105,7 +106,9 @@ public void Setup_SetsUpInputFormatters() var options = GetOptions(); // Assert - Assert.Empty(options.InputFormatters); + Assert.Collection( + options.InputFormatters, + formatter => Assert.IsType(formatter)); } [Fact] diff --git a/src/Mvc/test/Mvc.FunctionalTests/InputFormatterTests.cs b/src/Mvc/test/Mvc.FunctionalTests/InputFormatterTests.cs index 5b7b8a189cff..48b1f1aaa5a7 100644 --- a/src/Mvc/test/Mvc.FunctionalTests/InputFormatterTests.cs +++ b/src/Mvc/test/Mvc.FunctionalTests/InputFormatterTests.cs @@ -42,148 +42,6 @@ public async Task CheckIfXmlInputFormatterIsBeingCalled() 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")] diff --git a/src/Mvc/test/Mvc.FunctionalTests/JsonInputFormatterTestBase.cs b/src/Mvc/test/Mvc.FunctionalTests/JsonInputFormatterTestBase.cs new file mode 100644 index 000000000000..a8940bceb6ed --- /dev/null +++ b/src/Mvc/test/Mvc.FunctionalTests/JsonInputFormatterTestBase.cs @@ -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 : IClassFixture> where TStartup : class + { + protected JsonInputFormatterTestBase(MvcTestFixture fixture) + { + var factory = fixture.Factories.FirstOrDefault() ?? fixture.WithWebHostBuilder(ConfigureWebHostBuilder); + Client = factory.CreateDefaultClient(); + } + + private static void ConfigureWebHostBuilder(IWebHostBuilder builder) => + builder.UseStartup(); + + 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); + } + + } +} \ No newline at end of file diff --git a/src/Mvc/test/Mvc.FunctionalTests/JsonOutputFormatterTestBase.cs b/src/Mvc/test/Mvc.FunctionalTests/JsonOutputFormatterTestBase.cs new file mode 100644 index 000000000000..228d078f08c6 --- /dev/null +++ b/src/Mvc/test/Mvc.FunctionalTests/JsonOutputFormatterTestBase.cs @@ -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 : IClassFixture> where TStartup : class + { + protected JsonOutputFormatterTestBase(MvcTestFixture fixture) + { + var factory = fixture.Factories.FirstOrDefault() ?? fixture.WithWebHostBuilder(ConfigureWebHostBuilder); + Client = factory.CreateDefaultClient(); + } + + private static void ConfigureWebHostBuilder(IWebHostBuilder builder) => + builder.UseStartup(); + + public HttpClient Client { get; } + + [Fact] + public virtual async Task SerializableErrorIsReturnedInExpectedFormat() + { + // Arrange + var input = "" + + "" + + "2foo"; + + 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>(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()); + } + } +} \ No newline at end of file diff --git a/src/Mvc/test/Mvc.FunctionalTests/JsonOutputFormatterTests.cs b/src/Mvc/test/Mvc.FunctionalTests/JsonOutputFormatterTests.cs deleted file mode 100644 index 2d764da29adb..000000000000 --- a/src/Mvc/test/Mvc.FunctionalTests/JsonOutputFormatterTests.cs +++ /dev/null @@ -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> - { - public JsonOutputFormatterTests(MvcTestFixture 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 = "" + - "" + - "2foo"; - - 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>(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); - } - } -} \ No newline at end of file diff --git a/src/Mvc/test/Mvc.FunctionalTests/NewtonsoftJsonInputFormatterTest.cs b/src/Mvc/test/Mvc.FunctionalTests/NewtonsoftJsonInputFormatterTest.cs new file mode 100644 index 000000000000..ea1fcb661190 --- /dev/null +++ b/src/Mvc/test/Mvc.FunctionalTests/NewtonsoftJsonInputFormatterTest.cs @@ -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 + { + public NewtonsoftJsonInputFormatterTest(MvcTestFixture 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); + } + } +} \ No newline at end of file diff --git a/src/Mvc/test/Mvc.FunctionalTests/NewtonsoftJsonOutputFormatterTest.cs b/src/Mvc/test/Mvc.FunctionalTests/NewtonsoftJsonOutputFormatterTest.cs new file mode 100644 index 000000000000..58e77ca4082c --- /dev/null +++ b/src/Mvc/test/Mvc.FunctionalTests/NewtonsoftJsonOutputFormatterTest.cs @@ -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 + { + public NewtonsoftJsonOutputFormatterTest(MvcTestFixture 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); + } + } +} diff --git a/src/Mvc/test/Mvc.FunctionalTests/SystemTextJsonInputFormatterTest.cs b/src/Mvc/test/Mvc.FunctionalTests/SystemTextJsonInputFormatterTest.cs new file mode 100644 index 000000000000..9948fc50bdb8 --- /dev/null +++ b/src/Mvc/test/Mvc.FunctionalTests/SystemTextJsonInputFormatterTest.cs @@ -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 + { + public SystemTextJsonInputFormatterTest(MvcTestFixture 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); + } + } +} \ No newline at end of file diff --git a/src/Mvc/test/Mvc.FunctionalTests/SystemTextJsonOutputFormatterTest.cs b/src/Mvc/test/Mvc.FunctionalTests/SystemTextJsonOutputFormatterTest.cs new file mode 100644 index 000000000000..321c4b1de1d2 --- /dev/null +++ b/src/Mvc/test/Mvc.FunctionalTests/SystemTextJsonOutputFormatterTest.cs @@ -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 + { + public SystemTextJsonOutputFormatterTest(MvcTestFixture 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()); + } + } +} \ No newline at end of file diff --git a/src/Mvc/test/WebSites/FormatterWebSite/Controllers/JsonInputFormatterController.cs b/src/Mvc/test/WebSites/FormatterWebSite/Controllers/JsonInputFormatterController.cs new file mode 100644 index 000000000000..ccd6ca3e36a6 --- /dev/null +++ b/src/Mvc/test/WebSites/FormatterWebSite/Controllers/JsonInputFormatterController.cs @@ -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 IntValue(int value) + { + return value; + } + } +} diff --git a/src/Mvc/test/WebSites/FormatterWebSite/Controllers/JsonOutputFormatterController.cs b/src/Mvc/test/WebSites/FormatterWebSite/Controllers/JsonOutputFormatterController.cs new file mode 100644 index 000000000000..bf8b004553a4 --- /dev/null +++ b/src/Mvc/test/WebSites/FormatterWebSite/Controllers/JsonOutputFormatterController.cs @@ -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 IntResult() => 2; + + [HttpGet] + public ActionResult StringResult() => "Hello world"; + + [HttpGet] + public ActionResult StringWithUnicodeResult() => "Hello Mr. 🦊"; + + [HttpGet] + public ActionResult SimpleModelResult() => + new SimpleModel { Id = 10, Name = "Test", StreetName = "Some street" }; + + [HttpGet] + public ActionResult> CollectionModelResult() => + new[] + { + new SimpleModel { Id = 10, Name = "TestName" }, + new SimpleModel { Id = 11, Name = "TestName1", StreetName = "Some street" }, + }; + + [HttpGet] + public ActionResult> DictionaryResult() => + new Dictionary + { + ["SomeKey"] = "Value0", + ["DifferentKey"] = "Value1", + ["Key3"] = null, + }; + + [HttpGet] + public ActionResult LargeObjectResult() => + new SimpleModel + { + Id = 10, + Name = "This is long so we can test large objects " + new string('a', 1024 * 65), + }; + + [HttpGet] + public ActionResult PolymorphicResult() => new DeriviedModel + { + Id = 10, + Name = "test", + Address = "Some address", + }; + + [HttpGet] + public ActionResult 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; } + } + } +} diff --git a/src/Mvc/test/WebSites/FormatterWebSite/FormatterWebSite.csproj b/src/Mvc/test/WebSites/FormatterWebSite/FormatterWebSite.csproj index 3dd0c5179c0b..7eddbaa68cd7 100644 --- a/src/Mvc/test/WebSites/FormatterWebSite/FormatterWebSite.csproj +++ b/src/Mvc/test/WebSites/FormatterWebSite/FormatterWebSite.csproj @@ -1,4 +1,4 @@ - + netcoreapp3.0 @@ -9,6 +9,7 @@ + diff --git a/src/Mvc/test/WebSites/FormatterWebSite/StartupWithJsonInputFormatter.cs b/src/Mvc/test/WebSites/FormatterWebSite/StartupWithJsonInputFormatter.cs new file mode 100644 index 000000000000..8a65c09db7be --- /dev/null +++ b/src/Mvc/test/WebSites/FormatterWebSite/StartupWithJsonInputFormatter.cs @@ -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(); + }); + } + } +} +