Skip to content

Commit c2469ac

Browse files
committed
Changes per PR
1 parent 9485e7f commit c2469ac

25 files changed

+317
-119
lines changed

src/Mvc/Mvc.Core/ref/Microsoft.AspNetCore.Mvc.Core.netcoreapp3.0.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1939,6 +1939,11 @@ public sealed partial class ActionResultStatusCodeAttribute : System.Attribute
19391939
{
19401940
public ActionResultStatusCodeAttribute() { }
19411941
}
1942+
public sealed partial class AsyncEnumerableReader
1943+
{
1944+
public AsyncEnumerableReader(Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.Mvc.MvcOptions> mvcOptions) { }
1945+
public System.Threading.Tasks.Task<System.Collections.ICollection> ReadAsync(System.Collections.Generic.IAsyncEnumerable<object> value) { throw null; }
1946+
}
19421947
public partial class CompatibilitySwitch<TValue> : Microsoft.AspNetCore.Mvc.Infrastructure.ICompatibilitySwitch where TValue : struct
19431948
{
19441949
public CompatibilitySwitch(string name) { }
@@ -2087,7 +2092,7 @@ public partial class ObjectResultExecutor : Microsoft.AspNetCore.Mvc.Infrastruct
20872092
{
20882093
[System.ObsoleteAttribute("This constructor is obsolete and will be removed in a future release.")]
20892094
public ObjectResultExecutor(Microsoft.AspNetCore.Mvc.Infrastructure.OutputFormatterSelector formatterSelector, Microsoft.AspNetCore.Mvc.Infrastructure.IHttpResponseStreamWriterFactory writerFactory, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory) { }
2090-
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) { }
2095+
public ObjectResultExecutor(Microsoft.AspNetCore.Mvc.Infrastructure.OutputFormatterSelector formatterSelector, Microsoft.AspNetCore.Mvc.Infrastructure.IHttpResponseStreamWriterFactory writerFactory, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory, Microsoft.AspNetCore.Mvc.Infrastructure.AsyncEnumerableReader asyncEnumerableReader) { }
20912096
protected Microsoft.AspNetCore.Mvc.Infrastructure.OutputFormatterSelector FormatterSelector { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
20922097
protected Microsoft.Extensions.Logging.ILogger Logger { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
20932098
protected System.Func<System.IO.Stream, System.Text.Encoding, System.IO.TextWriter> WriterFactory { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }

src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,7 @@ internal static void AddMvcCoreServices(IServiceCollection services)
260260
services.TryAddSingleton<IActionResultExecutor<ContentResult>, ContentResultExecutor>();
261261
services.TryAddSingleton<IActionResultExecutor<JsonResult>, SystemTextJsonResultExecutor>();
262262
services.TryAddSingleton<IClientErrorFactory, ProblemDetailsClientErrorFactory>();
263+
services.TryAddSingleton<AsyncEnumerableReader>();
263264

264265
//
265266
// Route Handlers

src/Mvc/Mvc.Core/src/Formatters/SystemTextJsonInputFormatter.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ public class SystemTextJsonInputFormatter : TextInputFormatter, IInputFormatterE
2020
/// Initializes a new instance of <see cref="SystemTextJsonInputFormatter"/>.
2121
/// </summary>
2222
/// <param name="options">The <see cref="JsonOptions"/>.</param>
23-
public SystemTextJsonInputFormatter(JsonOptions options)
23+
public SystemTextJsonInputFormatter(
24+
JsonOptions options)
2425
{
2526
SerializerOptions = options.JsonSerializerOptions;
2627

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections;
6+
using System.Collections.Concurrent;
7+
using System.Collections.Generic;
8+
using System.Diagnostics;
9+
using System.Reflection;
10+
using System.Threading.Tasks;
11+
using Microsoft.AspNetCore.Mvc.Core;
12+
using Microsoft.Extensions.Internal;
13+
using Microsoft.Extensions.Options;
14+
15+
namespace Microsoft.AspNetCore.Mvc.Infrastructure
16+
{
17+
using ReaderFunc = Func<IAsyncEnumerable<object>, Task<ICollection>>;
18+
19+
/// <summary>
20+
/// Type that reads an <see cref="IAsyncEnumerable{T}"/> instance into a
21+
/// generic collection instance.
22+
/// </summary>
23+
/// <remarks>
24+
/// This type is used to create a strongly typed synchronous <see cref="ICollection{T}"/> instance from
25+
/// an <see cref="IAsyncEnumerable{T}"/>. An accurate <see cref="ICollection{T}"/> is required for XML formatters to
26+
/// correctly serialize.
27+
/// </remarks>
28+
public sealed class AsyncEnumerableReader
29+
{
30+
private readonly MethodInfo Converter = typeof(AsyncEnumerableReader).GetMethod(
31+
nameof(ReadInternal),
32+
BindingFlags.NonPublic | BindingFlags.Instance);
33+
34+
private readonly ConcurrentDictionary<Type, ReaderFunc> _asyncEnumerableConverters =
35+
new ConcurrentDictionary<Type, ReaderFunc>();
36+
private readonly MvcOptions _mvcOptions;
37+
38+
/// <summary>
39+
/// Initializes a new instance of <see cref="AsyncEnumerableReader"/>.
40+
/// </summary>
41+
/// <param name="mvcOptions">Accessor to <see cref="MvcOptions"/>.</param>
42+
public AsyncEnumerableReader(IOptions<MvcOptions> mvcOptions)
43+
{
44+
_mvcOptions = mvcOptions?.Value ?? throw new ArgumentNullException(nameof(mvcOptions));
45+
}
46+
47+
/// <summary>
48+
/// Reads a <see cref="IAsyncEnumerable{T}"/> into an <see cref="ICollection{T}"/>.
49+
/// </summary>
50+
/// <param name="value">The <see cref="IAsyncEnumerable{T}"/> to read.</param>
51+
/// <returns>The <see cref="ICollection"/>.</returns>
52+
public Task<ICollection> ReadAsync(IAsyncEnumerable<object> value)
53+
{
54+
if (value == null)
55+
{
56+
throw new ArgumentNullException(nameof(value));
57+
}
58+
59+
var type = value.GetType();
60+
if (!_asyncEnumerableConverters.TryGetValue(type, out var result))
61+
{
62+
var enumerableType = ClosedGenericMatcher.ExtractGenericInterface(type, typeof(IAsyncEnumerable<>));
63+
Debug.Assert(enumerableType != null);
64+
65+
var enumeratedObjectType = enumerableType.GetGenericArguments()[0];
66+
67+
var converter = (ReaderFunc)Converter
68+
.MakeGenericMethod(enumeratedObjectType)
69+
.CreateDelegate(typeof(ReaderFunc), this);
70+
71+
_asyncEnumerableConverters.TryAdd(type, converter);
72+
result = converter;
73+
}
74+
75+
return result(value);
76+
}
77+
78+
private async Task<ICollection> ReadInternal<T>(IAsyncEnumerable<object> value)
79+
{
80+
var asyncEnumerable = (IAsyncEnumerable<T>)value;
81+
var result = new List<T>();
82+
var count = 0;
83+
84+
await foreach (var item in asyncEnumerable)
85+
{
86+
if (count++ >= _mvcOptions.MaxIAsyncEnumerableBufferLimit)
87+
{
88+
throw new InvalidOperationException(Resources.FormatObjectResultExecutor_MaxEnumerationExceeded(
89+
nameof(AsyncEnumerableReader),
90+
value.GetType()));
91+
}
92+
93+
result.Add(item);
94+
}
95+
96+
return result;
97+
}
98+
}
99+
}

src/Mvc/Mvc.Core/src/Infrastructure/ObjectResultExecutor.cs

Lines changed: 7 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,14 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
5-
using System.Collections.Concurrent;
65
using System.Collections.Generic;
76
using System.Diagnostics;
87
using System.IO;
9-
using System.Reflection;
108
using System.Text;
119
using System.Threading.Tasks;
1210
using Microsoft.AspNetCore.Http;
13-
using Microsoft.AspNetCore.Mvc.Core;
1411
using Microsoft.AspNetCore.Mvc.Formatters;
15-
using Microsoft.Extensions.Internal;
1612
using Microsoft.Extensions.Logging;
17-
using Microsoft.Extensions.Options;
1813

1914
namespace Microsoft.AspNetCore.Mvc.Infrastructure
2015
{
@@ -23,15 +18,7 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
2318
/// </summary>
2419
public class ObjectResultExecutor : IActionResultExecutor<ObjectResult>
2520
{
26-
private delegate Task<object> ReadAsyncEnumerableDelegate(object value);
27-
28-
private readonly MethodInfo Converter = typeof(ObjectResultExecutor).GetMethod(
29-
nameof(ReadAsyncEnumerable),
30-
BindingFlags.NonPublic | BindingFlags.Instance);
31-
32-
private readonly ConcurrentDictionary<Type, ReadAsyncEnumerableDelegate> _asyncEnumerableConverters =
33-
new ConcurrentDictionary<Type, ReadAsyncEnumerableDelegate>();
34-
private readonly MvcOptions _mvcOptions;
21+
private readonly AsyncEnumerableReader _asyncEnumerableReader;
3522

3623
/// <summary>
3724
/// Creates a new <see cref="ObjectResultExecutor"/>.
@@ -44,7 +31,7 @@ public ObjectResultExecutor(
4431
OutputFormatterSelector formatterSelector,
4532
IHttpResponseStreamWriterFactory writerFactory,
4633
ILoggerFactory loggerFactory)
47-
: this(formatterSelector, writerFactory, loggerFactory, mvcOptions: null)
34+
: this(formatterSelector, writerFactory, loggerFactory, asyncEnumerableReader: null)
4835
{
4936
}
5037

@@ -54,12 +41,12 @@ public ObjectResultExecutor(
5441
/// <param name="formatterSelector">The <see cref="OutputFormatterSelector"/>.</param>
5542
/// <param name="writerFactory">The <see cref="IHttpResponseStreamWriterFactory"/>.</param>
5643
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
57-
/// <param name="mvcOptions">Accessor to <see cref="MvcOptions"/>.</param>
44+
/// <param name="asyncEnumerableReader">The <see cref="AsyncEnumerableReader"/>.</param>
5845
public ObjectResultExecutor(
5946
OutputFormatterSelector formatterSelector,
6047
IHttpResponseStreamWriterFactory writerFactory,
6148
ILoggerFactory loggerFactory,
62-
IOptions<MvcOptions> mvcOptions)
49+
AsyncEnumerableReader asyncEnumerableReader)
6350
{
6451
if (formatterSelector == null)
6552
{
@@ -79,7 +66,7 @@ public ObjectResultExecutor(
7966
FormatterSelector = formatterSelector;
8067
WriterFactory = writerFactory.CreateWriter;
8168
Logger = loggerFactory.CreateLogger<ObjectResultExecutor>();
82-
_mvcOptions = mvcOptions?.Value ?? throw new ArgumentNullException(nameof(mvcOptions));
69+
_asyncEnumerableReader = asyncEnumerableReader ?? throw new ArgumentNullException(nameof(asyncEnumerableReader));
8370
}
8471

8572
/// <summary>
@@ -138,7 +125,7 @@ public virtual Task ExecuteAsync(ActionContext context, ObjectResult result)
138125

139126
private async Task ExecuteAsyncEnumerable(ActionContext context, ObjectResult result, IAsyncEnumerable<object> asyncEnumerable)
140127
{
141-
var enumerated = await EnumerateAsyncEnumerable(asyncEnumerable);
128+
var enumerated = await _asyncEnumerableReader.ReadAsync(asyncEnumerable);
142129
await ExecuteAsyncCore(context, result, enumerated.GetType(), enumerated);
143130
}
144131

@@ -191,49 +178,6 @@ private static void InferContentTypes(ActionContext context, ObjectResult result
191178
}
192179
}
193180

194-
private Task<object> EnumerateAsyncEnumerable(IAsyncEnumerable<object> value)
195-
{
196-
var type = value.GetType();
197-
if (!_asyncEnumerableConverters.TryGetValue(type, out var result))
198-
{
199-
var enumerableType = ClosedGenericMatcher.ExtractGenericInterface(type, typeof(IAsyncEnumerable<>));
200-
result = null;
201-
if (enumerableType != null)
202-
{
203-
var enumeratedObjectType = enumerableType.GetGenericArguments()[0];
204-
205-
var converter = (ReadAsyncEnumerableDelegate)Converter
206-
.MakeGenericMethod(enumeratedObjectType)
207-
.CreateDelegate(typeof(ReadAsyncEnumerableDelegate), this);
208-
209-
_asyncEnumerableConverters.TryAdd(type, converter);
210-
result = converter;
211-
}
212-
}
213-
214-
return result(value);
215-
}
216-
217-
private async Task<object> ReadAsyncEnumerable<T>(object value)
218-
{
219-
var asyncEnumerable = (IAsyncEnumerable<T>)value;
220-
var result = new List<T>();
221-
var count = 0;
222-
223-
await foreach (var item in asyncEnumerable)
224-
{
225-
if (count++ >= _mvcOptions.MaxIAsyncEnumerableBufferLimit)
226-
{
227-
throw new InvalidOperationException(Resources.FormatObjectResultExecutor_MaxEnumerationExceeded(
228-
nameof(ObjectResultExecutor),
229-
_mvcOptions.MaxIAsyncEnumerableBufferLimit,
230-
value.GetType()));
231-
}
232-
233-
result.Add(item);
234-
}
235-
236-
return result;
237-
}
181+
238182
}
239183
}

src/Mvc/Mvc.Core/src/Infrastructure/SystemTextJsonResultExecutor.cs

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
5+
using System.Collections.Generic;
56
using System.IO;
67
using System.Text;
78
using System.Text.Json;
@@ -25,13 +26,16 @@ internal sealed class SystemTextJsonResultExecutor : IActionResultExecutor<JsonR
2526

2627
private readonly JsonOptions _options;
2728
private readonly ILogger<SystemTextJsonResultExecutor> _logger;
29+
private readonly AsyncEnumerableReader _reader;
2830

2931
public SystemTextJsonResultExecutor(
3032
IOptions<JsonOptions> options,
31-
ILogger<SystemTextJsonResultExecutor> logger)
33+
ILogger<SystemTextJsonResultExecutor> logger,
34+
AsyncEnumerableReader reader)
3235
{
3336
_options = options.Value;
3437
_logger = logger;
38+
_reader = reader;
3539
}
3640

3741
public async Task ExecuteAsync(ActionContext context, JsonResult result)
@@ -70,8 +74,15 @@ public async Task ExecuteAsync(ActionContext context, JsonResult result)
7074
var writeStream = GetWriteStream(context.HttpContext, resolvedContentTypeEncoding);
7175
try
7276
{
73-
var type = result.Value?.GetType() ?? typeof(object);
74-
await JsonSerializer.WriteAsync(writeStream, result.Value, type, jsonSerializerOptions);
77+
var value = result.Value;
78+
if (value is IAsyncEnumerable<object> asyncEnumerable)
79+
{
80+
Log.EagerlyReadingAsyncEnumerable(_logger, asyncEnumerable);
81+
value = await _reader.ReadAsync(asyncEnumerable);
82+
}
83+
84+
var type = value?.GetType() ?? typeof(object);
85+
await JsonSerializer.WriteAsync(writeStream, value, type, jsonSerializerOptions);
7586
await writeStream.FlushAsync();
7687
}
7788
finally
@@ -123,11 +134,22 @@ private static class Log
123134
new EventId(1, "JsonResultExecuting"),
124135
"Executing JsonResult, writing value of type '{Type}'.");
125136

137+
private static readonly Action<ILogger, string, Exception> _eagerlyReadingAsyncEnumerable = LoggerMessage.Define<string>(
138+
LogLevel.Debug,
139+
new EventId(2, "EagerReadAsyncEnumerable"),
140+
"Eagerly reading IAsyncEnumerable instance of type '{Type}'.");
141+
126142
public static void JsonResultExecuting(ILogger logger, object value)
127143
{
128144
var type = value == null ? "null" : value.GetType().FullName;
129145
_jsonResultExecuting(logger, type, null);
130146
}
147+
148+
public static void EagerlyReadingAsyncEnumerable(ILogger logger, object value)
149+
{
150+
var type = value == null ? "null" : value.GetType().FullName;
151+
_eagerlyReadingAsyncEnumerable(logger, type, null);
152+
}
131153
}
132154
}
133155
}

src/Mvc/Mvc.Core/src/Properties/Resources.Designer.cs

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)