Skip to content

Add support for IAsyncEnumerable<T> where T is value type #17154

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 2 commits into from
Nov 18, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 25 additions & 26 deletions src/Mvc/Mvc.Core/src/Infrastructure/AsyncEnumerableReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
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;
Expand All @@ -17,8 +16,6 @@ namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson
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.
Expand All @@ -34,8 +31,8 @@ internal sealed class AsyncEnumerableReader
nameof(ReadInternal),
BindingFlags.NonPublic | BindingFlags.Instance);

private readonly ConcurrentDictionary<Type, ReaderFunc> _asyncEnumerableConverters =
new ConcurrentDictionary<Type, ReaderFunc>();
private readonly ConcurrentDictionary<Type, Func<object, Task<ICollection>>> _asyncEnumerableConverters =
new ConcurrentDictionary<Type, Func<object, Task<ICollection>>>();
private readonly MvcOptions _mvcOptions;

/// <summary>
Expand All @@ -48,37 +45,39 @@ public AsyncEnumerableReader(MvcOptions mvcOptions)
}

/// <summary>
/// Reads a <see cref="IAsyncEnumerable{T}"/> into an <see cref="ICollection{T}"/>.
/// Attempts to produces a delagate that 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)
/// <param name="type">The type to read.</param>
/// <param name="reader">A delegate that when awaited reads the <see cref="IAsyncEnumerable{T}"/>.</param>
/// <returns><see langword="true" /> when <paramref name="type"/> is an instance of <see cref="IAsyncEnumerable{T}"/>, othwerise <see langword="false"/>.</returns>
public bool TryGetReader(Type type, out Func<object, Task<ICollection>> reader)
{
if (value == null)
{
throw new ArgumentNullException(nameof(value));
}

var type = value.GetType();
if (!_asyncEnumerableConverters.TryGetValue(type, out var result))
if (!_asyncEnumerableConverters.TryGetValue(type, out reader))
{
var enumerableType = ClosedGenericMatcher.ExtractGenericInterface(type, typeof(IAsyncEnumerable<>));
Debug.Assert(enumerableType != null);

var enumeratedObjectType = enumerableType.GetGenericArguments()[0];
if (enumerableType is null)
{
// Not an IAsyncEnumerable<T>. Cache this result so we avoid reflection the next time we see this type.
reader = null;
_asyncEnumerableConverters.TryAdd(type, reader);
}
else
{
var enumeratedObjectType = enumerableType.GetGenericArguments()[0];

var converter = (ReaderFunc)Converter
.MakeGenericMethod(enumeratedObjectType)
.CreateDelegate(typeof(ReaderFunc), this);
var converter = (Func<object, Task<ICollection>>)Converter
.MakeGenericMethod(enumeratedObjectType)
.CreateDelegate(typeof(Func<object, Task<ICollection>>), this);

_asyncEnumerableConverters.TryAdd(type, converter);
result = converter;
reader = converter;
_asyncEnumerableConverters.TryAdd(type, reader);
}
}

return result(value);
return reader != null;
}

private async Task<ICollection> ReadInternal<T>(IAsyncEnumerable<object> value)
private async Task<ICollection> ReadInternal<T>(object value)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is way smaller of a change than I expected.

