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:
Pranav K 2019-06-25 10:53:43 -07:00 committed by GitHub
parent 550710f304
commit b10c8a6fc3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 727 additions and 74 deletions

View File

@ -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.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.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 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 MaxModelBindingCollectionSize { get { throw null; } set { } }
public int MaxModelBindingRecursionDepth { get { throw null; } set { } } public int MaxModelBindingRecursionDepth { get { throw null; } set { } }
public int MaxModelValidationErrors { 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> 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) { }
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.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 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; } } protected System.Func<System.IO.Stream, System.Text.Encoding, System.IO.TextWriter> WriterFactory { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }

View File

@ -0,0 +1,102 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.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;
}
}
}

View File

@ -10,6 +10,7 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Mvc.Infrastructure namespace Microsoft.AspNetCore.Mvc.Infrastructure
{ {
@ -18,16 +19,35 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
/// </summary> /// </summary>
public class ObjectResultExecutor : IActionResultExecutor<ObjectResult> public class ObjectResultExecutor : IActionResultExecutor<ObjectResult>
{ {
private readonly AsyncEnumerableReader _asyncEnumerableReader;
/// <summary> /// <summary>
/// Creates a new <see cref="ObjectResultExecutor"/>. /// Creates a new <see cref="ObjectResultExecutor"/>.
/// </summary> /// </summary>
/// <param name="formatterSelector">The <see cref="OutputFormatterSelector"/>.</param> /// <param name="formatterSelector">The <see cref="OutputFormatterSelector"/>.</param>
/// <param name="writerFactory">The <see cref="IHttpResponseStreamWriterFactory"/>.</param> /// <param name="writerFactory">The <see cref="IHttpResponseStreamWriterFactory"/>.</param>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param> /// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
[Obsolete("This constructor is obsolete and will be removed in a future release.")]
public ObjectResultExecutor( public ObjectResultExecutor(
OutputFormatterSelector formatterSelector, OutputFormatterSelector formatterSelector,
IHttpResponseStreamWriterFactory writerFactory, IHttpResponseStreamWriterFactory writerFactory,
ILoggerFactory loggerFactory) 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) if (formatterSelector == null)
{ {
@ -47,6 +67,8 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
FormatterSelector = formatterSelector; FormatterSelector = formatterSelector;
WriterFactory = writerFactory.CreateWriter; WriterFactory = writerFactory.CreateWriter;
Logger = loggerFactory.CreateLogger<ObjectResultExecutor>(); Logger = loggerFactory.CreateLogger<ObjectResultExecutor>();
var options = mvcOptions?.Value ?? throw new ArgumentNullException(nameof(mvcOptions));
_asyncEnumerableReader = new AsyncEnumerableReader(options);
} }
/// <summary> /// <summary>
@ -87,16 +109,37 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
InferContentTypes(context, result); InferContentTypes(context, result);
var objectType = result.DeclaredType; var objectType = result.DeclaredType;
if (objectType == null || objectType == typeof(object)) if (objectType == null || objectType == typeof(object))
{ {
objectType = result.Value?.GetType(); 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( var formatterContext = new OutputFormatterWriteContext(
context.HttpContext, context.HttpContext,
WriterFactory, WriterFactory,
objectType, objectType,
result.Value); value);
var selectedFormatter = FormatterSelector.SelectFormatter( var selectedFormatter = FormatterSelector.SelectFormatter(
formatterContext, formatterContext,
@ -138,5 +181,21 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
result.ContentTypes.Add("application/problem+xml"); 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);
}
} }
} }

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
@ -25,13 +26,16 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
private readonly JsonOptions _options; private readonly JsonOptions _options;
private readonly ILogger<SystemTextJsonResultExecutor> _logger; private readonly ILogger<SystemTextJsonResultExecutor> _logger;
private readonly AsyncEnumerableReader _asyncEnumerableReader;
public SystemTextJsonResultExecutor( public SystemTextJsonResultExecutor(
IOptions<JsonOptions> options, IOptions<JsonOptions> options,
ILogger<SystemTextJsonResultExecutor> logger) ILogger<SystemTextJsonResultExecutor> logger,
IOptions<MvcOptions> mvcOptions)
{ {
_options = options.Value; _options = options.Value;
_logger = logger; _logger = logger;
_asyncEnumerableReader = new AsyncEnumerableReader(mvcOptions.Value);
} }
public async Task ExecuteAsync(ActionContext context, JsonResult result) public async Task ExecuteAsync(ActionContext context, JsonResult result)
@ -70,8 +74,15 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
var writeStream = GetWriteStream(context.HttpContext, resolvedContentTypeEncoding); var writeStream = GetWriteStream(context.HttpContext, resolvedContentTypeEncoding);
try try
{ {
var type = result.Value?.GetType() ?? typeof(object); var value = result.Value;
await JsonSerializer.WriteAsync(writeStream, result.Value, type, jsonSerializerOptions); 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(); await writeStream.FlushAsync();
} }
finally finally
@ -123,11 +134,19 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
new EventId(1, "JsonResultExecuting"), new EventId(1, "JsonResultExecuting"),
"Executing JsonResult, writing value of type '{Type}'."); "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) public static void JsonResultExecuting(ILogger logger, object value)
{ {
var type = value == null ? "null" : value.GetType().FullName; var type = value == null ? "null" : value.GetType().FullName;
_jsonResultExecuting(logger, type, null); _jsonResultExecuting(logger, type, null);
} }
public static void BufferingAsyncEnumerable(ILogger logger, IAsyncEnumerable<object> asyncEnumerable)
=> _bufferingAsyncEnumerable(logger, asyncEnumerable.GetType().FullName, null);
} }
} }
} }

