Skip to content

Commit ed0af15

Browse files
committed
Use System.Text.Json's IAsyncEnumerable support
1 parent 232785c commit ed0af15

9 files changed

+193
-190
lines changed

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

+2-34
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,6 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
2121
/// </summary>
2222
public class ObjectResultExecutor : IActionResultExecutor<ObjectResult>
2323
{
24-
private readonly AsyncEnumerableReader _asyncEnumerableReaderFactory;
25-
2624
/// <summary>
2725
/// Creates a new <see cref="ObjectResultExecutor"/>.
2826
/// </summary>
@@ -54,8 +52,6 @@ public ObjectResultExecutor(
5452
FormatterSelector = formatterSelector;
5553
WriterFactory = writerFactory.CreateWriter;
5654
Logger = loggerFactory.CreateLogger<ObjectResultExecutor>();
57-
var options = mvcOptions?.Value ?? throw new ArgumentNullException(nameof(mvcOptions));
58-
_asyncEnumerableReaderFactory = new AsyncEnumerableReader(options);
5955
}
6056

6157
/// <summary>
@@ -103,23 +99,9 @@ public virtual Task ExecuteAsync(ActionContext context, ObjectResult result)
10399
}
104100

105101
var value = result.Value;
106-
107-
if (value != null && _asyncEnumerableReaderFactory.TryGetReader(value.GetType(), out var reader))
108-
{
109-
return ExecuteAsyncEnumerable(context, result, value, reader);
110-
}
111-
112102
return ExecuteAsyncCore(context, result, objectType, value);
113103
}
114104

115-
private async Task ExecuteAsyncEnumerable(ActionContext context, ObjectResult result, object asyncEnumerable, Func<object, Task<ICollection>> reader)
116-
{
117-
Log.BufferingAsyncEnumerable(Logger, asyncEnumerable);
118-
119-
var enumerated = await reader(asyncEnumerable);
120-
await ExecuteAsyncCore(context, result, enumerated.GetType(), enumerated);
121-
}
122-
123105
private Task ExecuteAsyncCore(ActionContext context, ObjectResult result, Type? objectType, object? value)
124106
{
125107
var formatterContext = new OutputFormatterWriteContext(
@@ -166,21 +148,7 @@ private static void InferContentTypes(ActionContext context, ObjectResult result
166148
}
167149
}
168150

169-
private static class Log
170-
{
171-
private static readonly Action<ILogger, string?, Exception?> _bufferingAsyncEnumerable = LoggerMessage.Define<string?>(
172-
LogLevel.Debug,
173-
new EventId(1, "BufferingAsyncEnumerable"),
174-
"Buffering IAsyncEnumerable instance of type '{Type}'.",
175-
skipEnabledCheck: true);
176-
177-
public static void BufferingAsyncEnumerable(ILogger logger, object asyncEnumerable)
178-
{
179-
if (logger.IsEnabled(LogLevel.Debug))
180-
{
181-
_bufferingAsyncEnumerable(logger, asyncEnumerable.GetType().FullName, null);
182-
}
183-
}
184-
}
151+
// Removed Log.
152+
// new EventId(1, "BufferingAsyncEnumerable")
185153
}
186154
}

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

+1-19
Original file line numberDiff line numberDiff line change
@@ -69,12 +69,6 @@ public async Task ExecuteAsync(ActionContext context, JsonResult result)
6969
Log.JsonResultExecuting(_logger, result.Value);
7070

7171
var value = result.Value;
72-
if (value != null && _asyncEnumerableReaderFactory.TryGetReader(value.GetType(), out var reader))
73-
{
74-
Log.BufferingAsyncEnumerable(_logger, value);
75-
value = await reader(value);
76-
}
77-
7872
var objectType = value?.GetType() ?? typeof(object);
7973

8074
// Keep this code in sync with SystemTextJsonOutputFormatter
@@ -147,11 +141,7 @@ private static class Log
147141
"Executing JsonResult, writing value of type '{Type}'.",
148142
skipEnabledCheck: true);
149143

150-
private static readonly Action<ILogger, string?, Exception?> _bufferingAsyncEnumerable = LoggerMessage.Define<string?>(
151-
LogLevel.Debug,
152-
new EventId(2, "BufferingAsyncEnumerable"),
153-
"Buffering IAsyncEnumerable instance of type '{Type}'.",
154-
skipEnabledCheck: true);
144+
// EventId 2 BufferingAsyncEnumerable
155145

156146
public static void JsonResultExecuting(ILogger logger, object? value)
157147
{
@@ -161,14 +151,6 @@ public static void JsonResultExecuting(ILogger logger, object? value)
161151
_jsonResultExecuting(logger, type, null);
162152
}
163153
}
164-
165-
public static void BufferingAsyncEnumerable(ILogger logger, object asyncEnumerable)
166-
{
167-
if (logger.IsEnabled(LogLevel.Debug))
168-
{
169-
_bufferingAsyncEnumerable(logger, asyncEnumerable.GetType().FullName, null);
170-
}
171-
}
172154
}
173155
}
174156
}