{
var asyncEnumerable = (IAsyncEnumerable<T>)value;
var result = new List<T>();
Expand Down
15 changes: 8 additions & 7 deletions src/Mvc/Mvc.Core/src/Infrastructure/ObjectResultExecutor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
Expand All @@ -19,7 +20,7 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
/// </summary>
public class ObjectResultExecutor : IActionResultExecutor<ObjectResult>
{
private readonly AsyncEnumerableReader _asyncEnumerableReader;
private readonly AsyncEnumerableReader _asyncEnumerableReaderFactory;

/// <summary>
/// Creates a new <see cref="ObjectResultExecutor"/>.
Expand Down Expand Up @@ -68,7 +69,7 @@ public ObjectResultExecutor(
WriterFactory = writerFactory.CreateWriter;
Logger = loggerFactory.CreateLogger<ObjectResultExecutor>();
var options = mvcOptions?.Value ?? throw new ArgumentNullException(nameof(mvcOptions));
_asyncEnumerableReader = new AsyncEnumerableReader(options);
_asyncEnumerableReaderFactory = new AsyncEnumerableReader(options);
}

/// <summary>
Expand Down Expand Up @@ -117,19 +118,19 @@ public virtual Task ExecuteAsync(ActionContext context, ObjectResult result)

var value = result.Value;

if (value is IAsyncEnumerable<object> asyncEnumerable)
if (value != null && _asyncEnumerableReaderFactory.TryGetReader(value.GetType(), out var reader))
{
return ExecuteAsyncEnumerable(context, result, asyncEnumerable);
return ExecuteAsyncEnumerable(context, result, value, reader);
}

return ExecuteAsyncCore(context, result, objectType, value);
}

private async Task ExecuteAsyncEnumerable(ActionContext context, ObjectResult result, IAsyncEnumerable<object> asyncEnumerable)
private async Task ExecuteAsyncEnumerable(ActionContext context, ObjectResult result, object asyncEnumerable, Func<object, Task<ICollection>> reader)
{
Log.BufferingAsyncEnumerable(Logger, asyncEnumerable);

var enumerated = await _asyncEnumerableReader.ReadAsync(asyncEnumerable);
var enumerated = await reader(asyncEnumerable);
await ExecuteAsyncCore(context, result, enumerated.GetType(), enumerated);
}

Expand Down Expand Up @@ -194,7 +195,7 @@ static Log()
"Buffering IAsyncEnumerable instance of type '{Type}'.");
}

public static void BufferingAsyncEnumerable(ILogger logger, IAsyncEnumerable<object> asyncEnumerable)
public static void BufferingAsyncEnumerable(ILogger logger, object asyncEnumerable)
=> _bufferingAsyncEnumerable(logger, asyncEnumerable.GetType().FullName, null);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ internal sealed class SystemTextJsonResultExecutor : IActionResultExecutor<JsonR

private readonly JsonOptions _options;
private readonly ILogger<SystemTextJsonResultExecutor> _logger;
private readonly AsyncEnumerableReader _asyncEnumerableReader;
private readonly AsyncEnumerableReader _asyncEnumerableReaderFactory;

public SystemTextJsonResultExecutor(
IOptions<JsonOptions> options,
Expand All @@ -36,7 +36,7 @@ public SystemTextJsonResultExecutor(
{
_options = options.Value;
_logger = logger;
_asyncEnumerableReader = new AsyncEnumerableReader(mvcOptions.Value);
_asyncEnumerableReaderFactory = new AsyncEnumerableReader(mvcOptions.Value);
}

public async Task ExecuteAsync(ActionContext context, JsonResult result)
Expand Down Expand Up @@ -76,10 +76,10 @@ public async Task ExecuteAsync(ActionContext context, JsonResult result)
try
{
var value = result.Value;
if (value is IAsyncEnumerable<object> asyncEnumerable)
if (value != null && _asyncEnumerableReaderFactory.TryGetReader(value.GetType(), out var reader))
{
Log.BufferingAsyncEnumerable(_logger, asyncEnumerable);
value = await _asyncEnumerableReader.ReadAsync(asyncEnumerable);
Log.BufferingAsyncEnumerable(_logger, value);
value = await reader(value);
}

var type = value?.GetType() ?? typeof(object);
Expand Down Expand Up @@ -154,7 +154,7 @@ public static void JsonResultExecuting(ILogger logger, object value)
_jsonResultExecuting(logger, type, null);
}

public static void BufferingAsyncEnumerable(ILogger logger, IAsyncEnumerable<object> asyncEnumerable)
public static void BufferingAsyncEnumerable(ILogger logger, object asyncEnumerable)
=> _bufferingAsyncEnumerable(logger, asyncEnumerable.GetType().FullName, null);
}
}
Expand Down
Loading