View File

@ -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<ICompatibilitySwitch> IEnumerable<ICompatibilitySwitch>.GetEnumerator() => _switches.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => _switches.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => _switches.GetEnumerator();

View File

@ -1746,6 +1746,20 @@ namespace Microsoft.AspNetCore.Mvc.Core
internal static string FormatProperty_MustBeInstanceOfType(object p0, object p1, object p2) internal static string FormatProperty_MustBeInstanceOfType(object p0, object p1, object p2)
=> string.Format(CultureInfo.CurrentCulture, GetString("Property_MustBeInstanceOfType"), p0, p1, 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) private static string GetString(string name, params string[] formatterNames)
{ {
var value = _resourceManager.GetString(name); var value = _resourceManager.GetString(name);

View File

@ -503,5 +503,8 @@
</data> </data>
<data name="Property_MustBeInstanceOfType" xml:space="preserve"> <data name="Property_MustBeInstanceOfType" xml:space="preserve">
<value>Property '{0}.{1}' must be an instance of type '{2}'.</value> <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&lt;&gt;' 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> </root>

View File

@ -275,7 +275,8 @@ namespace Microsoft.AspNetCore.Mvc
services.AddSingleton<IActionResultExecutor<ObjectResult>>(new ObjectResultExecutor( services.AddSingleton<IActionResultExecutor<ObjectResult>>(new ObjectResultExecutor(
new DefaultOutputFormatterSelector(options, NullLoggerFactory.Instance), new DefaultOutputFormatterSelector(options, NullLoggerFactory.Instance),
new TestHttpResponseStreamWriterFactory(), new TestHttpResponseStreamWriterFactory(),
NullLoggerFactory.Instance)); NullLoggerFactory.Instance,
options));
return services.BuildServiceProvider(); return services.BuildServiceProvider();
} }

View File

@ -183,7 +183,8 @@ namespace Microsoft.AspNetCore.Mvc
services.AddSingleton<IActionResultExecutor<ObjectResult>>(new ObjectResultExecutor( services.AddSingleton<IActionResultExecutor<ObjectResult>>(new ObjectResultExecutor(
new DefaultOutputFormatterSelector(options, NullLoggerFactory.Instance), new DefaultOutputFormatterSelector(options, NullLoggerFactory.Instance),
new TestHttpResponseStreamWriterFactory(), new TestHttpResponseStreamWriterFactory(),
NullLoggerFactory.Instance)); NullLoggerFactory.Instance,
options));
return services.BuildServiceProvider(); return services.BuildServiceProvider();
} }

View File