src/Mvc/Mvc.Core/test/Formatters/SystemTextJsonOutputFormatterTest.cs

+43-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
// Copyright (c) .NET Foundation. All rights reserved.
1+
// Copyright (c) .NET Foundation. All rights reserved.
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;
7+
using System.Linq;
68
using System.Text;
79
using System.Text.Json;
810
using System.Text.Json.Serialization;
@@ -81,6 +83,36 @@ public async Task WriteResponseBodyAsync_WithNonUtf8Encoding_FormattingErrorsAre
8183
await Assert.ThrowsAsync<TimeZoneNotFoundException>(() => formatter.WriteResponseBodyAsync(outputFormatterContext, Encoding.GetEncoding("utf-16")));
8284
}
8385

86+
[Fact]
87+
public async Task WriteResponseBodyAsync_ForLargeAsyncEnumerable()
88+
{
89+
// Arrange
90+
var expected = new MemoryStream();
91+
await JsonSerializer.SerializeAsync(expected, LargeAsync(), new JsonSerializerOptions(JsonSerializerDefaults.Web));
92+
var formatter = GetOutputFormatter();
93+
var mediaType = MediaTypeHeaderValue.Parse("application/json; charset=utf-8");
94+
var encoding = CreateOrGetSupportedEncoding(formatter, "utf-8", isDefaultEncoding: true);
95+
96+
var body = new MemoryStream();
97+
var actionContext = GetActionContext(mediaType, body);
98+
99+
var asyncEnumerable = LargeAsync();
100+
var outputFormatterContext = new OutputFormatterWriteContext(
101+
actionContext.HttpContext,
102+
new TestHttpResponseStreamWriterFactory().CreateWriter,
103+
asyncEnumerable.GetType(),
104+
asyncEnumerable)
105+
{
106+
ContentType = new StringSegment(mediaType.ToString()),
107+
};
108+
109+
// Act
110+
await formatter.WriteResponseBodyAsync(outputFormatterContext, Encoding.GetEncoding("utf-8"));
111+
112+
// Assert
113+
Assert.Equal(expected.ToArray(), body.ToArray());
114+
}
115+
84116
private class Person
85117
{
86118
public string Name { get; set; }
@@ -108,5 +140,15 @@ public override void Write(Utf8JsonWriter writer, ThrowingFormatterModel value,
108140
throw new TimeZoneNotFoundException();
109141
}
110142
}
143+
144+
private static async IAsyncEnumerable<int> LargeAsync()
145+
{
146+
await Task.Yield();
147+
// MvcOptions.MaxIAsyncEnumerableBufferLimit is 8192. Pick some value larger than that.
148+
foreach (var i in Enumerable.Range(0, 9000))
149+
{
150+
yield return i;
151+
}
152+
}
111153
}
112154
}

src/Mvc/Mvc.Core/test/Infrastructure/AsyncEnumerableReaderTest.cs

+27-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) .NET Foundation. All rights reserved.
1+
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
@@ -190,6 +190,19 @@ public async Task Reader_ThrowsIfBufferLimitIsReached()
190190
Assert.Equal(expected, ex.Message);
191191
}
192192

193+
[Fact]
194+
public async Task Reader_ThrowsIfIAsyncEnumerableThrows()
195+
{
196+
// Arrange
197+
var enumerable = ThrowingAsyncEnumerable();
198+
var options = new MvcOptions();
199+
var readerFactory = new AsyncEnumerableReader(options);
200+
201+
// Act & Assert
202+
Assert.True(readerFactory.TryGetReader(enumerable.GetType(), out var reader));
203+
await Assert.ThrowsAsync<TimeZoneNotFoundException>(() => reader(enumerable));
204+
}
205+
193206
public static async IAsyncEnumerable<string> TestEnumerable(int count = 3)
194207
{
195208
await Task.Yield();
@@ -225,5 +238,18 @@ public IAsyncEnumerator<string> GetAsyncEnumerator(CancellationToken cancellatio
225238
IAsyncEnumerator<object> IAsyncEnumerable<object>.GetAsyncEnumerator(CancellationToken cancellationToken)
226239
=> GetAsyncEnumerator(cancellationToken);
227240
}
241+
242+
private static async IAsyncEnumerable<string> ThrowingAsyncEnumerable()
243+
{
244+
await Task.Yield();
245+
for (var i = 0; i < 10; i++)
246+
{
247+
yield return $"Hello {i}";
248+
if (i == 5)
249+
{
250+
throw new TimeZoneNotFoundException();
251+
}
252+
}
253+
}
228254
}
229255
}

