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

@ -504,4 +504,7 @@
<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

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

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]

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