@ -139,7 +139,8 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test
services.AddSingleton<IActionResultExecutor<ObjectResult>>(new ObjectResultExecutor( services.AddSingleton<IActionResultExecutor<ObjectResult>>(new ObjectResultExecutor(
new DefaultOutputFormatterSelector(options, NullLoggerFactory.Instance), new DefaultOutputFormatterSelector(options, NullLoggerFactory.Instance),
new TestHttpResponseStreamWriterFactory(), new TestHttpResponseStreamWriterFactory(),
NullLoggerFactory.Instance)); NullLoggerFactory.Instance,
options));
return services.BuildServiceProvider(); return services.BuildServiceProvider();
} }

View File

@ -97,7 +97,8 @@ namespace Microsoft.AspNetCore.Mvc
services.AddSingleton<IActionResultExecutor<ObjectResult>>(new ObjectResultExecutor( services.AddSingleton<IActionResultExecutor<ObjectResult>>(new ObjectResultExecutor(
new DefaultOutputFormatterSelector(options, NullLoggerFactory.Instance), new DefaultOutputFormatterSelector(options, NullLoggerFactory.Instance),
new TestHttpResponseStreamWriterFactory(), new TestHttpResponseStreamWriterFactory(),
NullLoggerFactory.Instance)); NullLoggerFactory.Instance,
options));
return services.BuildServiceProvider(); return services.BuildServiceProvider();
} }

View File

@ -110,7 +110,8 @@ namespace Microsoft.AspNetCore.Mvc
services.AddSingleton<IActionResultExecutor<ObjectResult>>(new ObjectResultExecutor( services.AddSingleton<IActionResultExecutor<ObjectResult>>(new ObjectResultExecutor(
new DefaultOutputFormatterSelector(options, NullLoggerFactory.Instance), new DefaultOutputFormatterSelector(options, NullLoggerFactory.Instance),
new TestHttpResponseStreamWriterFactory(), new TestHttpResponseStreamWriterFactory(),
NullLoggerFactory.Instance)); NullLoggerFactory.Instance,
options));
return services.BuildServiceProvider(); return services.BuildServiceProvider();
} }

View File

@ -98,7 +98,8 @@ namespace Microsoft.AspNetCore.Mvc
services.AddSingleton<IActionResultExecutor<ObjectResult>>(new ObjectResultExecutor( services.AddSingleton<IActionResultExecutor<ObjectResult>>(new ObjectResultExecutor(
new DefaultOutputFormatterSelector(options, NullLoggerFactory.Instance), new DefaultOutputFormatterSelector(options, NullLoggerFactory.Instance),
new TestHttpResponseStreamWriterFactory(), new TestHttpResponseStreamWriterFactory(),
NullLoggerFactory.Instance)); NullLoggerFactory.Instance,
options));
return services.BuildServiceProvider(); return services.BuildServiceProvider();
} }

View File

@ -75,7 +75,8 @@ namespace Microsoft.AspNetCore.Mvc
services.AddSingleton<IActionResultExecutor<ObjectResult>>(new ObjectResultExecutor( services.AddSingleton<IActionResultExecutor<ObjectResult>>(new ObjectResultExecutor(
new DefaultOutputFormatterSelector(options, NullLoggerFactory.Instance), new DefaultOutputFormatterSelector(options, NullLoggerFactory.Instance),
new TestHttpResponseStreamWriterFactory(), new TestHttpResponseStreamWriterFactory(),
NullLoggerFactory.Instance)); NullLoggerFactory.Instance,
options));
return services.BuildServiceProvider(); return services.BuildServiceProvider();
} }

View File

@ -76,7 +76,8 @@ namespace Microsoft.AspNetCore.Mvc
services.AddSingleton<IActionResultExecutor<ObjectResult>>(new ObjectResultExecutor( services.AddSingleton<IActionResultExecutor<ObjectResult>>(new ObjectResultExecutor(
new DefaultOutputFormatterSelector(options, NullLoggerFactory.Instance), new DefaultOutputFormatterSelector(options, NullLoggerFactory.Instance),
new TestHttpResponseStreamWriterFactory(), new TestHttpResponseStreamWriterFactory(),
NullLoggerFactory.Instance)); NullLoggerFactory.Instance,
options));
return services.BuildServiceProvider(); return services.BuildServiceProvider();
} }

View File

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

View File

