Eagerly read IAsyncEnumerable{object} instances before formatting (#11118)
* Eagerly read IAsyncEnumerable{object} instances before formatting
Fixes https://github.com/aspnet/AspNetCore/issues/4833
This commit is contained in:
parent
550710f304
commit
b10c8a6fc3
|
|
@ -875,6 +875,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
public Microsoft.AspNetCore.Mvc.Filters.FilterCollection Filters { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
|
||||
public Microsoft.AspNetCore.Mvc.Formatters.FormatterMappings FormatterMappings { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
|
||||
public Microsoft.AspNetCore.Mvc.Formatters.FormatterCollection<Microsoft.AspNetCore.Mvc.Formatters.IInputFormatter> InputFormatters { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
|
||||
public int MaxIAsyncEnumerableBufferLimit { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||
public int MaxModelBindingCollectionSize { get { throw null; } set { } }
|
||||
public int MaxModelBindingRecursionDepth { get { throw null; } set { } }
|
||||
public int MaxModelValidationErrors { get { throw null; } set { } }
|
||||
|
|
@ -2086,7 +2087,9 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
|||
}
|
||||
public partial class ObjectResultExecutor : Microsoft.AspNetCore.Mvc.Infrastructure.IActionResultExecutor<Microsoft.AspNetCore.Mvc.ObjectResult>
|
||||
{
|
||||
[System.ObsoleteAttribute("This constructor is obsolete and will be removed in a future release.")]
|
||||
public ObjectResultExecutor(Microsoft.AspNetCore.Mvc.Infrastructure.OutputFormatterSelector formatterSelector, Microsoft.AspNetCore.Mvc.Infrastructure.IHttpResponseStreamWriterFactory writerFactory, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory) { }
|
||||
public ObjectResultExecutor(Microsoft.AspNetCore.Mvc.Infrastructure.OutputFormatterSelector formatterSelector, Microsoft.AspNetCore.Mvc.Infrastructure.IHttpResponseStreamWriterFactory writerFactory, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory, Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.Mvc.MvcOptions> mvcOptions) { }
|
||||
protected Microsoft.AspNetCore.Mvc.Infrastructure.OutputFormatterSelector FormatterSelector { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
|
||||
protected Microsoft.Extensions.Logging.ILogger Logger { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
|
||||
protected System.Func<System.IO.Stream, System.Text.Encoding, System.IO.TextWriter> WriterFactory { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
|
||||
|
|
|
|||
|
|
@ -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.Collections;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc.Core;
|
||||
using Microsoft.Extensions.Internal;
|
||||
|
||||
#if JSONNET
|
||||
namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson
|
||||
#else
|
||||
namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||
#endif
|
||||
{
|
||||
using ReaderFunc = Func<IAsyncEnumerable<object>, Task<ICollection>>;
|
||||
|
||||
/// <summary>
|
||||
/// Type that reads an <see cref="IAsyncEnumerable{T}"/> instance into a
|
||||
/// generic collection instance.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This type is used to create a strongly typed synchronous <see cref="ICollection{T}"/> instance from
|
||||
/// an <see cref="IAsyncEnumerable{T}"/>. An accurate <see cref="ICollection{T}"/> is required for XML formatters to
|
||||
/// correctly serialize.
|
||||
/// </remarks>
|
||||
internal sealed class AsyncEnumerableReader
|
||||
{
|
||||
private readonly MethodInfo Converter = typeof(AsyncEnumerableReader).GetMethod(
|
||||
nameof(ReadInternal),
|
||||
BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
|
||||
private readonly ConcurrentDictionary<Type, ReaderFunc> _asyncEnumerableConverters =
|
||||
new ConcurrentDictionary<Type, ReaderFunc>();
|
||||
private readonly MvcOptions _mvcOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of <see cref="AsyncEnumerableReader"/>.
|
||||
/// </summary>
|
||||
/// <param name="mvcOptions">Accessor to <see cref="MvcOptions"/>.</param>
|
||||
public AsyncEnumerableReader(MvcOptions mvcOptions)
|
||||
{
|
||||
_mvcOptions = mvcOptions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a <see cref="IAsyncEnumerable{T}"/> into an <see cref="ICollection{T}"/>.
|
||||
/// </summary>
|
||||
/// <param name="value">The <see cref="IAsyncEnumerable{T}"/> to read.</param>
|
||||
/// <returns>The <see cref="ICollection"/>.</returns>
|
||||
public Task<ICollection> ReadAsync(IAsyncEnumerable<object> value)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(value));
|
||||
}
|
||||
|
||||
var type = value.GetType();
|
||||
if (!_asyncEnumerableConverters.TryGetValue(type, out var result))
|
||||
{
|
||||
var enumerableType = ClosedGenericMatcher.ExtractGenericInterface(type, typeof(IAsyncEnumerable<>));
|
||||
Debug.Assert(enumerableType != null);
|
||||
|
||||
var enumeratedObjectType = enumerableType.GetGenericArguments()[0];
|
||||
|
||||
var converter = (ReaderFunc)Converter
|
||||
.MakeGenericMethod(enumeratedObjectType)
|
||||
.CreateDelegate(typeof(ReaderFunc), this);
|
||||
|
||||
_asyncEnumerableConverters.TryAdd(type, converter);
|
||||
result = converter;
|
||||
}
|
||||
|
||||
return result(value);
|
||||
}
|
||||
|
||||
private async Task<ICollection> ReadInternal<T>(IAsyncEnumerable<object> value)
|
||||
{
|
||||
var asyncEnumerable = (IAsyncEnumerable<T>)value;
|
||||
var result = new List<T>();
|
||||
var count = 0;
|
||||
|
||||
await foreach (var item in asyncEnumerable)
|
||||
{
|
||||
if (count++ >= _mvcOptions.MaxIAsyncEnumerableBufferLimit)
|
||||
{
|
||||
throw new InvalidOperationException(Resources.FormatObjectResultExecutor_MaxEnumerationExceeded(
|
||||
nameof(AsyncEnumerableReader),
|
||||
value.GetType()));
|
||||
}
|
||||
|
||||
result.Add(item);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ using System.Threading.Tasks;
|
|||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.Formatters;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||
{
|
||||
|
|
@ -18,16 +19,35 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
|||
/// </summary>
|
||||
public class ObjectResultExecutor : IActionResultExecutor<ObjectResult>
|
||||
{
|
||||
private readonly AsyncEnumerableReader _asyncEnumerableReader;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="ObjectResultExecutor"/>.
|
||||
/// </summary>
|
||||
/// <param name="formatterSelector">The <see cref="OutputFormatterSelector"/>.</param>
|
||||
/// <param name="writerFactory">The <see cref="IHttpResponseStreamWriterFactory"/>.</param>
|
||||
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
|
||||
[Obsolete("This constructor is obsolete and will be removed in a future release.")]
|
||||
public ObjectResultExecutor(
|
||||
OutputFormatterSelector formatterSelector,
|
||||
IHttpResponseStreamWriterFactory writerFactory,
|
||||
ILoggerFactory loggerFactory)
|
||||
: this(formatterSelector, writerFactory, loggerFactory, mvcOptions: null)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="ObjectResultExecutor"/>.
|
||||
/// </summary>
|
||||
/// <param name="formatterSelector">The <see cref="OutputFormatterSelector"/>.</param>
|
||||
/// <param name="writerFactory">The <see cref="IHttpResponseStreamWriterFactory"/>.</param>
|
||||
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
|
||||
/// <param name="mvcOptions">Accessor to <see cref="MvcOptions"/>.</param>
|
||||
public ObjectResultExecutor(
|
||||
OutputFormatterSelector formatterSelector,
|
||||
IHttpResponseStreamWriterFactory writerFactory,
|
||||
ILoggerFactory loggerFactory,
|
||||
IOptions<MvcOptions> mvcOptions)
|
||||
{
|
||||
if (formatterSelector == null)
|
||||
{
|
||||
|
|
@ -47,6 +67,8 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
|||
FormatterSelector = formatterSelector;
|
||||
WriterFactory = writerFactory.CreateWriter;
|
||||
Logger = loggerFactory.CreateLogger<ObjectResultExecutor>();
|
||||
var options = mvcOptions?.Value ?? throw new ArgumentNullException(nameof(mvcOptions));
|
||||
_asyncEnumerableReader = new AsyncEnumerableReader(options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -87,16 +109,37 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
|||
InferContentTypes(context, result);
|
||||
|
||||
var objectType = result.DeclaredType;
|
||||
|
||||
if (objectType == null || objectType == typeof(object))
|
||||
{
|
||||
objectType = result.Value?.GetType();
|
||||
}
|
||||
|
||||
var value = result.Value;
|
||||
|
||||
if (value is IAsyncEnumerable<object> asyncEnumerable)
|
||||
{
|
||||
return ExecuteAsyncEnumerable(context, result, asyncEnumerable);
|
||||
}
|
||||
|
||||
return ExecuteAsyncCore(context, result, objectType, value);
|
||||
}
|
||||
|
||||
private async Task ExecuteAsyncEnumerable(ActionContext context, ObjectResult result, IAsyncEnumerable<object> asyncEnumerable)
|
||||
{
|
||||
Log.BufferingAsyncEnumerable(Logger, asyncEnumerable);
|
||||
|
||||
var enumerated = await _asyncEnumerableReader.ReadAsync(asyncEnumerable);
|
||||
await ExecuteAsyncCore(context, result, enumerated.GetType(), enumerated);
|
||||
}
|
||||
|
||||
private Task ExecuteAsyncCore(ActionContext context, ObjectResult result, Type objectType, object value)
|
||||
{
|
||||
var formatterContext = new OutputFormatterWriteContext(
|
||||
context.HttpContext,
|
||||
WriterFactory,
|
||||
objectType,
|
||||
result.Value);
|
||||
value);
|
||||
|
||||
var selectedFormatter = FormatterSelector.SelectFormatter(
|
||||
formatterContext,
|
||||
|
|
@ -138,5 +181,21 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
|||
result.ContentTypes.Add("application/problem+xml");
|
||||
}
|
||||
}
|
||||
|
||||
private static class Log
|
||||
{
|
||||
private static readonly Action<ILogger, string, Exception> _bufferingAsyncEnumerable;
|
||||
|
||||
static Log()
|
||||
{
|
||||
_bufferingAsyncEnumerable = LoggerMessage.Define<string>(
|
||||
LogLevel.Debug,
|
||||
new EventId(1, "BufferingAsyncEnumerable"),
|
||||
"Buffering IAsyncEnumerable instance of type '{Type}'.");
|
||||
}
|
||||
|
||||
public static void BufferingAsyncEnumerable(ILogger logger, IAsyncEnumerable<object> asyncEnumerable)
|
||||
=> _bufferingAsyncEnumerable(logger, asyncEnumerable.GetType().FullName, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
// 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.Text.Json;
|
||||
|
|
@ -25,13 +26,16 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
|||
|
||||
private readonly JsonOptions _options;
|
||||
private readonly ILogger<SystemTextJsonResultExecutor> _logger;
|
||||
private readonly AsyncEnumerableReader _asyncEnumerableReader;
|
||||
|
||||
public SystemTextJsonResultExecutor(
|
||||
IOptions<JsonOptions> options,
|
||||
ILogger<SystemTextJsonResultExecutor> logger)
|
||||
ILogger<SystemTextJsonResultExecutor> logger,
|
||||
IOptions<MvcOptions> mvcOptions)
|
||||
{
|
||||
_options = options.Value;
|
||||
_logger = logger;
|
||||
_asyncEnumerableReader = new AsyncEnumerableReader(mvcOptions.Value);
|
||||
}
|
||||
|
||||
public async Task ExecuteAsync(ActionContext context, JsonResult result)
|
||||
|
|
@ -70,8 +74,15 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
|||
var writeStream = GetWriteStream(context.HttpContext, resolvedContentTypeEncoding);
|
||||
try
|
||||
{
|
||||
var type = result.Value?.GetType() ?? typeof(object);
|
||||
await JsonSerializer.WriteAsync(writeStream, result.Value, type, jsonSerializerOptions);
|
||||
var value = result.Value;
|
||||
if (value is IAsyncEnumerable<object> asyncEnumerable)
|
||||
{
|
||||
Log.BufferingAsyncEnumerable(_logger, asyncEnumerable);
|
||||
value = await _asyncEnumerableReader.ReadAsync(asyncEnumerable);
|
||||
}
|
||||
|
||||
var type = value?.GetType() ?? typeof(object);
|
||||
await JsonSerializer.WriteAsync(writeStream, value, type, jsonSerializerOptions);
|
||||
await writeStream.FlushAsync();
|
||||
}
|
||||
finally
|
||||
|
|
@ -123,11 +134,19 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
|||
new EventId(1, "JsonResultExecuting"),
|
||||
"Executing JsonResult, writing value of type '{Type}'.");
|
||||
|
||||
private static readonly Action<ILogger, string, Exception> _bufferingAsyncEnumerable = LoggerMessage.Define<string>(
|
||||
LogLevel.Debug,
|
||||
new EventId(2, "BufferingAsyncEnumerable"),
|
||||
"Buffering IAsyncEnumerable instance of type '{Type}'.");
|
||||
|
||||
public static void JsonResultExecuting(ILogger logger, object value)
|
||||
{
|
||||
var type = value == null ? "null" : value.GetType().FullName;
|
||||
_jsonResultExecuting(logger, type, null);
|
||||
}
|
||||
|
||||
public static void BufferingAsyncEnumerable(ILogger logger, IAsyncEnumerable<object> asyncEnumerable)
|
||||
=> _bufferingAsyncEnumerable(logger, asyncEnumerable.GetType().FullName, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -359,6 +359,19 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the most number of entries of an <see cref="IAsyncEnumerable{T}"/> that
|
||||
/// that <see cref="ObjectResultExecutor"/> will buffer.
|
||||
/// <para>
|
||||
/// When <see cref="ObjectResult.Value" /> is an instance of <see cref="IAsyncEnumerable{T}"/>,
|
||||
/// <see cref="ObjectResultExecutor"/> will eagerly read the enumeration and add to a synchronous collection
|
||||
/// prior to invoking the selected formatter.
|
||||
/// This property determines the most number of entries that the executor is allowed to buffer.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <value>Defaults to <c>8192</c>.</value>
|
||||
public int MaxIAsyncEnumerableBufferLimit { get; set; } = 8192;
|
||||
|
||||
IEnumerator<ICompatibilitySwitch> IEnumerable<ICompatibilitySwitch>.GetEnumerator() => _switches.GetEnumerator();
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() => _switches.GetEnumerator();
|
||||
|
|
|
|||
|
|
@ -1746,6 +1746,20 @@ namespace Microsoft.AspNetCore.Mvc.Core
|
|||
internal static string FormatProperty_MustBeInstanceOfType(object p0, object p1, object p2)
|
||||
=> string.Format(CultureInfo.CurrentCulture, GetString("Property_MustBeInstanceOfType"), p0, p1, p2);
|
||||
|
||||
/// <summary>
|
||||
/// '{0}' reached the configured maximum size of the buffer when enumerating a value of type `{1}'. This limit is in place to prevent infinite streams of `IAsyncEnumerable` from continuing indefinitely. If this is not a programming mistake, consider ways to reduce the collection size, or consider manually converting '{1}' into a list rather than increasing the limit.
|
||||
/// </summary>
|
||||
internal static string ObjectResultExecutor_MaxEnumerationExceeded
|
||||
{
|
||||
get => GetString("ObjectResultExecutor_MaxEnumerationExceeded");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// '{0}' reached the configured maximum size of the buffer when enumerating a value of type `{1}'. This limit is in place to prevent infinite streams of `IAsyncEnumerable` from continuing indefinitely. If this is not a programming mistake, consider ways to reduce the collection size, or consider manually converting '{1}' into a list rather than increasing the limit.
|
||||
/// </summary>
|
||||
internal static string FormatObjectResultExecutor_MaxEnumerationExceeded(object p0, object p1)
|
||||
=> string.Format(CultureInfo.CurrentCulture, GetString("ObjectResultExecutor_MaxEnumerationExceeded"), p0, p1);
|
||||
|
||||
private static string GetString(string name, params string[] formatterNames)
|
||||
{
|
||||
var value = _resourceManager.GetString(name);
|
||||
|
|
|
|||
|
|
@ -503,5 +503,8 @@
|
|||
</data>
|
||||
<data name="Property_MustBeInstanceOfType" xml:space="preserve">
|
||||
<value>Property '{0}.{1}' must be an instance of type '{2}'.</value>
|
||||
</data>
|
||||
</data>
|
||||
<data name="ObjectResultExecutor_MaxEnumerationExceeded" xml:space="preserve">
|
||||
<value>'{0}' reached the configured maximum size of the buffer when enumerating a value of type '{1}'. This limit is in place to prevent infinite streams of 'IAsyncEnumerable<>' from continuing indefinitely. If this is not a programming mistake, consider ways to reduce the collection size, or consider manually converting '{1}' into a list rather than increasing the limit.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
|
|
|||
|
|
@ -275,7 +275,8 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
services.AddSingleton<IActionResultExecutor<ObjectResult>>(new ObjectResultExecutor(
|
||||
new DefaultOutputFormatterSelector(options, NullLoggerFactory.Instance),
|
||||
new TestHttpResponseStreamWriterFactory(),
|
||||
NullLoggerFactory.Instance));
|
||||
NullLoggerFactory.Instance,
|
||||
options));
|
||||
|
||||
return services.BuildServiceProvider();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -183,7 +183,8 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
services.AddSingleton<IActionResultExecutor<ObjectResult>>(new ObjectResultExecutor(
|
||||
new DefaultOutputFormatterSelector(options, NullLoggerFactory.Instance),
|
||||
new TestHttpResponseStreamWriterFactory(),
|
||||
NullLoggerFactory.Instance));
|
||||
NullLoggerFactory.Instance,
|
||||
options));
|
||||
|
||||
return services.BuildServiceProvider();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -139,7 +139,8 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test
|
|||
services.AddSingleton<IActionResultExecutor<ObjectResult>>(new ObjectResultExecutor(
|
||||
new DefaultOutputFormatterSelector(options, NullLoggerFactory.Instance),
|
||||
new TestHttpResponseStreamWriterFactory(),
|
||||
NullLoggerFactory.Instance));
|
||||
NullLoggerFactory.Instance,
|
||||
options));
|
||||
|
||||
return services.BuildServiceProvider();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -97,7 +97,8 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
services.AddSingleton<IActionResultExecutor<ObjectResult>>(new ObjectResultExecutor(
|
||||
new DefaultOutputFormatterSelector(options, NullLoggerFactory.Instance),
|
||||
new TestHttpResponseStreamWriterFactory(),
|
||||
NullLoggerFactory.Instance));
|
||||
NullLoggerFactory.Instance,
|
||||
options));
|
||||
|
||||
return services.BuildServiceProvider();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -110,7 +110,8 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
services.AddSingleton<IActionResultExecutor<ObjectResult>>(new ObjectResultExecutor(
|
||||
new DefaultOutputFormatterSelector(options, NullLoggerFactory.Instance),
|
||||
new TestHttpResponseStreamWriterFactory(),
|
||||
NullLoggerFactory.Instance));
|
||||
NullLoggerFactory.Instance,
|
||||
options));
|
||||
|
||||
return services.BuildServiceProvider();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -98,7 +98,8 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
services.AddSingleton<IActionResultExecutor<ObjectResult>>(new ObjectResultExecutor(
|
||||
new DefaultOutputFormatterSelector(options, NullLoggerFactory.Instance),
|
||||
new TestHttpResponseStreamWriterFactory(),
|
||||
NullLoggerFactory.Instance));
|
||||
NullLoggerFactory.Instance,
|
||||
options));
|
||||
|
||||
return services.BuildServiceProvider();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,7 +75,8 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
services.AddSingleton<IActionResultExecutor<ObjectResult>>(new ObjectResultExecutor(
|
||||
new DefaultOutputFormatterSelector(options, NullLoggerFactory.Instance),
|
||||
new TestHttpResponseStreamWriterFactory(),
|
||||
NullLoggerFactory.Instance));
|
||||
NullLoggerFactory.Instance,
|
||||
options));
|
||||
|
||||
return services.BuildServiceProvider();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,7 +76,8 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
services.AddSingleton<IActionResultExecutor<ObjectResult>>(new ObjectResultExecutor(
|
||||
new DefaultOutputFormatterSelector(options, NullLoggerFactory.Instance),
|
||||
new TestHttpResponseStreamWriterFactory(),
|
||||
NullLoggerFactory.Instance));
|
||||
NullLoggerFactory.Instance,
|
||||
options));
|
||||
|
||||
return services.BuildServiceProvider();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,84 @@
|
|||
// 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.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||
{
|
||||
public class AsyncEnumerableReaderTest
|
||||
{
|
||||
[Fact]
|
||||
public async Task ReadAsync_ReadsIAsyncEnumerable()
|
||||
{
|
||||
// Arrange
|
||||
var options = new MvcOptions();
|
||||
var reader = new AsyncEnumerableReader(options);
|
||||
|
||||
// Act
|
||||
var result = await reader.ReadAsync(TestEnumerable());
|
||||
|
||||
// Assert
|
||||
var collection = Assert.IsAssignableFrom<ICollection<string>>(result);
|
||||
Assert.Equal(new[] { "0", "1", "2", }, collection);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReadAsync_ReadsIAsyncEnumerable_ImplementingMultipleAsyncEnumerableInterfaces()
|
||||
{
|
||||
// This test ensures the reader does not fail if you have a type that implements IAsyncEnumerable for multiple Ts
|
||||
// Arrange
|
||||
var options = new MvcOptions();
|
||||
var reader = new AsyncEnumerableReader(options);
|
||||
|
||||
// Act
|
||||
var result = await reader.ReadAsync(new MultiAsyncEnumerable());
|
||||
|
||||
// Assert
|
||||
var collection = Assert.IsAssignableFrom<ICollection<object>>(result);
|
||||
Assert.Equal(new[] { "0", "1", "2", }, collection);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReadAsync_ThrowsIfBufferimitIsReached()
|
||||
{
|
||||
// Arrange
|
||||
var enumerable = TestEnumerable(11);
|
||||
var expected = $"'AsyncEnumerableReader' reached the configured maximum size of the buffer when enumerating a value of type '{enumerable.GetType()}'. " +
|
||||
"This limit is in place to prevent infinite streams of 'IAsyncEnumerable<>' from continuing indefinitely. If this is not a programming mistake, " +
|
||||
$"consider ways to reduce the collection size, or consider manually converting '{enumerable.GetType()}' into a list rather than increasing the limit.";
|
||||
var options = new MvcOptions { MaxIAsyncEnumerableBufferLimit = 10 };
|
||||
var reader = new AsyncEnumerableReader(options);
|
||||
|
||||
// Act
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => reader.ReadAsync(enumerable));
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, ex.Message);
|
||||
}
|
||||
|
||||
public static async IAsyncEnumerable<string> TestEnumerable(int count = 3)
|
||||
{
|
||||
await Task.Yield();
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
yield return i.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
public class MultiAsyncEnumerable : IAsyncEnumerable<object>, IAsyncEnumerable<string>
|
||||
{
|
||||
public IAsyncEnumerator<string> GetAsyncEnumerator(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return TestEnumerable().GetAsyncEnumerator(cancellationToken);
|
||||
}
|
||||
|
||||
IAsyncEnumerator<object> IAsyncEnumerable<object>.GetAsyncEnumerator(CancellationToken cancellationToken)
|
||||
=> GetAsyncEnumerator(cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1567,7 +1567,8 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
|||
services.AddSingleton<IActionResultExecutor<ObjectResult>>(new ObjectResultExecutor(
|
||||
new DefaultOutputFormatterSelector(options, NullLoggerFactory.Instance),
|
||||
new TestHttpResponseStreamWriterFactory(),
|
||||
NullLoggerFactory.Instance));
|
||||
NullLoggerFactory.Instance,
|
||||
options));
|
||||
|
||||
httpContext.Response.Body = new MemoryStream();
|
||||
httpContext.RequestServices = services.BuildServiceProvider();
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
// 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.Linq;
|
||||
using System.Text;
|
||||
|
|
@ -310,6 +311,24 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
|||
Assert.StartsWith("Property 'JsonResult.SerializerSettings' must be an instance of type", ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_SerializesAsyncEnumerables()
|
||||
{
|
||||
// Arrange
|
||||
var expected = Encoding.UTF8.GetBytes(JsonSerializer.ToString(new[] { "Hello", "world" }));
|
||||
|
||||
var context = GetActionContext();
|
||||
var result = new JsonResult(TestAsyncEnumerable());
|
||||
var executor = CreateExecutor();
|
||||
|
||||
// Act
|
||||
await executor.ExecuteAsync(context, result);
|
||||
|
||||
// Assert
|
||||
var written = GetWrittenBytes(context.HttpContext);
|
||||
Assert.Equal(expected, written);
|
||||
}
|
||||
|
||||
protected IActionResultExecutor<JsonResult> CreateExecutor() => CreateExecutor(NullLoggerFactory.Instance);
|
||||
|
||||
protected abstract IActionResultExecutor<JsonResult> CreateExecutor(ILoggerFactory loggerFactory);
|
||||
|
|
@ -354,5 +373,12 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
|||
{
|
||||
public string Property { get; set; }
|
||||
}
|
||||
|
||||
private async IAsyncEnumerable<string> TestAsyncEnumerable()
|
||||
{
|
||||
await Task.Yield();
|
||||
yield return "Hello";
|
||||
yield return "world";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
// 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.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
|
@ -209,8 +210,8 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
|||
public async Task ExecuteAsync_FallsBackOnFormattersInOptions()
|
||||
{
|
||||
// Arrange
|
||||
var options = Options.Create(new MvcOptions());
|
||||
options.Value.OutputFormatters.Add(new TestJsonOutputFormatter());
|
||||
var options = new MvcOptions();
|
||||
options.OutputFormatters.Add(new TestJsonOutputFormatter());
|
||||
|
||||
var executor = CreateExecutor(options: options);
|
||||
|
||||
|
|
@ -300,8 +301,8 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
|||
string expectedContentType)
|
||||
{
|
||||
// Arrange
|
||||
var options = Options.Create(new MvcOptions());
|
||||
options.Value.RespectBrowserAcceptHeader = false;
|
||||
var options = new MvcOptions();
|
||||
options.RespectBrowserAcceptHeader = false;
|
||||
|
||||
var executor = CreateExecutor(options: options);
|
||||
|
||||
|
|
@ -337,8 +338,8 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
|||
string expectedContentType)
|
||||
{
|
||||
// Arrange
|
||||
var options = Options.Create(new MvcOptions());
|
||||
options.Value.RespectBrowserAcceptHeader = true;
|
||||
var options = new MvcOptions();
|
||||
options.RespectBrowserAcceptHeader = true;
|
||||
|
||||
var executor = CreateExecutor(options: options);
|
||||
|
||||
|
|
@ -360,6 +361,106 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
|||
MediaTypeAssert.Equal(expectedContentType, responseContentType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ObjectResult_ReadsAsyncEnumerables()
|
||||
{
|
||||
// Arrange
|
||||
var executor = CreateExecutor();
|
||||
var result = new ObjectResult(AsyncEnumerable());
|
||||
var formatter = new TestJsonOutputFormatter();
|
||||
result.Formatters.Add(formatter);
|
||||
|
||||
var actionContext = new ActionContext()
|
||||
{
|
||||
HttpContext = GetHttpContext(),
|
||||
};
|
||||
|
||||
// Act
|
||||
await executor.ExecuteAsync(actionContext, result);
|
||||
|
||||
// Assert
|
||||
var formatterContext = formatter.LastOutputFormatterContext;
|
||||
Assert.Equal(typeof(List<string>), formatterContext.ObjectType);
|
||||
var value = Assert.IsType<List<string>>(formatterContext.Object);
|
||||
Assert.Equal(new[] { "Hello 0", "Hello 1", "Hello 2", "Hello 3", }, value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ObjectResult_Throws_IfEnumerableThrows()
|
||||
{
|
||||
// Arrange
|
||||
var executor = CreateExecutor();
|
||||
var result = new ObjectResult(AsyncEnumerable(throwError: true));
|
||||
var formatter = new TestJsonOutputFormatter();
|
||||
result.Formatters.Add(formatter);
|
||||
|
||||
var actionContext = new ActionContext()
|
||||
{
|
||||
HttpContext = GetHttpContext(),
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<TimeZoneNotFoundException>(() => executor.ExecuteAsync(actionContext, result));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ObjectResult_AsyncEnumeration_AtLimit()
|
||||
{
|
||||
// Arrange
|
||||
var count = 24;
|
||||
var executor = CreateExecutor(options: new MvcOptions { MaxIAsyncEnumerableBufferLimit = count });
|
||||
var result = new ObjectResult(AsyncEnumerable(count: count));
|
||||
var formatter = new TestJsonOutputFormatter();
|
||||
result.Formatters.Add(formatter);
|
||||
|
||||
var actionContext = new ActionContext()
|
||||
{
|
||||
HttpContext = GetHttpContext(),
|
||||
};
|
||||
|
||||
// Act
|
||||
await executor.ExecuteAsync(actionContext, result);
|
||||
|
||||
// Assert
|
||||
var formatterContext = formatter.LastOutputFormatterContext;
|
||||
var value = Assert.IsType<List<string>>(formatterContext.Object);
|
||||
Assert.Equal(24, value.Count);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(25)]
|
||||
[InlineData(1024)]
|
||||
public async Task ObjectResult_Throws_IfEnumerationExceedsLimit(int count)
|
||||
{
|
||||
// Arrange
|
||||
var executor = CreateExecutor(options: new MvcOptions { MaxIAsyncEnumerableBufferLimit = 24 });
|
||||
var result = new ObjectResult(AsyncEnumerable(count: count));
|
||||
var formatter = new TestJsonOutputFormatter();
|
||||
result.Formatters.Add(formatter);
|
||||
|
||||
var actionContext = new ActionContext()
|
||||
{
|
||||
HttpContext = GetHttpContext(),
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => executor.ExecuteAsync(actionContext, result));
|
||||
}
|
||||
|
||||
private static async IAsyncEnumerable<string> AsyncEnumerable(int count = 4, bool throwError = false)
|
||||
{
|
||||
await Task.Yield();
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
yield return $"Hello {i}";
|
||||
}
|
||||
|
||||
if (throwError)
|
||||
{
|
||||
throw new TimeZoneNotFoundException();
|
||||
}
|
||||
}
|
||||
|
||||
private static IServiceCollection CreateServices()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
|
|
@ -379,10 +480,12 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
|||
return httpContext;
|
||||
}
|
||||
|
||||
private static ObjectResultExecutor CreateExecutor(IOptions<MvcOptions> options = null)
|
||||
private static ObjectResultExecutor CreateExecutor(MvcOptions options = null)
|
||||
{
|
||||
var selector = new DefaultOutputFormatterSelector(options ?? Options.Create<MvcOptions>(new MvcOptions()), NullLoggerFactory.Instance);
|
||||
return new ObjectResultExecutor(selector, new TestHttpResponseStreamWriterFactory(), NullLoggerFactory.Instance);
|
||||
options ??= new MvcOptions();
|
||||
var optionsAccessor = Options.Create(options);
|
||||
var selector = new DefaultOutputFormatterSelector(optionsAccessor, NullLoggerFactory.Instance);
|
||||
return new ObjectResultExecutor(selector, new TestHttpResponseStreamWriterFactory(), NullLoggerFactory.Instance, optionsAccessor);
|
||||
}
|
||||
|
||||
private class CannotWriteFormatter : IOutputFormatter
|
||||
|
|
@ -409,8 +512,11 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
|||
SupportedEncodings.Add(Encoding.UTF8);
|
||||
}
|
||||
|
||||
public OutputFormatterWriteContext LastOutputFormatterContext { get; private set; }
|
||||
|
||||
public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
|
||||
{
|
||||
LastOutputFormatterContext = context;
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,10 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
|||
{
|
||||
protected override IActionResultExecutor<JsonResult> CreateExecutor(ILoggerFactory loggerFactory)
|
||||
{
|
||||
return new SystemTextJsonResultExecutor(Options.Create(new JsonOptions()), loggerFactory.CreateLogger<SystemTextJsonResultExecutor>());
|
||||
return new SystemTextJsonResultExecutor(
|
||||
Options.Create(new JsonOptions()),
|
||||
loggerFactory.CreateLogger<SystemTextJsonResultExecutor>(),
|
||||
Options.Create(new MvcOptions()));
|
||||
}
|
||||
|
||||
protected override object GetIndentedSettings()
|
||||
|
|
|
|||
|
|
@ -97,10 +97,12 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
private static IServiceProvider CreateServices()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
var options = Options.Create(new MvcOptions());
|
||||
services.AddSingleton<IActionResultExecutor<ObjectResult>>(new ObjectResultExecutor(
|
||||
new DefaultOutputFormatterSelector(Options.Create(new MvcOptions()), NullLoggerFactory.Instance),
|
||||
new DefaultOutputFormatterSelector(options, NullLoggerFactory.Instance),
|
||||
new TestHttpResponseStreamWriterFactory(),
|
||||
NullLoggerFactory.Instance));
|
||||
NullLoggerFactory.Instance,
|
||||
options));
|
||||
services.AddSingleton<ILoggerFactory>(NullLoggerFactory.Instance);
|
||||
|
||||
return services.BuildServiceProvider();
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<PackageTags>aspnetcore;aspnetcoremvc;json</PackageTags>
|
||||
<IsShippingPackage>true</IsShippingPackage>
|
||||
<DefineConstants>$(DefineConstants);JSONNET</DefineConstants>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
@ -21,5 +22,6 @@
|
|||
|
||||
<ItemGroup>
|
||||
<Compile Include="..\..\Mvc.Core\src\Formatters\ResponseContentTypeHelper.cs" />
|
||||
<Compile Include="..\..\Mvc.Core\src\Infrastructure\AsyncEnumerableReader.cs" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson
|
|||
{
|
||||
private static readonly Action<ILogger, Exception> _jsonInputFormatterException;
|
||||
|
||||
private static readonly Action<ILogger, string, Exception> _jsonResultExecuting;
|
||||
|
||||
static NewtonsoftJsonLoggerExtensions()
|
||||
{
|
||||
|
|
@ -18,22 +17,11 @@ namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson
|
|||
LogLevel.Debug,
|
||||
new EventId(1, "JsonInputException"),
|
||||
"JSON input formatter threw an exception.");
|
||||
|
||||
_jsonResultExecuting = LoggerMessage.Define<string>(
|
||||
LogLevel.Information,
|
||||
new EventId(1, "JsonResultExecuting"),
|
||||
"Executing JsonResult, writing value of type '{Type}'.");
|
||||
}
|
||||
|
||||
public static void JsonInputException(this ILogger logger, Exception exception)
|
||||
{
|
||||
_jsonInputFormatterException(logger, exception);
|
||||
}
|
||||
|
||||
public static void JsonResultExecuting(this ILogger logger, object value)
|
||||
{
|
||||
var type = value == null ? "null" : value.GetType().FullName;
|
||||
_jsonResultExecuting(logger, type, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,10 +3,9 @@
|
|||
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.IO;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.Formatters;
|
||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
|
|
@ -32,6 +31,7 @@ namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson
|
|||
private readonly MvcOptions _mvcOptions;
|
||||
private readonly MvcNewtonsoftJsonOptions _jsonOptions;
|
||||
private readonly IArrayPool<char> _charPool;
|
||||
private readonly AsyncEnumerableReader _asyncEnumerableReader;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="NewtonsoftJsonResultExecutor"/>.
|
||||
|
|
@ -73,6 +73,7 @@ namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson
|
|||
_mvcOptions = mvcOptions?.Value ?? throw new ArgumentNullException(nameof(mvcOptions));
|
||||
_jsonOptions = jsonOptions.Value;
|
||||
_charPool = new JsonArrayPool<char>(charPool);
|
||||
_asyncEnumerableReader = new AsyncEnumerableReader(_mvcOptions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -111,7 +112,7 @@ namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson
|
|||
response.StatusCode = result.StatusCode.Value;
|
||||
}
|
||||
|
||||
_logger.JsonResultExecuting(result.Value);
|
||||
Log.JsonResultExecuting(_logger, result.Value);
|
||||
|
||||
var responseStream = response.Body;
|
||||
FileBufferingWriteStream fileBufferingWriteStream = null;
|
||||
|
|
@ -131,7 +132,14 @@ namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson
|
|||
jsonWriter.AutoCompleteOnClose = false;
|
||||
|
||||
var jsonSerializer = JsonSerializer.Create(jsonSerializerSettings);
|
||||
jsonSerializer.Serialize(jsonWriter, result.Value);
|
||||
var value = result.Value;
|
||||
if (result.Value is IAsyncEnumerable<object> asyncEnumerable)
|
||||
{
|
||||
Log.BufferingAsyncEnumerable(_logger, asyncEnumerable);
|
||||
value = await _asyncEnumerableReader.ReadAsync(asyncEnumerable);
|
||||
}
|
||||
|
||||
jsonSerializer.Serialize(jsonWriter, value);
|
||||
}
|
||||
|
||||
if (fileBufferingWriteStream != null)
|
||||
|
|
@ -168,5 +176,33 @@ namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson
|
|||
return settingsFromResult;
|
||||
}
|
||||
}
|
||||
|
||||
private static class Log
|
||||
{
|
||||
private static readonly Action<ILogger, string, Exception> _jsonResultExecuting;
|
||||
private static readonly Action<ILogger, string, Exception> _bufferingAsyncEnumerable;
|
||||
|
||||
static Log()
|
||||
{
|
||||
_jsonResultExecuting = LoggerMessage.Define<string>(
|
||||
LogLevel.Information,
|
||||
new EventId(1, "JsonResultExecuting"),
|
||||
"Executing JsonResult, writing value of type '{Type}'.");
|
||||
|
||||
_bufferingAsyncEnumerable = LoggerMessage.Define<string>(
|
||||
LogLevel.Debug,
|
||||
new EventId(1, "BufferingAsyncEnumerable"),
|
||||
"Buffering IAsyncEnumerable instance of type '{Type}'.");
|
||||
}
|
||||
|
||||
public static void JsonResultExecuting(ILogger logger, object value)
|
||||
{
|
||||
var type = value == null ? "null" : value.GetType().FullName;
|
||||
_jsonResultExecuting(logger, type, null);
|
||||
}
|
||||
|
||||
public static void BufferingAsyncEnumerable(ILogger logger, IAsyncEnumerable<object> asyncEnumerable)
|
||||
=> _bufferingAsyncEnumerable(logger, asyncEnumerable.GetType().FullName, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -108,6 +108,20 @@ namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson
|
|||
internal static string FormatTempData_CannotSerializeType(object p0, object p1)
|
||||
=> string.Format(CultureInfo.CurrentCulture, GetString("TempData_CannotSerializeType"), p0, p1);
|
||||
|
||||
/// <summary>
|
||||
/// '{0}' reached the configured maximum size of the buffer when enumerating a value of type `{1}'. This limit is in place to prevent infinite streams of `IAsyncEnumerable` from continuing indefinitely. If this is not a programming mistake, consider ways to reduce the collection size, or consider manually converting '{1}' into a list rather than increasing the limit.
|
||||
/// </summary>
|
||||
internal static string ObjectResultExecutor_MaxEnumerationExceeded
|
||||
{
|
||||
get => GetString("ObjectResultExecutor_MaxEnumerationExceeded");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// '{0}' reached the configured maximum size of the buffer when enumerating a value of type `{1}'. This limit is in place to prevent infinite streams of `IAsyncEnumerable` from continuing indefinitely. If this is not a programming mistake, consider ways to reduce the collection size, or consider manually converting '{1}' into a list rather than increasing the limit.
|
||||
/// </summary>
|
||||
internal static string FormatObjectResultExecutor_MaxEnumerationExceeded(object p0, object p1)
|
||||
=> string.Format(CultureInfo.CurrentCulture, GetString("ObjectResultExecutor_MaxEnumerationExceeded"), p0, p1);
|
||||
|
||||
private static string GetString(string name, params string[] formatterNames)
|
||||
{
|
||||
var value = _resourceManager.GetString(name);
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
|
||||
Example:
|
||||
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
|
|
@ -26,36 +26,36 @@
|
|||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
|
|
@ -126,6 +126,9 @@
|
|||
<data name="JsonHelperMustBeAnInstanceOfNewtonsoftJson" xml:space="preserve">
|
||||
<value>Parameter '{0}' must be an instance of {1} provided by the '{2}' package. Configure the correct instance using '{3}' in your startup.</value>
|
||||
</data>
|
||||
<data name="ObjectResultExecutor_MaxEnumerationExceeded" xml:space="preserve">
|
||||
<value>'{0}' reached the configured maximum size of the buffer when enumerating a value of type '{1}'. This limit is in place to prevent infinite streams of 'IAsyncEnumerable<>' from continuing indefinitely. If this is not a programming mistake, consider ways to reduce the collection size, or consider manually converting '{1}' into a list rather than increasing the limit.</value>
|
||||
</data>
|
||||
<data name="Property_MustBeInstanceOfType" xml:space="preserve">
|
||||
<value>Property '{0}.{1}' must be an instance of type '{2}'.</value>
|
||||
</data>
|
||||
|
|
@ -138,4 +141,4 @@
|
|||
<data name="TempData_CannotSerializeType" xml:space="preserve">
|
||||
<value>The '{0}' cannot serialize an object of type '{1}'.</value>
|
||||
</data>
|
||||
</root>
|
||||
</root>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="..\..\Mvc.Core\test\Infrastructure\AsyncEnumerableReaderTest.cs" />
|
||||
<Compile Include="..\..\Mvc.Core\test\Formatters\JsonInputFormatterTestBase.cs" />
|
||||
<Compile Include="..\..\Mvc.Core\test\Formatters\JsonOutputFormatterTestBase.cs" />
|
||||
<Compile Include="..\..\Mvc.Core\test\Infrastructure\JsonResultExecutorTestBase.cs" />
|
||||
|
|
|
|||
|
|
@ -0,0 +1,97 @@
|
|||
// 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.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml.Linq;
|
||||
using FormatterWebSite;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
||||
{
|
||||
public class AsyncEnumerableTestBase : IClassFixture<MvcTestFixture<StartupWithJsonFormatter>>
|
||||
{
|
||||
public AsyncEnumerableTestBase(MvcTestFixture<StartupWithJsonFormatter> fixture)
|
||||
{
|
||||
Client = fixture.CreateDefaultClient();
|
||||
}
|
||||
|
||||
public HttpClient Client { get; }
|
||||
|
||||
[Fact]
|
||||
public Task AsyncEnumerableReturnedWorks() => AsyncEnumerableWorks();
|
||||
|
||||
[Fact]
|
||||
public Task AsyncEnumerableWrappedInTask() => AsyncEnumerableWorks();
|
||||
|
||||
private async Task AsyncEnumerableWorks()
|
||||
{
|
||||
// Act
|
||||
var response = await Client.GetAsync("asyncenumerable/getallprojects");
|
||||
|
||||
// Assert
|
||||
await response.AssertStatusCodeAsync(HttpStatusCode.OK);
|
||||
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
|
||||
// Some sanity tests to verify things serialized correctly.
|
||||
var projects = JsonSerializer.Parse<List<Project>>(content, TestJsonSerializerOptionsProvider.Options);
|
||||
Assert.Equal(10, projects.Count);
|
||||
Assert.Equal("Project0", projects[0].Name);
|
||||
Assert.Equal("Project9", projects[9].Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AsyncEnumerableExceptionsAreThrown()
|
||||
{
|
||||
// Act
|
||||
var response = await Client.GetAsync("asyncenumerable/GetAllProjectsWithError");
|
||||
|
||||
// Assert
|
||||
await response.AssertStatusCodeAsync(HttpStatusCode.InternalServerError);
|
||||
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
|
||||
// Verify that the exception shows up in the callstack
|
||||
Assert.Contains(nameof(InvalidTimeZoneException), content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AsyncEnumerableWithXmlFormatterWorks()
|
||||
{
|
||||
// Arrange
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, "asyncenumerable/getallprojects");
|
||||
request.Headers.Add("Accept", "application/xml");
|
||||
|
||||
// Act
|
||||
var response = await Client.SendAsync(request);
|
||||
|
||||
// Assert
|
||||
await response.AssertStatusCodeAsync(HttpStatusCode.OK);
|
||||
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
|
||||
// Some sanity tests to verify things serialized correctly.
|
||||
var xml = XDocument.Parse(content);
|
||||
var @namespace = xml.Root.Name.NamespaceName;
|
||||
var projects = xml.Root.Elements(XName.Get("Project", @namespace));
|
||||
Assert.Equal(10, projects.Count());
|
||||
|
||||
Assert.Equal("Project0", GetName(projects.ElementAt(0)));
|
||||
Assert.Equal("Project9", GetName(projects.ElementAt(9)));
|
||||
|
||||
string GetName(XElement element)
|
||||
{
|
||||
var name = element.Element(XName.Get("Name", @namespace));
|
||||
Assert.NotNull(name);
|
||||
|
||||
return name.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -261,9 +261,13 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
Content = new StringContent(@"{ ""Id"": ""S-1-5-21-1004336348-1177238915-682003330-512"" }", Encoding.UTF8, "application/json"),
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => Client.SendAsync(requestMessage));
|
||||
Assert.Equal(expected, ex.Message);
|
||||
// Act
|
||||
var response = await Client.SendAsync(requestMessage);
|
||||
|
||||
// Assert
|
||||
await response.AssertStatusCodeAsync(HttpStatusCode.InternalServerError);
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
Assert.Contains(expected, content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -356,4 +360,4 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
// 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.Text.Json;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
||||
{
|
||||
internal static class TestJsonSerializerOptionsProvider
|
||||
{
|
||||
public static JsonSerializerOptions Options = new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
// 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.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace FormatterWebSite.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[Route("{controller}/{action}")]
|
||||
public class AsyncEnumerableController : ControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
public IAsyncEnumerable<Project> GetAllProjects()
|
||||
=> GetAllProjectsCore();
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IAsyncEnumerable<Project>> GetAllProjectsAsTask()
|
||||
{
|
||||
await Task.Yield();
|
||||
return GetAllProjectsCore();
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public IAsyncEnumerable<Project> GetAllProjectsWithError()
|
||||
=> GetAllProjectsCore(true);
|
||||
|
||||
public async IAsyncEnumerable<Project> GetAllProjectsCore(bool throwError = false)
|
||||
{
|
||||
await Task.Delay(5);
|
||||
for (var i = 0; i < 10; i++)
|
||||
{
|
||||
if (throwError && i == 5)
|
||||
{
|
||||
throw new InvalidTimeZoneException();
|
||||
}
|
||||
|
||||
yield return new Project
|
||||
{
|
||||
Id = i,
|
||||
Name = $"Project{i}",
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -26,6 +26,8 @@ namespace FormatterWebSite
|
|||
|
||||
public void Configure(IApplicationBuilder app)
|
||||
{
|
||||
app.UseDeveloperExceptionPage();
|
||||
|
||||
app.UseRouting();
|
||||
app.UseEndpoints(endpoints =>
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue