-
Notifications
You must be signed in to change notification settings - Fork 10.3k
Eagerly read IAsyncEnumerable{object} instances before formatting #11118
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
102 changes: 102 additions & 0 deletions
102
src/Mvc/Mvc.Core/src/Infrastructure/AsyncEnumerableReader.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,6 +10,7 @@ | |
using Microsoft.AspNetCore.Http; | ||
using Microsoft.AspNetCore.Mvc.Formatters; | ||
using Microsoft.Extensions.Logging; | ||
using Microsoft.Extensions.Options; | ||
|
||
namespace Microsoft.AspNetCore.Mvc.Infrastructure | ||
{ | ||
|
@@ -18,16 +19,35 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure | |
/// </summary> | ||
public class ObjectResultExecutor : IActionResultExecutor<ObjectResult> | ||
{ | ||
private readonly AsyncEnumerableReader _asyncEnumerableReader; | ||
|
||
/// <summary> | ||
/// Creates a new <see cref="ObjectResultExecutor"/>. | ||
/// </summary> | ||
/// <param name="formatterSelector">The <see cref="OutputFormatterSelector"/>.</param> | ||
/// <param name="writerFactory">The <see cref="IHttpResponseStreamWriterFactory"/>.</param> | ||
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param> | ||
[Obsolete("This constructor is obsolete and will be removed in a future release.")] | ||
public ObjectResultExecutor( | ||
OutputFormatterSelector formatterSelector, | ||
IHttpResponseStreamWriterFactory writerFactory, | ||
ILoggerFactory loggerFactory) | ||
: this(formatterSelector, writerFactory, loggerFactory, mvcOptions: null) | ||
{ | ||
} | ||
|
||
/// <summary> | ||
/// Creates a new <see cref="ObjectResultExecutor"/>. | ||
/// </summary> | ||
/// <param name="formatterSelector">The <see cref="OutputFormatterSelector"/>.</param> | ||
/// <param name="writerFactory">The <see cref="IHttpResponseStreamWriterFactory"/>.</param> | ||
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param> | ||
/// <param name="mvcOptions">Accessor to <see cref="MvcOptions"/>.</param> | ||
public ObjectResultExecutor( | ||
OutputFormatterSelector formatterSelector, | ||
IHttpResponseStreamWriterFactory writerFactory, | ||
ILoggerFactory loggerFactory, | ||
IOptions<MvcOptions> mvcOptions) | ||
{ | ||
if (formatterSelector == null) | ||
{ | ||
|
@@ -47,6 +67,8 @@ public ObjectResultExecutor( | |
FormatterSelector = formatterSelector; | ||
WriterFactory = writerFactory.CreateWriter; | ||
Logger = loggerFactory.CreateLogger<ObjectResultExecutor>(); | ||
var options = mvcOptions?.Value ?? throw new ArgumentNullException(nameof(mvcOptions)); | ||
_asyncEnumerableReader = new AsyncEnumerableReader(options); | ||
} | ||
|
||
/// <summary> | ||
|
@@ -87,16 +109,37 @@ public virtual Task ExecuteAsync(ActionContext context, ObjectResult result) | |
InferContentTypes(context, result); | ||
|
||
var objectType = result.DeclaredType; | ||
|
||
if (objectType == null || objectType == typeof(object)) | ||
{ | ||
objectType = result.Value?.GetType(); | ||
} | ||
|
||
var value = result.Value; | ||
|
||
if (value is IAsyncEnumerable<object> asyncEnumerable) | ||
{ | ||
return ExecuteAsyncEnumerable(context, result, asyncEnumerable); | ||
} | ||
|
||
return ExecuteAsyncCore(context, result, objectType, value); | ||
} | ||
|
||
private async Task ExecuteAsyncEnumerable(ActionContext context, ObjectResult result, IAsyncEnumerable<object> asyncEnumerable) | ||
{ | ||
Log.BufferingAsyncEnumerable(Logger, asyncEnumerable); | ||
|
||
var enumerated = await _asyncEnumerableReader.ReadAsync(asyncEnumerable); | ||
await ExecuteAsyncCore(context, result, enumerated.GetType(), enumerated); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we be logging here? |
||
} | ||
|
||
private Task ExecuteAsyncCore(ActionContext context, ObjectResult result, Type objectType, object value) | ||
{ | ||
var formatterContext = new OutputFormatterWriteContext( | ||
context.HttpContext, | ||
WriterFactory, | ||
objectType, | ||
result.Value); | ||
value); | ||
|
||
var selectedFormatter = FormatterSelector.SelectFormatter( | ||
formatterContext, | ||
|
@@ -138,5 +181,21 @@ private static void InferContentTypes(ActionContext context, ObjectResult result | |
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); | ||
} | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a public API, so this should probably throw instead of assert. Someone could pass in any type here.