@ -1567,7 +1567,8 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
services.AddSingleton<IActionResultExecutor<ObjectResult>>(new ObjectResultExecutor( services.AddSingleton<IActionResultExecutor<ObjectResult>>(new ObjectResultExecutor(
new DefaultOutputFormatterSelector(options, NullLoggerFactory.Instance), new DefaultOutputFormatterSelector(options, NullLoggerFactory.Instance),
new TestHttpResponseStreamWriterFactory(), new TestHttpResponseStreamWriterFactory(),
NullLoggerFactory.Instance)); NullLoggerFactory.Instance,
options));
httpContext.Response.Body = new MemoryStream(); httpContext.Response.Body = new MemoryStream();
httpContext.RequestServices = services.BuildServiceProvider(); httpContext.RequestServices = services.BuildServiceProvider();

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text; 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); 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 IActionResultExecutor<JsonResult> CreateExecutor() => CreateExecutor(NullLoggerFactory.Instance);
protected abstract IActionResultExecutor<JsonResult> CreateExecutor(ILoggerFactory loggerFactory); protected abstract IActionResultExecutor<JsonResult> CreateExecutor(ILoggerFactory loggerFactory);
@ -354,5 +373,12 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
{ {
public string Property { get; set; } public string Property { get; set; }
} }
private async IAsyncEnumerable<string> TestAsyncEnumerable()
{
await Task.Yield();
yield return "Hello";
yield return "world";
}
} }
} }

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Collections.Generic;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
@ -209,8 +210,8 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
public async Task ExecuteAsync_FallsBackOnFormattersInOptions() public async Task ExecuteAsync_FallsBackOnFormattersInOptions()
{ {
// Arrange // Arrange
var options = Options.Create(new MvcOptions()); var options = new MvcOptions();
options.Value.OutputFormatters.Add(new TestJsonOutputFormatter()); options.OutputFormatters.Add(new TestJsonOutputFormatter());
var executor = CreateExecutor(options: options); var executor = CreateExecutor(options: options);
@ -300,8 +301,8 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
string expectedContentType) string expectedContentType)
{ {
// Arrange // Arrange
var options = Options.Create(new MvcOptions()); var options = new MvcOptions();
options.Value.RespectBrowserAcceptHeader = false; options.RespectBrowserAcceptHeader = false;
var executor = CreateExecutor(options: options); var executor = CreateExecutor(options: options);
@ -337,8 +338,8 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
string expectedContentType) string expectedContentType)
{ {
// Arrange // Arrange
var options = Options.Create(new MvcOptions()); var options = new MvcOptions();
options.Value.RespectBrowserAcceptHeader = true; options.RespectBrowserAcceptHeader = true;
var executor = CreateExecutor(options: options); var executor = CreateExecutor(options: options);
@ -360,6 +361,106 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
MediaTypeAssert.Equal(expectedContentType, responseContentType); 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() private static IServiceCollection CreateServices()
{ {
var services = new ServiceCollection(); var services = new ServiceCollection();
@ -379,10 +480,12 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
return httpContext; 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); options ??= new MvcOptions();
return new ObjectResultExecutor(selector, new TestHttpResponseStreamWriterFactory(), NullLoggerFactory.Instance); 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 private class CannotWriteFormatter : IOutputFormatter
@ -409,8 +512,11 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
SupportedEncodings.Add(Encoding.UTF8); SupportedEncodings.Add(Encoding.UTF8);
} }
public OutputFormatterWriteContext LastOutputFormatterContext { get; private set; }
public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding) public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
{ {
LastOutputFormatterContext = context;
return Task.FromResult(0); return Task.FromResult(0);
} }
} }

View File