src/Mvc/Mvc.Core/test/Infrastructure/JsonResultExecutorTestBase.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -333,7 +333,7 @@ public async Task ExecuteAsync_WithNullValue()
333333
public async Task ExecuteAsync_SerializesAsyncEnumerables()
334334
{
335335
// Arrange
336-
var expected = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(new[] { "Hello", "world" }));
336+
var expected = JsonSerializer.Serialize(new[] { "Hello", "world" });
337337

338338
var context = GetActionContext();
339339
var result = new JsonResult(TestAsyncEnumerable());
@@ -344,7 +344,7 @@ public async Task ExecuteAsync_SerializesAsyncEnumerables()
344344

345345
// Assert
346346
var written = GetWrittenBytes(context.HttpContext);
347-
Assert.Equal(expected, written);
347+
Assert.Equal(expected, Encoding.UTF8.GetString(written));
348348
}
349349

350350
[Fact]

src/Mvc/Mvc.Core/test/Infrastructure/ObjectResultExecutorTest.cs

-100
Original file line numberDiff line numberDiff line change
@@ -459,106 +459,6 @@ public async Task ObjectResult_NullValue()
459459
Assert.Null(formatterContext.Object);
460460
}
461461

462-
[Fact]
463-
public async Task ObjectResult_ReadsAsyncEnumerables()
464-
{
465-
// Arrange
466-
var executor = CreateExecutor();
467-
var result = new ObjectResult(AsyncEnumerable());
468-
var formatter = new TestJsonOutputFormatter();
469-
result.Formatters.Add(formatter);
470-
471-
var actionContext = new ActionContext()
472-
{
473-
HttpContext = GetHttpContext(),
474-
};
475-
476-
// Act
477-
await executor.ExecuteAsync(actionContext, result);
478-
479-
// Assert
480-
var formatterContext = formatter.LastOutputFormatterContext;
481-
Assert.Equal(typeof(List<string>), formatterContext.ObjectType);
482-
var value = Assert.IsType<List<string>>(formatterContext.Object);
483-
Assert.Equal(new[] { "Hello 0", "Hello 1", "Hello 2", "Hello 3", }, value);
484-
}
485-
486-
[Fact]
487-
public async Task ObjectResult_Throws_IfEnumerableThrows()
488-
{
489-
// Arrange
490-
var executor = CreateExecutor();
491-
var result = new ObjectResult(AsyncEnumerable(throwError: true));
492-
var formatter = new TestJsonOutputFormatter();
493-
result.Formatters.Add(formatter);
494-
495-
var actionContext = new ActionContext()
496-
{
497-
HttpContext = GetHttpContext(),
498-
};
499-
500-
// Act & Assert
501-
await Assert.ThrowsAsync<TimeZoneNotFoundException>(() => executor.ExecuteAsync(actionContext, result));
502-
}
503-
504-
[Fact]
505-
public async Task ObjectResult_AsyncEnumeration_AtLimit()
506-
{
507-
// Arrange
508-
var count = 24;
509-
var executor = CreateExecutor(options: new MvcOptions { MaxIAsyncEnumerableBufferLimit = count });
510-
var result = new ObjectResult(AsyncEnumerable(count: count));
511-
var formatter = new TestJsonOutputFormatter();
512-
result.Formatters.Add(formatter);
513-
514-
var actionContext = new ActionContext()
515-
{
516-
HttpContext = GetHttpContext(),
517-
};
518-
519-
// Act
520-
await executor.ExecuteAsync(actionContext, result);
521-
522-
// Assert
523-
var formatterContext = formatter.LastOutputFormatterContext;
524-
var value = Assert.IsType<List<string>>(formatterContext.Object);
525-
Assert.Equal(24, value.Count);
526-
}
527-
528-
[Theory]
529-
[InlineData(25)]
530-
[InlineData(1024)]
531-
public async Task ObjectResult_Throws_IfEnumerationExceedsLimit(int count)
532-
{
533-
// Arrange
534-
var executor = CreateExecutor(options: new MvcOptions { MaxIAsyncEnumerableBufferLimit = 24 });
535-
var result = new ObjectResult(AsyncEnumerable(count: count));
536-
var formatter = new TestJsonOutputFormatter();
537-
result.Formatters.Add(formatter);
538-
539-
var actionContext = new ActionContext()
540-
{
541-
HttpContext = GetHttpContext(),
542-
};
543-
544-
// Act & Assert
545-
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => executor.ExecuteAsync(actionContext, result));
546-
}
547-
548-
private static async IAsyncEnumerable<string> AsyncEnumerable(int count = 4, bool throwError = false)
549-
{
550-
await Task.Yield();
551-
for (var i = 0; i < count; i++)
552-
{
553-
yield return $"Hello {i}";
554-
}
555-
556-
if (throwError)
557-
{
558-
throw new TimeZoneNotFoundException();
559-
}
560-
}
561-
562462
private static IServiceCollection CreateServices()
563463
{
564464
var services = new ServiceCollection();

0 commit comments

Comments
 (0)