@ -11,7 +11,10 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
{ {
protected override IActionResultExecutor<JsonResult> CreateExecutor(ILoggerFactory loggerFactory) 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() protected override object GetIndentedSettings()

View File

@ -97,10 +97,12 @@ namespace Microsoft.AspNetCore.Mvc
private static IServiceProvider CreateServices() private static IServiceProvider CreateServices()
{ {
var services = new ServiceCollection(); var services = new ServiceCollection();
var options = Options.Create(new MvcOptions());
services.AddSingleton<IActionResultExecutor<ObjectResult>>(new ObjectResultExecutor( services.AddSingleton<IActionResultExecutor<ObjectResult>>(new ObjectResultExecutor(
new DefaultOutputFormatterSelector(Options.Create(new MvcOptions()), NullLoggerFactory.Instance), new DefaultOutputFormatterSelector(options, NullLoggerFactory.Instance),
new TestHttpResponseStreamWriterFactory(), new TestHttpResponseStreamWriterFactory(),
NullLoggerFactory.Instance)); NullLoggerFactory.Instance,
options));
services.AddSingleton<ILoggerFactory>(NullLoggerFactory.Instance); services.AddSingleton<ILoggerFactory>(NullLoggerFactory.Instance);
return services.BuildServiceProvider(); return services.BuildServiceProvider();

View File

@ -6,6 +6,7 @@
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<PackageTags>aspnetcore;aspnetcoremvc;json</PackageTags> <PackageTags>aspnetcore;aspnetcoremvc;json</PackageTags>
<IsShippingPackage>true</IsShippingPackage> <IsShippingPackage>true</IsShippingPackage>
<DefineConstants>$(DefineConstants);JSONNET</DefineConstants>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@ -21,5 +22,6 @@
<ItemGroup> <ItemGroup>
<Compile Include="..\..\Mvc.Core\src\Formatters\ResponseContentTypeHelper.cs" /> <Compile Include="..\..\Mvc.Core\src\Formatters\ResponseContentTypeHelper.cs" />
<Compile Include="..\..\Mvc.Core\src\Infrastructure\AsyncEnumerableReader.cs" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -10,7 +10,6 @@ namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson
{ {
private static readonly Action<ILogger, Exception> _jsonInputFormatterException; private static readonly Action<ILogger, Exception> _jsonInputFormatterException;
private static readonly Action<ILogger, string, Exception> _jsonResultExecuting;
static NewtonsoftJsonLoggerExtensions() static NewtonsoftJsonLoggerExtensions()
{ {
@ -18,22 +17,11 @@ namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson
LogLevel.Debug, LogLevel.Debug,
new EventId(1, "JsonInputException"), new EventId(1, "JsonInputException"),
"JSON input formatter threw an exception."); "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) public static void JsonInputException(this ILogger logger, Exception exception)
{ {
_jsonInputFormatterException(logger, 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);
}
} }
} }

View File

@ -3,10 +3,9 @@
using System; using System;
using System.Buffers; using System.Buffers;
using System.IO; using System.Collections.Generic;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.WebUtilities; using Microsoft.AspNetCore.WebUtilities;
@ -32,6 +31,7 @@ namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson
private readonly MvcOptions _mvcOptions; private readonly MvcOptions _mvcOptions;
private readonly MvcNewtonsoftJsonOptions _jsonOptions; private readonly MvcNewtonsoftJsonOptions _jsonOptions;
private readonly IArrayPool<char> _charPool; private readonly IArrayPool<char> _charPool;
private readonly AsyncEnumerableReader _asyncEnumerableReader;
/// <summary> /// <summary>
/// Creates a new <see cref="NewtonsoftJsonResultExecutor"/>. /// Creates a new <see cref="NewtonsoftJsonResultExecutor"/>.
@ -73,6 +73,7 @@ namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson
_mvcOptions = mvcOptions?.Value ?? throw new ArgumentNullException(nameof(mvcOptions)); _mvcOptions = mvcOptions?.Value ?? throw new ArgumentNullException(nameof(mvcOptions));
_jsonOptions = jsonOptions.Value; _jsonOptions = jsonOptions.Value;
_charPool = new JsonArrayPool<char>(charPool); _charPool = new JsonArrayPool<char>(charPool);
_asyncEnumerableReader = new AsyncEnumerableReader(_mvcOptions);
} }
/// <summary> /// <summary>
@ -111,7 +112,7 @@ namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson
response.StatusCode = result.StatusCode.Value; response.StatusCode = result.StatusCode.Value;
} }
_logger.JsonResultExecuting(result.Value); Log.JsonResultExecuting(_logger, result.Value);
var responseStream = response.Body; var responseStream = response.Body;
FileBufferingWriteStream fileBufferingWriteStream = null; FileBufferingWriteStream fileBufferingWriteStream = null;
@ -131,7 +132,14 @@ namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson
jsonWriter.AutoCompleteOnClose = false; jsonWriter.AutoCompleteOnClose = false;
var jsonSerializer = JsonSerializer.Create(jsonSerializerSettings); 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) if (fileBufferingWriteStream != null)
@ -168,5 +176,33 @@ namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson
return settingsFromResult; 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);
}
} }
} }

View File

@ -108,6 +108,20 @@ namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson
internal static string FormatTempData_CannotSerializeType(object p0, object p1) internal static string FormatTempData_CannotSerializeType(object p0, object p1)
=> string.Format(CultureInfo.CurrentCulture, GetString("TempData_CannotSerializeType"), p0, 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) private static string GetString(string name, params string[] formatterNames)
{ {
var value = _resourceManager.GetString(name); var value = _resourceManager.GetString(name);

View File

@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<root> <root>
<!-- <!--
Microsoft ResX Schema Microsoft ResX Schema
Version 2.0 Version 2.0
The primary goals of this format is to allow a simple XML format The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes various data types are done through the TypeConverter classes
associated with the data types. associated with the data types.
Example: Example:
... ado.net/XML headers & schema ... ... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</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> <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment> <comment>This is a comment</comment>
</data> </data>
There are any number of "resheader" rows that contain simple There are any number of "resheader" rows that contain simple
name/value pairs. name/value pairs.
Each data row contains a name, and value. The row also contains a Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture. text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the Classes that don't support this are serialized and stored with the
mimetype set. mimetype set.
The mimetype is used for serialized objects, and tells the The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly: extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below. read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64 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 : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding. : and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64 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 : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding. : and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64 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 : using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding. : and then encoded with base64 encoding.
--> -->
@ -126,6 +126,9 @@
<data name="JsonHelperMustBeAnInstanceOfNewtonsoftJson" xml:space="preserve"> <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> <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>
<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&lt;&gt;' 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"> <data name="Property_MustBeInstanceOfType" xml:space="preserve">
<value>Property '{0}.{1}' must be an instance of type '{2}'.</value> <value>Property '{0}.{1}' must be an instance of type '{2}'.</value>
</data> </data>
@ -138,4 +141,4 @@
<data name="TempData_CannotSerializeType" xml:space="preserve"> <data name="TempData_CannotSerializeType" xml:space="preserve">
<value>The '{0}' cannot serialize an object of type '{1}'.</value> <value>The '{0}' cannot serialize an object of type '{1}'.</value>
</data> </data>
</root> </root>

View File

@ -12,6 +12,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="..\..\Mvc.Core\test\Infrastructure\AsyncEnumerableReaderTest.cs" />
<Compile Include="..\..\Mvc.Core\test\Formatters\JsonInputFormatterTestBase.cs" /> <Compile Include="..\..\Mvc.Core\test\Formatters\JsonInputFormatterTestBase.cs" />
<Compile Include="..\..\Mvc.Core\test\Formatters\JsonOutputFormatterTestBase.cs" /> <Compile Include="..\..\Mvc.Core\test\Formatters\JsonOutputFormatterTestBase.cs" />
<Compile Include="..\..\Mvc.Core\test\Infrastructure\JsonResultExecutorTestBase.cs" /> <Compile Include="..\..\Mvc.Core\test\Infrastructure\JsonResultExecutorTestBase.cs" />

View File

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

View File

@ -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"), Content = new StringContent(@"{ ""Id"": ""S-1-5-21-1004336348-1177238915-682003330-512"" }", Encoding.UTF8, "application/json"),
}; };
// Act & Assert // Act
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => Client.SendAsync(requestMessage)); var response = await Client.SendAsync(requestMessage);
Assert.Equal(expected, ex.Message);
// Assert
await response.AssertStatusCodeAsync(HttpStatusCode.InternalServerError);
var content = await response.Content.ReadAsStringAsync();
Assert.Contains(expected, content);
} }
[Fact] [Fact]
@ -356,4 +360,4 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
}); });
} }
} }
} }

View File

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

View File

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

View File

@ -26,6 +26,8 @@ namespace FormatterWebSite
public void Configure(IApplicationBuilder app) public void Configure(IApplicationBuilder app)
{ {
app.UseDeveloperExceptionPage();
app.UseRouting(); app.UseRouting();
app.UseEndpoints(endpoints => app.UseEndpoints(endpoints =>
{ {